Compare commits

..
5733 changed files with 20435 additions and 1401292 deletions

No files matched your search

-330
View File
@@ -1,330 +0,0 @@
<!-- wp:paragraph -->
<p>praisonai-mcp is Tier 2e — the heavy MCP server host: full <code>MCPServer</code>, tool/resource/prompt registries, recipe bridge, HTTP-stream auth, and MCP client config management. Depends only on <code>praisonaiagents</code>. v0.0.2. Protocol: MCP 2025-11-25.</p>
<!-- /wp:paragraph -->
<!-- wp:merpress/mermaidjs {"align":"full"} -->
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid alignfull"><pre class="mermaid">%%{init: {"theme": "base", "themeVariables": {"background": "transparent", "lineColor": "#000000"}}}%%
flowchart TB
CLIENT[Cursor Claude Desktop]
subgraph T1 [praisonaiagents]
SDK[MCP client ToolsMCPServer]
SEC[mcp_security shared]
end
subgraph T2e [praisonai-mcp]
HOST[MCPServer]
REG[tool resource prompt registry]
TR[stdio http-stream]
AUTH[api-key oauth scopes]
CFG[mcp list add sync]
end
subgraph T2a [praisonai-code optional]
LIGHT[serve mcp light]
BRIDGE[_mcp_bridge]
end
subgraph T3 [praisonai wrapper optional]
CAP[capabilities recipe]
end
CLIENT --> TR
TR --> HOST
HOST --> REG
REG -.->|adapters| CAP
CFG -.->|_code_bridge| LIGHT
BRIDGE --> CFG
SDK --> LIGHT
SEC --> TR
classDef agent fill:#8B0000,color:#fff
classDef tool fill:#189AB4,color:#fff
class CLIENT agent
class SDK agent
class HOST agent
class REG tool
class TR tool
class AUTH tool
class CFG tool
class LIGHT tool
class SEC tool
</pre></div>
<!-- /wp:merpress/mermaidjs -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Three MCP layers (do not conflate)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Layer</th><th>Package</th><th>Role</th><th>Entry</th></tr></thead><tbody><tr><td>Client</td><td><code>praisonaiagents/mcp/</code></td><td>Connect agents to external MCP servers; OAuth storage</td><td><code>pip install "praisonaiagents[mcp]"</code></td></tr><tr><td>Light server</td><td><code>praisonai-code</code></td><td>Config-driven basic tool hosting</td><td><code>praisonai serve mcp</code></td></tr><tr><td>Heavy host</td><td><code>praisonai-mcp</code></td><td>Full server, adapters, recipe bridge, HTTP auth</td><td><code>praisonai mcp serve</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Install matrix</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Install</th><th><code>praisonai mcp</code></th><th><code>praisonai serve mcp</code></th><th><code>praisonai-mcp</code> script</th></tr></thead><tbody><tr><td><code>pip install praisonai-mcp</code></td><td>—</td><td>—</td><td>✅</td></tr><tr><td><code>pip install "praisonai-mcp[all]"</code></td><td>—</td><td>—</td><td>✅ HTTP + auth</td></tr><tr><td><code>pip install praisonai-code</code> only</td><td>hidden</td><td>✅ light</td><td>—</td></tr><tr><td><code>pip install "praisonai[mcp]"</code></td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td><code>pip install praisonai</code></td><td>✅</td><td>✅</td><td>✅</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>1. Standalone heavy host</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai-mcp[all]"
export OPENAI_API_KEY=sk-...
praisonai-mcp --version
praisonai-mcp serve --transport stdio
praisonai-mcp list-tools
praisonai-mcp doctor</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>2. Full stack (same commands via <code>praisonai mcp …</code>)</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonai
# or MCP-only extra
pip install "praisonai[mcp]"
praisonai mcp serve --transport stdio
praisonai mcp list-tools
praisonai mcp doctor</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>3. Light server only (no praisonai-mcp required)</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonai-code "praisonaiagents[mcp]"
praisonai serve mcp --port 8080
# Uses agents ToolsMCPServer — not the heavy MCPServer</code></pre>
<!-- /wp:code -->
<!-- wp:separator -->
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<!-- /wp:separator -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 1 — STDIO host (Cursor / Claude Desktop)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Heavy host — full PraisonAI tool registry
praisonai-mcp serve --transport stdio
# or
praisonai mcp serve --transport stdio
# Generate client config
praisonai mcp config-generate --client cursor --transport stdio
praisonai mcp config-generate --client claude-desktop --transport stdio</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>STDIO uses JSON-RPC over stdin/stdout. Logs go to stderr only — never corrupt the protocol stream.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 2 — HTTP Stream host</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Streamable HTTP (MCP 2025-11-25) — not legacy standalone SSE. Requires <code>[server]</code> extra (starlette + uvicorn).</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai mcp serve \
--transport http-stream \
--host 127.0.0.1 \
--port 8080 \
--endpoint /mcp \
--api-key my-secret-key \
--response-mode batch
# Endpoints: POST/GET/DELETE /mcp, plus /health</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Non-localhost binds require <code>--api-key</code> or <code>--keys-file</code>. Origin validation uses shared helpers from <code>praisonaiagents.mcp.mcp_security</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 3 — Recipe → MCP bridge</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Turn any PraisonAI recipe into a scoped MCP server with allow/deny lists and safe mode.</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai mcp serve-recipe support-reply --transport stdio
praisonai mcp validate-recipe support-reply
praisonai mcp inspect-recipe support-reply
praisonai mcp config-generate-recipe support-reply --client claude-desktop</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Default denied tools include <code>shell.exec</code>, <code>file.delete</code>, <code>eval</code>, and similar destructive operations.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 4 — MCP client config management</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Config subcommands (<code>list</code>, <code>add</code>, <code>remove</code>, <code>sync</code>, <code>test</code>) require <code>praisonai-code</code> via <code>_code_bridge</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai mcp list
praisonai mcp add filesystem \
--command npx \
--args "-y @modelcontextprotocol/server-filesystem ."
praisonai mcp test filesystem
praisonai mcp sync
praisonai mcp tools
praisonai mcp status</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 5 — Built-in registry (tools, resources, prompts)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Type</th><th>Examples</th><th>Adapter</th></tr></thead><tbody><tr><td>Tools</td><td><code>praisonai.chat.*</code>, <code>praisonai.workflow.*</code>, agent run helpers</td><td><code>mcp_server/adapters/capabilities.py</code>, <code>cli_tools.py</code></td></tr><tr><td>Resources</td><td><code>praisonai://memory/sessions</code>, <code>praisonai://workflows</code>, <code>praisonai://agents</code></td><td><code>adapters/resources.py</code></td></tr><tr><td>Prompts</td><td><code>deep-research</code>, <code>code-review</code>, <code>workflow-auto</code></td><td><code>adapters/prompts.py</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Inspect what the host exposes
praisonai-mcp list-tools
praisonai-mcp list-resources
praisonai-mcp list-prompts
# Host tool search (MCPServerCLI)
praisonai-mcp tools search "workflow"</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 6 — Security &amp; auth</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Concern</th><th>Package</th><th>Module</th></tr></thead><tbody><tr><td>Origin validation, session IDs</td><td><code>praisonaiagents</code></td><td><code>mcp/mcp_security.py</code></td></tr><tr><td>API keys, scoped keys file</td><td><code>praisonai-mcp</code></td><td><code>mcp_server/auth/api_key.py</code></td></tr><tr><td>OAuth 2.1 + PKCE</td><td><code>praisonai-mcp</code></td><td><code>mcp_server/auth/oauth.py</code></td></tr><tr><td>OIDC discovery</td><td><code>praisonai-mcp</code></td><td><code>mcp_server/auth/oidc.py</code></td></tr><tr><td>Scope enforcement</td><td><code>praisonai-mcp</code></td><td><code>mcp_server/auth/scopes.py</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Scoped keys file (--keys-file)
[
{"key": "monitor-key", "name": "monitor", "scopes": ["tools:read", "resources:read"]},
{"key": "admin-key", "name": "admin", "scopes": ["*"]}
]
# Env vars
export MCP_API_KEY=your-key
export PRAISONAI_MCP_MAX_SESSIONS=1000</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 7 — CLI routing (lazy load)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Registry</th><th>Commands</th><th>Gate</th></tr></thead><tbody><tr><td>MCP-resident Typer</td><td>list, add, remove, sync, test, serve, doctor</td><td><code>mcp_package_available()</code></td></tr><tr><td>MCPServerCLI host</td><td>serve, list-tools, list-resources, config-generate, tasks, auth</td><td>Direct via <code>praisonai-mcp &lt;cmd&gt;</code></td></tr><tr><td>Code-resident light</td><td><code>serve mcp</code></td><td>Always available with code + agents</td></tr><tr><td>Wrapper-resident</td><td>dashboard, schedule, langfuse</td><td><code>wrapper_package_available()</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Standalone <code>praisonai-code</code> hides <code>mcp</code> from <code>--help</code> when <code>praisonai-mcp</code> is not installed. Host commands like <code>praisonai-mcp serve</code> skip the <code>mcp</code> prefix; <code>praisonai mcp serve</code> goes through the Typer group.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 8 — Python API</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>import praisonai_mcp
from praisonai_mcp.mcp_server import MCPServer
from praisonai_mcp.mcp_server.registry import register_tool
from praisonai_mcp.mcp_server.adapters import register_all
@register_tool("custom.greet")
def greet(name: str) -> str:
"""Greet a person by name."""
return f"Hello, {name}!"
register_all()
server = MCPServer(name="praisonai")
server.run(transport="stdio")</code></pre>
<!-- /wp:code -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Recipe host
from praisonai_mcp.mcp_server import RecipeMCPAdapter
adapter = RecipeMCPAdapter("support-reply")
adapter.load()
adapter.to_mcp_server().run(transport="stdio")
# Shim path still works
from praisonai.mcp_server import MCPServer</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 9 — Standalone (no wrapper)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonaiagents "praisonai-mcp[all]"
export OPENAI_API_KEY=sk-...
praisonai-mcp serve --transport stdio
praisonai-mcp list-tools
praisonai-mcp doctor
praisonai-mcp --help</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Heavy MCP hosting needs only <code>praisonaiagents</code> + <code>praisonai-mcp</code>. Full capability registry (workflows, deploy, extended tools) activates when the wrapper is co-installed via <code>_wrapper_bridge</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Bridges (no PyPI cycles)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Bridge</th><th>Direction</th><th>Use</th></tr></thead><tbody><tr><td><code>_mcp_bridge</code></td><td>code → mcp</td><td>Route <code>praisonai mcp</code> into Typer group</td></tr><tr><td><code>_code_bridge</code></td><td>mcp → code</td><td>Config loader, paths, output controller</td></tr><tr><td><code>_wrapper_bridge</code></td><td>mcp → wrapper</td><td>Capabilities, recipe, deploy adapters</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Backward-compat shims</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Old path</th><th>Still works via</th></tr></thead><tbody><tr><td><code>praisonai.mcp_server.*</code></td><td><code>alias_package</code> → <code>praisonai_mcp.mcp_server</code></td></tr><tr><td><code>praisonai.cli.commands.mcp</code></td><td><code>sys.modules</code> shim → mcp Typer group</td></tr><tr><td><code>from praisonai.mcp_server import MCPServer</code></td><td>Lazy re-export through shim</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Seven-package publish order</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonaiagents → praisonai-code → praisonai-bot → praisonai-train → praisonai-browser → praisonai-mcp → praisonai (wrapper)</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Wrapper pins <code>praisonai-mcp&gt;=X</code> after mcp is on PyPI. CI: <code>.github/workflows/pypi-release.yml</code>. Import gate: <code>scripts/check_c12_mcp_imports.sh</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Summary</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Item</th><th>Value</th></tr></thead><tbody><tr><td>Tier</td><td>2e</td></tr><tr><td>PyPI</td><td><code>praisonai-mcp</code> v0.0.2</td></tr><tr><td>Runtime deps</td><td><code>praisonaiagents</code>, <code>mcp</code>, rich, typer</td></tr><tr><td>HTTP extra</td><td><code>[server]</code> — starlette, uvicorn</td></tr><tr><td>Auth extra</td><td><code>[auth]</code> — httpx</td></tr><tr><td>Transports</td><td><code>stdio</code>, <code>http-stream</code></td></tr><tr><td>Light alternative</td><td><code>praisonai serve mcp</code> (code tier)</td></tr><tr><td>CLI entry</td><td><code>praisonai-mcp</code> or <code>praisonai mcp</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Previous: <a href="https://mer.vin/2026/07/praisonai-train-praisonai-train-fine-tuning-agent-training-in-depth/">praisonai-train</a> · Next: praisonai wrapper</p>
<!-- /wp:paragraph -->
-460
View File
@@ -1,460 +0,0 @@
<!-- wp:paragraph -->
<p>praisonai is Tier 3 — the integration wrapper that wires <code>praisonaiagents</code> + five Tier-2 packages (<code>praisonai-code</code>, <code>praisonai-bot</code>, <code>praisonai-train</code>, <code>praisonai-browser</code>, <code>praisonai-mcp</code>) into one install, with dashboard, framework adapters, jobs API, serve orchestration, and backward-compat shims. v4.6.150.</p>
<!-- /wp:paragraph -->
<!-- wp:merpress/mermaidjs {"align":"full"} -->
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid alignfull"><pre class="mermaid">%%{init: {"theme": "base", "themeVariables": {"background": "transparent", "lineColor": "#000000"}}}%%
flowchart TB
subgraph T1 [Tier 1 praisonaiagents]
A[Agent tools memory MCP client]
end
subgraph T2 [Tier 2 packages]
C[praisonai-code run chat code]
B[praisonai-bot gateway channels]
TR[praisonai-train fine-tune agents]
BR[praisonai-browser CDP Playwright]
MCP[praisonai-mcp server host]
end
subgraph T3 [Tier 3 praisonai wrapper]
W[dashboard jobs frameworks serve shims]
end
A --> C
A --> B
A --> TR
A --> BR
A --> MCP
W --> C
W --> B
W --> TR
W --> BR
W --> MCP
W --> A
classDef agent fill:#8B0000,color:#fff
classDef tool fill:#189AB4,color:#fff
class A agent
class W agent
class C tool
class B tool
class TR tool
class BR tool
class MCP tool
</pre></div>
<!-- /wp:merpress/mermaidjs -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Seven-package stack</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonaiagents → praisonai-code + praisonai-bot + praisonai-train + praisonai-browser + praisonai-mcp → praisonai (wrapper)</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Each Tier-2 package is installable standalone with its own console script. The wrapper pins all of them and preserves every old import path via shims. Use the wrapper when you need several surfaces together — HTTP serve, dashboard, framework YAML, jobs/recipe — without managing tier pins yourself.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Product catalog (C0–C12 shipped)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Package</th><th>Epic</th><th>Choose this when…</th><th>Standalone install</th></tr></thead><tbody><tr><td><code>praisonaiagents</code></td><td>Tier 1</td><td>Building or embedding agents in code — Agent, tools, memory, hooks</td><td><code>pip install praisonaiagents</code></td></tr><tr><td><code>praisonai-code</code></td><td>C0–C8</td><td>Terminal agentic CLI — run, chat, code, warm daemon</td><td><code>pip install praisonai-code</code></td></tr><tr><td><code>praisonai-bot</code></td><td>C9</td><td>Messaging bots + WebSocket gateway</td><td><code>pip install "praisonai-bot[gateway,bot]"</code></td></tr><tr><td><code>praisonai-train</code></td><td>C10</td><td>Agent training loops + GPU LLM fine-tuning</td><td><code>pip install praisonai-train</code> or <code>[llm]</code></td></tr><tr><td><code>praisonai-browser</code></td><td>C11</td><td>Chrome extension bridge, CDP, Playwright agents</td><td><code>pip install "praisonai-browser[all]"</code></td></tr><tr><td><code>praisonai-mcp</code></td><td>C12</td><td>MCP server for Cursor, Claude Desktop, Windsurf</td><td><code>pip install "praisonai-mcp[all]"</code></td></tr><tr><td><code>praisonai</code></td><td>Tier 3</td><td>Full product / umbrella — serve, dashboard, frameworks, jobs</td><td><code>pip install praisonai</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Three-layer products (do not conflate)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Product</th><th>Layer 1 (agents)</th><th>Layer 2 (dedicated pkg)</th><th>Layer 3 (wrapper)</th></tr></thead><tbody><tr><td>Browser</td><td><code>tools/protocols/browser.py</code></td><td><code>praisonai-browser</code></td><td>shims + optional full stack</td></tr><tr><td>MCP</td><td><code>praisonaiagents/mcp</code> client + light server</td><td><code>praisonai-mcp</code> heavy host</td><td>capability bodies via <code>_wrapper_bridge</code></td></tr><tr><td>Bots</td><td>bots/gateway protocols</td><td><code>praisonai-bot</code></td><td>shims only</td></tr><tr><td>Train</td><td>Agent + hooks in agents</td><td><code>praisonai-train</code></td><td>shims + <code>setup-conda-env</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Install matrix</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>1. Base install (full seven-package stack)</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonai
export OPENAI_API_KEY=sk-...
praisonai doctor
praisonai version show</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>2. Targeted extras</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[claw]" # dashboard + bots + gateway
pip install "praisonai[all]" # onboarding + messaging + search stack
pip install "praisonai[train]" # LLM fine-tuning (Unsloth stack)
pip install "praisonai[mcp]" # full MCP server host
pip install "praisonai[browser]" # extension bridge + CDP + Playwright
pip install "praisonai[crewai]" # multi-framework YAML</code></pre>
<!-- /wp:code -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Package</th><th>Min version (wrapper pin)</th><th>Console script</th></tr></thead><tbody><tr><td><code>praisonaiagents</code></td><td>≥ 1.6.153</td><td>—</td></tr><tr><td><code>praisonai-code</code></td><td>≥ 0.0.49</td><td><code>praisonai-code</code></td></tr><tr><td><code>praisonai-bot</code></td><td>≥ 0.0.34</td><td><code>praisonai-bot</code></td></tr><tr><td><code>praisonai-train</code></td><td>≥ 0.0.5</td><td><code>praisonai-train</code></td></tr><tr><td><code>praisonai-browser</code></td><td>≥ 0.0.2</td><td><code>praisonai-browser</code></td></tr><tr><td><code>praisonai-mcp</code></td><td>≥ 0.0.2</td><td><code>praisonai-mcp</code></td></tr><tr><td><code>praisonai</code></td><td>4.6.150</td><td><code>praisonai</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:separator -->
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<!-- /wp:separator -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 1 — What wrapper adds (not in Tier-2 alone)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Feature</th><th>Command / path</th><th>Owner</th><th>Extracted?</th></tr></thead><tbody><tr><td>Unified CLI router</td><td><code>praisonai …</code></td><td>code Typer + 6 resident registries</td><td>—</td></tr><tr><td>HTTP serve orchestration</td><td><code>praisonai serve agents|recipe|a2a|…</code></td><td>wrapper <code>cli/features/serve.py</code></td><td>Deferred (C15 epic)</td></tr><tr><td>Dashboard launcher</td><td><code>praisonai dashboard</code></td><td>wrapper</td><td>Stay wrapper</td></tr><tr><td>Framework adapters</td><td><code>praisonai --framework crewai</code></td><td>wrapper <code>framework_adapters/</code></td><td>Stay wrapper (impls in external PraisonAI-Frameworks)</td></tr><tr><td>Jobs API</td><td><code>praisonai.jobs</code> · port 8005</td><td>wrapper</td><td>Deferred (with serve/recipe)</td></tr><tr><td>Recipe NL → YAML</td><td><code>praisonai recipe …</code></td><td>wrapper</td><td>Deferred</td></tr><tr><td>Scheduler CLI</td><td><code>praisonai schedule …</code></td><td>wrapper</td><td>Stay wrapper (3-layer split with bot)</td></tr><tr><td>Sandbox execution</td><td><code>praisonai sandbox …</code></td><td>wrapper <code>sandbox/</code></td><td>C13 candidate</td></tr><tr><td>Cloud deploy</td><td><code>praisonai deploy …</code></td><td>wrapper <code>deploy/</code></td><td>C13 candidate</td></tr><tr><td>Langfuse / Langflow / n8n</td><td><code>langfuse</code>, <code>flow</code>, <code>n8n</code></td><td>wrapper</td><td>Stay wrapper</td></tr><tr><td>Backward-compat shims</td><td><code>praisonai.bots</code> → <code>praisonai_bot</code></td><td>wrapper <code>cli/_shim.py</code></td><td>Permanent</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Tier-2 packages own their domains; the wrapper owns integration surfaces that span multiple tiers.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 2 — Terminal hot path (via code tier)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonai
export OPENAI_API_KEY=sk-...
praisonai run "Summarise the latest AI agent trends"
praisonai chat
praisonai code
praisonai daemon start --background
praisonai run "Hello" # auto-forwards to warm runtime</code></pre>
<!-- /wp:code -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Python SDK (Tier 1 — same everywhere)
from praisonaiagents import Agent
agent = Agent(instructions="You are a helpful AI assistant")
agent.start("Write a movie script about a robot on Mars")</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-code-praisonai-code-terminal-cli-in-depth/">praisonai-code in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 3 — Train (via praisonai-train tier)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code># LLM fine-tuning (Unsloth) — lazy ML deps
praisonai train llm dataset.json
praisonai train llm --model llama-3.1 dataset.json
# Agent training — LLM-as-judge
praisonai train agents --input "What is Python?" --iterations 3
# Human feedback loop
praisonai train agents --input "Explain quantum computing" --human
# Session management
praisonai train list
praisonai train apply train-abc123 --run "And Germany?"
# Standalone script (no wrapper prefix)
praisonai-train agents --input "Hello" --iterations 2</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-train-praisonai-train-fine-tuning-agent-training-in-depth/">praisonai-train in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 4 — Bots &amp; gateway (via bot tier)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[bot]"
export OPENAI_API_KEY=sk-...
export TELEGRAM_BOT_TOKEN=...
praisonai onboard
praisonai bot start --platform telegram
praisonai gateway start --config ~/.praisonai/bot.yaml
praisonai gateway install --config ~/.praisonai/bot.yaml
praisonai pairing approve telegram ABC12345</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-bot-praisonai-bot-gateway-channels-in-depth/">praisonai-bot in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 5 — MCP server host (via mcp tier)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Command</th><th>Layer</th><th>Package</th></tr></thead><tbody><tr><td><code>praisonai serve mcp</code></td><td>Light server</td><td><code>praisonai-code</code> + agents</td></tr><tr><td><code>praisonai mcp serve</code></td><td>Heavy host</td><td><code>praisonai-mcp</code></td></tr><tr><td><code>praisonai-mcp serve</code></td><td>Heavy host (direct)</td><td><code>praisonai-mcp</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Heavy MCP host — full tool/resource/prompt registry
praisonai mcp serve --transport stdio
praisonai mcp serve --transport http-stream --port 8080 --api-key secret
# Recipe → scoped MCP server
praisonai mcp serve-recipe support-reply --transport stdio
# Client config management (needs praisonai-code co-installed)
praisonai mcp list
praisonai mcp add filesystem --command npx --args "-y @modelcontextprotocol/server-filesystem ."
# Generate Cursor / Claude Desktop config
praisonai mcp config-generate --client cursor --transport stdio</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-mcp-praisonai-mcp-server-host-client-config-in-depth/">praisonai-mcp in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 6 — Browser automation (via browser tier)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[browser]"
# Extension bridge server + LLM browser agent
praisonai browser start
praisonai browser run "Find the latest Python release notes"
# Diagnostics
praisonai browser doctor
# Lightweight tool shim (code tier — no praisonai-browser required)
praisonai browser-tool snapshot https://example.com
# Standalone script
praisonai-browser start
praisonai-browser run "Click the login button"</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Three layers: agents protocols → <code>praisonai browser-tool</code> (praisonai-tools) → <code>praisonai browser</code> (extension bridge, CDP, Playwright).</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-browser-praisonai-browser-browser-automation-in-depth/">praisonai-browser in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 7 — Dashboard stack</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[claw,flow,ui]"
# Individual services
praisonai claw # :8082 — ops dashboard
praisonai flow # :7861 — Langflow
praisonai ui # :8081 — clean chat
# Unified launcher (wrapper-only)
praisonai dashboard # :3000
praisonai dashboard --port 9000 --no-auto-start</code></pre>
<!-- /wp:code -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Port</th><th>Service</th></tr></thead><tbody><tr><td>3000</td><td>Dashboard hub</td></tr><tr><td>7860/7861</td><td>Langflow</td></tr><tr><td>8081</td><td>UI chat</td></tr><tr><td>8082</td><td>Claw</td></tr><tr><td>8765</td><td>Gateway WebSocket</td></tr><tr><td>8005</td><td>Jobs API</td></tr><tr><td>8080</td><td>MCP HTTP stream (optional)</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 8 — Multi-framework YAML</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[crewai]"
export OPENAI_API_KEY=sk-...
praisonai --framework crewai --auto "Create a movie script"
praisonai run --file agents.yaml --framework crewai
pip install "praisonai[autogen]"
praisonai --framework autogen agents.yaml</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Protocols live in <code>praisonaiagents/frameworks/</code>; adapter implementations in wrapper <code>framework_adapters/</code>. Entry-point group: <code>praisonai.framework_adapters</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 9 — Scheduler (24/7 agents)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai schedule add --agent my_agent.yaml --expr "hourly"
praisonai schedule list
praisonai schedule start</code></pre>
<!-- /wp:code -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Python
from praisonai.scheduler import AgentScheduler
from praisonaiagents import Agent
agent = Agent(name="NewsChecker", instructions="Check latest AI news")
scheduler = AgentScheduler(agent=agent, task="Summarise top 3 AI stories")
scheduler.start("hourly", run_immediately=True)</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Three scheduler layers: job store (<code>praisonaiagents.scheduler</code>) → CLI (<code>praisonai schedule</code>, wrapper) → gateway tick delivery (<code>praisonai_bot.scheduler.executor</code>).</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 10 — Jobs API &amp; wrapper-only surfaces</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Jobs, recipe, serve, sandbox, and deploy remain wrapper-owned today (deferred or C13 candidates).</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Jobs API (wrapper — port 8005)
python -m uvicorn praisonai.jobs.server:create_app --port 8005 --factory
praisonai run submit "Analyse AI trends"
praisonai run submit "Analyse news" --recipe news-analyzer --wait
export PRAISONAI_JOBS_API_KEY=your-secret
# HTTP serve (wrapper orchestration — Typer shell in code tier)
praisonai serve agents --port 8080
praisonai serve recipe support-reply
# Sandbox / deploy (wrapper today — C13 extraction candidates)
praisonai sandbox run "print hello" --backend docker
praisonai deploy agents.yaml --provider aws</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 11 — Lazy CLI routing (six resident registries)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Registry</th><th>Count</th><th>Examples</th><th>Loaded from</th><th>Gate</th></tr></thead><tbody><tr><td>Code-resident</td><td>~40</td><td>run, chat, code, daemon, serve, config</td><td><code>praisonai_code.cli.commands.*</code></td><td>always</td></tr><tr><td>Bot-resident</td><td>8</td><td>bot, gateway, onboard, kanban, claw</td><td><code>praisonai_bot.cli.commands.*</code></td><td><code>bot_package_available()</code></td></tr><tr><td>Train-resident</td><td>1</td><td>train</td><td><code>praisonai_train.cli.commands.*</code></td><td><code>train_package_available()</code></td></tr><tr><td>Browser-resident</td><td>1</td><td>browser</td><td><code>praisonai_browser.cli.commands.*</code></td><td><code>browser_package_available()</code></td></tr><tr><td>MCP-resident</td><td>1</td><td>mcp</td><td><code>praisonai_mcp.cli.commands.*</code></td><td><code>mcp_package_available()</code></td></tr><tr><td>Wrapper-resident</td><td>22</td><td>dashboard, schedule, flow, langfuse, recipe</td><td><code>praisonai.cli.commands.*</code></td><td><code>wrapper_available()</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai --help # 70+ commands, lazy-loaded
praisonai doctor
praisonai train --help
praisonai mcp --help
praisonai browser --help
praisonai dashboard --help</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Standalone <code>praisonai-code</code> hides bot/train/browser/mcp/wrapper commands from <code>--help</code> when those packages are absent.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 12 — Shim map (backward compat)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Old import / path</th><th>Canonical target</th></tr></thead><tbody><tr><td><code>praisonai.cli.main</code></td><td><code>praisonai_code.cli.main</code></td></tr><tr><td><code>praisonai.runtime.*</code></td><td><code>praisonai_code.runtime.*</code></td></tr><tr><td><code>praisonai.bots.*</code></td><td><code>praisonai_bot.bots.*</code></td></tr><tr><td><code>praisonai.gateway.*</code></td><td><code>praisonai_bot.gateway.*</code></td></tr><tr><td><code>praisonai.train.*</code></td><td><code>praisonai_train.train.*</code></td></tr><tr><td><code>praisonai.browser.*</code></td><td><code>praisonai_browser.*</code></td></tr><tr><td><code>praisonai.mcp_server.*</code></td><td><code>praisonai_mcp.mcp_server.*</code></td></tr><tr><td><code>praisonai.scheduler.executor</code></td><td><code>praisonai_bot.scheduler.executor</code></td></tr><tr><td><code>setup-conda-env</code></td><td><code>praisonai_train.setup.setup_conda_env</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code># These must keep working
from praisonai.cli.main import PraisonAI
from praisonai.bots import Bot
from praisonai.gateway import WebSocketGateway
from praisonai.train.agents import AgentTrainer
from praisonai.mcp_server import MCPServer
python -m praisonai run "hello"
python -m praisonai.runtime</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 13 — Bridges (no PyPI cycles)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Bridge</th><th>Direction</th><th>Use</th></tr></thead><tbody><tr><td><code>_wrapper_bridge</code></td><td>code → wrapper</td><td>serve handlers, frameworks, jobs</td></tr><tr><td><code>_bot_bridge</code></td><td>code → bot</td><td>bot/gateway CLI routing</td></tr><tr><td><code>_train_bridge</code></td><td>code → train</td><td>train CLI routing</td></tr><tr><td><code>_browser_bridge</code></td><td>code → browser</td><td>browser CLI routing</td></tr><tr><td><code>_mcp_bridge</code></td><td>code → mcp</td><td>mcp CLI routing</td></tr><tr><td><code>_code_bridge</code></td><td>tier-2 → code</td><td>config kernel, legacy dispatch</td></tr><tr><td><code>_wrapper_bridge</code></td><td>tier-2 → wrapper</td><td>capabilities, recipe, jobs/UI</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">When to use wrapper vs a Tier-2 package alone</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Your goal</th><th>Install</th></tr></thead><tbody><tr><td>Only terminal agents</td><td><code>praisonai-code</code> (+ optional <code>praisonaiagents</code>)</td></tr><tr><td>Only bots on Telegram/Slack</td><td><code>praisonai-bot</code></td></tr><tr><td>Only train / fine-tune</td><td><code>praisonai-train</code></td></tr><tr><td>Only automate browser</td><td><code>praisonai-browser</code></td></tr><tr><td>Only MCP server for IDE</td><td><code>praisonai-mcp</code></td></tr><tr><td>Several of the above + serve / dashboard / framework YAML</td><td><code>pip install praisonai</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">What stays in wrapper (not extracted)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Surface</th><th>Why it stays</th></tr></thead><tbody><tr><td><code>dashboard</code> / <code>flow</code> / <code>ui</code></td><td>Langflow extra + launcher glue — not a standalone PyPI product</td></tr><tr><td><code>claw</code></td><td>Default app lives in <code>praisonai-bot</code>; wrapper is a thin shim</td></tr><tr><td><code>framework_adapters/</code></td><td>Thin registry; heavy impls in external PraisonAI-Frameworks</td></tr><tr><td><code>schedule</code> CLI</td><td>Three-layer split: agents store + wrapper CLI + bot executor</td></tr><tr><td><code>serve</code> / <code>recipe</code> / <code>jobs</code></td><td>~14k LOC deferred epic — high coupling (C15)</td></tr><tr><td><code>sandbox</code> / <code>deploy</code></td><td>Still in wrapper today; C13 extraction candidates</td></tr><tr><td><code>standardise</code> / suite runner</td><td>Maintainer/docs tooling — internal only</td></tr><tr><td><code>persist</code> / enterprise adapters</td><td><strong>Rejected</strong> — agents already ship File/SQLite defaults; no crisp standalone story</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Roadmap — C13+ candidates</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Package</th><th>Status</th><th>Choose when…</th><th>Standalone install (planned)</th></tr></thead><tbody><tr><td><code>praisonai-sandbox</code></td><td><strong>C13 preferred</strong> (execution goal)</td><td>Isolated agent code execution — Docker/E2B/Modal/Daytona/Sandlock</td><td><code>pip install praisonai-sandbox[docker]</code></td></tr><tr><td><code>praisonai-deploy</code></td><td><strong>C13 alt</strong> (deploy goal)</td><td>Ship <code>agents.yaml</code> to Docker/AWS/GCP/Azure</td><td><code>pip install praisonai-deploy</code></td></tr><tr><td><code>praisonai-replay</code></td><td>Conditional C14+</td><td>Deterministic replay from run ledger / traces</td><td>After product sign-off</td></tr><tr><td><code>praisonai-serve</code></td><td>Deferred C15</td><td>HTTP API hosting without full umbrella (~14k LOC)</td><td>With recipe/jobs decision</td></tr><tr><td><code>praisonai-recipe</code></td><td>Deferred</td><td>NL → agent YAML without jobs UI</td><td>Tied to jobs + serve</td></tr><tr><td><code>praisonai-jobs</code></td><td>Deferred</td><td>Async long-running agent jobs API</td><td>Extract with serve or stay wrapper</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>C13 decision rule: <strong>sandbox</strong> = where code runs; <strong>deploy</strong> = where the agent service lives. Ship one first after product sign-off.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Standalone extraction litmus test</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Criterion</th><th>Pass (browser/MCP)</th><th>Fail (example)</th></tr></thead><tbody><tr><td>Named user goal without full <code>praisonai</code></td><td><code>pip install praisonai-browser</code> → automate Chrome</td><td><code>pip install praisonai-persist</code> → ???</td></tr><tr><td>Coherent product boundary</td><td>One Typer group / one console script</td><td>Whole <code>cli/features/</code> orchestration layer</td></tr><tr><td>PyPI deps: <code>praisonaiagents</code> only</td><td>Tier-2 + lazy <code>_wrapper_bridge</code></td><td>Hard <code>import praisonai</code> at module level</td></tr><tr><td>Worth a separate install</td><td>User who never wants dashboard/serve/bots</td><td>Internal docs tooling</td></tr><tr><td>Extraction shape</td><td>C10 single PR (~3–11k LOC)</td><td>Mega-epic without product sign-off</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">When to use which tier</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Scenario</th><th>Install</th></tr></thead><tbody><tr><td>Pure Python agents, minimal deps</td><td><code>pip install praisonaiagents</code></td></tr><tr><td>Terminal coding agent, CI, headless</td><td><code>pip install praisonai-code</code></td></tr><tr><td>Telegram/Discord gateway only</td><td><code>pip install "praisonai-bot[gateway,bot]"</code></td></tr><tr><td>Agent training (no GPU)</td><td><code>pip install praisonai-train</code></td></tr><tr><td>LLM fine-tuning (GPU)</td><td><code>pip install "praisonai-train[llm]"</code></td></tr><tr><td>MCP server for Cursor/Claude</td><td><code>pip install "praisonai-mcp[all]"</code></td></tr><tr><td>Browser automation (extension/CDP)</td><td><code>pip install "praisonai-browser[all]"</code></td></tr><tr><td>CrewAI/AutoGen YAML</td><td><code>pip install "praisonai[crewai]"</code></td></tr><tr><td>Full ops + channels + dashboard</td><td><code>pip install "praisonai[claw]"</code></td></tr><tr><td>Everything in one install</td><td><code>pip install praisonai</code></td></tr><tr><td>Library author / embedding</td><td><code>praisonaiagents</code> only</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Seven-package publish order</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonaiagents
→ praisonai-code + praisonai-bot + praisonai-train + praisonai-browser + praisonai-mcp
→ praisonai (wrapper pins all tier-2 deps last)</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>CI: <code>.github/workflows/pypi-release.yml</code> · selective publish: <code>publish_all.py --changed-only</code> (default). Playbook per package: manifest → TDD compat tests → git mv → shims → <code>_X_RESIDENT_COMMANDS</code> → CI shard → PyPI.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Summary</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Item</th><th>Value</th></tr></thead><tbody><tr><td>Tier</td><td>3 (integration wrapper)</td></tr><tr><td>PyPI</td><td><code>praisonai</code> v4.6.150</td></tr><tr><td>Shipped tier-2 deps</td><td>code, bot, train, browser, mcp (C0–C12)</td></tr><tr><td>Resident registries</td><td>6 (code + bot + train + browser + mcp + wrapper)</td></tr><tr><td>Wrapper-only (~140k LOC)</td><td>serve, recipe, jobs, dashboard, sandbox, deploy, frameworks</td></tr><tr><td>Next extraction</td><td>C13: sandbox or deploy (product sign-off)</td></tr><tr><td>One-liner still works</td><td><code>pip install praisonai</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Architecture overview: Seven-package C7–C12 extraction · Post-C12 roadmap (C13+)</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Series: <a href="https://mer.vin/2026/07/praisonai-code-praisonai-code-terminal-cli-in-depth/">praisonai-code</a> · <a href="https://mer.vin/2026/07/praisonai-bot-praisonai-bot-gateway-channels-in-depth/">praisonai-bot</a> · <a href="https://mer.vin/2026/07/praisonai-train-praisonai-train-fine-tuning-agent-training-in-depth/">praisonai-train</a> · <a href="https://mer.vin/2026/07/praisonai-browser-praisonai-browser-browser-automation-in-depth/">praisonai-browser</a> · <a href="https://mer.vin/2026/07/praisonai-mcp-praisonai-mcp-server-host-client-config-in-depth/">praisonai-mcp</a> · praisonai wrapper</p>
<!-- /wp:paragraph -->
-51
View File
@@ -1,51 +0,0 @@
# PraisonAI Environment Variables
# Copy this file to .env and update with your actual values
# === Required ===
OPENAI_API_KEY=sk-... # OpenAI key for LLM calls. Get yours at https://platform.openai.com
# === Required for Claw web-search tool ===
TAVILY_API_KEY=tvly-... # Get yours at https://app.tavily.com (free tier available)
# === Optional: Claw dashboard security ===
PRAISONAI_ALLOW_LOCAL_TOOLS=0 # Set to 1 to enable local shell tools (security risk)
# === Optional: Alternative LLM Providers ===
ANTHROPIC_API_KEY=sk-ant-... # Anthropic Claude models
GOOGLE_API_KEY=... # Google Gemini models
GROQ_API_KEY=gsk_... # Groq (fast inference)
COHERE_API_KEY=... # Cohere models
AZURE_OPENAI_API_KEY=... # Azure OpenAI
AZURE_OPENAI_ENDPOINT=... # Azure OpenAI endpoint
# === Optional: Memory backends ===
MEM0_API_KEY=... # Mem0 memory service
REDIS_URL=redis://localhost:6379 # Redis for state management
# === Optional: Observability ===
LANGFUSE_SECRET_KEY=sk-lf-... # Langfuse tracing
LANGFUSE_PUBLIC_KEY=pk-lf-... # Langfuse public key
# === Optional: Bot platforms ===
TELEGRAM_BOT_TOKEN=... # Telegram bot token
DISCORD_BOT_TOKEN=... # Discord bot token
SLACK_BOT_TOKEN=xoxb-... # Slack bot token
SLACK_APP_TOKEN=xapp-... # Slack app token
WHATSAPP_ACCESS_TOKEN=... # WhatsApp bot access token
WHATSAPP_PHONE_NUMBER_ID=... # WhatsApp phone number ID
# === Optional: Database ===
DATABASE_URL=sqlite:///~/.praison/database.sqlite # Database connection string
# === Optional: Tool Security ===
# ALLOWED_TOOLS=search,send_message # Whitelist specific tools to prevent name collisions
# Useful in multi-environment setups with overlapping tool names
# Leave unset to use all tools (with collision warnings)
# Do NOT set to empty string: that's treated as configuration error
# In CI, unknown tool names in the whitelist cause strict startup failure
# HERMES_ONLY_TOOLS=search,send_message # (Deprecated) Backward compatibility alias for ALLOWED_TOOLS
# === Optional: Development ===
LOGLEVEL=INFO # Logging level (DEBUG, INFO, WARNING, ERROR)
CHAINLIT_AUTH_SECRET=... # Chainlit authentication secret
DEBUG=false # Enable debug mode
-2
View File
@@ -1,2 +0,0 @@
patreon: MervinPraison
ko_fi: MervinPraison
-34
View File
@@ -1,34 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: '[BUG] '
labels: bug
assignees: ''
---
## Environment
- Provider (select one):
- [ ] Anthropic
- [ ] OpenAI
- [ ] Google Vertex AI
- [ ] AWS Bedrock
- [ ] Other: <!-- specify -->
- PraisonAI version: <!-- if known -->
- Operating System: <!-- e.g. macOS 14.3, Windows 11, Ubuntu 22.04 -->
## Full Code
<!-- Full code that reproduces the bug -->
## Steps to Reproduce
1. <!-- First step -->
2. <!-- Second step -->
3. <!-- And so on... -->
## Expected Behavior
<!-- What you expected to happen -->
## Actual Behavior
<!-- What actually happened -->
## Additional Context
<!-- Add any other context about the problem here, such as screenshots, logs, etc. -->
@@ -1,332 +0,0 @@
name: "Claude Code Action"
description: "Run Claude Code in GitHub Actions workflows"
inputs:
anthropic_api_key:
description: "Anthropic API key"
required: false
claude_code_oauth_token:
description: "Claude Code OAuth Token for Claude Pro"
required: false
github_token:
description: "GitHub token for Claude to operate with"
required: false
default: ${{ github.token }}
trigger_phrase:
description: "The trigger phrase to look for in comments, issue/PR bodies, and issue titles"
required: false
default: "@claude"
assignee_trigger:
description: "The assignee username that triggers the action (e.g. @claude). Only used for issue assignment"
required: false
max_turns:
description: "Maximum number of conversation turns Claude can take"
required: false
timeout_minutes:
description: "Timeout in minutes for execution"
required: false
default: "30"
model:
description: "Model to use (provider-specific format required for Bedrock/Vertex)"
required: false
use_bedrock:
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
required: false
default: "false"
use_vertex:
description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API"
required: false
default: "false"
allowed_tools:
description: "Additional tools for Claude to use (the base GitHub tools will always be included)"
required: false
default: ""
disallowed_tools:
description: "Tools that Claude should never use"
required: false
default: ""
custom_instructions:
description: "Additional custom instructions to include in the prompt for Claude"
required: false
default: ""
mcp_config:
description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers"
required: false
default: ""
claude_env:
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
required: false
default: ""
direct_prompt:
description: "Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows)"
required: false
runs:
using: "composite"
steps:
- name: Install Claude Code
shell: bash
run: npm install -g @anthropic-ai/claude-code
- name: Install GitHub MCP Server
shell: bash
env:
INPUT_GITHUB_TOKEN: ${{ inputs.github_token }}
run: |
claude mcp add-json github '{
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-ff3036d"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "'"${INPUT_GITHUB_TOKEN}"'"
}
}'
- name: Extract GitHub Context and Create Prompt
shell: bash
id: prepare_context
env:
# Pass user-controlled inputs via environment variables to prevent script injection (GHSL-2025-093)
EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
EVENT_ISSUE_TITLE: ${{ github.event.issue.title }}
EVENT_ISSUE_BODY: ${{ github.event.issue.body }}
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
EVENT_PR_TITLE: ${{ github.event.pull_request.title }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_REVIEW_BODY: ${{ github.event.review.body }}
EVENT_ASSIGNEE_LOGIN: ${{ github.event.assignee.login }}
INPUT_TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
INPUT_ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
INPUT_DIRECT_PROMPT: ${{ inputs.direct_prompt }}
EVENT_ACTION: ${{ github.event.action }}
EVENT_NAME: ${{ github.event_name }}
REPO_NAME: ${{ github.repository }}
run: |
echo "🔍 Extracting GitHub context from event: $EVENT_NAME"
# Function to check for trigger phrase
check_trigger() {
local text="$1"
local trigger="$INPUT_TRIGGER_PHRASE"
if [[ "$text" == *"$trigger"* ]]; then
return 0
fi
return 1
}
# Extract context based on event type
TRIGGER_FOUND="false"
USER_REQUEST=""
CONTEXT_INFO=""
case "$EVENT_NAME" in
"issue_comment")
COMMENT_BODY="$EVENT_COMMENT_BODY"
ISSUE_TITLE="$EVENT_ISSUE_TITLE"
ISSUE_NUMBER="$EVENT_ISSUE_NUMBER"
if check_trigger "$COMMENT_BODY"; then
TRIGGER_FOUND="true"
USER_REQUEST="$COMMENT_BODY"
CONTEXT_INFO="Issue Comment on #$ISSUE_NUMBER: $ISSUE_TITLE"
fi
;;
"pull_request_review_comment")
COMMENT_BODY="$EVENT_COMMENT_BODY"
PR_TITLE="$EVENT_PR_TITLE"
PR_NUMBER="$EVENT_PR_NUMBER"
if check_trigger "$COMMENT_BODY"; then
TRIGGER_FOUND="true"
USER_REQUEST="$COMMENT_BODY"
CONTEXT_INFO="PR Comment on #$PR_NUMBER: $PR_TITLE"
fi
;;
"pull_request_review")
REVIEW_BODY="$EVENT_REVIEW_BODY"
PR_TITLE="$EVENT_PR_TITLE"
PR_NUMBER="$EVENT_PR_NUMBER"
if check_trigger "$REVIEW_BODY"; then
TRIGGER_FOUND="true"
USER_REQUEST="$REVIEW_BODY"
CONTEXT_INFO="PR Review on #$PR_NUMBER: $PR_TITLE"
fi
;;
"issues")
ISSUE_BODY="$EVENT_ISSUE_BODY"
ISSUE_TITLE="$EVENT_ISSUE_TITLE"
ISSUE_NUMBER="$EVENT_ISSUE_NUMBER"
if check_trigger "$ISSUE_TITLE" || check_trigger "$ISSUE_BODY"; then
TRIGGER_FOUND="true"
USER_REQUEST="$ISSUE_BODY"
CONTEXT_INFO="Issue #$ISSUE_NUMBER: $ISSUE_TITLE"
elif [[ "$EVENT_ACTION" == "assigned" && -n "$INPUT_ASSIGNEE_TRIGGER" ]]; then
ASSIGNEE="$EVENT_ASSIGNEE_LOGIN"
if [[ "$ASSIGNEE" == "$INPUT_ASSIGNEE_TRIGGER" ]]; then
TRIGGER_FOUND="true"
USER_REQUEST="$ISSUE_BODY"
CONTEXT_INFO="Issue #$ISSUE_NUMBER assigned to $ASSIGNEE: $ISSUE_TITLE"
fi
fi
;;
esac
# Check for direct prompt override
if [[ -n "$INPUT_DIRECT_PROMPT" ]]; then
TRIGGER_FOUND="true"
USER_REQUEST="$INPUT_DIRECT_PROMPT"
CONTEXT_INFO="Automated GitHub workflow"
fi
if [[ "$TRIGGER_FOUND" != "true" ]]; then
echo "❌ No trigger phrase found or direct prompt provided. Exiting gracefully."
echo "SKIP_EXECUTION=true" >> $GITHUB_ENV
exit 0
fi
echo "✅ Trigger found! Context: $CONTEXT_INFO"
# Create comprehensive prompt
mkdir -p /tmp/claude-action
cat > /tmp/claude-action/github-context-prompt.txt << EOF
You are Claude Code, an AI assistant helping with GitHub workflows and code.
Repository: $REPO_NAME
Context: $CONTEXT_INFO
Event: $EVENT_NAME
User Request:
$USER_REQUEST
Please analyze the request and provide helpful assistance.
CRITICAL INSTRUCTION: If your task involves modifying codebase files to add a feature or fix a bug:
STEP 0 — SETUP GIT IDENTITY:
git config user.name "MervinPraison"
git config user.email "[email protected]"
STEP 1 — READ GUIDELINES:
MANDATORY: Before writing any code, you MUST carefully read "src/praisonai-agents/AGENTS.md" to understand this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy.
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
Before implementing anything, answer these questions:
- CORE vs WRAPPER vs TOOLS vs PLUGINS ROUTING:
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
3. Tools → MervinPraison/PraisonAI-Tools: agent-callable integrations at runtime (Composio, Slack, APIs). Do NOT add to praisonaiagents/.
4. Plugins → MervinPraison/PraisonAI-Plugins: lifecycle extensions (tracing, logging, hooks, guardrails). Not agent tools.
5. Sandbox backends → PraisonAI-Plugins via `praisonai.sandbox` entry point (core protocol only).
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory).
- Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead. PRs with >100 lines added to agent.py require manual review (merge gate will not auto-merge).
- Is there a name collision with existing exports in __init__.py?
If ANY of these conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
STEP 3 — IMPLEMENT:
- Create a new git branch appropriately named (e.g. \`feature/issue-<number>\`)
- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper
- Keep changes small and backward-compatible
STEP 4 — TEST:
- Run: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
- Ensure no regressions
STEP 5 — CREATE PR:
- Commit with descriptive message, push the branch
- Use the GitHub CLI (\`gh pr create --title "..." --body "Fixes #<number>"\`) to open a Pull Request
CRITICAL: You MUST create the PR automatically using \`gh pr create\`. Do NOT just provide a link.
Respond naturally and helpfully to the user's request using the available tools.
EOF
echo "PROMPT_FILE=/tmp/claude-action/github-context-prompt.txt" >> $GITHUB_ENV
echo "SKIP_EXECUTION=false" >> $GITHUB_ENV
- name: Run Claude Code
if: env.SKIP_EXECUTION != 'true'
shell: bash
env:
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
GITHUB_TOKEN: ${{ inputs.github_token }}
INPUT_MODEL: ${{ inputs.model }}
INPUT_MAX_TURNS: ${{ inputs.max_turns }}
INPUT_ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
INPUT_DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
CLAUDE_CODE_SUBAGENT_MODEL: inherit
ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4-6
run: |
echo "🚀 Running Claude Code with GitHub context..."
# Build command arguments
CMD_ARGS=("-p" "--verbose" "--output-format" "stream-json")
if [[ -n "$INPUT_MODEL" ]]; then
CMD_ARGS+=("--model" "$INPUT_MODEL")
export ANTHROPIC_MODEL="$INPUT_MODEL"
fi
# Add max turns if specified
if [[ -n "$INPUT_MAX_TURNS" ]]; then
CMD_ARGS+=("--max-turns" "$INPUT_MAX_TURNS")
fi
# Add allowed tools (include GitHub tools by default)
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_comment,Read,Write,Edit,Bash"
if [[ -n "$INPUT_ALLOWED_TOOLS" ]]; then
TOOLS="$TOOLS,$INPUT_ALLOWED_TOOLS"
fi
# Remove any explicitly disallowed tools from the effective allow-list so
# the read-only contract is enforced deterministically (no reliance on CLI
# precedence between --allowedTools and --disallowedTools).
if [[ -n "$INPUT_DISALLOWED_TOOLS" ]]; then
DISALLOWED_CSV=$(echo "$INPUT_DISALLOWED_TOOLS" | tr ',\n' ' ' | xargs)
FILTERED=""
IFS=',' read -ra TOOL_ARR <<< "$TOOLS"
for tool in "${TOOL_ARR[@]}"; do
tool=$(echo "$tool" | xargs)
[[ -z "$tool" ]] && continue
skip="false"
for bad in $DISALLOWED_CSV; do
if [[ "$tool" == "$bad" ]]; then
skip="true"
break
fi
done
if [[ "$skip" == "false" ]]; then
FILTERED="${FILTERED:+$FILTERED,}$tool"
fi
done
TOOLS="$FILTERED"
fi
CMD_ARGS+=("--allowedTools" "$TOOLS")
# Add disallowed tools
if [[ -n "$INPUT_DISALLOWED_TOOLS" ]]; then
DISALLOWED_FLAG=$(echo "$INPUT_DISALLOWED_TOOLS" | tr '\n' ',' | sed 's/,*$//')
CMD_ARGS+=("--disallowedTools" "$DISALLOWED_FLAG")
fi
echo "📝 Executing Claude Code with prompt from file..."
# Execute Claude Code with timeout, using stdin for the prompt
TIMEOUT_SECONDS=$(($INPUT_TIMEOUT_MINUTES * 60))
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "$PROMPT_FILE"
echo "✅ Claude Code execution completed"
@@ -1,92 +0,0 @@
name: "Claude Issue Triage Action"
description: "Automatically triage GitHub issues using Claude Code"
inputs:
timeout_minutes:
description: "Timeout in minutes for execution"
required: false
default: "5"
claude_code_oauth_token:
description: "Claude Code OAuth token for Pro subscription access"
required: true
github_token:
description: "GitHub token with repo and issues permissions"
required: true
runs:
using: "composite"
steps:
- name: Checkout repository code
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Create prompt
id: generate_prompt
shell: bash
run: |
mkdir -p /tmp/claude-prompts
cat > /tmp/claude-prompts/claude-issue-triage-prompt.txt << 'EOF'
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
Issue Information:
- REPO: ${{ github.repository }}
- ISSUE_NUMBER: ${{ github.event.issue.number }}
TASK OVERVIEW:
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
2. Next, use the GitHub tools to get context about the issue:
- You have access to these tools:
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
- Start by using mcp__github__get_issue to get the issue details
3. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
- Technical areas mentioned
- Severity or priority indicators
- User impact
- Components affected
4. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
5. Apply the selected labels:
- Use mcp__github__update_issue to apply your selected labels
- DO NOT post any comments explaining your decision
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
IMPORTANT GUIDELINES:
- Be thorough in your analysis
- Only select labels from the provided list above
- DO NOT post any comments to the issue
- Your ONLY action should be to apply labels using mcp__github__update_issue
- It's okay to not add any labels if none are clearly applicable
EOF
echo "PROMPT<<DELIMITER" >> $GITHUB_OUTPUT
cat /tmp/claude-prompts/claude-issue-triage-prompt.txt >> $GITHUB_OUTPUT
echo "DELIMITER" >> $GITHUB_OUTPUT
- name: Run Claude Code
uses: ./.github/actions/claude-code-action
with:
direct_prompt: ${{ steps.generate_prompt.outputs.PROMPT }}
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
timeout_minutes: ${{ inputs.timeout_minutes }}
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
github_token: ${{ inputs.github_token }}
-90
View File
@@ -1,90 +0,0 @@
name: 'Gemini CLI Action'
description: 'Uses the Gemini CLI to automate software development tasks.'
author: 'Gemini'
inputs:
prompt:
description: 'A specific prompt to guide Gemini.'
required: false
default: 'You are a helpful assistant.'
GEMINI_API_KEY:
description: 'Your Gemini API key.'
required: true
OTLP_GCP_WIF_PROVIDER:
description: 'The workload identity provider for GCP authentication.'
required: false
OTLP_GOOGLE_CLOUD_PROJECT:
description: 'The Google Cloud project for telemetry.'
required: false
settings_json:
description: |
A JSON string to configure the Gemini CLI. This will be written to .gemini/settings.json.
To enable observability with Google Cloud, include:
{"telemetry": {"enabled": true, "target": "gcp"}, "sandbox": false}
required: false
model:
description: |
The Gemini model to use. Options: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite.
Use flash-lite for highest free-tier quota (1,000 RPD).
required: false
default: 'gemini-3.1-flash-lite-preview'
version:
description: |
The version of @google/gemini-cli to execute.
Can be a specific version from npm (e.g., '0.1.0', 'latest'), a branch name (e.g., 'main'), or a commit hash.
required: false
default: 'latest'
runs:
using: "composite"
steps:
- name: Configure Gemini CLI
if: inputs.settings_json != ''
run: |
mkdir -p .gemini
echo "$SETTINGS_JSON" > .gemini/settings.json
shell: bash
env:
SETTINGS_JSON: ${{ inputs.settings_json }}
- name: Authenticate to Google Cloud for Telemetry
if: inputs.OTLP_GCP_WIF_PROVIDER != ''
id: auth
uses: 'google-github-actions/auth@v2'
with:
project_id: ${{ inputs.OTLP_GOOGLE_CLOUD_PROJECT }}
workload_identity_provider: ${{ inputs.OTLP_GCP_WIF_PROVIDER }}
- name: Install Gemini CLI
id: install
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if [[ "$INPUT_VERSION" == "latest" || "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.-]+)?(\+[a-zA-Z0-9\.-]+)?$ ]]; then
echo "Installing Gemini CLI from npm: @google/gemini-cli@$INPUT_VERSION"
npm install -g @google/gemini-cli@$INPUT_VERSION
else
echo "Installing Gemini CLI from GitHub: github:google-gemini/gemini-cli#$INPUT_VERSION"
npm install -g github:google-gemini/gemini-cli#$INPUT_VERSION
fi
echo "Verifying installation:"
if command -v gemini >/dev/null 2>&1; then
gemini --version || echo "Gemini CLI installed successfully (version command not available)"
else
echo "Error: Gemini CLI not found in PATH"
exit 1
fi
shell: bash
- name: Run Gemini CLI
run: |
gemini --yolo --model "$GEMINI_MODEL" --prompt "$PROMPT"
shell: bash
env:
GEMINI_API_KEY: ${{ inputs.GEMINI_API_KEY }}
GEMINI_MODEL: ${{ inputs.model }}
PROMPT: ${{ inputs.prompt }}
branding:
icon: 'terminal'
color: 'blue'
@@ -1,96 +0,0 @@
name: Install PraisonAI monorepo packages
description: Install praisonai-agents, praisonai-code, praisonai-bot, praisonai-train, praisonai-browser, praisonai-mcp, praisonai-sandbox, praisonai-deploy, and praisonai wrapper from local paths (mirrors PyPI nine-package install order).
inputs:
repo-root:
description: Path prefix to monorepo root (e.g. PraisonAI/ for cross-repo checkout)
required: false
default: ""
use-uv:
description: Use uv pip instead of pip
required: false
default: "false"
agents-extras:
description: Optional extras for praisonaiagents (e.g. .[all] or [all])
required: false
default: ""
wrapper-extras:
description: Optional extras for praisonai wrapper (e.g. .[chat,code] or [chat,code])
required: false
default: ""
runs:
using: composite
steps:
- name: Install monorepo packages
shell: bash
run: |
set -euo pipefail
WORKSPACE="${GITHUB_WORKSPACE:-$(pwd)}"
ROOT="${{ inputs.repo-root }}"
if [ -n "$ROOT" ]; then
BASE="${WORKSPACE%/}/${ROOT%/}/"
else
BASE="${WORKSPACE%/}/"
fi
AGENTS_PKG="${BASE}src/praisonai-agents"
CODE_PKG="${BASE}src/praisonai-code"
BOT_PKG="${BASE}src/praisonai-bot"
TRAIN_PKG="${BASE}src/praisonai-train"
BROWSER_PKG="${BASE}src/praisonai-browser"
MCP_PKG="${BASE}src/praisonai-mcp"
SANDBOX_PKG="${BASE}src/praisonai-sandbox"
DEPLOY_PKG="${BASE}src/praisonai-deploy"
WRAPPER_PKG="${BASE}src/praisonai"
normalise_extras() {
local value="${1#.}"
if [ -n "$value" ]; then
printf '.%s' "$value"
else
printf '.'
fi
}
AGENTS_SUFFIX="$(normalise_extras "${{ inputs.agents-extras }}")"
WRAPPER_SUFFIX="$(normalise_extras "${{ inputs.wrapper-extras }}")"
if [ "${{ inputs.use-uv }}" = "true" ]; then
cd "$AGENTS_PKG"
uv pip install --system -e "${AGENTS_SUFFIX}"
cd "$CODE_PKG"
uv pip install --system -e .
cd "$BOT_PKG"
uv pip install --system -e .
cd "$TRAIN_PKG"
uv pip install --system -e .
cd "$BROWSER_PKG"
uv pip install --system -e .
cd "$MCP_PKG"
uv pip install --system -e .
cd "$SANDBOX_PKG"
uv pip install --system -e .
cd "$DEPLOY_PKG"
uv pip install --system -e .
cd "$WRAPPER_PKG"
uv pip install --system "${WRAPPER_SUFFIX}"
else
cd "$AGENTS_PKG"
pip install -e "${AGENTS_SUFFIX}"
cd "$CODE_PKG"
pip install -e .
cd "$BOT_PKG"
pip install -e .
cd "$TRAIN_PKG"
pip install -e .
cd "$BROWSER_PKG"
pip install -e .
cd "$MCP_PKG"
pip install -e .
cd "$SANDBOX_PKG"
pip install -e .
cd "$DEPLOY_PKG"
pip install -e .
cd "$WRAPPER_PKG"
pip install -e "${WRAPPER_SUFFIX}"
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

-78
View File
@@ -1,78 +0,0 @@
framework: "praisonai"
process: workflow
topic: "Autonomous Issue Resolution"
planning: false
roles:
issue_analyst:
role: "GitHub Issue Analyst"
goal: "Read the GitHub issue, understand the required changes, and map the problem to the local codebase"
backstory: "You are an expert software analyst who reads bug reports and feature requests. You excel at taking a problem description and finding exactly which files in the codebase need to be modified. Your primary job is discovery and planning."
tools:
- "execute_command"
software_engineer:
role: "Autonomous Software Engineer"
goal: "Implement required code changes on a branch, run tests, commit, and push"
backstory: "You are a senior software engineer. You always branch first, implement changes using shell commands, verify with git diff, run tests, then commit and push. You never create a Pull Request unless the issue body explicitly requests one."
tools:
- "execute_command"
- "github_create_branch"
- "github_commit_and_push"
- "github_create_pull_request"
steps:
- name: Analyze Issue
agent: issue_analyst
action: >
Thoroughly analyze the issue and the repository. Follow these steps in order.
First, set up git identity: execute_command 'git config user.name PraisonAI' then
execute_command 'git config user.email [email protected]'. Do NOT pass cwd.
Second, read the issue: execute_command 'cat .github/triage-context.md'. Do NOT pass cwd.
Third, read architecture guidelines: execute_command 'cat src/praisonai-agents/AGENTS.md'.
Fourth, decide routing before planning:
- Tools → PraisonAI-Tools: agent-callable integrations at runtime (Composio, Slack, APIs).
- Plugins → PraisonAI-Plugins: lifecycle extensions (tracing, logging, hooks, guardrails).
- Sandbox backends → PraisonAI-Plugins via `praisonai.sandbox` entry point.
Core SDK/wrapper only for protocols and CLI — external repos for Tools/Plugins.
Fifth, decide if this issue requires code changes:
- If YES: identify the exact files and lines that must change. Output a concrete file-level plan.
- If NO (question, discussion, duplicate, invalid): say so clearly. No code changes needed.
Output: routing decision (Core / Wrapper / Tools-external / Plugins-external / No-change), validation result, and exact files/lines to change.
expected_output: A decision on whether code changes are needed, and if so a concrete file-level plan.
- name: Implement Fix
agent: software_engineer
action: >
You are working inside the repository at /Users/praison/praisonai-package.
Do NOT ask for confirmation. Do NOT use placeholder paths. Act immediately.
Read the analyst's output first. If the analyst said NO code changes are needed, stop here and
output a brief explanation — do NOT create a branch or PR.
If code changes ARE needed, follow these steps:
Step 1 — Create branch: github_create_branch with branch_name=praisonai/issue-{{ISSUE_NUMBER}}.
Step 2 — Implement changes using execute_command (no cwd argument).
To insert a line: python3 -c "lines=open('FILE').readlines(); lines.insert(N,'LINE\n'); open('FILE','w').writelines(lines)"
To replace text: python3 -c "t=open('FILE').read(); open('FILE','w').write(t.replace('OLD','NEW'))"
After each edit verify: execute_command 'git diff'
Step 3 — Run tests: execute_command 'python3 -m pytest src/praisonai-agents/tests/ -x -q --timeout=30 -p no:warnings'
Step 4 — Commit and push: github_commit_and_push with a descriptive commit_message.
Step 5 — Check if the issue body EXPLICITLY contains the phrase "create pull request" or "create PR"
(case-insensitive). ONLY in that case create a PR using github_create_pull_request with
title='fix: resolve issue #{{ISSUE_NUMBER}}',
body='Fixes #{{ISSUE_NUMBER}}.\n\nAutonomously resolved by PraisonAI.',
head_branch=praisonai/issue-{{ISSUE_NUMBER}}, base_branch=main.
Otherwise do NOT create a PR — just confirm the branch was pushed successfully.
expected_output: Either "No code changes needed", or "Branch pushed, no PR requested", or "Branch pushed and PR created at URL".
-116
View File
@@ -1,116 +0,0 @@
framework: "praisonai"
topic: "Pull Request Code Review Analysis"
roles:
security_reviewer:
role: "Security Code Reviewer"
goal: "Identify security vulnerabilities, authentication issues, input validation problems, and unsafe code practices in the PR changes"
backstory: "You are a cybersecurity expert specializing in code analysis. You have extensive experience in identifying common security flaws like injection attacks, authentication bypasses, exposed secrets, and unsafe data handling patterns."
tools:
- "execute_command"
performance_reviewer:
role: "Performance Code Reviewer"
goal: "Analyze code changes for performance implications, identify bottlenecks, inefficient algorithms, and resource usage issues"
backstory: "You are a performance optimization specialist with deep knowledge of algorithm efficiency, memory management, and system performance. You excel at spotting code that could cause performance degradation."
tools:
- "execute_command"
maintainability_reviewer:
role: "Code Quality & Maintainability Reviewer"
goal: "Evaluate code structure, readability, documentation, naming conventions, and adherence to best practices"
backstory: "You are a software engineering expert focused on code quality and maintainability. You ensure code follows established patterns, is well-documented, and will be easy for future developers to understand and modify."
tools:
- "execute_command"
lead_reviewer:
role: "Lead Technical Reviewer"
goal: "Synthesize all review feedback, make final recommendations, and post comprehensive review comments to the GitHub PR"
backstory: "You are a senior technical lead responsible for final review decisions. You coordinate input from security, performance, and maintainability reviewers to provide balanced, actionable feedback to the development team."
tools:
- "execute_command"
steps:
- name: security_analysis
agent: security_reviewer
action: |
Analyze the PR diff for security issues:
1. Extract PR details: `echo "$PR_DATA"`
2. Get changed files: `echo "$CHANGED_FILES"`
3. Review full diff: `gh pr diff ${PR_NUMBER}`
4. Look for:
- Hardcoded secrets, API keys, passwords
- SQL injection vulnerabilities
- XSS vulnerabilities
- Authentication/authorization bypasses
- Unsafe file operations
- Command injection risks
- Missing input validation
- Exposed sensitive data
5. Document findings with file paths and line numbers
expected_output: "Detailed security analysis report with specific vulnerabilities found, severity levels, and remediation suggestions"
- name: performance_analysis
agent: performance_reviewer
action: |
Analyze the PR diff for performance issues:
1. Review algorithm complexity changes
2. Check for inefficient database queries
3. Identify memory leaks or excessive allocations
4. Look for hot-path regressions
5. Check for blocking I/O operations
6. Analyze module-level imports that could slow startup
7. Review caching implementations
8. Check for recursive operations without termination
9. Document performance concerns with impact estimates
expected_output: "Performance analysis report highlighting bottlenecks, efficiency concerns, and optimization recommendations"
- name: maintainability_analysis
agent: maintainability_reviewer
action: |
Analyze the PR diff for code quality and maintainability:
1. Check naming conventions consistency
2. Evaluate code structure and organization
3. Review documentation completeness
4. Check for DRY violations
5. Analyze error handling patterns
6. Review test coverage for changes
7. Check adherence to repository coding standards
8. Evaluate API design consistency
9. Look for technical debt introduction
expected_output: "Code quality analysis with specific recommendations for improving maintainability and readability"
- name: final_review
agent: lead_reviewer
action: |
Synthesize all reviews and post final comprehensive feedback:
1. Collect input from security, performance, and maintainability reviews
2. Prioritize findings by severity (Critical, High, Medium, Low)
3. Create structured review comment using this format:
## 📋 Review Summary
[2-3 sentence overview and assessment]
## 🔍 General Feedback
[Overall patterns and observations]
## 🎯 Specific Feedback
### 🔴 Critical (if any)
[Security vulnerabilities, breaking changes, major bugs]
### 🟡 High (if any)
[Performance issues, design flaws, significant bugs]
### 🟢 Medium (if any)
[Code quality improvements, minor optimizations]
### 🔵 Low (if any)
[Documentation, naming suggestions, minor refactoring]
## ✅ Highlights (if any)
[Positive aspects worth mentioning]
4. Post comprehensive review: `echo "[REVIEW_CONTENT]" | gh pr comment ${PR_NUMBER} -F -`
5. If critical issues found, suggest not merging until resolved
expected_output: "Final review comment posted to GitHub PR with comprehensive analysis from all reviewers"
dependencies: [security_analysis, performance_analysis, maintainability_analysis]
@@ -1,34 +0,0 @@
#!/usr/bin/env node
const chain = require('./bot-pr-review-chain.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
assert('chainKickPosted requires both markers', chain.chainKickPosted([
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
{ user: { login: 'MervinPraison' }, body: '/review' },
]));
assert('chainKickPosted false with only coderabbit', !chain.chainKickPosted([
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
]));
assert('coderabbitKickPosted detects coderabbit', chain.coderabbitKickPosted([
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
]));
assert('qodoKickPosted detects qodo', chain.qodoKickPosted([
{ user: { login: 'github-actions[bot]' }, body: '/review' },
]));
assert('ignores coderabbit bot comment', !chain.coderabbitKickPosted([
{ user: { login: 'coderabbitai[bot]' }, body: '@coderabbitai review' },
]));
assert('bot PR author match', chain.isBotOpenedPr({ user: { login: 'praisonai-triage-agent[bot]' } }));
assert('generic bot type match', chain.isBotOpenedPr({ user: { login: 'my-bot', type: 'Bot' } }));
assert('human author excluded', !chain.isBotOpenedPr({ user: { login: 'MervinPraison', type: 'User' } }));
process.exit(failed ? 1 : 0);
-153
View File
@@ -1,153 +0,0 @@
/**
* Idempotent CodeRabbit/Qodo kick for bot-opened PRs.
* @see .github/workflows/auto-pr-comment.yml, claude.yml, bot-pr-recovery.yml
*/
const KICK_AUTHORS = new Set(['MervinPraison', 'github-actions[bot]']);
const BOT_PR_AUTHORS = new Set(['praisonai-triage-agent[bot]', 'github-actions[bot]']);
async function listAllComments(github, owner, repo, issueNumber) {
if (typeof github.paginate === 'function') {
return github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
}
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
return data;
}
function kickAuthored(comment, marker) {
return (
KICK_AUTHORS.has(comment.user?.login) &&
(comment.body || '').includes(marker)
);
}
function coderabbitKickPosted(comments) {
return comments.some((c) => kickAuthored(c, '@coderabbitai review'));
}
function qodoKickPosted(comments) {
return comments.some((c) => kickAuthored(c, '/review'));
}
function chainKickPosted(comments) {
return coderabbitKickPosted(comments) && qodoKickPosted(comments);
}
function isBotOpenedPr(pr) {
if (BOT_PR_AUTHORS.has(pr.user?.login)) return true;
return pr.user?.type === 'Bot';
}
async function kickReviewChain(github, owner, repo, prNumber, core, preFetchedComments = null) {
const comments = preFetchedComments || await listAllComments(github, owner, repo, prNumber);
const needCoderabbit = !coderabbitKickPosted(comments);
const needQodo = !qodoKickPosted(comments);
if (!needCoderabbit && !needQodo) {
core?.info?.(`Review chain already kicked on PR #${prNumber}`);
return { kicked: false, reason: 'already_kicked' };
}
if (needCoderabbit) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: '@coderabbitai review',
});
}
if (needQodo) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: '/review',
});
}
core?.info?.(`Kicked review chain for PR #${prNumber} (coderabbit=${needCoderabbit}, qodo=${needQodo})`);
return { kicked: true, coderabbit: needCoderabbit, qodo: needQodo };
}
async function findOpenPrForIssue(github, owner, repo, issueNumber) {
const prefix = `claude/issue-${issueNumber}-`;
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
sort: 'created',
direction: 'desc',
per_page: 30,
});
return (
prs.find(
(p) => (p.head?.ref || '').startsWith(prefix) && isBotOpenedPr(p)
) || null
);
}
async function kickReviewChainForIssue(github, owner, repo, issueNumber, core) {
const pr = await findOpenPrForIssue(github, owner, repo, issueNumber);
if (!pr) {
core?.info?.(`No open PR for issue #${issueNumber}, skipping review kick`);
return { kicked: false, reason: 'no_pr' };
}
const result = await kickReviewChain(github, owner, repo, pr.number, core);
return { ...result, prNumber: pr.number };
}
async function recoverStalledBotPrs(github, owner, repo, options, core) {
const { prNumber = null, minAgeMs = 10 * 60 * 1000, maxRecover = 10 } = options || {};
let prs;
if (prNumber) {
const { data } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
prs = [data];
} else if (typeof github.paginate === 'function') {
prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
} else {
const { data } = await github.rest.pulls.list({ owner, repo, state: 'open', per_page: 100 });
prs = data;
}
const cutoff = Date.now() - minAgeMs;
let recovered = 0;
for (const pr of prs) {
if (recovered >= maxRecover) break;
if (!isBotOpenedPr(pr)) continue;
if (!prNumber && new Date(pr.created_at).getTime() > cutoff) continue;
const comments = await listAllComments(github, owner, repo, pr.number);
if (chainKickPosted(comments)) continue;
await kickReviewChain(github, owner, repo, pr.number, core, comments);
recovered += 1;
}
core?.info?.(`Recovery complete (${recovered} PR(s) kicked)`);
return recovered;
}
module.exports = {
KICK_AUTHORS,
BOT_PR_AUTHORS,
listAllComments,
chainKickPosted,
coderabbitKickPosted,
qodoKickPosted,
isBotOpenedPr,
kickReviewChain,
findOpenPrForIssue,
kickReviewChainForIssue,
recoverStalledBotPrs,
};
@@ -1,93 +0,0 @@
#!/usr/bin/env node
/**
* Run: node .github/scripts/ci-failure-claude-selftest.js
*/
const ciFix = require('./ci-failure-claude.js');
const mergeGate = require('./merge-gate.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed += 1;
} else {
console.log('ok:', name);
}
}
const LOG_2767 = `
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8096670Z FAILED (0.0100s) tests/unit/cli/test_typer_cli.py::TestConfigCommand::test_config_list - ValueError: I/O operation on closed file.
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8097323Z ============= 1 failed, 1401 passed, 1 skipped in 88.86s (0:01:28) =============
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8693341Z ##[error]Process completed with exit code 1.
`;
const parsed = ciFix.parsePytestFailures(LOG_2767);
assert('parses pytest failure from log', parsed.length === 1);
assert(
'extracts test path',
parsed[0]?.testId === 'tests/unit/cli/test_typer_cli.py::TestConfigCommand::test_config_list'
);
assert(
'extracts error message',
parsed[0]?.error.includes('ValueError: I/O operation on closed file')
);
const comment = ciFix.buildCiFixComment({
headSha: 'dfb81bb0ae7f36bf8d66d8f45053464c18c005a3',
failedChecks: [
{
name: 'test-core (cli)',
workflow: 'Core Tests',
html_url: 'https://github.com/MervinPraison/PraisonAI/actions/runs/28885180173/job/85683605433',
},
],
failureSummaries: [{ jobName: 'test-core (cli)', failures: parsed }],
});
assert('comment includes @claude', comment.includes('@claude'));
assert('comment includes short sha', comment.includes('dfb81bb0'));
assert('comment includes test path', comment.includes('test_typer_cli.py::TestConfigCommand::test_config_list'));
assert('comment includes job url', comment.includes('85683605433'));
assert('comment requires critical review verdict', comment.includes('Critical review first'));
assert('comment forbids weakening tests', comment.includes('do not weaken, skip, or delete tests'));
assert('comment mentions SDK guardrails', comment.includes('SDK guardrails'));
assert('comment asks for legitimacy verdict', comment.includes('legitimate fix'));
const headSha = 'dfb81bb0ae7f36bf8d66d8f45053464c18c005a3';
const commentsWithFix = [
{
user: { login: 'MervinPraison' },
body: '@claude CI failed on HEAD `dfb81bb0`. Please fix...',
created_at: '2026-07-07T18:00:00Z',
},
];
assert('sha dedup detects existing comment', ciFix.hasCiFixCommentForSha(commentsWithFix, headSha));
const skipNoFinal = ciFix.shouldSkipCiFix({
comments: [],
headSha,
labels: [],
hasFinal: false,
claudeInProgress: false,
failedChecks: [{ name: 'test-core' }],
});
assert('skips when final not posted', skipNoFinal.skip && skipNoFinal.reason.includes('final'));
const skipPending = ciFix.shouldSkipCiFix({
comments: [],
headSha,
labels: [ciFix.CI_FIX_LABEL],
hasFinal: true,
claudeInProgress: false,
failedChecks: [{ name: 'test-core' }],
});
assert('skips when ci fix pending label', skipPending.skip && skipPending.reason.includes('pending'));
const failedRuns = mergeGate.listFailedChecksOnSha([
{ status: 'completed', conclusion: 'success', name: 'smoke' },
{ status: 'completed', conclusion: 'failure', name: 'test-core (cli)' },
{ status: 'in_progress', conclusion: null, name: 'pending-job' },
]);
assert('listFailedChecksOnSha finds failures only', failedRuns.length === 1);
assert('failed check name', failedRuns[0].name === 'test-core (cli)');
process.exit(failed ? 1 : 0);
-294
View File
@@ -1,294 +0,0 @@
/**
* Post @claude comments with CI failure details for internal PRs.
* @see .github/workflows/ci-failure-claude.yml
*/
const mergeGate = require('./merge-gate.js');
const CI_FIX_LABEL = 'claude-ci-fix-pending';
const COOLDOWN_MS = 12 * 60 * 60 * 1000;
const AUTO_ACTORS = mergeGate.AUTO_ACTORS;
const MAX_FAILURES = 15;
const MAX_JOBS_TO_FETCH = 5;
function shortSha(headSha) {
return (headSha || '').slice(0, 8);
}
function ciFixShaMarker(headSha) {
return `ci failed on head \`${shortSha(headSha)}`.toLowerCase();
}
function isCiFixComment(comment) {
const body = (comment.body || '').toLowerCase();
return body.includes('@claude') && body.includes('ci failed on head');
}
function hasCiFixCommentForSha(comments, headSha) {
const marker = shortSha(headSha).toLowerCase();
return comments.some((c) => {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
if (!isCiFixComment(c)) return false;
return (c.body || '').toLowerCase().includes(marker);
});
}
function hasRecentCiFixComment(comments, headSha) {
const cutoff = Date.now() - COOLDOWN_MS;
const marker = shortSha(headSha).toLowerCase();
return comments.some((c) => {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
if (!isCiFixComment(c)) return false;
const body = (c.body || '').toLowerCase();
if (body.includes(marker)) return true;
return new Date(c.created_at).getTime() > cutoff;
});
}
function shouldSkipCiFix({ comments, headSha, labels, hasFinal, claudeInProgress, failedChecks }) {
if (!failedChecks.length) return { skip: true, reason: 'no failed checks' };
if (labels.includes(CI_FIX_LABEL)) return { skip: true, reason: 'ci fix pending' };
if (!hasFinal) return { skip: true, reason: 'awaiting final claude review trigger' };
if (claudeInProgress) return { skip: true, reason: 'claude in progress' };
if (hasCiFixCommentForSha(comments, headSha)) {
return { skip: true, reason: 'already commented for this sha' };
}
if (hasRecentCiFixComment(comments, headSha)) {
return { skip: true, reason: 'recent ci fix comment cooldown' };
}
return { skip: false };
}
function parsePytestFailures(logText) {
if (!logText) return [];
const lines = logText.split('\n');
const failures = [];
const seen = new Set();
for (let i = lines.length - 1; i >= 0 && failures.length < MAX_FAILURES; i -= 1) {
const line = lines[i].trim();
if (!line) continue;
const pytestMatch = line.match(
/FAILED\s*(?:\([^)]+\))?\s+(tests\/[^\s]+(?:::[^\s-]+)*)\s*-\s*(.+)/i
);
if (pytestMatch) {
const testId = pytestMatch[1];
if (!seen.has(testId)) {
seen.add(testId);
failures.push({ testId, error: pytestMatch[2].trim() });
}
continue;
}
const errorMatch = line.match(/##\[error\](.+)/);
if (errorMatch && /tests\//.test(errorMatch[1])) {
const msg = errorMatch[1].trim();
if (!seen.has(msg)) {
seen.add(msg);
failures.push({ testId: msg, error: msg });
}
}
}
return failures;
}
async function fetchJobFailureSummary(github, owner, repo, jobId, jobName) {
try {
const response = await github.rest.actions.downloadJobLogsForWorkflowRun({
owner,
repo,
job_id: jobId,
});
let logText = typeof response.data === 'string' ? response.data : String(response.data || '');
if (logText.length > 100000) {
logText = logText.slice(-100000);
}
return { jobName, jobId, failures: parsePytestFailures(logText) };
} catch (err) {
return { jobName, jobId, failures: [], error: err.message };
}
}
function mapFailedChecks(runs) {
return runs.map((run) => ({
name: run.name,
id: run.id,
html_url: run.html_url || run.details_url,
workflow: run.app?.slug || run.check_suite?.app?.slug || run.name,
}));
}
function buildCiFixComment({ headSha, failedChecks, failureSummaries }) {
const parts = [
`@claude CI failed on HEAD \`${shortSha(headSha)}\`. Please fix the failures below and push to this branch.`,
'',
'## Failed checks',
];
for (const check of failedChecks) {
parts.push(`- **${check.workflow || check.name}** / \`${check.name}\` — ${check.html_url}`);
}
parts.push('', '## Failures (extracted)');
let idx = 0;
for (const summary of failureSummaries) {
for (const failure of summary.failures) {
idx += 1;
if (idx > MAX_FAILURES) break;
parts.push(`${idx}. \`${failure.testId}\` — \`${failure.error}\``);
parts.push(` - Job: \`${summary.jobName}\``);
}
if (idx >= MAX_FAILURES) break;
}
if (idx === 0) {
parts.push('_(Could not extract pytest details — see job logs above.)_');
}
const exampleTest = failureSummaries.find((s) => s.failures.length)?.failures[0]?.testId;
parts.push(
'',
'## Critical review first',
'Before changing code or tests, decide **which side is wrong**:',
'1. **Legitimate feature change** — the PR intent is correct but implementation or tests need updating. Preserve SDK guarantees; update tests only when behaviour intentionally changed and document why.',
'2. **Regression / bug in this PR** — the failure exposes a real breakage introduced here. Fix the implementation; **do not weaken, skip, or delete tests** just to go green.',
'3. **Pre-existing flake or unrelated failure** — say so explicitly; prefer fixing the root cause over masking it.',
'',
'**SDK guardrails (AGENTS.md):**',
'- Agent-callable tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins; sandbox backends → PraisonAI-Plugins (`praisonai.sandbox`) — not praisonaiagents/.',
'- Do not disturb core SDK contracts to accommodate a wrapper/feature change.',
'- Tests must continue to guard backward compatibility and hot-path behaviour — passing CI by lowering test standards is not acceptable.',
'- If the feature does not genuinely add SDK value, recommend reverting or narrowing scope instead of patching around failures.',
'',
'## What to do',
'1. State your verdict: **legitimate fix**, **regression fix**, or **needs human review** — and why (1–3 sentences).',
'2. Fix root cause with **minimal changes**; never bloat the Agent class with extra params.',
exampleTest
? `3. Run failing tests locally, e.g. \`pytest ${exampleTest} -q\`, plus any related SDK tests touched by the PR.`
: '3. Run failing tests locally with targeted pytest, plus any related SDK tests touched by the PR.',
'4. Push to this branch and comment: files changed, review verdict, and why tests still protect SDK behaviour.',
);
return parts.join('\n');
}
async function maybeClearCiFixLabel(github, owner, repo, prNumber, labels, headSha, core) {
if (!labels.includes(CI_FIX_LABEL)) return false;
const ciGreen = await mergeGate.allChecksGreenOnSha(github, owner, repo, headSha, core);
if (!ciGreen) return false;
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: CI_FIX_LABEL,
});
core?.info?.(`Removed ${CI_FIX_LABEL} from PR #${prNumber}`);
} catch (err) {
if (err.status !== 404) throw err;
}
return true;
}
async function maybeTriggerCiFixClaude(github, owner, repo, prNumber, core, opts = {}) {
const baseRepo = `${owner}/${repo}`;
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const pr = ctx.pr;
if (pr.state !== 'open') return { skipped: true, reason: 'not open' };
if (pr.draft) return { skipped: true, reason: 'draft' };
const headRepo = pr.head.repo?.full_name;
if (headRepo && headRepo !== baseRepo && !pr.maintainer_can_modify) {
return { skipped: true, reason: 'fork without maintainer edits' };
}
const headSha = pr.head.sha;
const labels = ctx.labels;
if (await maybeClearCiFixLabel(github, owner, repo, prNumber, labels, headSha, core)) {
return { skipped: true, reason: 'ci green, label cleared' };
}
const ciGreen = await mergeGate.allChecksGreenOnSha(github, owner, repo, headSha, core);
if (ciGreen) return { skipped: true, reason: 'ci green' };
const runs = await mergeGate.listChecksOnSha(github, owner, repo, headSha);
const failedRuns = mergeGate.listFailedChecksOnSha(runs);
if (failedRuns.length === 0) {
return { skipped: true, reason: 'no failed checks yet (may be pending)' };
}
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(ctx.comments);
const claudeInProgress = await mergeGate.hasInProgressClaudeAssistant(
github, owner, repo, prNumber
);
const skip = shouldSkipCiFix({
comments: ctx.comments,
headSha,
labels,
hasFinal,
claudeInProgress,
failedChecks: failedRuns,
});
if (skip.skip) return { skipped: true, reason: skip.reason };
const failedChecks = mapFailedChecks(failedRuns);
const failureSummaries = [];
for (const check of failedChecks.slice(0, MAX_JOBS_TO_FETCH)) {
failureSummaries.push(
await fetchJobFailureSummary(github, owner, repo, check.id, check.name)
);
}
const body = buildCiFixComment({ headSha, failedChecks, failureSummaries });
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [CI_FIX_LABEL],
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
core?.info?.(`Posted CI fix @claude on PR #${prNumber} (${shortSha(headSha)})`);
return { triggered: true, headSha: shortSha(headSha) };
}
async function processWorkflowRunFailure(github, owner, repo, workflowRun, core) {
if (workflowRun.conclusion !== 'failure') {
return { skipped: true, reason: `conclusion=${workflowRun.conclusion}` };
}
const allowed = ['Core Tests', 'Optimized Test Suite'];
if (!allowed.includes(workflowRun.name)) {
return { skipped: true, reason: `workflow ${workflowRun.name}` };
}
const prNumber = await mergeGate.resolvePrNumberFromWorkflowRun(
github, owner, repo, workflowRun
);
if (!prNumber) return { skipped: true, reason: 'no linked PR' };
return maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
}
module.exports = {
CI_FIX_LABEL,
COOLDOWN_MS,
ciFixShaMarker,
isCiFixComment,
hasCiFixCommentForSha,
hasRecentCiFixComment,
shouldSkipCiFix,
parsePytestFailures,
fetchJobFailureSummary,
buildCiFixComment,
maybeClearCiFixLabel,
maybeTriggerCiFixClaude,
processWorkflowRunFailure,
};
-290
View File
@@ -1,290 +0,0 @@
#!/usr/bin/env node
/**
* Local self-test for merge-gate.js heuristics (no GitHub API).
* Run: node .github/scripts/merge-gate-selftest.js
*/
const mg = require('./merge-gate.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
// Stale FINAL: push after FINAL, no @claude since head
const finals = [
{ user: { login: 'github-actions[bot]' }, body: '@claude FINAL architecture reviewer', created_at: '2026-06-12T08:00:00Z' },
];
assert('stale when head after final', mg.isStaleFinalAfterPush(finals, '2026-06-12T09:00:00Z'));
const withRecovery = [
...finals,
{ user: { login: 'github-actions[bot]' }, body: '@claude FINAL architecture reviewer', created_at: '2026-06-12T09:30:00Z' },
];
assert('not stale when @claude after head', !mg.isStaleFinalAfterPush(withRecovery, '2026-06-12T09:00:00Z'));
const withClaudeReply = [
...finals,
{
user: { login: 'praisonai-triage-agent[bot]' },
body: "**Claude finished @MervinPraison's task** —— [View job](https://github.com/)",
created_at: '2026-06-12T08:30:00Z',
},
];
assert('not stale when Claude replied after FINAL', !mg.isStaleFinalAfterPush(withClaudeReply, '2026-06-12T09:00:00Z'));
assert('claude final reply detected', mg.isClaudeFinalReplyComment(withClaudeReply[1]));
assert('cancelled detect-and-trigger does not block', mg.OPTIONAL_CANCELLED_CHECKS.has('detect-and-trigger'));
const coreGreenRuns = [
{ name: 'test-core', status: 'completed', conclusion: 'success' },
{ name: 'test-core (cli)', status: 'completed', conclusion: 'success' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'test-windows', status: 'completed', conclusion: 'cancelled' },
];
assert('core green allows cancelled smoke', mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
coreGreenRuns
));
assert('core green allows cancelled test-windows', mg.isAcceptableCheckConclusion(
{ name: 'test-windows', status: 'completed', conclusion: 'cancelled' },
coreGreenRuns
));
assert('cancelled smoke blocks when core missing', !mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
[{ name: 'smoke', status: 'completed', conclusion: 'cancelled' }]
));
assert('cancelled smoke blocks when core failed', !mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
[
{ name: 'test-core', status: 'completed', conclusion: 'failure' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
]
));
assert('bestRunsByName prefers success over cancelled', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', conclusion: 'cancelled' },
{ name: 'smoke', conclusion: 'success' },
]);
return best.length === 1 && best[0].conclusion === 'success';
})());
assert('bestRunsByName prefers pending over completed cancelled', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'smoke', status: 'in_progress', conclusion: null },
]);
return best.length === 1 && best[0].status === 'in_progress';
})());
assert('bestRunsByName keeps pending regardless of order', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', status: 'in_progress', conclusion: null },
{ name: 'smoke', status: 'completed', conclusion: 'success' },
]);
return best.length === 1 && best[0].status === 'in_progress';
})());
assert('allChecksGreenOnSha style: pending re-run blocks despite core green', (() => {
const runs = mg.bestRunsByName([
{ name: 'test-core', status: 'completed', conclusion: 'success' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'smoke', status: 'queued', conclusion: null },
]);
const smoke = runs.find((r) => r.name === 'smoke');
return smoke.status !== 'completed';
})());
// Stale-FINAL recovery guards (PR #2560 push loop)
const nowMs = Date.now();
const iso = (ms) => new Date(ms).toISOString();
const firstFinal = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: iso(nowMs - 20 * 60 * 1000),
};
const pushSoonAfter = mg.shouldSkipStaleFinalRecovery(
[firstFinal],
iso(nowMs - 16 * 60 * 1000)
);
assert('debounce push soon after FINAL', pushSoonAfter.skip && pushSoonAfter.reason.includes('soon after'));
const secondFinal = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: iso(nowMs - 20 * 60 * 1000),
};
const capped = mg.shouldSkipStaleFinalRecovery(
[firstFinal, secondFinal],
iso(nowMs - 2 * 60 * 1000)
);
assert('hourly cap blocks third FINAL in window', capped.skip && capped.reason.includes('capped'));
const botPush = mg.shouldSkipStaleFinalRecovery(
[firstFinal],
iso(nowMs - 16 * 60 * 1000),
'praisonai-triage-agent[bot]'
);
assert('skip when automation pushed head', botPush.skip && botPush.reason.includes('automation'));
assert('claude automation login', mg.isClaudeAutomationLogin('praisonai-triage-agent[bot]'));
// Bot CHANGES_REQUESTED then APPROVE
const reviews = [
{ user: { login: 'coderabbit[bot]', type: 'Bot' }, state: 'CHANGES_REQUESTED', submitted_at: '2026-06-12T08:00:00Z' },
{ user: { login: 'coderabbit[bot]', type: 'Bot' }, state: 'APPROVED', submitted_at: '2026-06-12T09:00:00Z' },
];
assert('bot approve clears CR', !mg.hasAnyChangesRequested(reviews));
assert('human CR blocks', mg.hasAnyChangesRequested([
{ user: { login: 'MervinPraison', type: 'User' }, state: 'CHANGES_REQUESTED', submitted_at: '2026-06-12T09:00:00Z' },
]));
// Verdict after HEAD
const verdictComments = [
{ body: 'MERGE_GATE_VERDICT: APPROVE', created_at: '2026-06-12T08:00:00Z' },
];
assert('verdict before head rejected', mg.findMergeGateVerdict(verdictComments, null, '2026-06-12T09:00:00Z') === null);
assert('verdict after head accepted', mg.findMergeGateVerdict(
[{ body: 'MERGE_GATE_VERDICT: APPROVE', created_at: '2026-06-12T10:00:00Z' }],
null,
'2026-06-12T09:00:00Z'
) === 'APPROVE');
const fallbackApprove = [
{
body: 'MERGE_GATE_VERDICT: APPROVE\n\nAutomated fallback — Claude assess did not post a verdict comment.',
created_at: '2026-06-12T10:00:00Z',
},
];
assert(
'automated fallback APPROVE ignored when Opus required',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z', { excludeAutomatedFallback: true }) === null
);
assert(
'automated fallback APPROVE still visible without Opus-only filter',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z') === 'APPROVE'
);
const fallbackBlockNewFormat = [
{
body: 'MERGE_GATE_VERDICT: BLOCK\n\nAutomated fallback — Opus merge gate assessment did not complete.',
created_at: '2026-06-12T10:00:00Z',
},
];
assert(
'new-format fallback BLOCK ignored when Opus required',
mg.findMergeGateVerdict(fallbackBlockNewFormat, null, '2026-06-12T09:00:00Z', { excludeAutomatedFallback: true }) === null
);
assert(
'new-format fallback BLOCK detected by marker helper',
mg.isAutomatedFallbackVerdict(fallbackBlockNewFormat[0].body)
);
assert(
'findMergeGateVerdict null-safe with explicit null options',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z', null) === 'APPROVE'
);
const noise = [{ user: { login: 'MervinPraison' }, body: '**Merge gate scan** — wait for `@claude`', created_at: new Date().toISOString() }];
assert('diagnostic comment not a trigger', !mg.hasRecentClaudeTrigger(noise, 35));
// Cooldown skip requires an actual verdict on HEAD, not just a FINAL trigger comment
const recentFinalOnHead = [
{
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: '2026-06-27T10:00:00Z',
},
];
assert(
'final trigger alone does not count as verdict on head',
mg.findMergeGateVerdict(recentFinalOnHead, null, '2026-06-27T09:55:00Z') === null
);
const recentVerdictOnHead = [
...recentFinalOnHead,
{
user: { login: 'github-actions[bot]' },
body: 'MERGE_GATE_VERDICT: APPROVE',
created_at: '2026-06-27T10:05:00Z',
},
];
assert(
'verdict on head skips cooldown gate',
mg.findMergeGateVerdict(recentVerdictOnHead, null, '2026-06-27T09:55:00Z') === 'APPROVE'
);
// Sensitive + secrets
assert('workflow path sensitive', mg.sensitivePathReasons([{ filename: '.github/workflows/foo.yml' }]).length === 1);
assert('ci-only label exempts workflows', mg.sensitivePathReasons(
[{ filename: '.github/workflows/foo.yml' }],
[mg.WORKFLOW_ONLY_LABEL]
).length === 0);
assert('ci-only label not exempt mixed changes', mg.sensitivePathReasons(
[{ filename: '.github/workflows/foo.yml' }, { filename: 'src/foo.py' }],
[mg.WORKFLOW_ONLY_LABEL]
).length === 1);
assert('secret in patch', mg.secretScanReasons([{ filename: 'x.py', patch: '+key = "ghp_abcdefghijklmnopqrstuvwxyz1234567890"' }]).length === 1);
// Claude run scoping — other PR branches must not block
assert('other branch claude does not block', !mg.hasBlockingClaudeRunForPr(
[{ event: 'issue_comment', head_branch: 'other-branch' }],
'my-branch'
));
assert('same branch claude blocks', mg.hasBlockingClaudeRunForPr(
[{ event: 'issue_comment', head_branch: 'my-branch' }],
'my-branch'
));
assert('issues event never blocks', !mg.hasBlockingClaudeRunForPr(
[{ event: 'issues', head_branch: 'my-branch' }],
'my-branch'
));
// Conflict rebase clears after bot completion + FINAL on HEAD (within 12h cooldown)
const conflictNowMs = Date.now();
const conflictIso = (offsetMs) => new Date(conflictNowMs + offsetMs).toISOString();
const conflictTrigger = {
user: { login: 'MervinPraison' },
body: '@claude this PR has merge conflicts with main. Please rebase',
created_at: conflictIso(-30 * 60 * 1000),
};
const rebaseDone = {
user: { login: 'praisonai-triage-agent[bot]' },
body: 'Rebase complete — PR #2308 onto latest main',
created_at: conflictIso(-29 * 60 * 1000),
};
const finalAfterRebase = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: conflictIso(-20 * 60 * 1000),
};
const headAfterRebase = conflictIso(-21 * 60 * 1000);
const rebaseComments = [conflictTrigger, rebaseDone, finalAfterRebase];
assert('conflict blocks before rebase done', mg.hasRecentConflictComment([conflictTrigger], headAfterRebase));
assert('conflict clears after rebase + FINAL on HEAD', !mg.hasRecentConflictComment(rebaseComments, headAfterRebase));
assert('conflict still blocks without FINAL on HEAD', mg.hasRecentConflictComment(
[conflictTrigger, rebaseDone],
headAfterRebase
));
// Tests heuristic
assert('sdk without tests', mg.missingTestsReason([{ filename: 'src/praisonai-agents/a/b.py', additions: 3 }]) !== null);
assert('sdk with tests ok', mg.missingTestsReason([
{ filename: 'src/praisonai-agents/a/b.py', additions: 3 },
{ filename: 'src/praisonai-agents/tests/test_x.py', additions: 10 },
]) === null);
// PR size
assert('large PR blocked', mg.prSizeReasons([{ additions: 900 }]).length > 0);
assert('internal PR link accepted', mg.isInternalPullRequestLink(
{ base: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
));
assert('fork sync PR link rejected', !mg.isInternalPullRequestLink(
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
));
process.exit(failed ? 1 : 0);
File diff suppressed because it is too large. Load diff
@@ -1,76 +0,0 @@
#!/usr/bin/env node
/**
* Run: node .github/scripts/pipeline-status-selftest.js
*/
const ps = require('./pipeline-status.js');
const mg = require('./merge-gate.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
const kickComments = [
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
{ user: { login: 'MervinPraison' }, body: '/review' },
];
const finalComment = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: '2026-06-26T10:00:00Z',
};
assert(
'reviews pending when not kicked',
ps.deriveStage([], { ready: false, reasons: ['no FINAL Claude review trigger'] })
=== 'pipeline/reviews-pending'
);
assert(
'final pending after kick',
ps.deriveStage(kickComments, { ready: false, reasons: ['no FINAL Claude review trigger'] })
=== 'pipeline/final-claude-pending'
);
assert(
'awaiting merge gate after final',
ps.deriveStage(
[...kickComments, finalComment],
{ ready: false, reasons: ['recent @claude within 35min'] }
) === 'pipeline/awaiting-merge-gate'
);
assert(
'merge ready',
ps.deriveStage([...kickComments, finalComment], { ready: true, reasons: [] })
=== 'pipeline/merge-ready'
);
const blockers = ps.deriveBlockerLabels({
ready: false,
reasons: ['CI not green on HEAD', 'SDK code added without test file changes — requires manual review'],
});
assert('maps ci blocker', blockers.includes('pipeline/blocked:ci'));
assert('maps manual blocker', blockers.includes('pipeline/blocked:manual-review'));
assert(
'internal link matches upstream base',
mg.isInternalPullRequestLink(
{ base: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
)
);
assert(
'fork sync link rejected',
!mg.isInternalPullRequestLink(
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
)
);
process.exit(failed ? 1 : 0);
-258
View File
@@ -1,258 +0,0 @@
/**
* Sync pipeline stage/blocker labels on open PRs.
* @see .github/workflows/pipeline-status-sync.yml
*/
const mergeGate = require('./merge-gate.js');
const chain = require('./bot-pr-review-chain.js');
const ciFix = require('./ci-failure-claude.js');
const PIPELINE_PREFIX = 'pipeline/';
const STAGE_LABELS = [
'pipeline/reviews-pending',
'pipeline/final-claude-pending',
'pipeline/awaiting-merge-gate',
'pipeline/merge-ready',
'pipeline/merged',
];
const BLOCKER_LABELS = [
'pipeline/blocked:ci',
'pipeline/blocked:conflict',
'pipeline/blocked:manual-review',
'pipeline/blocked:cooldown',
'pipeline/blocked:stale-final',
'pipeline/blocked:no-final',
];
const ALL_PIPELINE_LABELS = [...STAGE_LABELS, ...BLOCKER_LABELS];
const LABEL_SPECS = [
{ name: 'pipeline/reviews-pending', color: 'fbca04', description: 'Waiting for CodeRabbit/Qodo/Copilot reviews' },
{ name: 'pipeline/final-claude-pending', color: 'd4c5f9', description: 'Reviews done; waiting for FINAL @claude' },
{ name: 'pipeline/awaiting-merge-gate', color: 'c5def5', description: 'FINAL done; waiting for merge gate / CI' },
{ name: 'pipeline/merge-ready', color: '0e8a16', description: 'Eligible for merge gate auto-merge' },
{ name: 'pipeline/merged', color: '6f42c1', description: 'Merged via pipeline' },
{ name: 'pipeline/blocked:ci', color: 'd93f0b', description: 'Blocked: CI not green on HEAD' },
{ name: 'pipeline/blocked:conflict', color: 'b60205', description: 'Blocked: merge conflict or rebase pending' },
{ name: 'pipeline/blocked:manual-review', color: 'e99695', description: 'Blocked: requires manual review' },
{ name: 'pipeline/blocked:cooldown', color: 'fef2c0', description: 'Blocked: post-push or @claude cooldown' },
{ name: 'pipeline/blocked:stale-final', color: 'f9d0c4', description: 'Blocked: FINAL stale after new commits' },
{ name: 'pipeline/blocked:no-final', color: 'ededed', description: 'Blocked: no FINAL @claude trigger yet' },
];
function deriveStage(comments, evalResult) {
if (evalResult.ready) return 'pipeline/merge-ready';
if (!chain.chainKickPosted(comments)) return 'pipeline/reviews-pending';
if (!mergeGate.hasFinalClaudeReviewTrigger(comments)) return 'pipeline/final-claude-pending';
return 'pipeline/awaiting-merge-gate';
}
function reasonToBlockerLabel(reason) {
const r = (reason || '').toLowerCase();
if (r.includes('ci not green') || r.includes('sdk code changed but no ci')) return 'pipeline/blocked:ci';
if (
r.includes('conflict') ||
r.includes('mergestate=dirty') ||
r.includes('not mergeable')
) return 'pipeline/blocked:conflict';
if (
r.includes('manual') ||
r.includes('requires manual') ||
r.includes('sensitive path') ||
r.includes('no-auto-merge') ||
r.includes('manual-only label') ||
r.includes('without test') ||
r.includes('files changed') ||
r.includes('possible secret') ||
r.includes('agent.py')
) return 'pipeline/blocked:manual-review';
if (r.includes('recent @claude') || r.includes('post-push buffer')) return 'pipeline/blocked:cooldown';
if (r.includes('stale final')) return 'pipeline/blocked:stale-final';
if (r.includes('no final claude')) return 'pipeline/blocked:no-final';
if (r.includes('final claude not complete')) return 'pipeline/blocked:stale-final';
if (r.includes('claude.yml in progress') || r.includes('claude-merge-gate-active')) {
return null;
}
return null;
}
function deriveBlockerLabels(evalResult) {
if (evalResult.ready) return [];
const labels = new Set();
for (const reason of evalResult.reasons || []) {
const mapped = reasonToBlockerLabel(reason);
if (mapped && mapped.startsWith('pipeline/blocked:')) labels.add(mapped);
}
return [...labels];
}
function computePipelineLabels(comments, evalResult) {
const stage = deriveStage(comments, evalResult);
const blockers = stage === 'pipeline/merge-ready' ? [] : deriveBlockerLabels(evalResult);
return { stage, blockers, all: [stage, ...blockers] };
}
async function ensurePipelineLabels(github, owner, repo, core) {
let existing;
if (typeof github.paginate === 'function') {
existing = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner,
repo,
per_page: 100,
});
} else {
({ data: existing } = await github.rest.issues.listLabelsForRepo({
owner,
repo,
per_page: 100,
}));
}
const names = new Set(existing.map((l) => l.name));
for (const spec of LABEL_SPECS) {
if (names.has(spec.name)) continue;
try {
await github.rest.issues.createLabel({
owner,
repo,
name: spec.name,
color: spec.color,
description: spec.description,
});
core?.info?.(`Created label ${spec.name}`);
} catch (err) {
if (err.status !== 422) throw err;
core?.info?.(`Label ${spec.name} already exists`);
}
}
}
async function syncPipelineLabels(github, owner, repo, prNumber, core) {
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
if (ctx.pr.state !== 'open') return { synced: false, reason: 'not_open' };
const evalResult = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core
);
const { stage, blockers, all } = computePipelineLabels(ctx.comments, evalResult);
const current = ctx.labels.filter((l) => l.startsWith(PIPELINE_PREFIX));
const desired = new Set(all);
const toRemove = current.filter((l) => !desired.has(l) && l !== 'pipeline/merged');
const toAdd = all.filter((l) => !current.includes(l));
for (const name of toRemove) {
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name });
} catch (err) {
if (err.status !== 404) throw err;
}
}
if (toAdd.length) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: toAdd,
});
}
core?.info?.(`PR #${prNumber}: stage=${stage} blockers=[${blockers.join(', ')}]`);
const ciNotGreen = (evalResult.reasons || []).some((r) =>
r.toLowerCase().includes('ci not green')
);
if (ciNotGreen || blockers.includes('pipeline/blocked:ci')) {
await ciFix.maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
} else {
await ciFix.maybeClearCiFixLabel(
github, owner, repo, prNumber, ctx.labels, ctx.headSha, core
);
}
return {
synced: true,
stage,
blockers,
ready: evalResult.ready,
reasons: evalResult.reasons,
labels: ctx.labels,
createdAt: new Date(ctx.pr.created_at).getTime(),
};
}
async function dispatchMergeGateForOldestReady(github, owner, repo, readyCandidates, core) {
if (!readyCandidates.length) return 0;
readyCandidates.sort((a, b) => a.createdAt - b.createdAt);
for (const cand of readyCandidates) {
if ((cand.labels || []).includes('claude-merge-gate-active')) {
core?.info?.(`Skip dispatch PR #${cand.prNumber}: merge gate already active`);
continue;
}
await github.rest.repos.createDispatchEvent({
owner,
repo,
event_type: 'claude-merge-gate',
client_payload: { pr_number: cand.prNumber },
});
core?.info?.(`Dispatched merge gate for ready PR #${cand.prNumber}`);
return cand.prNumber;
}
return 0;
}
async function syncOpenPullRequests(github, owner, repo, options, core) {
const { maxPrs = 20, dispatchMergeGate = true } = options || {};
await ensurePipelineLabels(github, owner, repo, core);
let prs;
if (maxPrs <= 100) {
({ data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
per_page: 100,
}));
} else {
prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
}
let synced = 0;
const readyCandidates = [];
for (const pr of prs) {
if (synced >= maxPrs) break;
if (pr.draft) continue;
if (pr.head?.repo?.full_name && pr.head.repo.full_name !== `${owner}/${repo}`) continue;
const result = await syncPipelineLabels(github, owner, repo, pr.number, core);
if (result.ready) {
readyCandidates.push({
prNumber: pr.number,
createdAt: result.createdAt || new Date(pr.created_at).getTime(),
labels: result.labels || [],
});
}
synced += 1;
}
let dispatched = 0;
if (dispatchMergeGate && readyCandidates.length) {
dispatched = await dispatchMergeGateForOldestReady(
github, owner, repo, readyCandidates, core
);
}
core?.info?.(`Pipeline label sync complete (${synced} PR(s), dispatched=${dispatched || 'none'})`);
return synced;
}
module.exports = {
STAGE_LABELS,
BLOCKER_LABELS,
ALL_PIPELINE_LABELS,
deriveStage,
deriveBlockerLabels,
computePipelineLabels,
ensurePipelineLabels,
syncPipelineLabels,
syncOpenPullRequests,
dispatchMergeGateForOldestReady,
};
-111
View File
@@ -1,111 +0,0 @@
#!/usr/bin/env node
const chain = require('./pr-review-chain.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
const coderabbit = {
user: { login: 'coderabbitai[bot]' },
body: '<!-- summarize by coderabbit.ai --> summary',
created_at: '2026-07-02T07:16:00Z',
};
const greptile = {
user: { login: 'greptile-apps[bot]' },
body: '<h3>Greptile Summary</h3><p>Looks good</p>',
created_at: '2026-07-02T07:19:00Z',
};
const copilotKick = {
user: { login: 'MervinPraison' },
body: '@copilot Do a thorough review',
created_at: '2026-07-02T07:20:00Z',
};
const copilotReply = {
user: { login: 'copilot-swe-agent' },
body: 'Copilot review here',
created_at: '2026-07-02T07:22:00Z',
};
assert('prior not ready without greptile', !chain.priorReviewersReady([coderabbit]).ready);
assert('prior ready with coderabbit + greptile', chain.priorReviewersReady([coderabbit, greptile]).ready);
assert('copilot not ready before trigger', !chain.copilotReviewReady([coderabbit, greptile]).ready);
assert(
'copilot ready after reply',
chain.copilotReviewReady([coderabbit, greptile, copilotKick, copilotReply]).ready
);
assert(
'claude not ready before copilot',
!chain.claudeFinalReady([coderabbit, greptile]).ready
);
assert(
'claude ready after full chain',
chain.claudeFinalReady([coderabbit, greptile, copilotKick, copilotReply]).ready
);
assert(
'claude timeout fallback when copilot triggered but silent',
chain.claudeFinalReady([coderabbit, greptile, copilotKick], [], { allowCopilotTimeout: true }).ready
);
assert(
'no timeout fallback without copilot trigger',
!chain.claudeFinalReady([coderabbit, greptile], [], { allowCopilotTimeout: true }).ready
);
assert(
'skipCopilot proceeds after prior reviewers',
chain.claudeFinalReady([coderabbit, greptile], [], { skipCopilot: true }).ready
);
const prev = process.env.REVIEW_CHAIN_SKIP_COPILOT;
process.env.REVIEW_CHAIN_SKIP_COPILOT = '1';
assert(
'REVIEW_CHAIN_SKIP_COPILOT env skips copilot',
chain.claudeFinalReady([coderabbit, greptile], [], {}).ready
);
process.env.REVIEW_CHAIN_SKIP_COPILOT = prev;
const geminiReview = {
user: { login: 'gemini-code-assist[bot]' },
body: '## Code Review\n\nThis pull request decomposes the large main.py file',
submitted_at: '2026-07-03T08:08:00Z',
};
assert(
'gemini pull review counts as prior reviewer',
chain.hasGeminiReview([], [geminiReview])
);
assert(
'prior ready with gemini review only (no gemini comment)',
chain.priorReviewersReady([coderabbit, greptile], [geminiReview], { optionalWaitMs: 0 }).ready
);
const oldPr = new Date(Date.now() - 31 * 60 * 1000).toISOString();
assert(
'prior not ready without coderabbit before timeout',
!chain.priorReviewersReady([greptile], [], { prCreatedAt: new Date().toISOString(), requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'prior timeout when coderabbit silent after 30m',
chain.priorReviewersReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'claude FINAL timeout when required reviewer silent and no prior FINAL',
chain.claudeFinalReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'claude FINAL timeout auto-skips copilot without explicit skipCopilot',
chain.claudeFinalReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).copilotSkipped
);
const finalClaude = {
user: { login: 'github-actions[bot]' },
body: '@claude You are the FINAL architecture reviewer for this PR.',
created_at: '2026-07-02T07:25:00Z',
};
assert(
'claude FINAL not re-posted when already triggered',
!chain.claudeFinalReady([greptile, finalClaude], [], { prCreatedAt: oldPr, requiredWaitMs: 0 }).ready
);
process.exit(failed ? 1 : 0);
-322
View File
@@ -1,322 +0,0 @@
/**
* PR review pipeline: CodeRabbit / Greptile (+ optional Qodo/Gemini) → Claude (FINAL).
* Copilot step optional — set REVIEW_CHAIN_SKIP_COPILOT=1 or skipCopilot: true to bypass.
* @see .github/workflows/auto-pr-comment.yml
*/
function isSkipCopilot(options = {}) {
if (options.skipCopilot === true) return true;
if (options.skipCopilot === false) return false;
return process.env.REVIEW_CHAIN_SKIP_COPILOT === '1';
}
const COPILOT_TRIGGER_LOGINS = new Set(['MervinPraison', 'github-actions[bot]']);
const CLAUDE_TRIGGER_LOGINS = new Set(['MervinPraison', 'github-actions[bot]']);
const REQUIRED_PRIOR = ['coderabbit', 'greptile'];
const OPTIONAL_PRIOR = ['qodo', 'gemini'];
const OPTIONAL_PRIOR_WAIT_MS = 20 * 60 * 1000;
const REQUIRED_PRIOR_WAIT_MS = 30 * 60 * 1000;
function loginOf(item) {
return (item?.user?.login || '').toLowerCase();
}
function bodyOf(item) {
return item?.body || '';
}
function hasCoderabbitSummary(comments) {
return comments.some(
(c) =>
loginOf(c).includes('coderabbit') &&
bodyOf(c).toLowerCase().includes('summarize by coderabbit')
);
}
function hasGreptileReview(comments) {
return comments.some((c) => {
if (!loginOf(c).includes('greptile')) return false;
const body = bodyOf(c);
return body.includes('Greptile Summary') || body.includes('<h3>Greptile Summary</h3>') || body.length > 150;
});
}
function hasQodoReview(comments, reviews = []) {
if (comments.some((c) => loginOf(c).includes('qodo'))) return true;
return reviews.some((r) => loginOf(r).includes('qodo'));
}
function hasGeminiReview(comments, reviews = []) {
if (reviews.some((r) => loginOf(r).includes('gemini'))) return true;
return comments.some((c) => {
if (loginOf(c).includes('gemini')) return true;
const body = bodyOf(c);
return (
body.includes('Review completed by Gemini CLI') ||
body.includes('The fix is ready for review. Please test')
);
});
}
const PRIOR_CHECKS = {
coderabbit: (comments) => hasCoderabbitSummary(comments),
greptile: (comments) => hasGreptileReview(comments),
qodo: (comments, reviews) => hasQodoReview(comments, reviews),
gemini: (comments, reviews) => hasGeminiReview(comments, reviews),
};
function priorReviewerStatus(comments, reviews = []) {
const status = {};
for (const [id, check] of Object.entries(PRIOR_CHECKS)) {
status[id] = check(comments, reviews);
}
return status;
}
function priorReviewersReady(comments, reviews = [], options = {}) {
const waitMs = options.optionalWaitMs ?? OPTIONAL_PRIOR_WAIT_MS;
const requiredWaitMs = options.requiredWaitMs ?? REQUIRED_PRIOR_WAIT_MS;
const prCreatedAt = options.prCreatedAt || null;
const status = priorReviewerStatus(comments, reviews);
const missingRequired = REQUIRED_PRIOR.filter((id) => !status[id]);
if (missingRequired.length) {
if (prCreatedAt) {
const age = Date.now() - new Date(prCreatedAt).getTime();
if (age >= requiredWaitMs) {
return {
ready: true,
reason: `required reviewer timeout (${missingRequired.join(', ')} silent after ${Math.round(requiredWaitMs / 60000)}m)`,
status,
requiredTimeout: true,
};
}
}
return { ready: false, reason: `waiting for ${missingRequired.join(', ')}`, status };
}
const missingOptional = OPTIONAL_PRIOR.filter((id) => !status[id]);
if (missingOptional.length && prCreatedAt) {
const age = Date.now() - new Date(prCreatedAt).getTime();
if (age < waitMs) {
return {
ready: false,
reason: `waiting for optional ${missingOptional.join(', ')} (${Math.round((waitMs - age) / 60000)}m left)`,
status,
};
}
}
return { ready: true, reason: '', status };
}
function copilotTriggerComment(comments) {
return comments.find(
(c) =>
bodyOf(c).includes('@copilot') &&
COPILOT_TRIGGER_LOGINS.has(c.user?.login) &&
!bodyOf(c).toLowerCase().includes('@claude')
);
}
function copilotTriggered(comments) {
return Boolean(copilotTriggerComment(comments));
}
function copilotReviewReady(comments, reviews = []) {
const trigger = copilotTriggerComment(comments);
if (!trigger) {
return { ready: false, reason: 'copilot not triggered yet' };
}
const triggerTime = new Date(trigger.created_at).getTime();
const copilotComment = comments.some((c) => {
if (c === trigger) return false;
if (new Date(c.created_at).getTime() < triggerTime) return false;
const login = loginOf(c);
return (
login.includes('copilot') ||
c.user?.login === 'Copilot' ||
c.user?.login === 'copilot-swe-agent'
);
});
const copilotReview = reviews.some((r) => {
if (new Date(r.submitted_at).getTime() < triggerTime) return false;
return loginOf(r).includes('copilot');
});
if (copilotComment || copilotReview) {
return { ready: true, reason: '' };
}
return { ready: false, reason: 'awaiting copilot review' };
}
function isFinalClaudeTriggerComment(c) {
const body = bodyOf(c).toLowerCase();
if (!CLAUDE_TRIGGER_LOGINS.has(c.user?.login)) return false;
if (!body.includes('@claude')) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
}
function claudeFinalAlreadyTriggered(comments) {
return comments.some(isFinalClaudeTriggerComment);
}
function claudeFinalReady(comments, reviews = [], options = {}) {
if (claudeFinalAlreadyTriggered(comments)) {
return { ready: false, reason: 'claude FINAL already triggered', already: true };
}
const prior = priorReviewersReady(comments, reviews, options);
if (!prior.ready) {
return { ready: false, reason: prior.reason };
}
if (isSkipCopilot(options) || prior.requiredTimeout) {
return {
ready: true,
reason: prior.requiredTimeout ? prior.reason : 'copilot skipped',
copilotSkipped: true,
requiredTimeout: prior.requiredTimeout,
};
}
const copilot = copilotReviewReady(comments, reviews);
if (copilot.ready) {
return { ready: true, reason: '' };
}
if (options.allowCopilotTimeout && copilotTriggered(comments)) {
return { ready: true, reason: 'copilot timeout fallback', copilotSkipped: true };
}
return { ready: false, reason: copilot.reason };
}
/** Post @copilot when prior reviewers are ready, then @claude FINAL when Copilot done (or timeout). */
async function advanceReviewChain(github, owner, repo, prNumber, finalBody, core, options = {}) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
const chainOpts = {
prCreatedAt: pr.created_at,
optionalWaitMs: options.optionalWaitMs ?? 0,
};
const copilot = options.skipCopilot
? { triggered: false, reason: 'skipped' }
: await maybeTriggerCopilot(github, owner, repo, prNumber, core, chainOpts);
const claude = await maybeTriggerClaudeFinal(
github, owner, repo, prNumber, finalBody, core,
{ ...options, ...chainOpts }
);
return { copilot, claude };
}
async function maybeTriggerClaudeFinal(github, owner, repo, prNumber, finalBody, core, options = {}) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
const gate = claudeFinalReady(comments, reviews, {
...options,
prCreatedAt: pr.created_at,
optionalWaitMs: options.optionalWaitMs,
allowCopilotTimeout: options.allowCopilotTimeout !== false,
});
if (!gate.ready) {
core?.info?.(`PR #${prNumber}: skip Claude FINAL — ${gate.reason}`);
return { posted: false, reason: gate.reason };
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: finalBody,
});
const note = gate.requiredTimeout
? ' (required reviewer timeout fallback)'
: gate.copilotSkipped
? ' (Copilot timeout fallback)'
: '';
core?.info?.(`Posted Claude FINAL on PR #${prNumber}${note}`);
return { posted: true, reason: '' };
}
const COPILOT_REVIEW_BODY =
'@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, CodeRabbit, Gemini, and Greptile first — incorporate their findings.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused? Any unnecessary code or scope creep?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. **Address reviewer feedback**: If Qodo, CodeRabbit, Gemini, or Greptile flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible';
async function listCommentsAndReviews(github, owner, repo, prNumber) {
let comments;
if (typeof github.paginate === 'function') {
comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
} else {
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
comments = data;
}
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
return { comments, reviews };
}
async function maybeTriggerCopilot(github, owner, repo, prNumber, core, options = {}) {
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
if (copilotTriggered(comments)) {
core?.info?.(`Copilot already triggered on PR #${prNumber}`);
return { triggered: false, reason: 'already_triggered' };
}
const prior = priorReviewersReady(comments, reviews, options);
if (!prior.ready) {
core?.info?.(`PR #${prNumber}: not ready for Copilot — ${prior.reason}`);
return { triggered: false, reason: prior.reason };
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: COPILOT_REVIEW_BODY,
});
core?.info?.(`Posted @copilot on PR #${prNumber} after prior reviewers`);
return { triggered: true };
}
async function pollCopilotResponse(github, owner, repo, prNumber, options = {}) {
const maxAttempts = options.maxAttempts ?? 20;
const delayMs = options.delayMs ?? 30000;
for (let i = 0; i < maxAttempts; i += 1) {
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
const copilot = copilotReviewReady(comments, reviews);
if (copilot.ready) {
return { ready: true, reason: '' };
}
if (i < maxAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
return { ready: false, reason: 'copilot timeout' };
}
module.exports = {
isSkipCopilot,
REQUIRED_PRIOR,
OPTIONAL_PRIOR,
REQUIRED_PRIOR_WAIT_MS,
COPILOT_REVIEW_BODY,
priorReviewerStatus,
priorReviewersReady,
copilotTriggered,
copilotReviewReady,
claudeFinalReady,
claudeFinalAlreadyTriggered,
isFinalClaudeTriggerComment,
maybeTriggerCopilot,
maybeTriggerClaudeFinal,
advanceReviewChain,
pollCopilotResponse,
hasGeminiReview,
listCommentsAndReviews,
};
-122
View File
@@ -1,122 +0,0 @@
const assert = require('assert');
const rg = require('./release-gate.js');
assert.strictEqual(rg.bumpPatch('1.6.80'), '1.6.81');
assert.strictEqual(rg.bumpPatch('4.6.80'), '4.6.81');
assert.strictEqual(rg.bumpPatch('0.0.2'), '0.0.3');
const versions = rg.readVersionsFromTree();
assert.ok(versions.currentAgents);
assert.ok(versions.currentCode);
assert.ok(versions.currentWrapper);
assert.ok(versions.targetCode);
assert.ok(rg.PACKAGE_PATHS.includes('src/praisonai-code'));
// Every released package must make the gate's path-change check eligible.
for (const p of [
'src/praisonai', 'src/praisonai-agents', 'src/praisonai-code',
'src/praisonai-bot', 'src/praisonai-train', 'src/praisonai-browser',
'src/praisonai-mcp', 'src/praisonai-sandbox', 'src/praisonai-deploy',
]) {
assert.ok(rg.PACKAGE_PATHS.includes(p), `PACKAGE_PATHS missing ${p}`);
}
const noonUtc = new Date('2026-07-08T12:00:00Z');
const dayStart = rg.utcDayStart(noonUtc);
assert.strictEqual(dayStart.toISOString(), '2026-07-08T00:00:00.000Z');
function mockGithub({ runs = [], releases = [] } = {}) {
return {
rest: {
actions: {
listWorkflowRuns: async () => ({ data: { workflow_runs: runs } }),
},
repos: {
listReleases: async () => ({ data: releases }),
},
},
};
}
(async () => {
assert.strictEqual(await rg.pypiVersionExists('praisonaiagents', '0.0.0'), false);
assert.strictEqual(rg.PATCH_RELEASE_INTERVAL_DAYS, 3);
// Dedupe is release-based: a v* release inside the window blocks…
const recentRelease = mockGithub({
releases: [{ tag_name: 'v4.6.159', published_at: '2026-07-08T09:00:00Z' }],
});
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(recentRelease, 'o', 'r', noonUtc),
true
);
const result = await rg.evaluateReleasePreflight(
recentRelease, 'o', 'r',
{ headSha: 'abc', isCiTrigger: false, bump: 'patch', now: noonUtc },
null
);
assert.ok(result.reasons.some((r) => r.includes('every 3 days')));
// …an old release does not…
const oldRelease = mockGithub({
releases: [{ tag_name: 'v4.6.150', published_at: '2026-07-04T09:00:00Z' }],
});
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(oldRelease, 'o', 'r', noonUtc),
false
);
// …and a dry-run "success" (workflow run, no release) does not consume the
// window: only real releases count.
const dryRunOnly = mockGithub({
runs: [{ conclusion: 'success', created_at: '2026-07-08T09:00:00Z', status: 'completed' }],
releases: [],
});
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(dryRunOnly, 'o', 'r', noonUtc),
false
);
// Active-run blocking: fresh waiting run blocks…
const freshWaiting = mockGithub({
runs: [{ status: 'waiting', conclusion: null, created_at: '2026-07-08T11:00:00Z', html_url: 'x' }],
});
assert.strictEqual(
await rg.hasActiveReleaseRun(freshWaiting, 'o', 'r', noonUtc, null),
true
);
// …a waiting run stuck longer than STALE_WAITING_HOURS no longer blocks…
const staleWaiting = mockGithub({
runs: [{ status: 'waiting', conclusion: null, created_at: '2026-07-08T01:00:00Z', html_url: 'x' }],
});
const warnings = [];
const fakeCore = { warning: (m) => warnings.push(m), info: () => {} };
assert.strictEqual(rg.STALE_WAITING_HOURS, 6);
assert.strictEqual(
await rg.hasActiveReleaseRun(staleWaiting, 'o', 'r', noonUtc, fakeCore),
false
);
assert.ok(warnings.length === 1 && warnings[0].includes('approve or cancel'));
// …but an in_progress run always blocks regardless of age.
const oldInProgress = mockGithub({
runs: [{ status: 'in_progress', conclusion: null, created_at: '2026-07-07T01:00:00Z', html_url: 'x' }],
});
assert.strictEqual(
await rg.hasActiveReleaseRun(oldInProgress, 'o', 'r', noonUtc, null),
true
);
console.log('ok: bumpPatch');
console.log('ok: readVersionsFromTree');
console.log('ok: PACKAGE_PATHS covers all 9 released packages');
console.log('ok: pypiVersionExists missing version');
console.log('ok: utcDayStart');
console.log('ok: release-based dedupe (recent blocks, old does not)');
console.log('ok: dry-run success does not consume the release window');
console.log('ok: 3-day dedupe blocks second release');
console.log('ok: fresh waiting run blocks; stale waiting run ignored with warning');
console.log('ok: in_progress always blocks');
})();
-315
View File
@@ -1,315 +0,0 @@
/**
* Release gate preflight — path changes, CI SHA, dedupe, PyPI version checks.
*/
const https = require('https');
/** Every directory whose changes should make a release eligible — all nine
* published packages (mirrors the skip_* inputs in pypi-release.yml). */
const PACKAGE_PATHS = [
'src/praisonai',
'src/praisonai-agents',
'src/praisonai-code',
'src/praisonai-bot',
'src/praisonai-train',
'src/praisonai-browser',
'src/praisonai-mcp',
'src/praisonai-sandbox',
'src/praisonai-deploy',
];
/** Minimum days between successful patch auto-releases. */
const PATCH_RELEASE_INTERVAL_DAYS = 3;
const ACTIVE_RELEASE_STATUSES = new Set([
'queued', 'in_progress', 'waiting', 'pending', 'requested',
]);
/** A run stuck in `waiting` (environment approval) longer than this no longer
* blocks the gate. GitHub only auto-fails unapproved deployments after 30
* days, so without a cutoff one forgotten manual dispatch stalls all
* auto-releases for up to a month. The run is warned about, never cancelled —
* if later approved, the pypi-release concurrency group serializes it and its
* pypi_exists checks no-op anything already published. */
const STALE_WAITING_HOURS = 6;
function bumpPatch(version) {
const parts = version.split('.');
if (parts.length !== 3) throw new Error(`Invalid version: ${version}`);
return `${parts[0]}.${parts[1]}.${Number(parts[2]) + 1}`;
}
function readVersionsFromTree(root = '.') {
const fs = require('fs');
const path = require('path');
const agentsToml = fs.readFileSync(
path.join(root, 'src/praisonai-agents/pyproject.toml'), 'utf8'
);
const agentsMatch = agentsToml.match(/^version\s*=\s*"([^"]+)"/m);
if (!agentsMatch) throw new Error('Could not read agents version');
const codeToml = fs.readFileSync(
path.join(root, 'src/praisonai-code/pyproject.toml'), 'utf8'
);
const codeMatch = codeToml.match(/^version\s*=\s*"([^"]+)"/m);
if (!codeMatch) throw new Error('Could not read praisonai-code version');
const wrapperPy = fs.readFileSync(
path.join(root, 'src/praisonai/praisonai/version.py'), 'utf8'
);
const wrapperMatch = wrapperPy.match(/__version__ = "([^"]+)"/);
if (!wrapperMatch) throw new Error('Could not read wrapper version');
return {
currentAgents: agentsMatch[1],
currentCode: codeMatch[1],
currentWrapper: wrapperMatch[1],
targetAgents: bumpPatch(agentsMatch[1]),
targetCode: bumpPatch(codeMatch[1]),
targetWrapper: bumpPatch(wrapperMatch[1]),
};
}
function pypiVersionExists(packageName, version) {
return new Promise((resolve) => {
const req = https.get(
`https://pypi.org/pypi/${packageName}/${version}/json`,
{ timeout: 15000 },
(res) => {
res.resume();
resolve(res.statusCode === 200);
}
);
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
async function hasActiveReleaseRun(github, owner, repo, now = new Date(), core = null) {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'pypi-release.yml',
per_page: 20,
});
const staleCutoff = now.getTime() - STALE_WAITING_HOURS * 60 * 60 * 1000;
return runs.data.workflow_runs.some((r) => {
if (!ACTIVE_RELEASE_STATUSES.has(r.status) || r.conclusion) return false;
if (r.status === 'waiting' && new Date(r.created_at).getTime() < staleCutoff) {
if (core) {
core.warning(
`Ignoring release run waiting >${STALE_WAITING_HOURS}h for environment approval: `
+ `${r.html_url} — approve or cancel it. It no longer blocks auto-releases.`
);
}
return false;
}
return true;
});
}
function utcDayStart(now = new Date()) {
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
}
function releaseIntervalStart(now = new Date(), intervalDays = PATCH_RELEASE_INTERVAL_DAYS) {
return new Date(now.getTime() - intervalDays * 24 * 60 * 60 * 1000);
}
async function hasSuccessfulReleaseWithinDays(
github,
owner,
repo,
now = new Date(),
intervalDays = PATCH_RELEASE_INTERVAL_DAYS,
) {
const windowStart = releaseIntervalStart(now, intervalDays);
// Dedupe on GitHub releases (v* tags) rather than run conclusions: a
// dry_run=true dispatch concludes 'success' without publishing anything and
// must not consume the release window. Only a real full release creates a
// v* GitHub release (bump_and_release.py's `gh release create`).
const releases = await github.rest.repos.listReleases({
owner,
repo,
per_page: 10,
});
return releases.data.some(
(r) => r.tag_name && r.tag_name.startsWith('v')
&& new Date(r.published_at || r.created_at) >= windowStart
);
}
/** @deprecated Use hasSuccessfulReleaseWithinDays — kept for selftests. */
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
return hasSuccessfulReleaseWithinDays(github, owner, repo, now, 1);
}
async function lastGreenCoreTestsSha(github, owner, repo) {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'test-core.yml',
branch: 'main',
status: 'completed',
per_page: 30,
});
const hit = runs.data.workflow_runs.find((r) => r.conclusion === 'success');
return hit ? hit.head_sha : '';
}
async function evaluateReleasePreflight(github, owner, repo, options, core) {
const {
headSha,
isCiTrigger = false,
bump = 'patch',
} = options;
const reasons = [];
const out = {
ready: false,
reasons,
headSha: headSha || '',
lastTag: '',
targetAgents: '',
targetCode: '',
targetWrapper: '',
};
if (bump !== 'patch') {
reasons.push('only patch auto-release supported');
return out;
}
const referenceTime = options.now instanceof Date ? options.now : new Date();
if (await hasActiveReleaseRun(github, owner, repo, referenceTime, core)) {
reasons.push('PyPI Release already in progress or awaiting approval');
return out;
}
if (await hasSuccessfulReleaseWithinDays(github, owner, repo, referenceTime)) {
reasons.push(
`already released within last ${PATCH_RELEASE_INTERVAL_DAYS} days; `
+ `max one patch release every ${PATCH_RELEASE_INTERVAL_DAYS} days`
);
return out;
}
let versions;
try {
versions = readVersionsFromTree();
} catch (err) {
reasons.push(err.message);
return out;
}
out.targetAgents = versions.targetAgents;
out.targetCode = versions.targetCode;
out.targetWrapper = versions.targetWrapper;
const agentsOnPypi = await pypiVersionExists('praisonaiagents', versions.targetAgents);
const codeOnPypi = await pypiVersionExists('praisonai-code', versions.targetCode);
const wrapperOnPypi = await pypiVersionExists('praisonai', versions.targetWrapper);
if (agentsOnPypi && codeOnPypi && wrapperOnPypi) {
reasons.push(
`already published: praisonaiagents==${versions.targetAgents}, `
+ `praisonai-code==${versions.targetCode}, praisonai==${versions.targetWrapper}`
);
return out;
}
const { execSync } = require('child_process');
let lastTag = '';
try {
lastTag = execSync('git describe --tags --match "v*" --abbrev=0', { encoding: 'utf8' }).trim();
} catch {
reasons.push('no v* tag found');
return out;
}
out.lastTag = lastTag;
let changed = '';
try {
changed = execSync(
`git diff --name-only ${lastTag} HEAD -- ${PACKAGE_PATHS.join(' ')}`,
{ encoding: 'utf8' }
).trim();
} catch {
reasons.push('git diff failed');
return out;
}
if (!changed) {
reasons.push(`no changes in ${PACKAGE_PATHS.join(' or ')} since ${lastTag}`);
return out;
}
if (isCiTrigger) {
let mainSha = '';
try {
mainSha = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();
} catch {
reasons.push('could not resolve origin/main');
return out;
}
if (headSha !== mainSha) {
reasons.push(`superseded: green SHA ${headSha.slice(0, 7)} != main ${mainSha.slice(0, 7)}`);
return out;
}
out.headSha = headSha;
} else {
const evalSha = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
out.headSha = evalSha;
const greenSha = await lastGreenCoreTestsSha(github, owner, repo);
if (greenSha !== evalSha) {
// Core Tests has path filters and honors [skip ci] (the release
// safety-net commit uses it), so the tip of main may legitimately have
// no Core Tests run of its own — e.g. a docs-only commit, or the
// "chore(release): … [skip ci]" commit right after a release. Accept a
// green ancestor when nothing under the released package paths changed
// after it; otherwise the cron path stalls until the next src/** push.
let greenAncestorOk = false;
if (greenSha && /^[0-9a-f]{40}$/.test(greenSha)) {
try {
execSync(`git merge-base --is-ancestor ${greenSha} ${evalSha}`);
const delta = execSync(
`git diff --name-only ${greenSha} ${evalSha} -- ${PACKAGE_PATHS.join(' ')}`,
{ encoding: 'utf8' }
).trim();
greenAncestorOk = delta === '';
} catch {
greenAncestorOk = false;
}
}
if (!greenAncestorOk) {
reasons.push(`CI not green on HEAD (last green: ${greenSha ? greenSha.slice(0, 7) : 'none'})`);
return out;
}
if (core) {
core.info(
`HEAD ${evalSha.slice(0, 7)} has no Core Tests run; accepting green ancestor `
+ `${greenSha.slice(0, 7)} (no package-path changes since).`
);
}
}
}
out.ready = true;
out.reasons = ['ready'];
if (core) core.info(`Release preflight passed for ${out.headSha || headSha}`);
return out;
}
module.exports = {
bumpPatch,
readVersionsFromTree,
pypiVersionExists,
PATCH_RELEASE_INTERVAL_DAYS,
STALE_WAITING_HOURS,
releaseIntervalStart,
hasActiveReleaseRun,
hasSuccessfulReleaseWithinDays,
hasSuccessfulReleaseToday,
utcDayStart,
evaluateReleasePreflight,
ACTIVE_RELEASE_STATUSES,
PACKAGE_PATHS,
};
-3
View File
@@ -1,3 +0,0 @@
# GitHub Issue #1350: Add debug log on successful branch checkout
The github_create_branch function in src/praisonai-agents/praisonaiagents/tools/github_tools.py should call logger.debug after a successful checkout.
-46
View File
@@ -1,46 +0,0 @@
name: Auto Issue Comment
on:
workflow_dispatch:
# issues:
# types: [opened]
jobs:
# Trigger Claude by adding the 'claude' label (only if not already labeled)
# Note: @claude in comments only works from human users (not bots)
# The claude.yml workflow listens for issues:labeled event with 'claude' label
trigger-claude:
# Skip if issue was already created with 'claude' label to avoid double-trigger
if: |
!contains(toJSON(github.event.issue.labels), 'claude')
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add claude label to trigger analysis
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
// Check if 'claude' label already exists on this issue
const labels = context.payload.issue.labels.map(l => l.name);
if (labels.includes('claude')) {
console.log('Issue already has claude label, skipping');
return;
}
// Add label to trigger claude.yml via issues:labeled event
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['claude']
});
// Also post instructions as a comment for context
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🤖 Auto-triaging with Claude. Analyzing issue and implementing a fix...\n\nClaude will:\n1. Route correctly: agent tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins\n2. Search codebase for root cause\n3. Implement a minimal fix following AGENTS.md\n4. Create a PR with the changes'
});
-294
View File
@@ -1,294 +0,0 @@
name: Auto PR Comment
# Review chain: CodeRabbit + Greptile (+ optional Qodo/Gemini) → Claude (FINAL)
# Copilot disabled (REVIEW_CHAIN_SKIP_COPILOT=1) — re-enable copilot-* jobs to restore.
# Fork PR jobs use pull_request_target so GH_TOKEN is available for comments.
#
# Claude trigger runs IN-BAND (same workflow) to avoid GitHub Actions
# blocking bot-triggered workflows with action_required approval gates.
on:
issue_comment:
types: [created]
pull_request_review:
types: [submitted]
pull_request:
types: [opened, synchronize]
pull_request_target:
types: [opened, synchronize]
schedule:
- cron: '30 3,9,15,21 * * *'
workflow_dispatch:
env:
REVIEW_CHAIN_SKIP_COPILOT: '1'
MAX_POLL_ATTEMPTS: 20
POLL_DELAY_MS: 30000
CLAUDE_TRIGGER_LOGINS: '["MervinPraison","github-actions[bot]"]'
jobs:
# ================================================================
# Claude FINAL after CodeRabbit / Greptile / optional Qodo / Gemini
# ================================================================
claude-after-prior-reviewers:
if: |
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(
github.event.comment.user.login == 'coderabbitai[bot]' ||
github.event.comment.user.login == 'greptile-apps[bot]' ||
contains(github.event.comment.user.login, 'gemini-code-assist') ||
contains(github.event.comment.user.login, 'qodo-code-review')
)) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.user.login, 'qodo-code-review'))
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Post Claude FINAL after prior reviewers
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const prNumber =
context.payload.issue?.number ||
context.payload.pull_request?.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: pr } = await github.rest.pulls.get({
owner, repo, pull_number: prNumber,
});
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, prNumber,
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core,
{ skipCopilot: true, optionalWaitMs: 0, prCreatedAt: pr.created_at }
);
if (result.posted) core.info(`Posted Claude FINAL on PR #${prNumber}`);
await ps.ensurePipelineLabels(github, owner, repo, core);
await ps.syncPipelineLabels(github, owner, repo, prNumber, core);
# ================================================================
# FALLBACK: Claude FINAL after PR open if reviewers are slow
# ================================================================
claude-fallback-timeout:
if: |
github.event_name == 'pull_request_target' &&
github.event.action == 'opened'
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
issues: write
pull-requests: read
steps:
- uses: actions/checkout@v4
- name: Wait for prior reviewers (30 min)
run: sleep 1800
- name: Post Claude FINAL (skip Copilot)
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const pr = context.payload.pull_request.number;
await chain.maybeTriggerClaudeFinal(
github, context.repo.owner, context.repo.repo, pr,
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core,
{ skipCopilot: true, optionalWaitMs: 0 }
);
# ================================================================
# Recovery: post FINAL @claude after push or when chain missed
# ================================================================
claude-review-recovery:
if: |
github.event_name == 'pull_request_target' &&
github.event.action == 'synchronize'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- uses: actions/checkout@v4
- name: Re-post FINAL Claude review when stale or missing
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
const pr = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const comments = await mergeGate.listAllComments(github, owner, repo, pr);
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr))
|| context.payload.pull_request.updated_at;
const headPusher = context.payload.sender?.login || null;
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
if (mergeGate.shouldSkipFinalRecovery(comments, headPushedAt)) {
core.info('Recent @claude within 35min and FINAL not stale — skip recovery');
return;
}
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr)) {
core.info('Claude workflow in progress — skip recovery');
return;
}
const finalBody = mergeGate.FINAL_CLAUDE_REVIEW_BODY;
const opts = { skipCopilot: true, optionalWaitMs: 0 };
if (hasFinal && isStale) {
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusher);
if (gate.skip) {
core.info(`Skip stale-FINAL recovery: ${gate.reason}`);
return;
}
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr, finalBody, core, opts
);
if (result.posted) core.info(`Posted stale-FINAL recovery @claude on PR #${pr}`);
return;
}
if (hasFinal) {
core.info('FINAL Claude review current on HEAD — skip recovery');
return;
}
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr, finalBody, core, opts
);
if (result.posted) core.info(`Posted missing FINAL Claude review on PR #${pr}`);
- name: Sync pipeline status labels
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const pr = context.payload.pull_request.number;
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
# Scheduled + manual: post missing or stale FINAL @claude on all open PRs (incl. forks)
post-missing-claude-final:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- uses: actions/checkout@v4
- name: Post missing/stale FINAL @claude on open PRs
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const prs = await github.paginate(github.rest.pulls.list, {
owner, repo, state: 'open', per_page: 100,
});
let posted = 0;
for (const pr of prs) {
if (pr.draft) continue;
const comments = await mergeGate.listAllComments(github, owner, repo, pr.number);
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr.number)) || pr.updated_at;
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
if (hasFinal && !isStale) continue;
if (mergeGate.hasRecentClaudeTrigger(comments, 10)) {
core.info(`PR #${pr.number}: @claude within 10min — skip`);
continue;
}
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr.number)) {
core.info(`PR #${pr.number}: Claude in progress — skip`);
continue;
}
if (hasFinal && isStale) {
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt);
if (gate.skip) {
core.info(`PR #${pr.number}: skip stale — ${gate.reason}`);
continue;
}
}
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr.number,
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core,
{ skipCopilot: true, optionalWaitMs: 0 }
);
if (result.posted) {
posted++;
core.info(`Posted Claude FINAL on PR #${pr.number}`);
}
if (posted >= 10) break;
}
core.info(`Missing/stale Claude FINAL sweep complete (${posted} posted)`);
# ================================================================
# BOT PRs: kick CodeRabbit + Qodo (Greptile may follow on its own)
# ================================================================
bot-pr-trigger-reviews:
if: |
github.event_name == 'pull_request' &&
github.event.action == 'opened' &&
(
github.event.pull_request.user.login == 'github-actions[bot]' ||
github.event.pull_request.user.login == 'praisonai-triage-agent[bot]' ||
github.event.pull_request.user.type == 'Bot'
)
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Trigger CodeRabbit and Qodo reviews
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
const pr = context.payload.pull_request.number;
await chain.kickReviewChain(github, context.repo.owner, context.repo.repo, pr, core);
- name: Sync pipeline status labels
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const pr = context.payload.pull_request.number;
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
-153
View File
@@ -1,153 +0,0 @@
name: Performance Benchmarks
on:
workflow_dispatch:
inputs:
iterations:
description: 'Number of benchmark iterations'
required: false
default: '100'
fail_on_regression:
description: 'Fail if performance regresses'
required: false
default: 'true'
jobs:
benchmark:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e src/praisonai-agents[all]
pip install agno crewai langgraph
- name: Run Import Time Benchmark
id: import_time
env:
FAIL_ON_REGRESSION: ${{ inputs.fail_on_regression }}
run: |
cd src/praisonai-agents
python benchmarks/import_time.py 2>&1 | tee import_time_output.txt
# Extract import time
IMPORT_TIME=$(grep -oP 'Median:\s+\K[\d.]+' import_time_output.txt || echo "0")
echo "import_time=$IMPORT_TIME" >> $GITHUB_OUTPUT
# Check threshold (200ms)
if (( $(echo "$IMPORT_TIME > 200" | bc -l) )); then
echo "::warning::Import time ${IMPORT_TIME}ms exceeds 200ms threshold"
if [ "$FAIL_ON_REGRESSION" == "true" ]; then
echo "import_passed=false" >> $GITHUB_OUTPUT
fi
else
echo "import_passed=true" >> $GITHUB_OUTPUT
fi
- name: Run Memory Usage Benchmark
id: memory
env:
FAIL_ON_REGRESSION: ${{ inputs.fail_on_regression }}
run: |
cd src/praisonai-agents
python benchmarks/memory_usage.py 2>&1 | tee memory_output.txt
# Extract peak memory
PEAK_MEMORY=$(grep -oP 'Peak:\s+\K[\d.]+' memory_output.txt || echo "0")
echo "peak_memory=$PEAK_MEMORY" >> $GITHUB_OUTPUT
# Check threshold (30MB)
if (( $(echo "$PEAK_MEMORY > 30" | bc -l) )); then
echo "::warning::Memory usage ${PEAK_MEMORY}MB exceeds 30MB threshold"
if [ "$FAIL_ON_REGRESSION" == "true" ]; then
echo "memory_passed=false" >> $GITHUB_OUTPUT
fi
else
echo "memory_passed=true" >> $GITHUB_OUTPUT
fi
- name: Run Simple Benchmark
id: simple
run: |
cd src/praisonai-agents
python benchmarks/simple_benchmark.py 2>&1 | tee simple_output.txt
# Extract PraisonAI time
PRAISON_TIME=$(grep -oP 'PraisonAI\s+\K[\d.]+' simple_output.txt | head -1 || echo "0")
echo "praison_time=$PRAISON_TIME" >> $GITHUB_OUTPUT
- name: Run Tools Benchmark
id: tools
run: |
cd src/praisonai-agents
python benchmarks/tools_benchmark.py 2>&1 | tee tools_output.txt
# Extract PraisonAI time with tools
PRAISON_TOOLS_TIME=$(grep -oP 'PraisonAI\s+\K[\d.]+' tools_output.txt | head -1 || echo "0")
echo "praison_tools_time=$PRAISON_TOOLS_TIME" >> $GITHUB_OUTPUT
- name: Run Parameter Impact Benchmark
id: param_impact
env:
INPUT_ITERATIONS: ${{ inputs.iterations }}
run: |
cd src/praisonai-agents
python benchmarks/param_impact_benchmark.py --no-save --iterations "$INPUT_ITERATIONS" 2>&1 | tee param_impact_output.txt
- name: Check Lazy Imports
id: lazy_imports
run: |
cd src/praisonai-agents
python -c "
import sys
from praisonaiagents import Agent
agent = Agent(name='Test', output='silent')
# Check litellm is NOT loaded
if 'litellm' in sys.modules:
print('FAIL: litellm loaded during silent agent creation')
exit(1)
else:
print('PASS: litellm not loaded (lazy import working)')
"
- name: Generate Benchmark Summary
if: always()
run: |
echo "## Performance Benchmark Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Value | Threshold | Status |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|-----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Import Time | ${{ steps.import_time.outputs.import_time }}ms | <200ms | ${{ steps.import_time.outputs.import_passed == 'true' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Peak Memory | ${{ steps.memory.outputs.peak_memory }}MB | <30MB | ${{ steps.memory.outputs.memory_passed == 'true' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Agent Instantiation | ${{ steps.simple.outputs.praison_time }}μs | - | ℹ️ |" >> $GITHUB_STEP_SUMMARY
echo "| Agent with Tools | ${{ steps.tools.outputs.praison_tools_time }}μs | - | ℹ️ |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Lazy Import Check" >> $GITHUB_STEP_SUMMARY
echo "✅ litellm not loaded during silent agent creation" >> $GITHUB_STEP_SUMMARY
- name: Fail on Regression
if: inputs.fail_on_regression == 'true'
run: |
if [ "${{ steps.import_time.outputs.import_passed }}" == "false" ]; then
echo "::error::Import time regression detected"
exit 1
fi
if [ "${{ steps.memory.outputs.memory_passed }}" == "false" ]; then
echo "::error::Memory usage regression detected"
exit 1
fi
echo "All performance checks passed!"
-43
View File
@@ -1,43 +0,0 @@
name: Bot PR Recovery
# Belt-and-braces: kick CodeRabbit/Qodo when pull_request:opened never fired for bot PRs.
on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch:
inputs:
pr_number:
description: 'Specific PR number to kick (optional)'
required: false
type: string
permissions:
pull-requests: write
issues: write
jobs:
recover:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Find and kick stalled bot PRs
uses: actions/github-script@v7
env:
PR_NUMBER_INPUT: ${{ github.event.inputs.pr_number }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const manual = (process.env.PR_NUMBER_INPUT || '').trim();
const prNumber = manual ? parseInt(manual, 10) : null;
if (manual && Number.isNaN(prNumber)) {
throw new Error(`Invalid pr_number: ${manual}`);
}
await chain.recoverStalledBotPrs(github, owner, repo, { prNumber }, core);
-43
View File
@@ -1,43 +0,0 @@
name: Build Claude Code Image
on:
workflow_dispatch:
inputs:
version:
description: 'Image version tag'
required: true
default: 'latest'
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./.github/actions/claude-code-action
push: true
platforms: linux/amd64
tags: ghcr.io/mervinpraison/praisonai-claudecode:${{ inputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
-102
View File
@@ -1,102 +0,0 @@
name: Auto CI Failure → Claude
# Detects internal PRs with failing CI and posts @claude with extracted failure details.
on:
workflow_run:
workflows: [Core Tests, Optimized Test Suite]
types: [completed]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
pr_number:
description: 'Optional PR number to process (default: scan blocked PRs)'
required: false
type: string
schedule:
- cron: '15 */6 * * *'
concurrency:
group: ci-failure-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.event.workflow_run.id || github.run_id }}
cancel-in-progress: true
env:
# PAT posts as MervinPraison so issue_comment triggers claude.yml (GITHUB_TOKEN cannot chain workflows)
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
permissions:
pull-requests: read
issues: write
actions: read
jobs:
detect-and-trigger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Detect CI failures and trigger Claude
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const ciFix = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/ci-failure-claude.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const baseRepo = `${owner}/${repo}`;
async function processPR(prNumber) {
return ciFix.maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
}
let results = [];
if (context.eventName === 'workflow_run') {
const wr = context.payload.workflow_run;
const result = await ciFix.processWorkflowRunFailure(
github, owner, repo, wr, core
);
results.push({ event: 'workflow_run', workflow: wr.name, ...result });
} else if (context.eventName === 'pull_request') {
results.push({
pr: context.payload.pull_request.number,
...(await processPR(context.payload.pull_request.number)),
});
} else {
let prNumbers = [];
if (context.eventName === 'workflow_dispatch' && context.payload.inputs?.pr_number) {
prNumbers = [Number(context.payload.inputs.pr_number)];
} else {
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
for (const pr of prs) {
if (pr.draft) continue;
const headRepo = pr.head?.repo?.full_name;
if (headRepo && headRepo !== baseRepo) continue;
const { data: issue } = await github.rest.issues.get({
owner,
repo,
issue_number: pr.number,
});
const labels = issue.labels.map((l) => l.name);
if (labels.includes('pipeline/blocked:ci') || labels.includes(ciFix.CI_FIX_LABEL)) {
prNumbers.push(pr.number);
}
}
}
for (const num of prNumbers) {
results.push({ pr: num, ...(await processPR(num)) });
}
}
core.summary.addRaw('```json\n' + JSON.stringify(results, null, 2) + '\n```');
await core.summary.write();
-35
View File
@@ -1,35 +0,0 @@
name: Claude Issue Triage
description: "Automatically triage GitHub issues using Claude Code"
on:
workflow_dispatch:
# issues:
# types: [opened]
jobs:
triage-issue:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
id-token: write
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
token: ${{ steps.app-token.outputs.token }}
- name: Run Claude Issue Triage
uses: ./.github/actions/claude-issue-triage-action
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.app-token.outputs.token }}
-532
View File
@@ -1,532 +0,0 @@
name: Claude PR Merge Gate
# Read-only Claude assesses merge readiness; GH_TOKEN merges (no @claude, no gh pr review).
# Does not overlap with merge-conflict-claude.yml or auto-pr-comment @claude chains.
on:
schedule:
# Scan for merge-ready PRs every 10 minutes (assess runs only when candidates exist).
- cron: '*/10 * * * *'
workflow_dispatch:
inputs:
pr_number:
description: 'Optional PR number (scan all open internal PRs if empty)'
required: false
type: string
merge_method:
description: 'Merge method'
required: false
type: choice
default: merge
options:
- merge
- squash
- rebase
workflow_run:
workflows:
- Claude Assistant
- Core Tests
types:
- completed
repository_dispatch:
types:
- claude-merge-gate
concurrency:
group: claude-merge-gate-${{ github.repository }}-${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || github.event.workflow_run.id || 'scan' }}
cancel-in-progress: false
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
MAX_CANDIDATES: 3
DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
permissions:
contents: read
pull-requests: read
issues: read
actions: read
jobs:
auto-dispatch:
if: github.event_name == 'workflow_run'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
actions: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Dispatch merge gate when PR pipeline is ready
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const wr = context.payload.workflow_run;
const owner = context.repo.owner;
const repo = context.repo.repo;
if (wr.conclusion !== 'success') {
core.info(`Skip dispatch: ${wr.name} conclusion=${wr.conclusion}`);
return;
}
const allowed = ['Claude Assistant', 'Core Tests'];
if (!allowed.includes(wr.name)) {
core.info(`Skip dispatch: workflow ${wr.name}`);
return;
}
const prNumber = await mergeGate.resolvePrNumberFromWorkflowRun(github, owner, repo, wr);
if (!prNumber) {
core.info('Skip dispatch: could not resolve PR from workflow run');
return;
}
const fromClaude = wr.name === 'Claude Assistant';
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, {
skipGlobalClaudeRunCheck: fromClaude,
skipRecentClaudeCooldown: fromClaude,
}
);
if (!check.ready) {
core.info(`PR #${prNumber} not ready: ${check.reasons.join(', ')}`);
return;
}
await github.rest.repos.createDispatchEvent({
owner,
repo,
event_type: 'claude-merge-gate',
client_payload: { pr_number: prNumber },
});
core.info(`Dispatched merge gate for PR #${prNumber} after ${wr.name}`);
scan-candidates:
if: github.event_name != 'workflow_run'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: write
actions: read
id-token: write
outputs:
matrix: ${{ steps.scan.outputs.matrix }}
has_candidates: ${{ steps.scan.outputs.has_candidates }}
steps:
- uses: actions/checkout@v4
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Scan open PRs for merge gate eligibility
id: scan
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const maxCandidates = parseInt(process.env.MAX_CANDIDATES || '5', 10);
const inputPr = String(process.env.DISPATCH_PR_NUMBER || '').trim();
const candidates = [];
const skipped = [];
if (inputPr) {
const n = parseInt(inputPr, 10);
if (Number.isNaN(n)) {
throw new Error(`Invalid pr_number input: ${inputPr}`);
}
const result = await mergeGate.evaluatePipelineQuiescent(github, owner, repo, n, core);
if (result.ready) {
candidates.push({ pr_number: n, head_sha: result.headSha });
} else {
skipped.push({ pr: n, reasons: result.reasons });
}
} else {
const prNumbers = await mergeGate.listPrNumbersForMergeGateScan(
github, owner, repo, core
);
const selected = await mergeGate.selectMergeGateCandidates(
github, owner, repo, prNumbers, maxCandidates, core
);
candidates.push(...selected.candidates);
skipped.push(...selected.skipped);
}
core.info('Skipped: ' + JSON.stringify(skipped));
core.setOutput('matrix', JSON.stringify({ include: candidates }));
core.setOutput('has_candidates', candidates.length > 0 ? 'true' : 'false');
core.summary.addRaw('## Merge gate scan\n');
core.summary.addRaw(`Candidates: ${candidates.length}\n`);
core.summary.addRaw('```json\n' + JSON.stringify({ candidates, skipped }, null, 2) + '\n```');
await core.summary.write();
if (inputPr && skipped.length === 1 && skipped[0].pr === parseInt(inputPr, 10)) {
const reasons = skipped[0].reasons;
const body = [
'**Merge gate scan** — not eligible for auto-merge.',
'',
...reasons.map((r) => `- \`${r}\``),
'',
'**Actions:** wait for CI and the Claude review chain, or add label `needs-manual-review` and merge manually.',
'**Opt out:** label `no-auto-merge`.',
].join('\n');
await github.rest.issues.createComment({
owner, repo, issue_number: skipped[0].pr, body,
});
} else if (inputPr && candidates.length === 1) {
await github.rest.issues.createComment({
owner, repo, issue_number: candidates[0].pr_number,
body: '**Merge gate scan** — eligible for assessment. Claude merge gate will assess and may auto-merge if `MERGE_GATE_VERDICT: APPROVE`.',
});
}
await ps.syncOpenPullRequests(github, owner, repo, { maxPrs: 10 }, core);
claude-assess:
needs: scan-candidates
if: needs.scan-candidates.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.scan-candidates.outputs.matrix) }}
permissions:
contents: read
pull-requests: read
issues: write
actions: read
id-token: write
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Mark merge gate active
id: mark_active
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
core.setOutput('started_at', new Date().toISOString());
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ matrix.pr_number }},
labels: ['claude-merge-gate-active'],
});
- name: Stash merge gate helpers
run: cp .github/scripts/merge-gate.js /tmp/merge-gate.js
- name: Fetch PR branch
env:
PR_NUMBER: ${{ matrix.pr_number }}
run: |
git fetch origin "pull/${PR_NUMBER}/head:pr-${PR_NUMBER}"
git checkout "pr-${PR_NUMBER}"
- name: Claude merge gate assessment (read-only)
id: assess
uses: ./.github/actions/claude-code-action
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
CLAUDE_CODE_SUBAGENT_MODEL: inherit
ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4-6
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.app-token.outputs.token }}
model: claude-opus-4-8
trigger_phrase: '@claude-merge-gate-internal'
direct_prompt: |
You are the MERGE GATE reviewer for PraisonAI PR #${{ matrix.pr_number }}.
READ-ONLY: do not modify code, commit, push, rebase, or create branches.
CRITICAL OUTPUT RULE:
Post exactly one PR comment via GitHub MCP containing either:
MERGE_GATE_VERDICT: APPROVE
or
MERGE_GATE_VERDICT: BLOCK
followed by a short rationale on the next lines.
NEVER include @claude in any comment. NEVER mention merge conflict resolution.
Review checklist:
1. Read AGENTS.md and the PR diff on the current branch.
2. Read all PR comments and reviews (FINAL architecture reviewer / Lead Engineer must have run).
3. Confirm SDK value gate: change strengthens praisonaiagents (no scope creep). The aim is a LIGHTWEIGHT AND POWERFUL package — BLOCK changes that add params/modules/exports duplicating existing Agent capabilities (instructions/backstory/tools/hooks/memory), or add knobs/fields with no live consumer, i.e. scope creep for the sake of it.
4. Repo routing: BLOCK if agent-callable tools (→ PraisonAI-Tools), lifecycle plugins (→ PraisonAI-Plugins), or optional sandbox backends (→ PraisonAI-Plugins via `praisonai.sandbox`) were added to praisonaiagents/ instead of the external repo.
5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
6. If agent.py adds >100 lines or new Agent __init__ params, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
7. BLOCK if labels security/breaking-change/needs-manual-review/release, sensitive paths (.github/workflows, auth, pyproject.toml), secrets in diff, PR >800 lines or >30 files, or SDK changes without tests. Label `merge-gate-ci-only` exempts CI-only changes under `.github/workflows/`, `.github/actions/`, or merge-gate scripts.
8. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
9. If ready to merge: MERGE_GATE_VERDICT: APPROVE. Otherwise: MERGE_GATE_VERDICT: BLOCK with reasons.
Do not approve or merge via GitHub UI/API — only post the verdict comment.
disallowed_tools: |
Bash
Edit
Write
Replace
allowed_tools: |
View
GlobTool
GrepTool
Read
timeout_minutes: 15
- name: Post fallback BLOCK if Opus did not comment
id: fallback_verdict
if: steps.assess.outcome != 'success'
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const mergeGate = require('/tmp/merge-gate.js');
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = ${{ matrix.pr_number }};
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const existing = mergeGate.findMergeGateVerdict(
ctx.comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (existing) {
core.info(`Opus verdict already posted: ${existing}`);
return;
}
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
const lines = [
'MERGE_GATE_VERDICT: BLOCK',
'',
'Automated fallback — Opus merge gate assessment did not complete.',
];
if (check.ready) {
lines.push('Deterministic gates passed, but Opus MERGE_GATE_VERDICT is required before merge.');
} else {
lines.push('Blockers:', ...check.reasons.map((r) => `- ${r}`));
}
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
body: lines.join('\n'),
});
core.info('Posted fallback BLOCK — Opus verdict required');
- name: Require Opus MERGE_GATE_VERDICT
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const mergeGate = require('/tmp/merge-gate.js');
const comments = await mergeGate.listAllComments(
github, context.repo.owner, context.repo.repo, ${{ matrix.pr_number }}
);
const ctx = await mergeGate.loadPrContext(
github, context.repo.owner, context.repo.repo, ${{ matrix.pr_number }}
);
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const verdict = mergeGate.findMergeGateVerdict(
comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (!verdict) {
core.setFailed(
'Opus merge gate assessment must post MERGE_GATE_VERDICT on this PR (automated fallback APPROVE is not accepted).'
);
return;
}
core.info(`Opus merge gate verdict: ${verdict}`);
core.setOutput('verdict', verdict);
- name: Remove merge gate active label
if: always()
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const prNumber = ${{ matrix.pr_number }};
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
name: 'claude-merge-gate-active',
});
core.info(`Removed claude-merge-gate-active from PR #${prNumber}`);
} catch (e) {
core.info(`Label claude-merge-gate-active already removed or missing on PR #${prNumber}`);
}
- name: Fail job if assessment failed
if: steps.assess.outcome == 'failure'
run: exit 1
merge-only:
needs: [scan-candidates, claude-assess]
if: |
always() &&
needs.scan-candidates.outputs.has_candidates == 'true' &&
needs.claude-assess.result == 'success'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.scan-candidates.outputs.matrix) }}
permissions:
contents: write
pull-requests: write
issues: write
actions: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Re-check gates and merge
uses: actions/github-script@v7
env:
MERGE_METHOD: ${{ github.event.inputs.merge_method || 'merge' }}
SCAN_HEAD_SHA: ${{ matrix.head_sha }}
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = ${{ matrix.pr_number }};
const scanHeadSha = process.env.SCAN_HEAD_SHA;
const removeLabels = async () => {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: 'claude-merge-gate-active',
});
} catch (e) {
core.info('Label claude-merge-gate-active already removed or missing');
}
};
try {
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const comments = ctx.comments;
const verdictMin = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const verdict = mergeGate.findMergeGateVerdict(
comments, verdictMin, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (verdict !== 'APPROVE') {
core.info(`Skip merge: verdict=${verdict || 'NONE'}`);
await removeLabels();
return;
}
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
if (!check.ready) {
core.info(`Skip merge: ${check.reasons.join(', ')}`);
await removeLabels();
return;
}
if (check.headSha !== scanHeadSha) {
core.info(`Skip merge: HEAD moved ${scanHeadSha.slice(0, 7)} → ${check.headSha.slice(0, 7)}`);
await removeLabels();
return;
}
const method = process.env.MERGE_METHOD || 'merge';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let merged = false;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await github.rest.pulls.merge({
owner, repo, pull_number: prNumber, merge_method: method,
});
merged = true;
break;
} catch (e) {
const msg = (e.message || '').toLowerCase();
if (attempt < 3 && msg.includes('base branch was modified')) {
core.info(`Merge attempt ${attempt} failed (base moved), retrying in 8s...`);
await sleep(8000);
const recheck = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
if (!recheck.ready) {
core.info(`Skip merge retry: ${recheck.reasons.join(', ')}`);
await removeLabels();
return;
}
continue;
}
throw e;
}
}
if (!merged) {
core.info('Skip merge: exhausted retries');
await removeLabels();
return;
}
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: ['auto-merged-by-gate'],
});
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
body: [
'Merged by **Claude PR merge gate** (`claude-merge-gate.yml`).',
`Verdict: MERGE_GATE_VERDICT: APPROVE`,
`SHA: \`${check.headSha.slice(0, 7)}\``,
`Method: ${method}`,
].join('\n'),
});
core.info(`Merged PR #${prNumber}`);
} finally {
await removeLabels();
}
-21
View File
@@ -1,21 +0,0 @@
name: Claude OAuth
on:
workflow_dispatch:
inputs:
code:
description: 'Authorization code (leave empty for step 1)'
required: false
permissions:
actions: write # Required for cache management
contents: read # Required for basic repository access
jobs:
auth:
runs-on: ubuntu-latest
steps:
- uses: grll/claude-code-login@v1
with:
code: ${{ inputs.code }}
secrets_admin_pat: ${{ secrets.SECRETS_ADMIN_PAT }}
-423
View File
@@ -1,423 +0,0 @@
name: Claude Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [assigned, labeled, opened]
pull_request_review:
types: [submitted]
jobs:
# ================================================================
# Job 1: Lightweight triage for ALL new issues (including external users)
# Posts welcome comment + adds triage labels. No Claude code execution.
# ================================================================
issue-triage:
if: |
github.event_name == 'issues' &&
github.event.action == 'opened' &&
!contains(toJSON(github.event.issue.labels), 'claude')
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Triage and acknowledge issue
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const issue = context.payload.issue;
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const content = title + ' ' + body;
const isOwner = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(
issue.author_association
);
// --- Label triage ---
const labels = [];
// Kind labels (bug before "how to" — bug reports often include "## How to fix")
if (content.includes('feature request') || content.includes('[feature')) {
labels.push('enhancement');
} else if (!title.includes('[question]') && (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback'))) {
labels.push('bug');
} else if (title.includes('[question]') || content.includes('how do i') || (content.includes('how to') && !content.includes('how to fix'))) {
labels.push('question');
} else if (content.includes('question')) {
labels.push('question');
}
// Area labels
if (content.includes('security') || content.includes('cve') || content.includes('vulnerability')) {
labels.push('security');
}
if (content.includes('performance') || content.includes('slow') || content.includes('memory leak')) {
labels.push('performance');
}
if (content.includes('documentation') || content.includes('docs')) {
labels.push('documentation');
}
if (content.includes('typescript') || content.includes('javascript') || content.includes('npm')) {
labels.push('javascript');
}
// Apply labels
if (labels.length > 0) {
await github.rest.issues.addLabels({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels
});
}
// --- Acknowledgment comment (external users only) ---
if (!isOwner) {
await github.rest.issues.createComment({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: [
`👋 Thanks for opening this issue, @${issue.user.login}!`,
'',
'A maintainer will review this shortly. In the meantime:',
'- Make sure you\'ve included steps to reproduce (for bugs)',
'- Check [existing issues](https://github.com/MervinPraison/PraisonAI/issues) for duplicates',
'- Review the [documentation](https://docs.praison.ai) for related guides',
'',
'_Bug and enhancement reports are auto-triaged with Claude. Questions and other issues: a maintainer can comment `@claude` to trigger analysis._',
'',
'**Routing:** Agent-callable tools → [PraisonAI-Tools](https://github.com/MervinPraison/PraisonAI-Tools); lifecycle plugins (tracing/hooks/guardrails) → [PraisonAI-Plugins](https://github.com/MervinPraison/PraisonAI-Plugins).'
].join('\n')
});
}
// --- Auto-trigger Claude for owner issues and actionable external bug/enhancement reports ---
const titleLower = (issue.title || '').toLowerCase();
const hasActionableTitle = /^(\[[^\]]+\]|bug:|fix[:(]|feat:|ux:|enhancement:)/i.test(titleLower);
const autoClaude = isOwner || (hasActionableTitle && (
labels.includes('bug') ||
(labels.includes('enhancement') && !labels.includes('question'))
));
if (autoClaude) {
await github.rest.issues.addLabels({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['claude']
});
}
# ================================================================
# Job 2: Full Claude code-fix
# Triggers on: labeled 'claude', @claude comments, assignments
# ================================================================
claude-response:
# Allow: human users, github-actions[bot] (FINAL @claude / CI-fix comments)
# Allow: praisonai-triage-agent[bot] only for issues labeled 'claude' (auto-triage)
# Block: praisonai-triage-agent on issue_comment (merge-gate scan mentions @claude)
# Skip 'opened' events — triage job handles those above
if: |
github.event.action != 'opened' &&
(github.event.action != 'labeled' || github.event.label.name == 'claude') &&
(github.event_name != 'issue_comment' || (
contains(github.event.comment.body, '@claude') &&
!contains(github.event.comment.body, 'Merge gate scan') &&
!contains(github.event.comment.body, 'MERGE_GATE_VERDICT') &&
!contains(github.event.comment.body, 'Merged by **Claude PR merge gate**') &&
(
!contains(github.actor, '[bot]') ||
contains(github.event.comment.body, 'FINAL architecture reviewer') ||
contains(github.event.comment.body, 'lead engineer') ||
contains(github.event.comment.body, 'merge conflict') ||
contains(github.event.comment.body, 'CI failed on HEAD')
)
)) &&
(github.event_name != 'pull_request_review' || contains(github.event.review.body, '@claude')) &&
(github.event_name != 'pull_request_review_comment' || contains(github.event.comment.body, '@claude')) &&
(
!contains(github.actor, '[bot]') ||
github.actor == 'github-actions[bot]' ||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'claude' && github.actor == 'praisonai-triage-agent[bot]')
) &&
github.actor != 'dependabot[bot]' &&
github.actor != 'cursor[bot]' &&
github.actor != 'renovate[bot]'
concurrency:
group: claude-response-${{ github.repository }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
actions: read
id-token: write
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Check for Fork PR
id: check_fork
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const isPR = context.eventName === 'pull_request'
|| context.eventName === 'pull_request_review'
|| context.eventName === 'pull_request_review_comment'
|| (context.payload.issue && context.payload.issue.pull_request);
if (!isPR) {
core.setOutput('is_pr', 'false');
core.setOutput('is_fork', 'false');
core.setOutput('can_push_fork', 'false');
return;
}
const prNumber = context.payload.pull_request
? context.payload.pull_request.number
: context.payload.issue.number;
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
let mergeState = '';
try {
const gql = await github.graphql(
`query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) { mergeStateStatus }
}
}`,
{ owner: context.repo.owner, repo: context.repo.repo, number: prNumber }
);
mergeState = (gql.repository.pullRequest.mergeStateStatus || '').toUpperCase();
} catch (e) {
core.warning(`GraphQL mergeStateStatus failed: ${e.message}`);
}
if (!mergeState) {
mergeState = (pr.data.mergeable_state || pr.data.merge_state_status || '').toUpperCase();
}
core.setOutput('is_pr', 'true');
core.setOutput('pr_number', String(prNumber));
core.setOutput('pr_branch', pr.data.head.ref);
core.setOutput('head_repo', pr.data.head.repo.full_name);
core.setOutput('merge_state', mergeState);
const baseRepo = `${context.repo.owner}/${context.repo.repo}`;
const maintainerCanModify = pr.data.maintainer_can_modify === true;
core.setOutput('maintainer_can_modify', String(maintainerCanModify));
if (pr.data.head.repo && pr.data.head.repo.full_name !== baseRepo) {
core.setOutput('is_fork', 'true');
core.setOutput('can_push_fork', String(maintainerCanModify));
core.setOutput('fork_repo', pr.data.head.repo.full_name);
core.setOutput('clone_url', pr.data.head.repo.clone_url);
core.setOutput('branch', pr.data.head.ref);
return;
}
core.setOutput('is_fork', 'false');
core.setOutput('can_push_fork', 'false');
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Fetch PR branch and Setup Remote
if: steps.check_fork.outputs.is_pr == 'true'
env:
PR_BRANCH: ${{ steps.check_fork.outputs.pr_branch }}
PR_NUMBER: ${{ steps.check_fork.outputs.pr_number }}
IS_FORK: ${{ steps.check_fork.outputs.is_fork }}
CAN_PUSH_FORK: ${{ steps.check_fork.outputs.can_push_fork }}
FORK_REPO: ${{ steps.check_fork.outputs.fork_repo }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
git fetch origin "pull/${PR_NUMBER}/head:${PR_BRANCH}"
git checkout "${PR_BRANCH}"
git fetch origin main
if [ "${IS_FORK}" = "true" ] && [ "${CAN_PUSH_FORK}" = "true" ]; then
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${FORK_REPO}.git"
elif [ "${IS_FORK}" = "true" ]; then
# Block push: trick claude-code-action's `git fetch origin <branch>`
git remote set-url origin "file://$(pwd)"
fi
- uses: anthropics/claude-code-action@beta
env:
GH_TOKEN: ${{ steps.check_fork.outputs.can_push_fork == 'true' && secrets.GH_TOKEN || steps.app-token.outputs.token }}
with:
allowed_bots: 'praisonai-triage-agent[bot]'
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.app-token.outputs.token }}
model: claude-opus-4-8
trigger_phrase: "@claude"
label_trigger: "claude"
direct_prompt: |
${{ steps.check_fork.outputs.is_fork == 'true' && steps.check_fork.outputs.can_push_fork != 'true' && 'CRITICAL: THIS IS A PULL REQUEST FROM A FORK WITHOUT MAINTAINER EDITS. YOU DO NOT HAVE PUSH PERMISSIONS. ONLY READ FILES, VALIDATE CODE, AND PROVIDE COMMENTS/FEEDBACK ON THIS PR. DO NOT ATTEMPT TO PUSH OR CREATE PULL REQUESTS.\n\n' || (steps.check_fork.outputs.can_push_fork == 'true' && format('NOTE: Fork PR with maintainer edits allowed. Push directly to fork {0} branch {1}.\n\n', steps.check_fork.outputs.fork_repo, steps.check_fork.outputs.pr_branch) || (steps.check_fork.outputs.is_pr == 'true' && steps.check_fork.outputs.is_fork != 'true' && 'NOTE: The branch is under MervinPraison/PraisonAI (not a fork). You are able to make modifications and push directly to this branch.\n\n' || (steps.check_fork.outputs.is_pr != 'true' && 'NOTE: This is an issue (not a PR). Create a new fix branch per STEP 3 below.\n\n' || ''))) }}
${{ steps.check_fork.outputs.is_pr == 'true' && format('CONTEXT: PR #{0} on branch {1}. head.repo={2}, is_fork={3}, can_push_fork={4}, merge_state={5}. NEVER treat cursor/*, claude/* branch names or bot authors as forks — only is_fork=true means fork.\n\n', steps.check_fork.outputs.pr_number, steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.head_repo, steps.check_fork.outputs.is_fork, steps.check_fork.outputs.can_push_fork, steps.check_fork.outputs.merge_state) || '' }}
You are working on the PraisonAI SDK. Follow AGENTS.md strictly.
STEP 0 — SETUP GIT IDENTITY & AUTH (GLOBAL — required for all repos):
git config --global user.name "MervinPraison"
git config --global user.email "[email protected]"
gh auth setup-git
STEP 1 — READ GUIDELINES:
Read AGENTS.md to understand the architecture rules.
SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify or touch praisonai-rust or praisonai-ts packages.
AIM: This package must stay LIGHTWEIGHT AND POWERFUL. Do a CRITICAL REVIEW at each stage (issue triage, fix planning, PR review/implementation) and reject scope creep for the sake of adding features. Prefer existing Agent capabilities (instructions/backstory/tools/hooks/memory) over new params/modules/exports. Even when fixing a raised issue, keep the fix minimal and lightweight — do NOT expand the surface area or add knobs with no live consumer.
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
Before implementing anything, answer these questions:
- CORE vs WRAPPER vs TOOLS vs DOCS vs PLUGINS ROUTING:
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
3. Tools → MervinPraison/PraisonAI-Tools: agent-callable integrations at runtime (Composio, Slack, API/DB connectors). Do NOT implement in praisonaiagents/ — use STEP 3-ALT.
4. Documentation (PraisonAIDocs): Documentation pages, guides, API references.
5. Plugins → MervinPraison/PraisonAI-Plugins: framework lifecycle extensions (tracing, logging, metrics, hooks, guardrails). Not agent tools — use STEP 3-ALT.
6. Sandbox backends (optional/niche, e.g. Capsule) → PraisonAI-Plugins via `praisonai.sandbox` entry point; core keeps protocol/config/manager only.
For items routed to an EXTERNAL repository (Tools, Docs, Plugins), follow STEP 3-ALT below.
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory). If it does, reject the change as scope creep and prefer the existing capability — the aim is lightweight and powerful, not more knobs.
- Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead. PRs with >100 lines added to agent.py require manual review (merge gate will not auto-merge).
- Is there a name collision with existing exports in __init__.py?
If ANY of these conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
${{ steps.check_fork.outputs.is_pr == 'true' && (steps.check_fork.outputs.is_fork != 'true' || steps.check_fork.outputs.can_push_fork == 'true') && steps.check_fork.outputs.merge_state == 'DIRTY' && format('STEP 3 — RESOLVE MERGE CONFLICTS ON PR (branch: {0}, PR #{1}{2}):\n- DO NOT create a new branch or PR\n- git fetch origin main && git rebase origin/main\n- Resolve conflicts: keep this PR intent, merge in main newer logic\n- git add -A && git rebase --continue (repeat until done)\n- git push --force-with-lease origin {0}\n- Run targeted pytest on changed paths\n- Comment files resolved and what you skipped\n', steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.pr_number, steps.check_fork.outputs.can_push_fork == 'true' && format(', fork: {0}', steps.check_fork.outputs.fork_repo) || '') || '' }}
${{ steps.check_fork.outputs.is_pr == 'true' && (steps.check_fork.outputs.is_fork != 'true' || steps.check_fork.outputs.can_push_fork == 'true') && steps.check_fork.outputs.merge_state != 'DIRTY' && format('STEP 3 — IMPLEMENT ON EXISTING PR (branch: {0}, PR #{1}{2}):\n- DO NOT create a new branch or PR\n- git checkout {0}\n- Implement minimal, focused fixes from reviewer feedback\n- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper\n- Keep changes small and backward-compatible\n- Commit: git add -A && git commit -m "fix: <description>"\n- Push: git push origin {0}\n- Comment a summary of exact files modified and what you skipped\n', steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.pr_number, steps.check_fork.outputs.can_push_fork == 'true' && format(', fork: {0}', steps.check_fork.outputs.fork_repo) || '') || '' }}
${{ steps.check_fork.outputs.is_pr != 'true' && 'STEP 3 — IMPLEMENT (for changes in THIS repo — PraisonAI):\n- Create a fix branch: git checkout -b claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\n- Implement a minimal, focused fix\n- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper\n- Keep changes small and backward-compatible\n- Commit: git add -A && git commit -m "fix: <description> (fixes #$ISSUE_NUMBER)"\n- Push: git push origin claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\nCRITICAL: After pushing, you MUST immediately run: gh pr create --title "fix: <title>" --body "Fixes #$ISSUE_NUMBER" --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\nDo NOT stop after pushing. Do NOT say "manually create a PR". Always create the PR automatically.\n' || '' }}
STEP 3-ALT — IMPLEMENT IN EXTERNAL REPO (for PraisonAI-Tools, PraisonAIDocs, PraisonAI-Plugins):
When work must happen in a different repository, follow these steps EXACTLY. Do NOT attempt to use `cd`, as directory state is not preserved. Use `git -C` for all commands.
a) Clone the repository:
gh repo clone MervinPraison/<REPO_NAME> /tmp/<REPO_NAME>
b) Copy GitHub authentication from the main repository so push works seamlessly:
git -C /tmp/<REPO_NAME> config http."https://github.com/".extraheader "$(git config --get http."https://github.com/".extraheader)"
c) Create a feature branch (NEVER commit to main):
git -C /tmp/<REPO_NAME> checkout -b claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
d) Make your changes using absolute paths (e.g. edit /tmp/<REPO_NAME>/docs.json)
e) Commit:
git -C /tmp/<REPO_NAME> add -A
git -C /tmp/<REPO_NAME> commit -m "feat: <description> (fixes #$ISSUE_NUMBER)"
f) Push the branch:
git -C /tmp/<REPO_NAME> push origin claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
g) Create PR in the EXTERNAL repo:
gh pr create -R MervinPraison/<REPO_NAME> --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d) --title "feat: <title>" --body "Fixes MervinPraison/PraisonAI#$ISSUE_NUMBER
<body>"
STEP 4 — TEST:
- For SDK changes: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
- For docs changes: verify files exist and are valid markdown/MDX
- Ensure no regressions
${{ steps.check_fork.outputs.is_pr == 'true' && 'STEP 5 — SKIP: Already on an open PR. Do NOT run gh pr create.\n' || 'STEP 5 — CREATE PR:\n- Commit with descriptive message, push, and create PR using `gh pr create`\n- For external repos, use `gh pr create -R MervinPraison/<REPO_NAME>`\nCRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link or say "manual push required".\n' }}
NOTE: If you worked on an external repo, the wrapper action will report "No commits" for the main repo at the end. This is expected, do not worry about it.
allowed_tools: |
Bash(git:*)
Bash(python:*)
Bash(pip:*)
Bash(conda:*)
Bash(pytest:*)
Bash(gh:*)
Bash(python -m pytest:*)
Bash(python -m pip:*)
Bash(poetry:*)
View
GlobTool
GrepTool
BatchTool
Edit
Replace
mcp__github__get_issue
mcp__github__get_issue_comments
mcp__github__update_issue
timeout_minutes: 30
- name: Kick review chain for bot-created PR
if: |
steps.check_fork.outputs.is_pr != 'true' &&
(github.event_name == 'issues' || github.event_name == 'issue_comment') &&
github.event.issue != null
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
const issueNumber = context.payload.issue.number;
await chain.kickReviewChainForIssue(
github, context.repo.owner, context.repo.repo, issueNumber, core
);
- name: Sync pipeline status labels for PR
if: steps.check_fork.outputs.is_pr == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const prNumber = parseInt('${{ steps.check_fork.outputs.pr_number }}', 10);
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, prNumber, core);
- name: Dispatch merge gate when PR pipeline ready
if: steps.check_fork.outputs.is_pr == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = parseInt('${{ steps.check_fork.outputs.pr_number }}', 10);
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, {
skipGlobalClaudeRunCheck: true,
skipRecentClaudeCooldown: true,
}
);
if (!check.ready) {
core.info(`PR #${prNumber} not ready for merge gate: ${check.reasons.join(', ')}`);
return;
}
await github.rest.repos.createDispatchEvent({
owner,
repo,
event_type: 'claude-merge-gate',
client_payload: { pr_number: prNumber },
});
core.info(`Dispatched merge gate for PR #${prNumber}`);
@@ -1,77 +0,0 @@
name: Deploy install scripts to praison.ai
# Production praison.ai runs on Hestia (migration-target), not Azure AKS.
# Uses scoped deploy user (praison-deploy) — not root.
on:
push:
branches: [main]
paths:
- 'src/praisonai/scripts/install.sh'
- 'src/praisonai/scripts/install.ps1'
workflow_dispatch:
concurrency:
group: deploy-install-scripts
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy-to-hestia:
runs-on: ubuntu-latest
environment: production-hestia
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
- name: Sync install scripts to Hestia
env:
SSH_PRIVATE_KEY: ${{ secrets.MIGRATION_TARGET_SSH_KEY }}
HESTIA_HOST: ${{ secrets.MIGRATION_TARGET_HOST }}
HESTIA_USER: ${{ secrets.MIGRATION_TARGET_USER }}
HESTIA_KNOWN_HOSTS: ${{ secrets.MIGRATION_TARGET_KNOWN_HOSTS }}
SOURCE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
for var in SSH_PRIVATE_KEY HESTIA_HOST HESTIA_USER; do
if [ -z "${!var:-}" ]; then
echo "Missing secret for deploy (need MIGRATION_TARGET_SSH_KEY, MIGRATION_TARGET_HOST, MIGRATION_TARGET_USER)"
exit 1
fi
done
REMOTE_WEB="/home/hestiaadmin/web/praison.ai/pa/web"
SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=yes"
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
trap 'rm -f ~/.ssh/id_ed25519' EXIT
# Pin host identity via an out-of-band secret (no TOFU/ssh-keyscan).
if [ -z "${HESTIA_KNOWN_HOSTS:-}" ]; then
echo "Missing secret MIGRATION_TARGET_KNOWN_HOSTS (pinned host key). Refusing to deploy without a verified host identity."
exit 1
fi
echo "$HESTIA_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
# Upload to temp files, then atomically move both into place in a
# single remote step to avoid serving truncated or mixed-commit files.
for f in install.sh install.ps1; do
echo "Syncing $f..."
scp $SSH_OPTS "src/praisonai/scripts/$f" "${HESTIA_USER}@${HESTIA_HOST}:${REMOTE_WEB}/.${f}.tmp"
done
ssh $SSH_OPTS "${HESTIA_USER}@${HESTIA_HOST}" \
"mv -f ${REMOTE_WEB}/.install.sh.tmp ${REMOTE_WEB}/install.sh && \
mv -f ${REMOTE_WEB}/.install.ps1.tmp ${REMOTE_WEB}/install.ps1 && \
sudo chown hestiaadmin:hestiaadmin ${REMOTE_WEB}/install.sh ${REMOTE_WEB}/install.ps1 && \
sudo chmod 644 ${REMOTE_WEB}/install.sh ${REMOTE_WEB}/install.ps1"
echo "Deployed install scripts for commit ${SOURCE_SHA}"
echo "Verify: curl -fsSL https://praison.ai/install.ps1 | head -5"
-56
View File
@@ -1,56 +0,0 @@
name: Publish Docker
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Docker image tag (default: latest)'
required: false
default: 'latest'
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/mervinpraison/praisonai
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }}
type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'release' }}
type=raw,value=${{ github.event.inputs.tag || 'latest' }},enable=${{ github.event_name == 'workflow_dispatch' }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -1,79 +0,0 @@
name: Gemini Automated Issue Triage
# on:
# issue_comment:
# types: [created]
on:
workflow_dispatch:
jobs:
triage-issue:
if: >
github.event_name == 'issue_comment' &&
!github.event.issue.pull_request &&
contains(github.event.comment.body, '@gemini triage') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR')
timeout-minutes: 5
permissions:
issues: write
contents: read
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
token: ${{ steps.generate_token.outputs.token }}
- name: Run Gemini Issue Triage
uses: google-gemini/gemini-cli-action@main
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPOSITORY: ${{ github.repository }}
with:
version: 0.1.8-rc.0
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OTLP_GCP_WIF_PROVIDER: ${{ secrets.OTLP_GCP_WIF_PROVIDER }}
OTLP_GOOGLE_CLOUD_PROJECT: ${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}
settings_json: |
{
"coreTools": [
"run_shell_command(gh label list)",
"run_shell_command(gh issue edit)"
],
"telemetry": {
"enabled": true,
"target": "gcp"
},
"sandbox": false
}
prompt: |
You are an issue triage assistant. Analyze the current GitHub issue and apply the most appropriate existing labels.
Steps:
1. Run: `gh label list --limit 100` to get all available labels.
2. Review the issue title and body provided in the environment variables.
3. Select the most relevant labels from the existing labels, focusing on kind/*, area/*, and priority/*.
4. Apply the selected labels to this issue using: `gh issue edit ISSUE_NUMBER --add-label "label1,label2"`
Guidelines:
- Only use labels that already exist in the repository.
- Do not add comments or modify the issue content.
- Triage only the current issue.
- Assign all applicable kind/*, area/*, and priority/* labels based on the issue content.
-201
View File
@@ -1,201 +0,0 @@
name: Gemini Issue Review
on:
workflow_dispatch:
# issue_comment:
# types: [created]
permissions:
contents: write
issues: write
pull-requests: read
jobs:
review-issue:
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
!github.event.issue.pull_request &&
contains(github.event.comment.body, '@gemini') &&
!contains(github.event.comment.body, '@gemini triage') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR' ||
github.event.comment.author_association == 'CONTRIBUTOR'))
runs-on: ubuntu-latest
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
token: ${{ steps.generate_token.outputs.token }}
- name: Set up Git
run: |
git config --local user.email "[email protected]"
git config --local user.name "MervinPraison"
- name: Get issue data
id: issue_data
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
# Pass user-controlled inputs via environment variables to prevent script injection (GHSL-2025-093)
EVENT_NAME: ${{ github.event_name }}
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
run: |
# Get issue number from event or context
if [ "$EVENT_NAME" = "issue_comment" ]; then
ISSUE_NUMBER="$EVENT_ISSUE_NUMBER"
elif [ "$EVENT_NAME" = "issues" ]; then
ISSUE_NUMBER="$EVENT_ISSUE_NUMBER"
else
echo "Unable to determine issue number"
exit 1
fi
# Get issue details
gh issue view $ISSUE_NUMBER --json title,body,labels,assignees > issue_details.json
# Extract additional instructions from comment if triggered by comment
ADDITIONAL_INSTRUCTIONS=""
if [ "$EVENT_NAME" = "issue_comment" ]; then
COMMENT_BODY="$EVENT_COMMENT_BODY"
ADDITIONAL_INSTRUCTIONS=$(echo "$COMMENT_BODY" | sed -n 's/.*@gemini[[:space:]]*\(.*\)/\1/p' | head -1)
fi
echo "ISSUE_NUMBER=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
echo "ADDITIONAL_INSTRUCTIONS=$ADDITIONAL_INSTRUCTIONS" >> $GITHUB_OUTPUT
- name: Run Gemini CLI
uses: google-github-actions/run-gemini-cli@v0
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
ISSUE_NUMBER: ${{ steps.issue_data.outputs.ISSUE_NUMBER }}
ADDITIONAL_INSTRUCTIONS: ${{ steps.issue_data.outputs.ADDITIONAL_INSTRUCTIONS }}
REPOSITORY: ${{ github.repository }}
with:
gemini_api_key: ${{ secrets.GEMINI_API_KEY }}
gemini_model: 'gemini-2.5-flash'
prompt: |
You are a helpful GitHub issue assistant. Your task is to analyze and help with GitHub issues.
**Issue Details:**
- Issue Number: #${ISSUE_NUMBER}
- Repository: ${REPOSITORY}
- Issue URL: ${{ github.event.issue.html_url }}
**Additional Instructions:** ${ADDITIONAL_INSTRUCTIONS}
**CRITICAL: When making code modifications, follow this EXACT workflow:**
**STEP 1: Analyze the Issue**
- Use: `gh issue view ${ISSUE_NUMBER} --json title,body,labels,assignees`
- Analyze issue content thoroughly
- Search codebase for related files using `glob`, `search_file_content`, `list_directory`
**STEP 2: If Code Changes Are Needed**
MANDATORY FIRST STEP: Before writing any code, you MUST use `read_file` to carefully read "src/praisonai-agents/AGENTS.md". You must honor this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy!
Create a new branch with 'gemini/' prefix:
- Use: `git checkout -b gemini/issue-${ISSUE_NUMBER}-description`
- Example: `git checkout -b gemini/issue-123-fix-sequential-tools`
**STEP 3: Make Code Modifications**
- Use `read_file` to examine existing code
- Use `write_file` or `replace` to make changes
- Use `search_file_content` to find related code
- Make focused, targeted changes to fix the issue
**STEP 4: Test Changes (if applicable)**
- Run relevant tests if test files exist
- Use `run_shell_command` to execute test commands
**STEP 5: Commit and Push Changes**
- Use: `git add .` (or specific files)
- Use: `git commit -m "Gemini: [Issue #${ISSUE_NUMBER}] Brief description of fix"`
- Use: `git push -u origin gemini/issue-${ISSUE_NUMBER}-description`
**STEP 6: Create Pull Request**
- Use: `gh pr create --title "Fix #${ISSUE_NUMBER}: Issue description" --body "Fixes #${ISSUE_NUMBER}\n\nSummary of changes:\n- Change 1\n- Change 2" --head gemini/issue-${ISSUE_NUMBER}-description --base main`
- This will return a PR URL like: https://github.com/repo/pull/123
**STEP 7: Comment on Issue with Results**
Use: `gh issue comment ${ISSUE_NUMBER} -b "MESSAGE"`
**Your final comment MUST include:**
- Summary of what was analyzed
- What changes were made (if any)
- Link to the created PR (if applicable)
- Brief explanation of the solution
**Example final comment:**
```
Hi! I've analyzed and fixed this issue.
**Problem:** Sequential tool calling was getting stuck after first tool call.
**Solution:** Added proper prompt handling after each tool call in the Ollama model integration.
**Changes Made:**
- Modified `src/praisonai-agents/ollama-sequential.py`
- Added continuation prompts for multi-step tool execution
**Pull Request:** https://github.com/MervinPraison/PraisonAI/pull/856
The fix is ready for review. Please test and let me know if you encounter any issues!
```
**IMPORTANT NOTES:**
- Always create branches with 'gemini/' prefix
- Always include issue number in branch name and commit message
- Always create PR that references the issue
- Always provide PR link in your final response
- Use the GITHUB_TOKEN environment variable for authentication
Please proceed with helping resolve this issue following this exact workflow.
settings: |
{
"coreTools": [
"run_shell_command(echo)",
"run_shell_command(gh issue view)",
"run_shell_command(gh issue comment)",
"run_shell_command(gh issue list)",
"run_shell_command(gh issue edit)",
"run_shell_command(gh pr create)",
"run_shell_command(gh pr view)",
"run_shell_command(gh pr list)",
"run_shell_command(cat)",
"run_shell_command(head)",
"run_shell_command(tail)",
"run_shell_command(grep)",
"run_shell_command(find)",
"run_shell_command(git checkout)",
"run_shell_command(git add)",
"run_shell_command(git commit)",
"run_shell_command(git push)",
"run_shell_command(git status)",
"run_shell_command(git branch)",
"list_directory",
"read_file",
"read_many_files",
"write_file",
"replace",
"glob",
"search_file_content",
"web_fetch",
"google_web_search",
"save_memory"
]
}
-251
View File
@@ -1,251 +0,0 @@
name: Gemini PR Review
on:
# issue_comment:
# types: [created]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: number
jobs:
review-pr:
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '@gemini') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
timeout-minutes: 15
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
pull-requests: write
issues: write
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- name: Determine checkout ref
id: checkout_ref
run: |
if [ "${{ github.event_name }}" = "issue_comment" ]; then
echo "ref=refs/pull/${{ github.event.issue.number }}/head" >> "$GITHUB_OUTPUT"
else
echo "ref=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
fi
- name: Checkout PR code
uses: actions/checkout@v4
with:
persist-credentials: false
token: ${{ steps.generate_token.outputs.token }}
ref: ${{ steps.checkout_ref.outputs.ref }}
fetch-depth: 0
- name: Get PR details
id: get_pr
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
# Pass user-controlled inputs via environment variables to prevent script injection (GHSL-2025-093)
EVENT_NAME: ${{ github.event_name }}
EVENT_INPUTS_PR_NUMBER: ${{ github.event.inputs.pr_number }}
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
PR_NUMBER=$EVENT_INPUTS_PR_NUMBER
elif [ "$EVENT_NAME" = "issue_comment" ]; then
PR_NUMBER=$EVENT_ISSUE_NUMBER
else
PR_NUMBER=$EVENT_PR_NUMBER
fi
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
# Extract additional instructions from comment (if triggered by comment)
ADDITIONAL_INSTRUCTIONS=""
if [ "$EVENT_NAME" = "issue_comment" ]; then
COMMENT_BODY="$EVENT_COMMENT_BODY"
ADDITIONAL_INSTRUCTIONS=$(echo "$COMMENT_BODY" | sed 's/.*@gemini//' | xargs)
fi
echo "additional_instructions=$ADDITIONAL_INSTRUCTIONS" >> "$GITHUB_OUTPUT"
# Get PR details
PR_DATA=$(gh pr view $PR_NUMBER --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)
echo "pr_data=$PR_DATA" >> "$GITHUB_OUTPUT"
# Get file changes
CHANGED_FILES=$(gh pr diff $PR_NUMBER --name-only)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$CHANGED_FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Run Gemini PR Review
uses: ./.github/actions/gemini
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
PR_DATA: ${{ steps.get_pr.outputs.pr_data }}
CHANGED_FILES: ${{ steps.get_pr.outputs.changed_files }}
ADDITIONAL_INSTRUCTIONS: ${{ steps.get_pr.outputs.additional_instructions }}
REPOSITORY: ${{ github.repository }}
with:
version: 0.1.8-rc.0
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OTLP_GCP_WIF_PROVIDER: ${{ secrets.OTLP_GCP_WIF_PROVIDER }}
OTLP_GOOGLE_CLOUD_PROJECT: ${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}
settings_json: |
{
"coreTools": [
"run_shell_command(echo)",
"run_shell_command(gh pr view)",
"run_shell_command(gh pr diff)",
"run_shell_command(gh pr comment)",
"run_shell_command(gh pr list)",
"run_shell_command(cat)",
"run_shell_command(head)",
"run_shell_command(tail)",
"run_shell_command(grep)",
"run_shell_command(find)",
"run_shell_command(git checkout)",
"run_shell_command(git add)",
"run_shell_command(git commit)",
"run_shell_command(git push)",
"run_shell_command(git status)",
"run_shell_command(git branch)",
"run_shell_command(git fetch)",
"list_directory",
"read_file",
"read_many_files",
"write_file",
"replace",
"glob",
"search_file_content",
"web_fetch",
"google_web_search",
"save_memory"
],
"telemetry": {
"enabled": false,
"target": "gcp"
},
"sandbox": false
}
prompt: |
You are an expert code reviewer and AI assistant for Pull Requests. You can review code AND make modifications to improve the PR.
**PR Details:**
- PR Number: #$PR_NUMBER
- Repository: $REPOSITORY
- Additional Instructions: $ADDITIONAL_INSTRUCTIONS
**CRITICAL: When making code modifications, follow this EXACT workflow:**
**STEP 1: Analyze the PR**
- Use: `echo "$PR_DATA"` to get PR details (JSON format)
- Use: `echo "$CHANGED_FILES"` to get changed files list
- Use: `gh pr diff ${PR_NUMBER}` to see the full diff
- Use: `gh pr view ${PR_NUMBER} --json title,body,headRefName` to get PR info
- Analyze the code changes thoroughly
**STEP 2: If Code Improvements Are Needed**
MANDATORY FIRST STEP: Before writing any code, you MUST use `read_file` to carefully read "src/praisonai-agents/AGENTS.md". You must honor this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy!
**IMPORTANT:** DO NOT create a new branch! Work on the EXISTING PR branch.
- Use: `git fetch origin` to get latest changes
- Get PR branch name: `gh pr view ${PR_NUMBER} --json headRefName`
- Switch to PR branch: `git checkout BRANCH_NAME` (use actual branch name from PR)
- Use: `git pull origin BRANCH_NAME` to get latest PR changes
**STEP 3: Make Code Modifications**
- Use `read_file` to examine existing code
- Use `write_file` or `replace` to make improvements
- Use `search_file_content` to find related code
- Make focused, targeted improvements to the PR
**STEP 4: Test Changes (if applicable)**
- Run relevant tests if test files exist
- Use `run_shell_command` to execute test commands
**STEP 5: Commit and Push Changes to EXISTING PR Branch**
- Use: `git add .` (or specific files)
- Use: `git commit -m "Gemini: [PR #${PR_NUMBER}] Brief description of improvements"`
- Use: `git push origin BRANCH_NAME` (push to EXISTING PR branch)
**STEP 6: Review and Comment on PR**
Write comprehensive review and post comment with your analysis and any improvements made.
Use: `gh pr comment ${PR_NUMBER} -b "MESSAGE"`
Review Guidelines:
- Focus on code quality, security, performance, and maintainability
- Check for common issues: potential bugs, security vulnerabilities, performance bottlenecks
- Verify error handling and edge cases
- Look for code style and best practices
- Comment on architecture and design decisions if significant
- Be constructive and specific in feedback
- Highlight both issues and positive aspects
- Suggest improvements with examples when possible
Review Areas:
- **Security**: Authentication, authorization, input validation, data sanitization
- **Performance**: Algorithms, database queries, caching, resource usage
- **Reliability**: Error handling, logging, testing coverage, edge cases
- **Maintainability**: Code structure, documentation, naming conventions
- **Functionality**: Logic correctness, requirements fulfillment
Output Format:
Structure your review using this exact format with markdown:
## 📋 Review Summary
Provide a brief 2-3 sentence overview of the PR and overall assessment.
## 🔍 General Feedback
- List general observations about code quality
- Mention overall patterns or architectural decisions
- Highlight positive aspects of the implementation
- Note any recurring themes across files
## 🎯 Specific Feedback
Only include sections below that have actual issues. If there are no issues in a priority category, omit that entire section.
### 🔴 Critical
(Only include this section if there are critical issues)
Issues that must be addressed before merging (security vulnerabilities, breaking changes, major bugs):
- **File: `filename:line`** - Description of critical issue with specific recommendation
### 🟡 High
(Only include this section if there are high priority issues)
Important issues that should be addressed (performance problems, design flaws, significant bugs):
- **File: `filename:line`** - Description of high priority issue with suggested fix
### 🟢 Medium
(Only include this section if there are medium priority issues)
Improvements that would enhance code quality (style issues, minor optimizations, better practices):
- **File: `filename:line`** - Description of medium priority improvement
### 🔵 Low
(Only include this section if there are suggestions)
Nice-to-have improvements and suggestions (documentation, naming, minor refactoring):
- **File: `filename:line`** - Description of suggestion or enhancement
**Note**: If no specific issues are found in any category, simply state "No specific issues identified in this review."
## ✅ Highlights
(Only include this section if there are positive aspects to highlight)
- Mention specific good practices or implementations
- Acknowledge well-written code sections
- Note improvements from previous versions
---
*Review completed by Gemini CLI*
@@ -1,165 +0,0 @@
name: Cross-Repo Integration Tests
on:
push:
branches: [ main ]
paths:
- 'src/praisonai/praisonai/integration/**'
- 'src/praisonai/tests/integration/test_aiui_*'
- 'src/praisonai/pyproject.toml'
- 'src/praisonai-code/**'
- '.github/workflows/integration-cross-repo.yml'
- '.github/actions/install-monorepo-packages/**'
pull_request:
branches: [ main ]
paths:
- 'src/praisonai/praisonai/integration/**'
- 'src/praisonai/tests/integration/test_aiui_*'
- 'src/praisonai/pyproject.toml'
- 'src/praisonai-code/**'
- '.github/workflows/integration-cross-repo.yml'
- '.github/actions/install-monorepo-packages/**'
workflow_dispatch:
jobs:
cross-repo-integration:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout PraisonAI
uses: actions/checkout@v4
with:
path: PraisonAI
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout PraisonAIUI
uses: actions/checkout@v4
with:
repository: MervinPraison/PraisonAIUI
path: PraisonAIUI
ref: main
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: python -m pip install --upgrade pip
- uses: PraisonAI/.github/actions/install-monorepo-packages
with:
repo-root: PraisonAI/
wrapper-extras: ".[ui,dev]"
- name: Install PraisonAIUI
run: |
cd PraisonAIUI
pip install -e .
pip install pytest pytest-cov
- name: Verify versions
run: |
python -c "import praisonaiagents; print(f'praisonaiagents: {praisonaiagents.__version__}')"
python -c "import praisonai; print(f'praisonai: {praisonai.__version__}')"
python -c "import praisonaiui; print(f'praisonaiui: {praisonaiui.__version__}')"
- name: Run PraisonAI integration tests
run: |
cd PraisonAI/src/praisonai
python -m pytest tests/integration/test_aiui_* -v
- name: Run PraisonAIUI integration tests
run: |
cd PraisonAIUI
if [ -f tests/integration/test_agentic_roundtrip.py ]; then
python -m pytest tests/integration/test_agentic_roundtrip.py -v
fi
if [ -f tests/test_feature_sdk_backends.py ]; then
python -m pytest tests/test_feature_sdk_backends.py -v
fi
- name: Test Pattern C CLI
run: |
cd PraisonAI/src/praisonai
# Test that ui-gateway command exists
python -m praisonai serve --help | grep ui-gateway
- name: Test public API exports
run: |
cd PraisonAI/src/praisonai
python -c "from praisonai import run_integrated_gateway; print('✓ run_integrated_gateway imported')"
python -c "from praisonai import configure_host; print('✓ configure_host imported')"
- name: Integration smoke test
env:
PRAISONAI_TEST_MODE: "1"
PYTHONPATH: ${{ github.workspace }}/PraisonAI/src/praisonai:${{ github.workspace }}/PraisonAI/src/praisonai-agents:${{ github.workspace }}/PraisonAIUI
run: |
cd PraisonAI/src/praisonai
python -c "
import sys
sys.path.insert(0, '.')
from praisonai.integration.host_app import configure_host
from praisonai import run_integrated_gateway
print('✓ Integration imports successful')
# Test configure_host with new parameters
try:
configure_host(
style='dashboard',
context_paths=['AGENTS.md'],
title='Test App'
)
print('✓ configure_host with new parameters works')
except Exception as e:
print(f'× configure_host failed: {e}')
sys.exit(1)
"
# Optional job with API key for agentic tests (if secret is available)
agentic-integration:
runs-on: ubuntu-latest
timeout-minutes: 30
if: ${{ vars.RUN_AGENTIC_TESTS == 'true' }}
needs: cross-repo-integration
steps:
- name: Checkout PraisonAI
uses: actions/checkout@v4
with:
path: PraisonAI
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout PraisonAIUI
uses: actions/checkout@v4
with:
repository: MervinPraison/PraisonAIUI
path: PraisonAIUI
ref: main
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: python -m pip install --upgrade pip
- uses: PraisonAI/.github/actions/install-monorepo-packages
with:
repo-root: PraisonAI/
wrapper-extras: ".[ui,dev]"
- name: Install PraisonAIUI
run: |
cd PraisonAIUI
pip install -e .
- name: Run agentic integration test
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
cd PraisonAI/src/praisonai
if [ -n "$OPENAI_API_KEY" ]; then
python -m pytest tests/integration/test_aiui_host_agentic.py -v
else
echo "OPENAI_API_KEY not available, skipping agentic tests"
fi
-185
View File
@@ -1,185 +0,0 @@
name: Auto Merge Conflict → Claude
# Detects internal PRs with mergeStateStatus=DIRTY and posts @claude to trigger claude.yml.
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
schedule:
- cron: '0 */6 * * *'
concurrency:
group: merge-conflict-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
# PAT posts as MervinPraison so issue_comment triggers claude.yml (GITHUB_TOKEN cannot chain workflows)
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
permissions:
pull-requests: write
issues: write
jobs:
detect-and-trigger:
runs-on: ubuntu-latest
steps:
- name: Detect conflicting PRs and trigger Claude
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const baseRepo = `${owner}/${repo}`;
const COOLDOWN_MS = 12 * 60 * 60 * 1000;
const AUTO_ACTORS = ['github-actions[bot]', 'MervinPraison'];
const LABEL = 'claude-conflict-pending';
const COMMENT_BODY = [
'@claude this PR has merge conflicts with `main`.',
'Please rebase onto latest `main`, resolve conflicts (keep this PR\'s intent, merge in newer main logic),',
'run targeted tests, and force-push with `--force-with-lease`.',
'Comment which files you resolved.',
'Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.',
'Keep the resolution minimal — the aim is a lightweight and powerful package; do not introduce scope creep while resolving conflicts.',
].join(' ');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function getMergeState(prNumber) {
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
mergeStateStatus
maintainerCanModify
isDraft
headRef { repository { nameWithOwner } }
}
}
}
`;
for (let attempt = 0; attempt < 3; attempt++) {
const result = await github.graphql(query, { owner, repo, number: prNumber });
const prGql = result.repository.pullRequest;
const status = (prGql?.mergeStateStatus || '').toUpperCase();
if (status && status !== 'UNKNOWN') {
return {
status,
isDraft: prGql.isDraft,
headRepo: prGql.headRef?.repository?.nameWithOwner,
maintainerCanModify: prGql.maintainerCanModify === true,
};
}
if (attempt < 2) await sleep(10000);
}
const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
return {
status: (pr.data.mergeable_state || '').toUpperCase(),
isDraft: pr.data.draft,
headRepo: pr.data.head.repo?.full_name,
maintainerCanModify: pr.data.maintainer_can_modify === true,
};
}
function hasFinalClaudeReviewTrigger(comments) {
return comments.some((c) => {
const body = (c.body || '').toLowerCase();
if (!AUTO_ACTORS.includes(c.user.login)) return false;
if (!body.includes('@claude')) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
});
}
function hasRecentAutoComment(comments) {
const cutoff = Date.now() - COOLDOWN_MS;
return comments.some((c) => {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
const body = (c.body || '').toLowerCase();
if (!body.includes('@claude') || !body.includes('merge conflict')) return false;
return new Date(c.created_at).getTime() > cutoff;
});
}
async function processPR(prNumber) {
const { status, isDraft, headRepo, maintainerCanModify } = await getMergeState(prNumber);
core.info(`PR #${prNumber}: mergeState=${status}, head=${headRepo}, draft=${isDraft}, maintainerCanModify=${maintainerCanModify}`);
const { data: issue } = await github.rest.issues.get({
owner,
repo,
issue_number: prNumber,
});
const labels = issue.labels.map((l) => l.name);
if (status !== 'DIRTY' && labels.includes(LABEL)) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: LABEL,
});
core.info(`Removed ${LABEL} from PR #${prNumber}`);
}
if (status !== 'DIRTY') return { skipped: true, reason: 'not dirty' };
if (isDraft) return { skipped: true, reason: 'draft' };
if (headRepo && headRepo !== baseRepo && !maintainerCanModify) {
return { skipped: true, reason: 'fork without maintainer edits' };
}
if (labels.includes(LABEL)) {
return { skipped: true, reason: 'conflict resolution pending' };
}
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
if (hasRecentAutoComment(comments)) {
return { skipped: true, reason: 'recent auto comment' };
}
if (!hasFinalClaudeReviewTrigger(comments)) {
return { skipped: true, reason: 'awaiting final claude review trigger' };
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [LABEL],
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: COMMENT_BODY,
});
return { triggered: true };
}
let prNumbers = [];
if (context.eventName === 'pull_request') {
prNumbers = [context.payload.pull_request.number];
} else {
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
prNumbers = prs.map((p) => p.number);
}
const results = [];
for (const num of prNumbers) {
results.push({ pr: num, ...(await processPR(num)) });
}
core.summary.addRaw('```json\n' + JSON.stringify(results, null, 2) + '\n```');
await core.summary.write();
-144
View File
@@ -1,144 +0,0 @@
name: Nightly Release Gate
# Preflight before PyPI Release (patch bump).
# Triggers: Core Tests success on main (workflow_run) or cron every 3 days at 00:00 UTC.
# Dedupe: max one successful patch release every 3 days (see release-gate.js).
on:
schedule:
- cron: '0 0 */3 * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Run preflight only; do not dispatch pypi-release.yml'
required: false
default: false
type: boolean
workflow_run:
workflows:
- Core Tests
types:
- completed
branches:
- main
permissions:
contents: read
actions: write
concurrency:
group: nightly-release-gate
cancel-in-progress: false
jobs:
preflight:
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule' ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.event == 'push' &&
!startsWith(github.event.workflow_run.head_commit.message, 'Release v') &&
!startsWith(github.event.workflow_run.head_commit.message, 'chore(release):')
)
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- name: Resolve trigger context
id: ctx
run: |
if [ "${{ github.event_name }}" = "workflow_run" ]; then
echo "checkout_sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT"
echo "source=ci" >> "$GITHUB_OUTPUT"
echo "is_ci=true" >> "$GITHUB_OUTPUT"
else
echo "checkout_sha=main" >> "$GITHUB_OUTPUT"
echo "source=nightly" >> "$GITHUB_OUTPUT"
echo "is_ci=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.ctx.outputs.checkout_sha }}
fetch-depth: 0
- name: Fetch main and tags
run: git fetch --tags origin main
# Guards the gate logic itself — no workflow ran these selftests before.
- name: Selftest release-gate script
run: node .github/scripts/release-gate-selftest.js
- name: Preflight — path changes, CI SHA, dedupe
id: preflight
uses: actions/github-script@v7
env:
HEAD_SHA: ${{ steps.ctx.outputs.checkout_sha }}
IS_CI: ${{ steps.ctx.outputs.is_ci }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const rg = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/release-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const headSha = process.env.HEAD_SHA;
const isCi = process.env.IS_CI === 'true';
const result = await rg.evaluateReleasePreflight(
github, owner, repo,
{ headSha, isCiTrigger: isCi, bump: 'patch' },
core
);
core.setOutput('should_release', result.ready ? 'true' : 'false');
core.setOutput('reason', result.reasons.join('; '));
core.setOutput('head_sha', result.headSha || headSha);
core.setOutput('last_tag', result.lastTag || '');
core.setOutput('target_agents', result.targetAgents || '');
core.setOutput('target_code', result.targetCode || '');
core.setOutput('target_wrapper', result.targetWrapper || '');
core.summary.addRaw('## Release gate preflight\n');
core.summary.addRaw(`Ready: ${result.ready}\n`);
core.summary.addRaw(`Reason: ${result.reasons.join('; ')}\n`);
if (result.targetAgents) {
core.summary.addRaw(
`Targets: praisonaiagents==${result.targetAgents}, `
+ `praisonai-code==${result.targetCode}, praisonai==${result.targetWrapper}\n`
);
}
await core.summary.write();
if (!result.ready) {
core.info(`Skip release: ${result.reasons.join('; ')}`);
}
- name: Dry run summary
if: inputs.dry_run == true
run: |
echo "Dry run — preflight only, no pypi-release dispatch."
echo "should_release=${{ steps.preflight.outputs.should_release }}"
echo "reason=${{ steps.preflight.outputs.reason }}"
- name: Dispatch PyPI Release (patch)
if: steps.preflight.outputs.should_release == 'true' && inputs.dry_run != true
run: |
set -euo pipefail
gh workflow run pypi-release.yml \
--repo "${GITHUB_REPOSITORY}" \
--ref main \
-f bump=patch \
-f source=${{ steps.ctx.outputs.source }} \
-f trigger_sha=${{ steps.preflight.outputs.head_sha }} \
-f dry_run=false
echo "Dispatched pypi-release.yml (patch, source=${{ steps.ctx.outputs.source }})"
if [ "${{ steps.ctx.outputs.source }}" = "ci" ]; then
echo "Uses pypi-auto environment (no manual approval)."
else
echo "Uses pypi-auto environment for nightly patch releases."
fi
@@ -1,42 +0,0 @@
name: Pipeline Status Sync
# Sync pipeline/* stage and blocker labels on open internal PRs.
on:
schedule:
- cron: '*/10 * * * *'
workflow_dispatch:
permissions:
pull-requests: read
issues: write
actions: write
id-token: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Sync pipeline labels on open PRs
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
await ps.syncOpenPullRequests(
github, context.repo.owner, context.repo.repo,
{ maxPrs: 30, dispatchMergeGate: true }, core
);
@@ -1,73 +0,0 @@
name: PraisonAI Issue Triage
on:
issue_comment:
types: [created]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue Number'
required: true
type: string
jobs:
triage:
runs-on: ubuntu-latest
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
!github.event.issue.pull_request &&
contains(github.event.comment.body, '@praisonai') &&
github.actor != 'github-actions[bot]' &&
github.event.comment.user.type != 'Bot' &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Generate GitHub App Token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.PRAISONAI_APP_ID }}
private_key: ${{ secrets.PRAISONAI_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Set up Git
run: |
git config --local user.email "[email protected]"
git config --local user.name "PraisonAI Bot"
- name: Install PraisonAI
run: pip install uv
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
agents-extras: ".[all]"
wrapper-extras: ".[all]"
- name: Run PraisonAI Issue Triage
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PRAISONAI_AUTO_APPROVE: "true"
ISSUE_NUMBER_INPUT: ${{ inputs.issue_number }}
ISSUE_NUMBER_EVENT: ${{ github.event.issue.number }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
export ISSUE_NUMBER="$ISSUE_NUMBER_INPUT"
else
export ISSUE_NUMBER="$ISSUE_NUMBER_EVENT"
fi
praisonai github triage --issue $ISSUE_NUMBER --agent-file .github/praisonai-issue-triage.yaml
-77
View File
@@ -1,77 +0,0 @@
name: PraisonAI PR Reviewer
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: 'Pull Request Number'
required: true
type: string
jobs:
review:
runs-on: ubuntu-latest
if: >
secrets.PRAISONAI_APP_ID != '' &&
secrets.PRAISONAI_APP_PRIVATE_KEY != '' &&
(
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '@praisonai') &&
github.actor != 'github-actions[bot]' &&
github.event.comment.user.type != 'Bot' &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
)
steps:
- name: Determine checkout ref
id: dest
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
PR_NUMBER_INPUT: ${{ inputs.pr_number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "${{ github.event_name }}" = "issue_comment" ]; then
echo "ref=refs/pull/$ISSUE_NUMBER/head" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "ref=refs/pull/$PR_NUMBER_INPUT/head" >> "$GITHUB_OUTPUT"
else
echo "ref=$PR_HEAD_SHA" >> "$GITHUB_OUTPUT"
fi
- name: Checkout Repository
uses: actions/checkout@v4
with:
persist-credentials: false
ref: ${{ steps.dest.outputs.ref }}
fetch-depth: 0
- name: Generate GitHub App Token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.PRAISONAI_APP_ID }}
private_key: ${{ secrets.PRAISONAI_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PraisonAI
run: pip install "praisonai[all]"
- name: Run PraisonAI PR Review
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
praisonai workflow run .github/praisonai-reviewer.yaml
File diff suppressed because it is too large. Load diff
-44
View File
@@ -1,44 +0,0 @@
name: Python Package (Multi-Version)
on:
schedule:
- cron: '0 4 * * 1' # Weekly Monday 4am UTC
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Install dependencies
run: python -m pip install --upgrade pip
- uses: ./.github/actions/install-monorepo-packages
with:
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Install test tools
run: |
python -m pip install duckduckgo_search pytest pytest-asyncio pytest-cov
python -m pip install "praisonaiagents[knowledge]"
- name: Run tests
run: |
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --timeout=60 || echo "Some tests failed"
-45
View File
@@ -1,45 +0,0 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
# Superseded by pypi-release.yml (manual workflow_dispatch with uv publish).
# Kept as workflow_dispatch-only fallback; release trigger removed to avoid
# duplicate PyPI publish when bump_and_release creates a GitHub release.
on:
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
if: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build src/praisonai
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/praisonai/dist/
-541
View File
@@ -1,541 +0,0 @@
name: Release Workflow
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to create release with (e.g., v2.0.10)'
required: true
fork_owner:
description: 'GitHub username of the fork owner'
required: true
default: 'tosin2013'
jobs:
test-ubuntu:
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-24.04]
framework: ['crewai', 'autogen']
fail-fast: false
runs-on: ${{ matrix.os }}
timeout-minutes: 90
steps:
# First checkout the upstream repo at the specified tag
- name: Checkout upstream repo
uses: actions/checkout@v4
with:
persist-credentials: false
repository: MervinPraison/PraisonAI
ref: ${{ github.event.inputs.tag }}
fetch-depth: 0
# Get the commit hash from the tag
- name: Get commit hash
id: get_commit
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
COMMIT_HASH=$(git rev-parse HEAD)
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
echo "Found commit: $COMMIT_HASH for tag $INPUT_TAG"
# Now checkout the fork at main branch
- name: Checkout fork
uses: actions/checkout@v4
with:
persist-credentials: false
repository: ${{ github.event.inputs.fork_owner }}/PraisonAI
ref: main
fetch-depth: 0
clean: true
# Sync with upstream commit
- name: Sync with upstream
run: |
git config --global user.name "MervinPraison"
git config --global user.email "[email protected]"
# Add upstream remote
git remote add upstream https://github.com/MervinPraison/PraisonAI.git
# Fetch from upstream
git fetch upstream --tags --force
- name: Hard reset to tag commit
env:
COMMIT_HASH: ${{ steps.get_commit.outputs.commit_hash }}
run: |
git reset --hard $COMMIT_HASH
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
echo "$HOME/.local/bin" >> $GITHUB_PATH
poetry config virtualenvs.in-project true
poetry config virtualenvs.create true
poetry --version
poetry config --list
- name: Install Dependencies
run: |
poetry lock #--no-update
poetry install --all-extras
poetry env info
# Verify venv exists
test -d .venv || { echo "Virtual environment not created!"; exit 1; }
test -f .venv/bin/activate || { echo "Activation script not found!"; exit 1; }
- name: Run setup script
timeout-minutes: 20
run: |
set -e # Exit on any error
curl -OL https://raw.githubusercontent.com/tosin2013/PraisonAI/refs/heads/main/setup.sh
chmod +x setup.sh
./setup.sh --all --cicd || { echo "Setup script failed"; exit 1; }
env:
OPENAI_API_KEY: ${{ secrets.GROQ_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
OPENAI_MODEL_NAME: "llama-3.3-70b-versatile"
OPENAI_API_BASE: "https://api.groq.com/openai/v1/"
OPENAI_API_BASE_URL: "https://api.groq.com/openai/v1/"
PRAISONAI_NON_INTERACTIVE: "true"
- name: Test Basic Functionality
run: |
set -e # Exit on any error
# Random sleep between 60-120 seconds to avoid API rate limits
SLEEP_TIME=$((RANDOM % 61 + 60))
echo "Sleeping for $SLEEP_TIME seconds to avoid API rate limits..."
sleep $SLEEP_TIME
lsof -ti:8084 | xargs -r kill -9 || true
source .venv/bin/activate || { echo "Failed to activate virtual environment"; exit 1; }
mkdir -p test_output || { echo "Failed to create test_output directory"; exit 1; }
echo "Testing basic CLI functionality..."
# Test auto generation with explicit framework
praisonai --framework ${{ matrix.framework }} --auto "create a short test script" 2>&1 | tee test_output/auto_test.log
# Verify the output file exists and has content
if [[ ! -s test_output/auto_test.log ]]; then
echo "Error: No output generated from auto test"
exit 1
fi
# Test auto generation without framework
echo "Basic functionality tests passed!"
env:
OPENAI_API_KEY: ${{ secrets.GROQ_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
OPENAI_MODEL_NAME: "llama-3.3-70b-versatile"
OPENAI_API_BASE: "https://api.groq.com/openai/v1/"
OPENAI_API_BASE_URL: "https://api.groq.com/openai/v1/"
LOGLEVEL: "debug"
PRAISONAI_NON_INTERACTIVE: "true"
- name: Test Framework-specific Features
continue-on-error: false
run: |
source .venv/bin/activate
# Random sleep between 60-120 seconds to avoid API rate limits
SLEEP_TIME=$((RANDOM % 61 + 60))
echo "Sleeping for $SLEEP_TIME seconds to avoid API rate limits..."
sleep $SLEEP_TIME
lsof -ti:8084 | xargs -r kill -9 || true
praisonai --framework ${{ matrix.framework }} --auto "create a movie script about Robots in Mars" 2>&1 | tee test_output/framework_output.txt || echo "Framework-specific test failed" >> test_output/errors.log
env:
OPENAI_API_KEY: ${{ secrets.GROQ_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
OPENAI_MODEL_NAME: "llama-3.3-70b-versatile"
OPENAI_API_BASE: "https://api.groq.com/openai/v1/"
OPENAI_API_BASE_URL: "https://api.groq.com/openai/v1/"
PRAISONAI_NON_INTERACTIVE: "true"
- name: Test Core Functionality
continue-on-error: false
run: |
source .venv/bin/activate
# Random sleep between 60-120 seconds to avoid API rate limits
SLEEP_TIME=$((RANDOM % 61 + 60))
echo "Sleeping for $SLEEP_TIME seconds to avoid API rate limits..."
sleep $SLEEP_TIME
echo "Testing core functionality..."
# Test package imports
python3 -c "from praisonai.cli import PraisonAI; from praisonai.auto import AutoGenerator; from praisonai.agents_generator import AgentsGenerator" || exit 1
# Test configuration loading
python3 -c "from praisonai.inc.config import generate_config; print(generate_config())" || exit 1
# Test tools loading
python3 -c "from praisonai.inbuilt_tools import *" || exit 1
echo "Core functionality tests passed!"
env:
OPENAI_API_KEY: ${{ secrets.GROQ_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
OPENAI_MODEL_NAME: "llama-3.3-70b-versatile"
OPENAI_API_BASE: "https://api.groq.com/openai/v1/"
OPENAI_API_BASE_URL: "https://api.groq.com/openai/v1/"
LOGLEVEL: "debug"
PRAISONAI_NON_INTERACTIVE: "true"
- name: Test Agent Generation
continue-on-error: false
run: |
source .venv/bin/activate
# Random sleep between 60-120 seconds to avoid API rate limits
SLEEP_TIME=$((RANDOM % 61 + 60))
echo "Sleeping for $SLEEP_TIME seconds to avoid API rate limits..."
sleep $SLEEP_TIME
echo "Testing agent generation..."
# Create a test YAML file
cat > test_agents.yaml <<EOF
topic: "Test automation task"
framework: "${{ matrix.framework }} "
roles:
- name: "Tester"
goal: "Test the system"
backstory: "Expert system tester"
tasks:
- description: "Verify system functionality"
expected_output: "Test results"
EOF
# Test YAML loading and agent generation
python3 -c '
from praisonai.agents_generator import AgentsGenerator
config = [{"model": "llama-3.3-70b-versatile", "base_url": "https://api.groq.com/openai/v1/"}]
generator = AgentsGenerator("test_agents.yaml", "${{ matrix.framework }} ", config)
print("Agent generation test passed!")
' || exit 1
echo "Agent generation tests passed!"
env:
OPENAI_API_KEY: ${{ secrets.GROQ_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
OPENAI_MODEL_NAME: "llama-3.3-70b-versatile"
OPENAI_API_BASE: "https://api.groq.com/openai/v1/"
OPENAI_API_BASE_URL: "https://api.groq.com/openai/v1/"
LOGLEVEL: "debug"
PRAISONAI_NON_INTERACTIVE: "true"
- name: Test Custom Tools Integration
continue-on-error: false
run: |
source .venv/bin/activate
# Random sleep between 60-120 seconds to avoid API rate limits
SLEEP_TIME=$((RANDOM % 61 + 60))
echo "Sleeping for $SLEEP_TIME seconds to avoid API rate limits..."
sleep $SLEEP_TIME
echo "Testing custom tools integration..."
# Create a test tool
cat > test_tool.py <<EOF
from duckduckgo_search import DDGS
from praisonai_tools import BaseTool
class InternetSearchTool(BaseTool):
name: str = "Internet Search Tool"
description: str = "Search Internet for relevant information"
def _run(self, query: str):
return {"status": "success", "query": query}
def test(self):
"""Test method to verify tool setup"""
test_query = "test query"
result = self._run(test_query)
assert result["status"] == "success"
assert result["query"] == test_query
return True
EOF
# Test tool loading and initialization
python3 -c '
import importlib.util
import sys
# Load the test tool
spec = importlib.util.spec_from_file_location("test_tool", "test_tool.py")
module = importlib.util.module_from_spec(spec)
sys.modules["test_tool"] = module
spec.loader.exec_module(module)
# Initialize and test the tool
tool = module.InternetSearchTool()
assert tool.test(), "Tool test failed"
print("Custom tool test passed!")
' || exit 1
echo "Custom tools integration test passed!"
env:
OPENAI_API_KEY: ${{ secrets.GROQ_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
OPENAI_MODEL_NAME: "llama-3.3-70b-versatile"
OPENAI_API_BASE: "https://api.groq.com/openai/v1/"
OPENAI_API_BASE_URL: "https://api.groq.com/openai/v1/"
LOGLEVEL: "debug"
PRAISONAI_NON_INTERACTIVE: "true"
- name: Test PraisonAI Agents Configuration
continue-on-error: false
run: |
source .venv/bin/activate
# Random sleep between 60-120 seconds to avoid API rate limits
SLEEP_TIME=$((RANDOM % 61 + 60))
echo "Sleeping for $SLEEP_TIME seconds to avoid API rate limits..."
sleep $SLEEP_TIME
echo "Testing agents configuration..."
mkdir -p test_output || { echo "Failed to create test_output directory"; exit 1; }
# Create test script
cat > test_agents.py <<EOF
from langchain_openai import ChatOpenAI
from crewai import Agent, Task, Crew
import os
import logging
import time
import random
logging.basicConfig(level="DEBUG")
logger = logging.getLogger(__name__)
def random_sleep(min_seconds=15, max_seconds=30):
"""Sleep for a random duration to prevent API rate limiting"""
duration = random.uniform(min_seconds, max_seconds)
logger.info(f"Rate limit protection: Sleeping for {duration:.2f} seconds...")
time.sleep(duration)
class RateLimitedChatOpenAI(ChatOpenAI):
"""ChatOpenAI with built-in rate limiting"""
def __call__(self, *args, **kwargs):
random_sleep()
return super().__call__(*args, **kwargs)
def run_test():
logger.info("Initializing test components...")
# Initialize the LLM with rate limiting
llm = RateLimitedChatOpenAI(
model=os.getenv('OPENAI_MODEL_NAME', 'llama-3.3-70b-versatile'),
base_url=os.getenv('OPENAI_API_BASE', 'https://api.groq.com/openai/v1/'),
api_key=os.getenv('OPENAI_API_KEY'),
temperature=0.7
)
random_sleep()
logger.info("Creating researcher agent...")
researcher = Agent(
role="Test Automation Expert",
goal="Verify system functionality and performance",
backstory="""You are an expert in test automation and system verification,
skilled in identifying potential issues and validating functionality.""",
verbose=True,
allow_delegation=False,
llm=llm
)
random_sleep()
logger.info("Creating validator agent...")
validator = Agent(
role="Quality Assurance Specialist",
goal="Validate test results and ensure system reliability",
backstory="""You are a QA specialist focused on ensuring
system reliability and proper functionality.""",
verbose=True,
allow_delegation=False,
llm=llm
)
random_sleep()
logger.info("Creating test tasks...")
test_task = Task(
description="Run a basic system verification test",
expected_output="A report on system functionality verification",
agent=researcher
)
random_sleep()
validation_task = Task(
description="Validate the test results and compile findings",
expected_output="A validation report of test findings",
agent=validator
)
random_sleep()
logger.info("Initializing crew...")
crew = Crew(
agents=[researcher, validator],
tasks=[test_task, validation_task],
verbose=2
)
logger.info("Starting crew execution...")
result = crew.kickoff()
logger.info("Test execution completed.")
logger.info(f"Result: {result}")
return result
if __name__ == "__main__":
try:
run_test()
except Exception as e:
logger.error(f"Test failed with error: {str(e)}", exc_info=True)
raise
EOF
# Run the test script with increased timeout
echo "Testing with Python API..."
timeout 1800 python3 test_agents.py 2>&1 | tee test_output/agents_test.log
# Verify the output
if [[ ! -s test_output/agents_test.log ]]; then
echo "Error: No output generated from agents test"
exit 1
fi
echo "Agents configuration tests passed!"
env:
OPENAI_API_KEY: ${{ secrets.GROQ_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
OPENAI_MODEL_NAME: "llama-3.3-70b-versatile"
OPENAI_API_BASE: "https://api.groq.com/openai/v1/"
OPENAI_API_BASE_URL: "https://api.groq.com/openai/v1/"
LOGLEVEL: "debug"
PRAISONAI_NON_INTERACTIVE: "true"
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: praisonai-output-${{ matrix.os }}-${{ matrix.framework }}
path: test_output/
retention-days: 14
create-release:
needs: [test-ubuntu]
runs-on: ubuntu-latest
if: success()
steps:
# First checkout the fork's main branch
- name: Checkout fork repo
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}
repository: ${{ github.event.inputs.fork_owner }}/PraisonAI
ref: main
# Add upstream and get commit from tag
- name: Get upstream tag commit
id: get_upstream_commit
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
# Add upstream remote
git remote add upstream https://github.com/MervinPraison/PraisonAI.git
# Delete local tag if it exists
git tag -d "$INPUT_TAG" || true
# Fetch from upstream including tags with force
git fetch upstream --tags --force
# Verify tag exists in upstream
if ! git ls-remote --tags upstream | grep -q "refs/tags/$INPUT_TAG$"; then
echo "::error::Tag $INPUT_TAG not found in upstream repository"
exit 1
fi
# Get the commit hash from the upstream tag
UPSTREAM_COMMIT=$(git rev-list -n 1 refs/tags/"$INPUT_TAG")
if [ -z "$UPSTREAM_COMMIT" ]; then
echo "::error::Could not find commit for tag $INPUT_TAG"
exit 1
fi
echo "upstream_commit=$UPSTREAM_COMMIT" >> $GITHUB_OUTPUT
echo "Found commit: $UPSTREAM_COMMIT for tag $INPUT_TAG"
# Create tag at the same commit
- name: Create tag at commit
env:
GIT_ASKPASS: /bin/echo
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
git config --global user.name "MervinPraison"
git config --global user.email "[email protected]"
# Remove tag if it exists locally
git tag -d "$INPUT_TAG" || true
# Remove tag if it exists remotely
git push origin :refs/tags/"$INPUT_TAG" || true
- name: Fetch tag from upstream
env:
UPSTREAM_COMMIT: ${{ steps.get_upstream_commit.outputs.upstream_commit }}
run: |
git fetch upstream $UPSTREAM_COMMIT --force
- name: Reset to upstream commit
env:
UPSTREAM_COMMIT: ${{ steps.get_upstream_commit.outputs.upstream_commit }}
run: |
git reset --hard $UPSTREAM_COMMIT
# Create and push tag
git tag -a "$INPUT_TAG" -m "Release $INPUT_TAG (synced from upstream MervinPraison/PraisonAI@$INPUT_TAG)"
git push origin "$INPUT_TAG"
# Create the release
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
tag_name: ${{ github.event.inputs.tag }}
release_name: Release ${{ github.event.inputs.tag }}
draft: false
prerelease: false
body: |
Release ${{ github.event.inputs.tag }} (synced from upstream)
This release is synchronized with MervinPraison/PraisonAI@${{ github.event.inputs.tag }}
Commit: ${{ steps.get_upstream_commit.outputs.upstream_commit }}
For installation:
```bash
pip install praisonai
```
Changes from upstream v2.0.10:
- Updated agent configuration tests
- Added rate limiting protection
- Improved test reliability
Forked from: MervinPraison/PraisonAI
@@ -1,41 +0,0 @@
name: Sync Claude Secrets
on:
workflow_dispatch:
inputs:
target_repo:
description: 'Destination repo (owner/name)'
required: true
default: 'MervinPraison/PraisonAIUI'
type: string
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Sync Claude secrets
env:
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
TARGET: ${{ inputs.target_repo }}
CLAUDE_APP_ID: ${{ secrets.CLAUDE_APP_ID }}
CLAUDE_APP_PRIVATE_KEY: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
GH_TOKEN_VALUE: ${{ secrets.GH_TOKEN }}
DOCS_GITHUB_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "SECRETS_ADMIN_PAT is not configured in this repository"
exit 1
fi
echo -n "$CLAUDE_APP_ID" | gh secret set CLAUDE_APP_ID --repo "$TARGET"
echo -n "$CLAUDE_APP_PRIVATE_KEY" | gh secret set CLAUDE_APP_PRIVATE_KEY --repo "$TARGET"
echo -n "$CLAUDE_CODE_OAUTH_TOKEN" | gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$TARGET"
echo -n "$GH_TOKEN_VALUE" | gh secret set GH_TOKEN --repo "$TARGET"
if [ -n "${DOCS_GITHUB_TOKEN:-}" ]; then
echo -n "$DOCS_GITHUB_TOKEN" | gh secret set DOCS_GITHUB_TOKEN --repo "$TARGET"
fi
echo "Synced CLAUDE_APP_ID, CLAUDE_APP_PRIVATE_KEY, CLAUDE_CODE_OAUTH_TOKEN, GH_TOKEN → $TARGET"
@@ -1,47 +0,0 @@
name: Terminal-Bench Smoke
# Manual (and optional weekly) smoke run of the `praisonai code` assistant on a
# small verified Terminal-Bench 2.1 subset via the Harbor harness.
# Never runs on PRs — it costs money (LLM calls) and is inherently flaky.
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
permissions:
contents: read
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Harbor + PraisonAI
run: |
python -m pip install --upgrade pip
pip install harbor
pip install -e src/praisonai-agents -e src/praisonai
- name: Run Terminal-Bench 2.1 smoke subset
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PYTHONPATH: .
run: |
harbor run -c examples/terminal_bench/job_code_smoke.yaml || true
- name: Upload Harbor results
if: always()
uses: actions/upload-artifact@v4
with:
name: harbor-results
path: |
runs/
**/results.json
if-no-files-found: warn
-204
View File
@@ -1,204 +0,0 @@
name: Comprehensive Test Suite
on:
workflow_dispatch: # Allow manual triggering
inputs:
test_type:
description: 'Type of tests to run'
required: true
default: 'all'
type: choice
options:
- all
- unit
- integration
- fast
- performance
- frameworks
- autogen
- crewai
release:
types: [published, prereleased]
schedule:
# Run comprehensive tests weekly on Sundays at 3 AM UTC
- cron: '0 3 * * 0'
jobs:
comprehensive-test:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL_NAME: gpt-4o-mini
strategy:
matrix:
python-version: ["3.11"]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-cov pytest-benchmark pytest-timeout
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Verify environment variables
run: |
if [ -n "$OPENAI_API_KEY" ]; then
echo "✅ OPENAI_API_KEY is set (length: ${#OPENAI_API_KEY})"
else
echo "⚠️ OPENAI_API_KEY is NOT set"
fi
- name: Run Comprehensive Test Suite
env:
INPUT_TEST_TYPE: ${{ github.event.inputs.test_type || 'all' }}
run: |
# Determine test type from input or default to 'all'
TEST_TYPE="$INPUT_TEST_TYPE"
echo "🧪 Running comprehensive test suite (type: $TEST_TYPE)"
case $TEST_TYPE in
"unit")
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --cov=praisonai --cov-report=xml --cov-branch || echo "Some unit tests failed"
;;
"integration")
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings --cov=praisonai --cov-report=xml --cov-branch || echo "Some integration tests failed"
;;
"fast")
cd src/praisonai && python tests/test_runner.py --pattern fast || echo "Some fast tests failed"
;;
"performance")
cd src/praisonai && python tests/test_runner.py --pattern "performance" || echo "Some performance tests failed"
;;
"frameworks")
cd src/praisonai && python tests/test_runner.py --pattern frameworks || echo "Some framework tests failed"
;;
"autogen")
cd src/praisonai && python tests/test_runner.py --pattern autogen || echo "Some autogen tests failed"
;;
"crewai")
cd src/praisonai && python tests/test_runner.py --pattern crewai || echo "Some crewai tests failed"
;;
"all"|*)
cd src/praisonai && python tests/test_runner.py --pattern all || echo "Some tests failed"
;;
esac
- name: Generate Comprehensive Test Report
if: always()
env:
INPUT_TEST_TYPE: ${{ github.event.inputs.test_type || 'all' }}
EVENT_NAME: ${{ github.event_name }}
run: |
echo "# 📋 Comprehensive Test Report" > comprehensive_report.md
echo "" >> comprehensive_report.md
echo "**Python Version:** ${{ matrix.python-version }}" >> comprehensive_report.md
echo "**Test Type:** $INPUT_TEST_TYPE" >> comprehensive_report.md
echo "**Trigger:** $EVENT_NAME" >> comprehensive_report.md
echo "**Status:** 🟢 All Tests Passed" >> comprehensive_report.md
echo "" >> comprehensive_report.md
echo "## 🧪 Test Categories Covered:" >> comprehensive_report.md
echo "" >> comprehensive_report.md
echo "### Unit Tests:" >> comprehensive_report.md
echo "- ✅ Core agent functionality" >> comprehensive_report.md
echo "- ✅ Async operations" >> comprehensive_report.md
echo "- ✅ Tool integrations" >> comprehensive_report.md
echo "- ✅ UI components" >> comprehensive_report.md
echo "" >> comprehensive_report.md
echo "### Integration Tests:" >> comprehensive_report.md
echo "- ✅ MCP (Model Context Protocol)" >> comprehensive_report.md
echo "- ✅ RAG (Retrieval Augmented Generation)" >> comprehensive_report.md
echo "- ✅ Base URL API mapping" >> comprehensive_report.md
echo "- ✅ Multi-agent workflows" >> comprehensive_report.md
echo "- ✅ AutoGen framework integration" >> comprehensive_report.md
echo "- ✅ CrewAI framework integration" >> comprehensive_report.md
echo "- 💬 LLM integrations (OpenAI, Anthropic, etc.)" >> comprehensive_report.md
echo "- 🖥️ UI frameworks (Gradio, Streamlit)" >> comprehensive_report.md
echo "- 📊 Memory and persistence" >> comprehensive_report.md
echo "- 🌐 Multi-modal capabilities" >> comprehensive_report.md
echo "- ✅ AutoGen framework integration" >> comprehensive_report.md
echo "- ✅ CrewAI framework integration" >> comprehensive_report.md
echo "" >> comprehensive_report.md
echo "### Key Features Tested:" >> comprehensive_report.md
echo "- 🤖 Agent creation and configuration" >> comprehensive_report.md
echo "- 📋 Task management and execution" >> comprehensive_report.md
echo "- 🔄 Sync/async workflows" >> comprehensive_report.md
echo "- 🛠️ Custom tools and error handling" >> comprehensive_report.md
echo "- 🧠 Knowledge bases and RAG" >> comprehensive_report.md
echo "- 🔌 MCP server connections" >> comprehensive_report.md
echo "- 💬 LLM integrations (OpenAI, Anthropic, etc.)" >> comprehensive_report.md
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: src/praisonai/coverage.xml
flags: comprehensive-tests
name: comprehensive-tests-coverage
fail_ci_if_error: false
verbose: true
- name: Upload Comprehensive Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: comprehensive-test-results-python-${{ matrix.python-version }}
path: |
comprehensive_report.md
src/praisonai/htmlcov/
src/praisonai/coverage.xml
src/praisonai/.coverage
retention-days: 30
test-matrix-summary:
runs-on: ubuntu-latest
needs: comprehensive-test
if: always()
steps:
- name: Generate Matrix Summary
run: |
echo "# 🎯 Test Matrix Summary" > matrix_summary.md
echo "" >> matrix_summary.md
echo "## Python Version Results:" >> matrix_summary.md
echo "- Python 3.10: ${{ needs.comprehensive-test.result }}" >> matrix_summary.md
echo "- Python 3.11: ${{ needs.comprehensive-test.result }}" >> matrix_summary.md
echo "" >> matrix_summary.md
echo "## Overall Status:" >> matrix_summary.md
if [ "${{ needs.comprehensive-test.result }}" == "success" ]; then
echo "✅ **All tests passed across all Python versions!**" >> matrix_summary.md
else
echo "❌ **Some tests failed. Check individual job logs for details.**" >> matrix_summary.md
fi
- name: Upload Matrix Summary
uses: actions/upload-artifact@v4
with:
name: test-matrix-summary
path: matrix_summary.md
retention-days: 30
-324
View File
@@ -1,324 +0,0 @@
name: Core Tests
on:
push:
branches: [ main, develop ]
paths:
- 'src/**'
- 'src/praisonai-deploy/infra/**'
- 'src/praisonai-bot/infra/**'
- 'scripts/check_helm_charts.sh'
- 'pyproject.toml'
- '.github/workflows/test-core.yml'
- '.github/actions/install-monorepo-packages/**'
pull_request:
branches: [ main, develop ]
paths:
- 'src/**'
- 'src/praisonai-deploy/infra/**'
- 'src/praisonai-bot/infra/**'
- 'scripts/check_helm_charts.sh'
- 'pyproject.toml'
- '.github/workflows/test-core.yml'
- '.github/actions/install-monorepo-packages/**'
workflow_dispatch:
permissions:
contents: read
env:
PYTHONPATH: ${{ github.workspace }}/src/praisonai-agents
PRAISONAI_ALLOW_NETWORK: '0'
PRAISONAI_LIVE_TESTS: '0'
PRAISONAI_TEST_TIER: main
jobs:
test-core-collect:
name: test-core-collect
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-timeout pytest-xdist
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Verify unit test collection
shell: bash
run: |
set -euo pipefail
cd src/praisonai
python -m pytest tests/unit/ \
-m "not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
--ignore=tests/fixtures \
--collect-only -q --disable-warnings
test-core-unit:
name: test-core (${{ matrix.shard }})
needs: test-core-collect
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- shard: cli
paths: >-
tests/unit/cli/
tests/unit/tui/
extra_ignore: --ignore=tests/unit/cli/test_message_queue.py
- shard: bots-gateway
workdir: src/praisonai-bot
paths: >-
tests/unit/bots/
tests/unit/gateway/
tests/unit/cli/
tests/unit/daemon/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents:${{ github.workspace }}/src/praisonai-code
- shard: train
workdir: src/praisonai-train
paths: >-
tests/unit/train/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents:${{ github.workspace }}/src/praisonai-code
- shard: mcp
workdir: src/praisonai-mcp
paths: >-
tests/mcp_server/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: sandbox
workdir: src/praisonai-sandbox
paths: >-
tests/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: deploy
workdir: src/praisonai-deploy
paths: >-
tests/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: subdirs
paths: >-
tests/unit/scheduler/
tests/unit/integrations/
tests/unit/mcp/
tests/unit/knowledge/
tests/unit/llm/
tests/unit/doctor/
tests/unit/code/
tests/unit/acp/
tests/unit/agent/
tests/unit/tools/
tests/unit/persistence/
tests/unit/security/
tests/unit/recipe/
tests/unit/storage/
tests/unit/rag/
tests/unit/replay/
tests/unit/escalation/
tests/unit/orchestration/
tests/unit/dev/
tests/unit/_dev/
tests/unit/context/
tests/unit/adapters/
tests/unit/templates/
tests/unit/suite_runner/
tests/unit/standardise/
extra_ignore: ""
- shard: root
paths: ""
extra_ignore: ""
root_glob: "true"
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-timeout pytest-xdist
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Run unit shard
shell: bash
run: |
set -euo pipefail
WORKDIR="${{ matrix.workdir || 'src/praisonai' }}"
if [ "$WORKDIR" = "src/praisonai-sandbox" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c13_sandbox_imports.sh"
fi
if [ "$WORKDIR" = "src/praisonai-deploy" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c14_deploy_imports.sh"
fi
if [ "$WORKDIR" = "src/praisonai-mcp" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c12_mcp_imports.sh"
fi
cd "$WORKDIR"
if [ -n "${{ matrix.pythonpath || '' }}" ]; then
export PYTHONPATH="${{ matrix.pythonpath }}:${PYTHONPATH:-}"
fi
PYTEST_ARGS=(
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere"
-q --tb=line --disable-warnings
--timeout=60 -n 2 --dist loadfile
--ignore=tests/fixtures
--ignore=tests/e2e
--ignore=tests/live
--ignore=tests/unit/test_langflow_components.py
)
if [ "${{ matrix.root_glob || 'false' }}" = "true" ]; then
shopt -s nullglob
ROOT_TESTS=(tests/unit/test_*.py)
shopt -u nullglob
EXCLUDE=(tests/unit/test_langflow_components.py)
FILTERED=()
for t in "${ROOT_TESTS[@]}"; do
skip=false
for e in "${EXCLUDE[@]}"; do
if [ "$t" = "$e" ]; then skip=true; break; fi
done
if [ "$skip" = false ]; then FILTERED+=("$t"); fi
done
if [ ${#FILTERED[@]} -eq 0 ]; then
echo "No root unit tests matched"
exit 0
fi
python -m pytest "${FILTERED[@]}" "${PYTEST_ARGS[@]}"
else
python -m pytest ${{ matrix.paths }} "${PYTEST_ARGS[@]}" ${{ matrix.extra_ignore }}
fi
test-core-integration:
name: test-core-integration
needs: test-core-unit
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-timeout
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Run integration tests
shell: bash
run: |
set -euo pipefail
cd src/praisonai
python -m pytest tests/integration/ \
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
-q --tb=line --disable-warnings \
--timeout=120 \
--ignore=tests/fixtures \
--ignore=tests/e2e \
--ignore=tests/live \
--ignore=tests/integration/test_tracker_complex_tasks.py \
--ignore=tests/integration/test_serve_integration.py
helm-charts:
name: helm-charts
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: v3.14.4
- name: Lint and template Helm charts
shell: bash
run: |
set -euo pipefail
bash "$GITHUB_WORKSPACE/scripts/check_helm_charts.sh"
test-core:
name: test-core
needs: [test-core-collect, test-core-unit, test-core-integration, helm-charts]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Report Core Tests result
shell: bash
run: |
set -euo pipefail
collect='${{ needs.test-core-collect.result }}'
unit='${{ needs.test-core-unit.result }}'
integration='${{ needs.test-core-integration.result }}'
helm='${{ needs.helm-charts.result }}'
echo "collect=$collect unit=$unit integration=$integration helm=$helm"
if [ "$collect" != "success" ] || [ "$unit" != "success" ] || [ "$integration" != "success" ] || [ "$helm" != "success" ]; then
echo "Core Tests failed"
exit 1
fi
echo "Core Tests passed"
-176
View File
@@ -1,176 +0,0 @@
name: Extended Tests
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
test-examples:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv pip install --system duckduckgo_search
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
echo "PRAISONAI_TELEMETRY_DISABLED=true" >> $GITHUB_ENV
echo "PRAISONAI_DISABLE_TELEMETRY=true" >> $GITHUB_ENV
echo "DO_NOT_TRACK=true" >> $GITHUB_ENV
- name: Test Key Example Scripts
run: |
echo "🧪 Testing key example scripts from praisonai-agents..."
# Create a timeout function for consistent handling
timeout_run() {
timeout 30s "$@" || echo "⏱️ $1 test completed/timed out"
}
# Test basic agent functionality
timeout_run python ${{ github.workspace }}/src/praisonai-agents/basic-agents.py
# Test async functionality
timeout_run python ${{ github.workspace }}/src/praisonai-agents/async_example.py
# Test knowledge/RAG functionality
timeout_run python ${{ github.workspace }}/src/praisonai-agents/knowledge-agents.py
# Test MCP functionality
timeout_run python ${{ github.workspace }}/src/praisonai-agents/mcp-basic.py
# Test UI functionality
timeout_run python ${{ github.workspace }}/src/praisonai-agents/ui.py
echo "✅ Example script testing completed"
continue-on-error: true
performance-test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv pip install --system pytest pytest-benchmark
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "PRAISONAI_TELEMETRY_DISABLED=true" >> $GITHUB_ENV
echo "PRAISONAI_DISABLE_TELEMETRY=true" >> $GITHUB_ENV
echo "DO_NOT_TRACK=true" >> $GITHUB_ENV
- name: Run Performance Benchmarks
run: |
echo "🏃 Running performance benchmarks..."
python -c "
import time
import sys
import statistics
sys.path.insert(0, 'src/praisonai')
print('🏃 Testing agent creation performance...')
times = []
try:
from praisonaiagents import Agent
for i in range(5):
start_time = time.time()
agent = Agent(name=f'PerfAgent{i}')
times.append(time.time() - start_time)
avg_time = statistics.mean(times)
print(f'✅ Average agent creation time: {avg_time:.3f}s')
print(f'📊 Min: {min(times):.3f}s, Max: {max(times):.3f}s')
except Exception as e:
print(f'❌ Agent creation benchmark failed: {e}')
print('🏃 Testing import performance...')
start_time = time.time()
try:
import praisonaiagents
import_time = time.time() - start_time
print(f'✅ Import completed in {import_time:.3f}s')
except Exception as e:
print(f'❌ Import benchmark failed: {e}')
print('🏃 Testing memory usage...')
try:
import psutil
import os
process = psutil.Process(os.getpid())
memory_mb = process.memory_info().rss / 1024 / 1024
print(f'📊 Memory usage: {memory_mb:.1f} MB')
except ImportError:
print('⚠️ psutil not available for memory testing')
except Exception as e:
print(f'❌ Memory benchmark failed: {e}')
"
continue-on-error: true
- name: Generate Performance Report
run: |
echo "## 📊 Performance Test Results" > performance_report.md
echo "" >> performance_report.md
echo "### Benchmarks Run:" >> performance_report.md
echo "- ⚡ Agent creation speed" >> performance_report.md
echo "- 📦 Import performance" >> performance_report.md
echo "- 💾 Memory usage" >> performance_report.md
echo "- 🧪 Example script execution" >> performance_report.md
echo "" >> performance_report.md
echo "_Performance results are logged in the CI output above._" >> performance_report.md
- name: Upload Performance Report
uses: actions/upload-artifact@v4
with:
name: performance-report
path: performance_report.md
retention-days: 5
-158
View File
@@ -1,158 +0,0 @@
name: Framework Integration Tests
on:
workflow_dispatch: # Allow manual triggering
inputs:
framework:
description: 'Framework to test'
required: true
default: 'all'
type: choice
options:
- all
- autogen
- crewai
schedule:
# Run framework tests daily at 6 AM UTC
- cron: '0 6 * * *'
push:
paths:
- 'tests/integration/autogen/**'
- 'tests/integration/crewai/**'
- '.github/workflows/test-frameworks.yml'
jobs:
framework-tests:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
python-version: [3.11]
framework: [autogen, crewai]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
- name: Test ${{ matrix.framework }} Framework
run: |
echo "🧪 Testing ${{ matrix.framework }} framework integration with Python ${{ matrix.python-version }}"
cd src/praisonai && python tests/test_runner.py --pattern ${{ matrix.framework }} --verbose --coverage
continue-on-error: false
- name: Generate Framework Test Report
if: always()
run: |
echo "# 🤖 ${{ matrix.framework }} Framework Test Report" > ${{ matrix.framework }}_report.md
echo "" >> ${{ matrix.framework }}_report.md
echo "**Framework:** ${{ matrix.framework }}" >> ${{ matrix.framework }}_report.md
echo "**Python Version:** ${{ matrix.python-version }}" >> ${{ matrix.framework }}_report.md
echo "**Date:** $(date -u)" >> ${{ matrix.framework }}_report.md
echo "**Trigger:** ${{ github.event_name }}" >> ${{ matrix.framework }}_report.md
echo "" >> ${{ matrix.framework }}_report.md
if [ "${{ matrix.framework }}" == "autogen" ]; then
echo "## AutoGen Integration Tests" >> ${{ matrix.framework }}_report.md
echo "- ✅ AutoGen import verification" >> ${{ matrix.framework }}_report.md
echo "- ✅ Basic agent creation through PraisonAI" >> ${{ matrix.framework }}_report.md
echo "- ✅ Conversation flow testing" >> ${{ matrix.framework }}_report.md
echo "- ✅ Configuration validation" >> ${{ matrix.framework }}_report.md
elif [ "${{ matrix.framework }}" == "crewai" ]; then
echo "## CrewAI Integration Tests" >> ${{ matrix.framework }}_report.md
echo "- ✅ CrewAI import verification" >> ${{ matrix.framework }}_report.md
echo "- ✅ Basic crew creation through PraisonAI" >> ${{ matrix.framework }}_report.md
echo "- ✅ Multi-agent workflow testing" >> ${{ matrix.framework }}_report.md
echo "- ✅ Agent collaboration verification" >> ${{ matrix.framework }}_report.md
echo "- ✅ Configuration validation" >> ${{ matrix.framework }}_report.md
fi
echo "" >> ${{ matrix.framework }}_report.md
echo "## Test Commands" >> ${{ matrix.framework }}_report.md
echo '```bash' >> ${{ matrix.framework }}_report.md
echo "# Run ${{ matrix.framework }} tests locally:" >> ${{ matrix.framework }}_report.md
echo "python tests/test_runner.py --pattern ${{ matrix.framework }} --verbose" >> ${{ matrix.framework }}_report.md
echo '```' >> ${{ matrix.framework }}_report.md
- name: Upload Framework Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ matrix.framework }}-test-results-python-${{ matrix.python-version }}
path: |
${{ matrix.framework }}_report.md
htmlcov/
coverage.xml
.coverage
retention-days: 14
framework-summary:
runs-on: ubuntu-latest
needs: framework-tests
if: always()
steps:
- name: Generate Framework Summary
run: |
echo "# 🚀 Framework Integration Test Summary" > framework_summary.md
echo "" >> framework_summary.md
echo "## Test Results by Framework and Python Version:" >> framework_summary.md
echo "" >> framework_summary.md
echo "### AutoGen Framework:" >> framework_summary.md
echo "- Python 3.11: ${{ needs.framework-tests.result }}" >> framework_summary.md
echo "" >> framework_summary.md
echo "### CrewAI Framework:" >> framework_summary.md
echo "- Python 3.11: ${{ needs.framework-tests.result }}" >> framework_summary.md
echo "" >> framework_summary.md
echo "## Overall Status:" >> framework_summary.md
if [ "${{ needs.framework-tests.result }}" == "success" ]; then
echo "✅ **All framework integration tests passed!**" >> framework_summary.md
else
echo "❌ **Some framework tests failed. Check individual job logs for details.**" >> framework_summary.md
fi
echo "" >> framework_summary.md
echo "## Frameworks Tested:" >> framework_summary.md
echo "- **AutoGen**: Microsoft's conversational AI framework" >> framework_summary.md
echo "- **CrewAI**: Multi-agent collaboration framework" >> framework_summary.md
echo "" >> framework_summary.md
echo "## Test Coverage:" >> framework_summary.md
echo "- Import verification" >> framework_summary.md
echo "- Agent/crew creation" >> framework_summary.md
echo "- Workflow execution" >> framework_summary.md
echo "- Configuration validation" >> framework_summary.md
echo "- Integration with PraisonAI" >> framework_summary.md
- name: Upload Framework Summary
uses: actions/upload-artifact@v4
with:
name: framework-test-summary
path: framework_summary.md
retention-days: 30
-857
View File
@@ -1,857 +0,0 @@
name: Optimized Test Suite
on:
push:
branches: [ main, develop ]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-optimized.yml'
pull_request:
branches: [ main, develop ]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-optimized.yml'
schedule:
# Nightly extended tests at 3 AM UTC
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
tier:
description: 'Test tier to run'
required: true
default: 'main'
type: choice
options:
- smoke
- main
- extended
- nightly
env:
PYTHONPATH: ${{ github.workspace }}/src/praisonai-agents
# Test gating environment variables
PRAISONAI_ALLOW_NETWORK: '0'
PRAISONAI_LIVE_TESTS: '0'
jobs:
# ============================================
# SMOKE TESTS - Fast unit tests, no network
# Target: <= 2 minutes
# ============================================
smoke:
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-smoke-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-smoke-
${{ runner.os }}-pip-
- name: Install minimal dependencies
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout prompt_toolkit
- uses: ./.github/actions/install-monorepo-packages
with:
agents-extras: ".[all]"
wrapper-extras: ".[chat,code]"
- name: Run Smoke Tests
env:
PRAISONAI_TEST_TIER: smoke
PRAISONAI_ALLOW_NETWORK: '0'
OPENAI_MODEL_NAME: gpt-4o-mini
shell: bash
run: |
set -euo pipefail
cd src/praisonai
python -m pytest tests/unit/test_c5_backward_compat.py \
tests/unit/test_c7_1_boundaries.py \
tests/unit/test_c9_backward_compat.py \
tests/unit/test_c9_1_boundaries.py \
tests/unit/test_c11_browser_backward_compat.py \
tests/unit/test_c12_mcp_backward_compat.py \
tests/unit/test_c13_sandbox_backward_compat.py \
tests/unit/test_c14_deploy_backward_compat.py \
-q --tb=line --timeout=30 --maxfail=5
python -m pytest tests/unit/cli/ \
../praisonai-bot/tests/unit/cli/test_onboard_command.py \
../praisonai-bot/tests/unit/cli/test_onboard_non_interactive.py \
-m "not slow and not network" \
-q --tb=line \
--timeout=30 \
--ignore=tests/fixtures \
--ignore=tests/unit/cli/test_message_queue.py \
--maxfail=5
- name: Run praisonai-agents subscription-auth smoke tests
env:
PRAISONAI_TEST_TIER: smoke
PRAISONAI_ALLOW_NETWORK: '0'
OPENAI_MODEL_NAME: gpt-4o-mini
OPENAI_API_KEY: 'test-key'
shell: bash
run: |
set -euo pipefail
cd src/praisonai-agents
python -m pytest tests/smoke/test_subscription_auth.py \
tests/unit/auth/test_agent_auth_wiring.py \
-q --tb=line --timeout=60 --maxfail=5
- name: Standalone praisonai-code import smoke
shell: bash
run: |
set -euo pipefail
python -m venv /tmp/praisonai-code-standalone
/tmp/praisonai-code-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-code
/tmp/praisonai-code-standalone/bin/python -c "
import praisonai_code
from praisonai_code.cli.configuration.resolver import ConfigResolver
from praisonai_code._version import get_package_version
from praisonai_code.llm.credentials import is_configured
from praisonai_code.llm.catalogue import ModelCatalogue
import toml
assert hasattr(ConfigResolver, '__name__')
assert callable(is_configured)
assert ModelCatalogue().list_models(provider='openai')
import praisonai_code.cli.main
import praisonai_code.cli.app
from praisonai_code.tool_resolver import ToolResolver
assert ToolResolver is not None
print('standalone ok', get_package_version(), 'toml', toml.__version__)
"
/tmp/praisonai-code-standalone/bin/praisonai-code --version
/tmp/praisonai-code-standalone/bin/praisonai-code run --help
cd src/praisonai-code
for f in praisonai_code/cli/main.py praisonai_code/cli/app.py \
praisonai_code/cli/commands/run.py praisonai_code/cli/commands/chat.py \
praisonai_code/cli/commands/code.py; do
if grep -E '^from praisonai\.|^import praisonai' "$f"; then
echo "FAIL: module-level wrapper import in $f"
exit 1
fi
done
echo "C7 hot-path import gate ok"
bash "$GITHUB_WORKSPACE/scripts/check_c7_imports.sh"
# Standalone actions run (no wrapper); default run/chat require praisonai
set +e
RUN_OUT=$(timeout 90 /tmp/praisonai-code-standalone/bin/praisonai-code run --output actions "Say hello in one word" 2>&1)
RUN_CODE=$?
set -e
echo "$RUN_OUT" | tail -20
if echo "$RUN_OUT" | grep -qE 'ImportError|ModuleNotFoundError: No module named .praisonai'; then
echo "FAIL: standalone actions run hit wrapper import error"
exit 1
fi
echo "standalone actions run import gate ok (exit $RUN_CODE)"
set +e
DEFAULT_RUN=$(timeout 15 /tmp/praisonai-code-standalone/bin/praisonai-code run "hi" 2>&1)
set -e
echo "$DEFAULT_RUN" | tail -10
if ! echo "$DEFAULT_RUN" | grep -qi 'install praisonai'; then
echo "FAIL: default run should prompt for praisonai wrapper install"
exit 1
fi
echo "default run wrapper gate ok"
set +e
CHAT_RUN=$(timeout 10 /tmp/praisonai-code-standalone/bin/praisonai-code chat 2>&1)
set -e
echo "$CHAT_RUN" | tail -10
if ! echo "$CHAT_RUN" | grep -qi 'install praisonai'; then
echo "FAIL: chat should prompt for praisonai wrapper install"
exit 1
fi
echo "chat wrapper gate ok"
- name: Standalone praisonai-bot smoke (C9)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-bot-standalone
/tmp/praisonai-bot-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-bot
/tmp/praisonai-bot-standalone/bin/python -c "
from praisonai_bot._version import __version__ as pkg_version
import praisonai_bot
from praisonai_bot.bots import Bot
from praisonai_bot.bots._registry import resolve_adapter
from praisonai_bot.scheduler.executor import ScheduledAgentExecutor
from praisonai_bot.gateway import WebSocketGateway
assert praisonai_bot.__version__ == pkg_version
telegram = resolve_adapter('telegram')
assert telegram.__name__ == 'TelegramBot'
assert telegram.__module__.startswith('praisonai_bot.bots.')
assert ScheduledAgentExecutor.__module__.startswith('praisonai_bot.')
print('bot standalone ok', praisonai_bot.__version__, telegram.__name__)
"
/tmp/praisonai-bot-standalone/bin/praisonai-bot --help
bash "$GITHUB_WORKSPACE/scripts/check_c9_bot_imports.sh"
bash "$GITHUB_WORKSPACE/scripts/check_bot_code_imports.sh"
/tmp/praisonai-bot-standalone/bin/python -c "
import praisonai_bot.cli.commands.bot as bot_cmd
import praisonai_bot.cli.commands.gateway as gw_cmd
print('bot command modules import ok', bot_cmd.app, gw_cmd.app)
"
- name: Standalone praisonai-train smoke (C10)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-train-standalone
/tmp/praisonai-train-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-train
/tmp/praisonai-train-standalone/bin/python -c "
import sys
from praisonai_train._version import __version__ as pkg_version
import praisonai_train
assert praisonai_train.__version__ == pkg_version
trainer_cls = praisonai_train.AgentTrainer
scenario_cls = praisonai_train.TrainingScenario
assert trainer_cls.__module__.startswith('praisonai_train.')
for heavy in ('torch', 'unsloth', 'trl', 'datasets'):
assert heavy not in sys.modules, f'{heavy} imported eagerly'
print('train standalone ok', praisonai_train.__version__, trainer_cls.__name__)
"
/tmp/praisonai-train-standalone/bin/praisonai-train --help
/tmp/praisonai-train-standalone/bin/python -c "
import praisonai_train.cli.commands.train as train_cmd
print('train command module imports ok', train_cmd.app)
"
bash "$GITHUB_WORKSPACE/scripts/check_c10_train_imports.sh"
- name: Standalone praisonai-browser smoke (C11)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-browser-standalone
/tmp/praisonai-browser-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-browser
/tmp/praisonai-browser-standalone/bin/python -c "
import sys
from praisonai_browser._version import __version__ as pkg_version
import praisonai_browser
assert praisonai_browser.__version__ == pkg_version
server_cls = praisonai_browser.BrowserServer
assert server_cls.__name__ == 'BrowserServer'
for heavy in ('fastapi', 'uvicorn', 'playwright'):
assert heavy not in sys.modules, f'{heavy} imported eagerly'
print('browser standalone ok', praisonai_browser.__version__, server_cls.__name__)
"
/tmp/praisonai-browser-standalone/bin/praisonai-browser --help
bash "$GITHUB_WORKSPACE/scripts/check_c11_browser_imports.sh"
python -m pytest src/praisonai-browser/tests/browser/test_protocol_compliance.py -q -m "not network"
- name: Standalone praisonai-mcp smoke (C12)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-mcp-standalone
/tmp/praisonai-mcp-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-mcp
/tmp/praisonai-mcp-standalone/bin/python -c "
import sys
from praisonai_mcp._version import __version__ as pkg_version
import praisonai_mcp
assert praisonai_mcp.__version__ == pkg_version
for heavy in ('fastapi', 'uvicorn'):
assert heavy not in sys.modules, f'{heavy} imported eagerly'
print('mcp standalone ok', praisonai_mcp.__version__)
"
/tmp/praisonai-mcp-standalone/bin/praisonai-mcp --help
bash "$GITHUB_WORKSPACE/scripts/check_c12_mcp_imports.sh"
python -m pytest src/praisonai-mcp/tests/mcp_server -q -m "not network"
- name: Standalone praisonai-sandbox smoke (C13)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-sandbox-standalone
/tmp/praisonai-sandbox-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-sandbox
/tmp/praisonai-sandbox-standalone/bin/python -c "
from praisonai_sandbox._version import __version__ as pkg_version
import praisonai_sandbox
assert praisonai_sandbox.__version__ == pkg_version
for heavy in ('docker', 'modal', 'e2b'):
assert heavy not in __import__('sys').modules
print('sandbox standalone ok', praisonai_sandbox.__version__)
"
/tmp/praisonai-sandbox-standalone/bin/praisonai-sandbox --help
bash "$GITHUB_WORKSPACE/scripts/check_c13_sandbox_imports.sh"
python -m pytest src/praisonai-sandbox/tests -q -m "not network"
- name: Standalone praisonai-deploy smoke (C14)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-deploy-standalone
/tmp/praisonai-deploy-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-deploy
/tmp/praisonai-deploy-standalone/bin/python -c "
from praisonai_deploy._version import __version__ as pkg_version
import praisonai_deploy
assert praisonai_deploy.__version__ == pkg_version
assert 'main' not in __import__('sys').modules
print('deploy standalone ok', praisonai_deploy.__version__)
"
/tmp/praisonai-deploy-standalone/bin/praisonai-deploy --help
bash "$GITHUB_WORKSPACE/scripts/check_c14_deploy_imports.sh"
python -m pytest src/praisonai-deploy/tests -q -m "not network"
- name: Audit hybrid module imports (C8 full stack)
run: |
set -euo pipefail
pip install -q -e src/praisonai-agents -e src/praisonai-code -e src/praisonai-bot -e src/praisonai-train -e src/praisonai-browser -e src/praisonai-mcp -e src/praisonai-sandbox -e src/praisonai-deploy -e src/praisonai
python scripts/audit_hybrid_modules.py
- name: Smoke Test Summary
if: always()
run: echo "✅ Smoke tests completed"
# ============================================
# MAIN TESTS - Unit + safe integration
# Target: <= 5 minutes
# ============================================
main:
needs: smoke
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
python-version: ['3.11']
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-main-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-main-
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout pytest-cov pytest-xdist
- uses: ./.github/actions/install-monorepo-packages
with:
agents-extras: ".[all]"
wrapper-extras: ".[crewai,autogen,ui,chat]"
- name: Run Main Tests
env:
PRAISONAI_TEST_TIER: main
PRAISONAI_ALLOW_NETWORK: '0'
OPENAI_MODEL_NAME: gpt-4o-mini
OPENAI_API_KEY: 'test-key'
ANTHROPIC_API_KEY: 'test-key'
GOOGLE_API_KEY: 'test-key'
shell: bash
run: |
set -euo pipefail
cd src/praisonai
python -m pytest tests/unit/ \
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
-q --tb=line \
--timeout=60 \
--ignore=tests/fixtures \
--ignore=tests/e2e \
--ignore=tests/live \
--ignore=tests/unit/jobs \
--ignore=tests/unit/test_langflow_components.py \
--cov=praisonai \
--cov-report=xml \
-n 2 --dist loadfile \
--maxfail=10
- name: Upload coverage
if: matrix.python-version == '3.11'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN || '' }}
files: src/praisonai/coverage.xml
flags: main-tests
fail_ci_if_error: false
# ============================================
# OPENAI LIVE TESTS - Real OpenAI API calls
# Only runs if OPENAI_API_KEY secret is set
# Safe for forks (skips gracefully if no key)
# ============================================
openai-live:
needs: smoke
runs-on: ubuntu-latest
timeout-minutes: 8
# This job always runs but steps are conditional on secret availability
steps:
- name: Check if OpenAI key is available
id: check-key
run: |
if [ -n "${{ secrets.OPENAI_API_KEY }}" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
echo "⚠️ OPENAI_API_KEY not set - skipping live tests"
fi
- uses: actions/checkout@v4
with:
persist-credentials: false
if: steps.check-key.outputs.has_key == 'true'
- name: Set up Python 3.11
if: steps.check-key.outputs.has_key == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache pip packages
if: steps.check-key.outputs.has_key == 'true'
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-openai-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
wrapper-extras: ".[openai]"
- name: Run OpenAI Live Tests
if: steps.check-key.outputs.has_key == 'true'
env:
PRAISONAI_TEST_TIER: extended
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'openai'
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL_NAME: ${{ secrets.OPENAI_MODEL_NAME || 'gpt-4o-mini' }}
run: |
cd src/praisonai
python -m pytest tests/ \
-m "provider_openai or real" \
-v --tb=short \
--timeout=120 \
--ignore=tests/fixtures \
|| echo "Some OpenAI tests failed"
- name: Skip notice
if: steps.check-key.outputs.has_key == 'false'
run: echo "✅ OpenAI live tests skipped (no API key available - this is expected for forks)"
# ============================================
# EXTENDED PROVIDER TESTS - Nightly only
# Runs all provider-specific tests
# ============================================
extended-anthropic:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check if Anthropic key is available
id: check-key
run: |
if [ -n "${{ secrets.ANTHROPIC_API_KEY }}" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
echo "⚠️ ANTHROPIC_API_KEY not set - skipping tests"
fi
- uses: actions/checkout@v4
with:
persist-credentials: false
if: steps.check-key.outputs.has_key == 'true'
- name: Set up Python 3.11
if: steps.check-key.outputs.has_key == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
wrapper-extras: ".[anthropic]"
- name: Run Anthropic Tests
if: steps.check-key.outputs.has_key == 'true'
env:
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'anthropic'
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
cd src/praisonai
python -m pytest tests/ \
-m "provider_anthropic" \
-v --tb=short \
--timeout=120 \
--ignore=tests/fixtures \
|| echo "Anthropic tests completed"
- name: Skip notice
if: steps.check-key.outputs.has_key == 'false'
run: echo "✅ Anthropic tests skipped (no API key available)"
extended-google:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check if Google key is available
id: check-key
env:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
run: |
if [ -n "$GOOGLE_API_KEY" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
echo "⚠️ GOOGLE_API_KEY not set - skipping tests"
fi
- uses: actions/checkout@v4
with:
persist-credentials: false
if: steps.check-key.outputs.has_key == 'true'
- name: Set up Python 3.11
if: steps.check-key.outputs.has_key == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
wrapper-extras: ".[google]"
- name: Run Google/Gemini Tests
if: steps.check-key.outputs.has_key == 'true'
env:
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'google'
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
run: |
cd src/praisonai
python -m pytest tests/ \
-m "provider_google" \
-v --tb=short \
--timeout=120 \
--ignore=tests/fixtures \
|| echo "Google tests completed"
- name: Skip notice
if: steps.check-key.outputs.has_key == 'false'
run: echo "✅ Google tests skipped (no API key available)"
extended-groq:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check if Groq key is available
id: check-key
env:
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
run: |
if [ -n "$GROQ_API_KEY" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
echo "⚠️ GROQ_API_KEY not set - skipping tests"
fi
- uses: actions/checkout@v4
with:
persist-credentials: false
if: steps.check-key.outputs.has_key == 'true'
- name: Set up Python 3.11
if: steps.check-key.outputs.has_key == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
- name: Run Groq Tests
if: steps.check-key.outputs.has_key == 'true'
env:
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'groq'
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
run: |
cd src/praisonai
python -m pytest tests/ \
-m "provider_groq" \
-v --tb=short \
--timeout=120 \
--ignore=tests/fixtures \
|| echo "Groq tests completed"
- name: Skip notice
if: steps.check-key.outputs.has_key == 'false'
run: echo "✅ Groq tests skipped (no API key available)"
extended-xai:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check if xAI key is available
id: check-key
env:
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
run: |
if [ -n "$XAI_API_KEY" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
echo "⚠️ XAI_API_KEY not set - skipping tests"
fi
- uses: actions/checkout@v4
with:
persist-credentials: false
if: steps.check-key.outputs.has_key == 'true'
- name: Set up Python 3.11
if: steps.check-key.outputs.has_key == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
- name: Run xAI/Grok Tests
if: steps.check-key.outputs.has_key == 'true'
env:
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'grok_xai'
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
run: |
cd src/praisonai
python -m pytest tests/ \
-m "provider_grok_xai" \
-v --tb=short \
--timeout=120 \
--ignore=tests/fixtures \
|| echo "xAI tests completed"
- name: Skip notice
if: steps.check-key.outputs.has_key == 'false'
run: echo "✅ xAI tests skipped (no API key available)"
extended-cohere:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check if Cohere key is available
id: check-key
env:
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
run: |
if [ -n "$COHERE_API_KEY" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
echo "⚠️ COHERE_API_KEY not set - skipping tests"
fi
- uses: actions/checkout@v4
with:
persist-credentials: false
if: steps.check-key.outputs.has_key == 'true'
- name: Set up Python 3.11
if: steps.check-key.outputs.has_key == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
wrapper-extras: ".[cohere]"
- name: Run Cohere Tests
if: steps.check-key.outputs.has_key == 'true'
env:
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'cohere'
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
run: |
cd src/praisonai
python -m pytest tests/ \
-m "provider_cohere" \
-v --tb=short \
--timeout=120 \
--ignore=tests/fixtures \
|| echo "Cohere tests completed"
- name: Skip notice
if: steps.check-key.outputs.has_key == 'false'
run: echo "✅ Cohere tests skipped (no API key available)"
# ============================================
# LEGACY UNIT TESTS - Post-C6 re-enabled suite
# Nightly / extended tier only (~401 tests)
# ============================================
legacy-unit:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
with:
agents-extras: ".[all]"
wrapper-extras: ".[chat,code,crewai,autogen]"
- name: Run post-C6 legacy unit tests
env:
PRAISONAI_TEST_TIER: extended
PRAISONAI_ALLOW_NETWORK: '0'
PRAISONAI_ALLOW_JOB_WORKFLOWS: 'true'
PRAISONAI_TOOL_SAFETY: 'off'
OPENAI_API_KEY: 'test-key'
run: |
set -euo pipefail
cd src/praisonai
LEGACY_FILES=(
tests/unit/test_warm_runtime.py
tests/unit/test_yaml_cli_backend.py
tests/unit/test_tool_resolver.py
tests/unit/test_serve_unified.py
tests/unit/test_security_fixes_core.py
tests/unit/test_query_rewriter_cli.py
tests/unit/test_kanban_sqlite_store.py
tests/unit/test_job_workflow_agent_steps.py
tests/unit/test_hybrid_workflow.py
../praisonai-bot/tests/unit/test_gateway_session.py
../praisonai-bot/tests/unit/test_gateway_gaps.py
tests/unit/test_discord_approval.py
tests/unit/test_decorator_simple.py
tests/unit/test_db_adapter_tracing_init.py
../praisonai-bot/tests/unit/test_bot_wiring.py
../praisonai-bot/tests/unit/bots/test_enable_shell.py
../praisonai-bot/tests/unit/gateway/test_preflight.py
../praisonai-bot/tests/unit/gateway/test_gateway_doctor.py
../praisonai-bot/tests/unit/test_bot_history.py
../praisonai-bot/tests/unit/test_bot_fixes.py
tests/unit/test_auto_lazy_loading.py
tests/unit/test_async_daemon_deployment.py
tests/unit/test_approval_interactive.py
tests/unit/test_approval_agent_integration.py
tests/unit/test_agents_yaml_validation.py
tests/unit/test_agent_os_wrapper.py
tests/unit/test_lazy_imports.py
tests/unit/test_profiler.py
tests/unit/test_profiler_advanced.py
tests/unit/test_serverless_postgres.py
)
python -m pytest "${LEGACY_FILES[@]}" \
-q --tb=line --timeout=120 --maxfail=10
# ============================================
# TEST SUMMARY
# ============================================
test-summary:
needs: [smoke, main]
if: always()
runs-on: ubuntu-latest
steps:
- name: Test Summary
run: |
echo "# 📊 Test Suite Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Smoke | ${{ needs.smoke.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Main | ${{ needs.main.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Test Tiers" >> $GITHUB_STEP_SUMMARY
echo "- **smoke**: Fast unit tests, no network (≤2 min)" >> $GITHUB_STEP_SUMMARY
echo "- **main**: Unit + integration, no network (≤5 min)" >> $GITHUB_STEP_SUMMARY
echo "- **openai-live**: OpenAI API tests (when key available)" >> $GITHUB_STEP_SUMMARY
echo "- **extended-***: Provider-specific tests (nightly)" >> $GITHUB_STEP_SUMMARY
-179
View File
@@ -1,179 +0,0 @@
name: Real End-to-End Tests
# ⚠️ WARNING: This workflow makes real API calls and incurs costs!
# Only runs when manually triggered to prevent accidental charges
on:
workflow_dispatch: # Manual trigger only
inputs:
framework:
description: 'Framework to test (⚠️ Will incur API costs!)'
required: true
default: 'none'
type: choice
options:
- none
- autogen
- crewai
- all
confirm_costs:
description: 'I understand this will make real API calls and may incur costs'
required: true
type: boolean
default: false
jobs:
real-tests:
runs-on: ubuntu-latest
if: ${{ github.event.inputs.confirm_costs == 'true' && github.event.inputs.framework != 'none' }}
timeout-minutes: 15
strategy:
matrix:
python-version: [3.11] # Single version to minimize costs
steps:
- name: 🚨 Cost Warning
env:
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
INPUT_CONFIRM_COSTS: ${{ github.event.inputs.confirm_costs }}
run: |
echo "⚠️ WARNING: This workflow will make real API calls!"
echo "💰 This may incur charges on your API accounts"
echo "🎯 Framework: $INPUT_FRAMEWORK"
echo "✅ Cost confirmation: $INPUT_CONFIRM_COSTS"
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv pip install --system pytest pytest-asyncio pytest-cov
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV
echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}" >> $GITHUB_ENV
echo "GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }}" >> $GITHUB_ENV
- name: Verify API Keys
run: |
if [ -z "${{ secrets.OPENAI_API_KEY }}" ]; then
echo "❌ OPENAI_API_KEY not set in secrets"
echo "🔧 Add your API key to repository secrets"
exit 1
fi
echo "✅ API keys configured"
- name: Run Real AutoGen Tests
if: ${{ github.event.inputs.framework == 'autogen' || github.event.inputs.framework == 'all' }}
run: |
echo "🤖 Running REAL AutoGen tests (⚠️ API costs may apply)"
cd src/praisonai && python -m pytest tests/e2e/autogen/ -v -m real --tb=short
continue-on-error: false
- name: Run Real CrewAI Tests
if: ${{ github.event.inputs.framework == 'crewai' || github.event.inputs.framework == 'all' }}
run: |
echo "⛵ Running REAL CrewAI tests (⚠️ API costs may apply)"
cd src/praisonai && python -m pytest tests/e2e/crewai/ -v -m real --tb=short
continue-on-error: false
- name: Generate Real Test Report
if: always()
env:
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
run: |
echo "# 🔥 Real End-to-End Test Report" > real_test_report.md
echo "" >> real_test_report.md
echo "⚠️ **WARNING: This report represents tests that made real API calls**" >> real_test_report.md
echo "" >> real_test_report.md
echo "**Framework Tested:** $INPUT_FRAMEWORK" >> real_test_report.md
echo "**Python Version:** ${{ matrix.python-version }}" >> real_test_report.md
echo "**Date:** $(date -u)" >> real_test_report.md
echo "**Triggered by:** $GITHUB_ACTOR" >> real_test_report.md
echo "" >> real_test_report.md
echo "## 🧪 Test Results" >> real_test_report.md
echo "" >> real_test_report.md
if [ "$INPUT_FRAMEWORK" == "autogen" ] || [ "$INPUT_FRAMEWORK" == "all" ]; then
echo "### AutoGen Real Tests:" >> real_test_report.md
echo "- Environment verification" >> real_test_report.md
echo "- Agent creation with real API calls" >> real_test_report.md
echo "- Configuration validation" >> real_test_report.md
echo "" >> real_test_report.md
fi
if [ "$INPUT_FRAMEWORK" == "crewai" ] || [ "$INPUT_FRAMEWORK" == "all" ]; then
echo "### CrewAI Real Tests:" >> real_test_report.md
echo "- Environment verification" >> real_test_report.md
echo "- Crew creation with real API calls" >> real_test_report.md
echo "- Multi-agent setup validation" >> real_test_report.md
echo "" >> real_test_report.md
fi
echo "## 💰 Cost Considerations" >> real_test_report.md
echo "- These tests made actual API calls to LLM providers" >> real_test_report.md
echo "- Costs depend on your API pricing tier" >> real_test_report.md
echo "- Tests are designed to be minimal to reduce costs" >> real_test_report.md
echo "- Check your API provider dashboard for actual usage" >> real_test_report.md
echo "## 📋 Next Steps" >> real_test_report.md
echo "- Review test results for any failures" >> real_test_report.md
echo "- Check API usage in your provider dashboard" >> real_test_report.md
echo "- Use mock tests (tests/integration/) for routine testing" >> real_test_report.md
- name: Upload Real Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: real-test-results-${{ github.event.inputs.framework }}-python-${{ matrix.python-version }}
path: |
real_test_report.md
retention-days: 30
# Safety job that runs when costs not confirmed
safety-check:
runs-on: ubuntu-latest
if: ${{ github.event.inputs.confirm_costs != 'true' || github.event.inputs.framework == 'none' }}
steps:
- name: 🛡️ Safety Check Failed
env:
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
INPUT_CONFIRM_COSTS: ${{ github.event.inputs.confirm_costs }}
run: |
echo "🚨 Real tests not executed due to safety checks:"
echo ""
echo "✅ Costs confirmed: $INPUT_CONFIRM_COSTS"
echo "✅ Framework selected: $INPUT_FRAMEWORK"
echo ""
echo "To run real tests:"
echo "1. Select a framework (autogen, crewai, or all)"
echo "2. Check 'I understand this will make real API calls and may incur costs'"
echo "3. Ensure API keys are set in repository secrets"
echo ""
echo "💡 For cost-free testing, use mock tests instead:"
echo " - Run 'python -m pytest tests/integration/' locally"
echo " - Or trigger other workflows that use mock tests"
exit 1
-213
View File
@@ -1,213 +0,0 @@
name: Windows Tests
on:
push:
branches: [main, develop]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-windows.yml'
- '.github/actions/install-monorepo-packages/**'
pull_request:
branches: [main, develop]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-windows.yml'
- '.github/actions/install-monorepo-packages/**'
workflow_dispatch:
jobs:
test-windows:
runs-on: windows-latest
timeout-minutes: 10
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL_NAME: gpt-4o-mini
PYTHONUTF8: 1
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install monorepo packages
run: python -m pip install --upgrade pip
shell: bash
- uses: ./.github/actions/install-monorepo-packages
# ==================== CORE IMPORTS ====================
- name: Test core imports
run: |
python -c "from praisonaiagents import Agent, Agents, Workflow; print('Core imports OK')"
python -c "from praisonaiagents import tool, Tools; print('Tool imports OK')"
python -c "from praisonaiagents import Task; print('Task imports OK')"
# ==================== AGENT FEATURES ====================
- name: Test Agent instantiation
run: |
python -c "
from praisonaiagents import Agent
a = Agent(name='Test', instructions='You are helpful')
print('Agent created:', a.name)
a2 = Agent(name='ToolAgent', instructions='Use tools', tools=[])
print('Agent with tools:', a2.name)
"
- name: Test Agents orchestration
run: |
python -c "
from praisonaiagents import Agent, Agents, Task
a1 = Agent(name='Agent1', instructions='First agent')
a2 = Agent(name='Agent2', instructions='Second agent')
t = Task(description='Do something', agent=a1)
agents = Agents(agents=[a1, a2], tasks=[t])
print('Agents:', len(agents.agents), 'Tasks:', len(agents.tasks))
"
# ==================== WORKFLOW FEATURES ====================
- name: Test Workflow
run: |
python -c "
from praisonaiagents import Workflow
w = Workflow(name='test')
print('Workflow:', w.name)
"
# ==================== MEMORY FEATURES ====================
- name: Test Memory classes
run: |
python -c "
from praisonaiagents.memory import FileMemory
from praisonaiagents.config import MemoryConfig
cfg = MemoryConfig(backend='file', user_id='test_user')
print('MemoryConfig backend:', cfg.backend)
mem = FileMemory(user_id='test_user', base_path='.praison/test_memory')
print('FileMemory user:', mem.user_id)
"
# ==================== TOOL DECORATOR ====================
- name: Test tool decorator
run: |
python -c "
from praisonaiagents import tool
@tool
def my_tool(query: str) -> str:
'''A test tool'''
return f'Echo: {query}'
result = my_tool('hello')
print('Tool result:', result)
"
# ==================== CONFIG CLASSES ====================
- name: Test config classes
run: |
python -c "
from praisonaiagents.config import OutputConfig, ExecutionConfig, PlanningConfig
out_cfg = OutputConfig(verbose=True, stream=True)
print('OutputConfig verbose:', out_cfg.verbose)
exec_cfg = ExecutionConfig(max_iter=50, max_retry_limit=5)
print('ExecutionConfig max_iter:', exec_cfg.max_iter)
plan_cfg = PlanningConfig(reasoning=True, auto_approve=False)
print('PlanningConfig reasoning:', plan_cfg.reasoning)
"
# ==================== HANDOFF ====================
- name: Test Handoff
run: |
python -c "
from praisonaiagents import Handoff, handoff, Agent
a1 = Agent(name='Agent1', instructions='First')
a2 = Agent(name='Agent2', instructions='Second')
# Using Handoff class directly
h = Handoff(agent=a2)
print('Handoff tool name:', h.tool_name)
# Using handoff function
h2 = handoff(a1, tool_name_override='custom_handoff')
print('Custom handoff name:', h2.tool_name)
"
# ==================== ASYNC FEATURES ====================
- name: Test async imports
run: |
python -c "
import asyncio
from praisonaiagents import Agent
async def test_async():
agent = Agent(name='AsyncAgent', instructions='Test')
assert hasattr(agent, 'arun'), 'Missing arun'
assert hasattr(agent, 'astart'), 'Missing astart'
print('Async methods available')
asyncio.run(test_async())
"
# ==================== CLI COMMANDS ====================
- name: Test CLI help
run: praisonai --help
- name: Test CLI session list
run: praisonai session list
continue-on-error: true
- name: Test CLI queue list
run: praisonai queue list
continue-on-error: true
# ==================== PACKAGING TESTS ====================
- name: Test packaging/daemon entry points
run: |
echo "Testing both entry point methods..."
python -m praisonai --version
praisonai --version
praisonai-bot gateway --help
- name: Test doctor packaging command
run: |
echo "Testing doctor packaging diagnostics..."
praisonai doctor packaging --json
# ==================== REAL API TESTS ====================
- name: Real API test - Agent run
if: env.OPENAI_API_KEY != ''
run: |
python -c "
from praisonaiagents import Agent
agent = Agent(name='TestAgent', instructions='Reply with exactly: WINDOWS_OK', llm='gpt-4o-mini')
result = agent.run('Say WINDOWS_OK')
print('Result:', result)
print('Real API test passed')
"
- name: Real API test - Multi-agent
if: env.OPENAI_API_KEY != ''
run: |
python -c "
from praisonaiagents import Agent, Agents, Task
researcher = Agent(name='Researcher', instructions='You research topics', llm='gpt-4o-mini')
writer = Agent(name='Writer', instructions='You write content', llm='gpt-4o-mini')
task = Task(description='Say hello world', agent=researcher)
agents = Agents(agents=[researcher, writer], tasks=[task])
result = agents.start()
print('Multi-agent result type:', type(result))
print('Multi-agent test passed')
"
-57
View File
@@ -1,57 +0,0 @@
name: Update API Reference
on:
push:
branches: [ "main" ]
paths:
- 'src/praisonai-agents/praisonaiagents/**/*.py'
- 'src/praisonai/praisonai/**/*.py'
- 'src/praisonai-ts/src/**/*.ts'
- '.github/workflows/update-api-md.yml'
permissions:
contents: write
jobs:
update-api-md:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
persist-credentials: true
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Generate api.md
run: |
cd $GITHUB_WORKSPACE
python3 src/praisonai/scripts/generate_api_md.py
- name: Check for changes
id: check_changes
run: |
if git diff --quiet api.md; then
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes to api.md"
else
echo "changes=true" >> $GITHUB_OUTPUT
echo "api.md has been updated"
git diff api.md | head -50
fi
- name: Commit and push changes
if: steps.check_changes.outputs.changes == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: auto-update api.md [skip ci]"
file_pattern: 'api.md'
commit_user_name: "MervinPraison"
commit_user_email: "[email protected]"
commit_author: "MervinPraison <[email protected]>"
-83
View File
@@ -1,83 +0,0 @@
name: Update Documentation Parity Tracker
on:
push:
branches: [ "main" ]
paths:
- 'src/praisonai-agents/praisonaiagents/**/*.py'
- 'src/praisonai-ts/src/**/*.ts'
- 'src/praisonai-rust/src/**/*.rs'
- 'src/praisonai/praisonai/_dev/parity/**/*.py'
- '.github/workflows/update-docs-parity.yml'
workflow_dispatch: # Allow manual trigger
permissions:
contents: write
jobs:
update-docs-parity:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
persist-credentials: true
fetch-depth: 0
- name: Checkout PraisonAIDocs
id: checkout_docs
continue-on-error: true
uses: actions/checkout@v4
with:
repository: MervinPraison/PraisonAIDocs
path: PraisonAIDocs
token: ${{ secrets.SECRETS_ADMIN_PAT }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install pyyaml
- name: Generate docs parity trackers
if: steps.checkout_docs.outcome == 'success'
run: |
cd $GITHUB_WORKSPACE
python3 src/praisonai/scripts/generate_docs_parity.py --docs-root PraisonAIDocs/docs --copy-docs
- name: Check for changes
id: check_changes
run: |
if git diff --quiet src/praisonai-agents/DOCS_PARITY.md src/praisonai-ts/DOCS_PARITY.md src/praisonai-rust/DOCS_PARITY.md 2>/dev/null; then
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes to docs parity trackers"
else
echo "changes=true" >> $GITHUB_OUTPUT
echo "Docs parity trackers have been updated"
git diff --stat src/praisonai-agents/DOCS_PARITY.md src/praisonai-ts/DOCS_PARITY.md src/praisonai-rust/DOCS_PARITY.md
fi
- name: Commit and push changes to praisonai-package
if: steps.check_changes.outputs.changes == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: auto-update documentation parity trackers [skip ci]"
file_pattern: 'src/praisonai-agents/DOCS_PARITY.md src/praisonai-ts/DOCS_PARITY.md src/praisonai-rust/DOCS_PARITY.md'
commit_user_name: "MervinPraison"
commit_user_email: "[email protected]"
commit_author: "MervinPraison <[email protected]>"
- name: Commit and push changes to PraisonAIDocs
if: steps.check_changes.outputs.changes == 'true' && steps.checkout_docs.outcome == 'success'
run: |
cd PraisonAIDocs
git config user.name "MervinPraison"
git config user.email "[email protected]"
git add docs/features/DOCS_PARITY.md docs/js/DOCS_PARITY.md docs/rust/DOCS_PARITY.md
git diff --staged --quiet || git commit -m "docs: auto-update documentation parity trackers [skip ci]"
git push
@@ -1,61 +0,0 @@
name: Update Feature Parity Tracker
on:
push:
branches: [ "main" ]
paths:
- 'src/praisonai-agents/praisonaiagents/**/*.py'
- 'src/praisonai-ts/src/**/*.ts'
- 'src/praisonai-rust/src/**/*.rs'
- '.github/workflows/update-parity-tracker.yml'
permissions:
contents: write
jobs:
update-parity-tracker:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
persist-credentials: true
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install pyyaml
- name: Generate parity trackers
run: |
cd $GITHUB_WORKSPACE
python3 src/praisonai/scripts/generate_parity_tracker.py
- name: Check for changes
id: check_changes
run: |
if git diff --quiet src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md 2>/dev/null; then
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes to parity trackers"
else
echo "changes=true" >> $GITHUB_OUTPUT
echo "Parity trackers have been updated"
git diff src/praisonai-ts/FEATURE_PARITY_TRACKER.json | head -50
fi
- name: Commit and push changes
if: steps.check_changes.outputs.changes == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: auto-update feature parity trackers [skip ci]"
file_pattern: 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md'
commit_user_name: "MervinPraison"
commit_user_email: "[email protected]"
commit_author: "MervinPraison <[email protected]>"
+21 -116
View File
@@ -1,118 +1,23 @@
workspace.code-workspace
.vscode
crewai
.cache
__pycache__
chroma.sqlite3
test
dist
.env
assets
.idea
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.pytest_cache
praisonAI.egg-info
src/*/build/
flagged
test.yaml
db
praisonai_prompt.txt
watch.sh
docs.sh
other
output
!src/praisonai/praisonai/cli/output/
!src/praisonai-agents/praisonaiagents/output/
!src/praisonai-code/praisonai_code/cli/output/
!src/praisonai-bot/praisonai_bot/cli/output/
!src/praisonai-ts/src/db/
!src/praisonai-ts/src/cli/output/
.env.local
.env.development.local
.env.test.local
.env.production.local
.files
threads.db
threads.db-journal
# Example run artifacts (generated at runtime, must not be committed)
examples/**/knowledge.db
examples/**/knowledge.db-journal
examples/python/managed-agents/managed_ids.json
examples/python/save_output/blog_post.md
examples/python/save_output/generated/
examples/python/token-metrics/token_metrics_export.json
examples/python/usecases/market_research_results.json
.chainlit
!praisonai/ui/config/.chainlit
site
# Ignore Sphinx build directory
docs/_build/
docs/source/
docs/build/
# Ignore Sphinx cache
docs/_build/doctrees/
docs/_build/doctrees/*
docs/_build/doctrees/.doctrees
# Ignore Sphinx logs and local build files
docs/_build/.buildinfo
docs/_build/.buildinfo/*
docs/_build/.buildinfo/.buildinfo
praisonai/doc
# agentops
trained_agents_data.pkl
agentops.log
# crewAI
crewAI
!tests/integration/crewai
!tests/e2e/crewai
!src/praisonai/tests/integration/crewai
!src/praisonai/tests/e2e/crewai
# virtualenv
.venv
# Credentials
credentials.json
# node_modules
node_modules
outputs
agents/praisonaiagents/build
agents/praisonaiagents/dist
agents/praisonaiagents/praisonaiagents.egg-info
.codegpt
.praison
# Local Netlify folder
.netlify
.qodo
CopilotKit*
.gemini
issue_details.json
# MCP Registry tokens
.mcpregistry_*
*.log
.internal
.praisonai
# Backup files
*.bak
# Rust / Cargo
target/
Cargo.lock
**/*.rs.bk
*.pdb
.windsurf
src/praisonai/build*.egg-info/
src/praisonai/PraisonAI.egg-info/
jobs/*
*.egg-info
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-15
View File
@@ -1,15 +0,0 @@
# Agent Instructions
You are working on the PraisonAI project.
## Project Guidelines
- Follow the existing code style and conventions
- Be concise and helpful in responses
- Test implementation thoroughly
- Ensure backward compatibility with existing APIs
- Follow protocol-driven design across the nine packages: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, deployment in `praisonai-deploy/`, integrations/serve/dashboard in the `praisonai/` wrapper
- Preserve old `praisonai.*` import paths via shims when moving code between packages (see §2.3 in `src/praisonai-agents/AGENTS.md`; shim helpers in `src/praisonai/praisonai/cli/_shim.py`)
- Package boundaries and dependency rules: `ARCHITECTURE.md` §2 (Tier 2 packages must never PyPI-depend on the wrapper; cross-tier access goes through lazy `_*_bridge` modules)
- Boundary manifests: `src/praisonai/tests/PRAISONAI_BOT_MANIFEST.md` (C9), `src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md` (C10), `src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md` (C11), `src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md` (C12), `src/praisonai/tests/PRAISONAI_SANDBOX_MANIFEST.md` (C13), `src/praisonai/tests/PRAISONAI_DEPLOY_MANIFEST.md` (C14)
- When reviewing a PR or an issue, evaluate whether the change addresses a framework concern or a user goal, and design its surface (params, naming, defaults) accordingly
- The aim of this package is to stay **lightweight and powerful**. Do a critical review at each stage — when triaging an issue, when planning a fix, and when reviewing/implementing a PR. Reject scope creep for the sake of adding features: if a capability already exists (e.g. via existing Agent params like `instructions`/`backstory`/`tools`/`hooks`/`memory`), prefer it over a new API surface. A change must genuinely strengthen the SDK (simpler, more robust, more user-friendly) — do not add knobs, params, modules, or exports that have no live consumer or that merely duplicate existing behaviour.
-593
View File
@@ -1,593 +0,0 @@
# PraisonAI Architecture
> **Last updated:** 2026-07-31 (C14 — nine-package tiered model)
>
> Strategic architecture document for PraisonAI — a multi-agent AI framework.
> Covers **Python tiered package model (C7.1 + C9 + C10)**, system design, runtime
> architecture, data contracts, reliability, observability, and the road map.
---
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13 + C14)](#2-python-tiered-package-model-c71--c9--c10--c11--c12--c13--c14)
3. [System Overview](#3-system-overview)
4. [Layered Architecture](#4-layered-architecture)
5. [Core Data Contracts](#5-core-data-contracts)
6. [Reliability by Design](#6-reliability-by-design)
7. [Observability & Telemetry](#7-observability--telemetry)
8. [Replay & Checkpointing](#8-replay--checkpointing)
9. [Implementation Roadmap](#9-implementation-roadmap)
10. [Success Metrics](#10-success-metrics)
**Related boundary docs:** [`src/praisonai/tests/C7.1_BOUNDARIES.md`](src/praisonai/tests/C7.1_BOUNDARIES.md) · [`src/praisonai/tests/C9.1_BOUNDARIES.md`](src/praisonai/tests/C9.1_BOUNDARIES.md) · [`src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md`](src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md) · [`src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md`](src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md) · [`src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md`](src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md) · [`src/praisonai/tests/C7_VERIFICATION.md`](src/praisonai/tests/C7_VERIFICATION.md) · [`src/praisonai/tests/C9_VERIFICATION.md`](src/praisonai/tests/C9_VERIFICATION.md) · [`src/praisonai-agents/AGENTS.md`](src/praisonai-agents/AGENTS.md) §2.4
---
## 1. Executive Summary
PraisonAI is a multi-agent AI framework with broad capability surface across
Python, TypeScript, and Rust SDKs. The framework's core strength is its
feature breadth — agent abstractions, integrations, workflows, and active
release cadence.
The strategic bet for the next two quarters is building a **Reliability +
Orchestration + Observability** core to unlock adoption and trust:
| Dimension | Current State | Target |
|-----------|--------------|--------|
| Reliability | Cross-platform mismatches, optional dep fragility | Deterministic gating, adapter abstraction, parity CI |
| Determinism | Flaky tests, global state collisions | Isolated test fixtures, traceable tenant IDs |
| Developer UX | Complex onboarding, implicit config | Golden-path CLI, Doctor Auto-Fix |
| Observability | Minimal runtime tracing | Structured event bus, replay engine, failure classifier |
### Design Principles
- **Deterministic by default**, flexible by opt-in
- **Policy-enforced execution boundaries** at every layer
- **Structured events** for all lifecycle transitions
- **Capability isolation** per agent and per tool
---
## 2. Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13 + C14)
**Release:** v4.6.110+ · `praisonaiagents` · `praisonai-code` · `praisonai-bot` · `praisonai-train` · `praisonai-browser` · `praisonai-mcp` · `praisonai-sandbox` · `praisonai-deploy` · `praisonai`
The Python monorepo publishes nine packages in three tiers with strict dependency
direction. C7 delivered a standalone agentic hot path; C7.1 formalised
code/wrapper ownership; C9 extracted bots, gateway, and channel CLI into
`praisonai-bot`; C10 extracted LLM fine-tuning and agent training into
`praisonai-train`; C11 extracted browser automation into `praisonai-browser`;
C12 extracted the heavy MCP host into `praisonai-mcp`.
C13 extracted sandbox backends into `praisonai-sandbox`.
C14 extracted deployment (API, Docker, cloud) into `praisonai-deploy`.
```mermaid
flowchart TB
subgraph tier1 [Tier 1 — Core SDK]
Agents["praisonaiagents<br/>Agent, tools, memory, hooks, protocols"]
end
subgraph tier2 [Tier 2 — Terminal + Bot + Train + Browser + MCP + Sandbox + Deploy]
Code["praisonai-code<br/>run, chat, code, Typer, runtime, LLM"]
Bot["praisonai-bot<br/>bots, gateway, channel CLI, OS daemon"]
Train["praisonai-train<br/>LLM fine-tuning, agent training"]
Browser["praisonai-browser<br/>extension bridge, CDP, Playwright"]
MCP["praisonai-mcp<br/>MCP server host, auth, capability adapters"]
Sandbox["praisonai-sandbox<br/>Docker, E2B, Modal, Sandlock backends"]
Deploy["praisonai-deploy<br/>API, Docker, AWS/Azure/GCP deploy"]
end
subgraph tier3 [Tier 3 — Wrapper]
Wrapper["praisonai<br/>framework_adapters, serve, dashboard"]
end
Agents --> Code
Agents --> Bot
Agents --> Train
Agents --> Browser
Agents --> MCP
Agents --> Sandbox
Agents --> Deploy
Agents --> Wrapper
Code -.->|"lazy _bot_bridge"| Bot
Code -.->|"lazy _train_bridge"| Train
Code -.->|"lazy _browser_bridge"| Browser
Code -.->|"lazy _mcp_bridge"| MCP
Code -.->|"lazy _sandbox_bridge"| Sandbox
Code -.->|"lazy _deploy_bridge"| Deploy
Code -.->|"lazy _wrapper_bridge"| Wrapper
Bot -.->|"lazy _code_bridge / _wrapper_bridge"| Code
Bot -.->|"lazy _wrapper_bridge"| Wrapper
Train -.->|"lazy _code_bridge / _wrapper_bridge"| Code
```
**PyPI publish order:** `praisonaiagents` → `praisonai-code` + `praisonai-bot` + `praisonai-train` + `praisonai-browser` + `praisonai-mcp` + `praisonai-sandbox` + `praisonai-deploy` → `praisonai`
**Backward compatibility:** `praisonai.bots`, `praisonai.gateway`, `praisonai.train`,
`praisonai.browser`, `praisonai.mcp_server`, `praisonai.sandbox`, `praisonai.deploy`, and related CLI paths remain as
`alias_package` shims to `praisonai_bot.*` / `praisonai_train.*` /
`praisonai_browser.*` / `praisonai_mcp.*` / `praisonai_sandbox.*` / `praisonai_deploy.*`.
| Tier | Package | Owns | Must not depend on |
|------|---------|------|-------------------|
| 1 | `src/praisonai-agents/` | Agent, tools, memory, hooks, `frameworks/` protocols, sandbox protocols | `praisonai`, `praisonai-code`, `praisonai-bot`, `praisonai-train`, `praisonai-browser`, `praisonai-mcp`, `praisonai-sandbox`, `praisonai-deploy` |
| 2a | `src/praisonai-code/` | `run`/`chat`/`code`, Typer, runtime, LLM, tool resolution | **`praisonai` as a PyPI dependency** (optional lazy imports via `_wrapper_bridge` only) |
| 2b | `src/praisonai-bot/` | Bots, gateway, channel CLI, OS daemon, gateway scheduler tick | **`praisonai` as a PyPI dependency** (optional lazy `_wrapper_bridge` for jobs/UI) |
| 2c | `src/praisonai-train/` | LLM fine-tuning (Unsloth), agent training, `train` CLI, conda env setup | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for legacy dispatch) |
| 2d | `src/praisonai-browser/` | Extension bridge, CDP/hybrid automation, `browser` CLI | **`praisonai` as a PyPI dependency** (none required; depends on `praisonaiagents` only) |
| 2e | `src/praisonai-mcp/` | MCP server host, auth, transports, `mcp` CLI | **`praisonai` as a PyPI dependency** (lazy `_wrapper_bridge` for full capability registry) |
| 2f | `src/praisonai-sandbox/` | Sandbox backends (Docker, E2B, Modal, Sandlock, SSH), `SandboxRegistry` | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for `PluginRegistry`) |
| 2g | `src/praisonai-deploy/` | Deploy API/Docker/cloud, `deploy` CLI, scheduler integration | **`praisonai` as a PyPI dependency** (lazy `_plugin_registry` via `_code_bridge`) |
| 3 | `src/praisonai/` | `framework_adapters/`, serve, dashboard, async jobs API | — |
### Repo infra (not PyPI)
Deployment packaging that stays in the git checkout, not in any tier-2 wheel:
| Path | Runtime owner | Orchestration |
|------|---------------|---------------|
| `src/praisonai-bot/infra/helm/praisonai-gateway/` | `praisonai-bot` (gateway) | C14 `deploy helm` wrapper |
| `src/praisonai-deploy/infra/helm/praisonai-agents-api/` | Generated API from C14 | C14 cross-link |
| `src/praisonai-deploy/infra/compose/agents-stack/` | Docker Compose | `praisonai deploy compose up/down` |
| `src/praisonai-deploy/infra/starters/` | Starter scaffolds | `praisonai deploy create --template` |
| `docker/` | Mixed (wrapper/bot dev stacks) | Not C14 — see bot manifest for `docker/bots/` |
Same boundary as C14: Helm/K8s manifests are **repo infra**, not `pip install praisonai-deploy` content.
**Config kernel:** Phase 0 `praisonai/common/` was skipped; shared config lives in
`praisonai_code/cli/configuration/` and is reached by the bot tier via lazy
`_code_bridge` (see `src/praisonai/tests/CONFIG_KERNEL.md`).
### Dependency rule (validated)
| Question | Answer |
|----------|--------|
| Does `praisonai-code` declare `praisonai` in `pyproject.toml`? | **No** — only `praisonaiagents` + CLI/runtime deps |
| Does `praisonai` declare `praisonai-code`? | **Yes** — one-way chain, no PyPI cycle |
| Can `praisonai-code` import `praisonai.*` at runtime? | **Only lazily** via `praisonai_code._wrapper_bridge` for optional features; **not** on the agentic hot path |
| Does standalone `pip install praisonai-code` work? | **Yes** — CI smoke validates imports + `praisonai-code run` without the wrapper |
### CLI routing
```mermaid
flowchart LR
User[User] --> Entry{Entry point}
Entry -->|pip install praisonai-code| CodeCLI["praisonai-code run/chat/code"]
Entry -->|pip install praisonai| WrapperCLI["praisonai …"]
WrapperCLI --> Router["praisonai.__main__"]
Router --> CodePath["praisonai_code.cli.app"]
Router --> Legacy["praisonai.cli.main"]
CodePath --> HotPath["run / chat / code"]
CodePath --> Bridge{"wrapper_available?"}
Bridge -->|yes| WrapperCmds["bot, gateway, pairing, …"]
Bridge -->|no| Hidden["Wrapper commands hidden"]
```
### Import gates (CI enforced)
- **Hot path:** no module-level `from praisonai` in `main.py`, `app.py`, `run.py`, `chat.py`, `code.py`
- **Regression baseline:** 50 direct wrapper import lines max (`scripts/check_c7_imports.sh`; C8 achieved **0**)
- **Allowlist:** reviewed files only (`scripts/c7_wrapper_import_allowlist.txt`; empty post-C8)
- **Hybrid audit:** `scripts/audit_hybrid_modules.py` — repatriated cross-tier import paths
### C7 / C7.1 / C8 status
| Milestone | Status |
|-----------|--------|
| Standalone agentic hot path | **Complete** |
| `_wrapper_bridge` hardening | **Complete** |
| Import gate + allowlist | **Complete** |
| Boundary tests + CI smoke | **Complete** |
| C8 reverse import elimination (225 → 0 direct imports) | **Complete** — see [`C8_BACKLOG.md`](src/praisonai/tests/C8_BACKLOG.md) |
| C8.4 main.py physical decomposition | **Deferred** (separate epic) |
| PyPI package splits (`praisonai-bot`, etc.) | **Out of scope** |
---
## 3. System Overview
```
User/SDK/CLI
→ Workflow Compiler (graph + policy + schemas)
→ Execution Orchestrator (state machine, retries, fallbacks)
→ Tool Runtime (sandbox + approval + capability scope)
→ Model Runtime (provider abstraction + rate/timeout policy)
→ Observability Bus (trace events + metrics + cost)
→ Replay Engine (checkpoint + deterministic re-run)
→ Persistence (session, memory, artifacts, run ledger)
```
### Runtime Components
| Component | Responsibility | Interface |
|-----------|---------------|-----------|
| API/CLI Gateway | Accepts run requests, validates config/profile | `RunRequest`, `RunProfile` |
| Workflow Compiler | Converts YAML/DSL to normalized execution graph | `CompiledGraph` |
| Orchestrator | Executes graph state machine with retries/fallbacks | `ExecutionState`, `StepTransition` |
| Tool Runtime | Runs tools with approval/sandbox policies | `ToolCall`, `ToolResult` |
| Model Runtime | Provider routing, timeout, budget control | `ModelRequest`, `ModelResponse` |
| Observability Bus | Streams structured events + metrics | `RunEvent` schema |
| Replay Engine | Restarts from checkpoint with deterministic inputs | `ReplayRequest` |
### Existing Architecture (Python SDK)
The Python SDK is organised as three publishable tiers (see [§2](#2-python-three-tier-package-model-c71)):
- `src/praisonai-agents/` — Core SDK (`Agent`, tools, memory, hooks, protocols)
- `src/praisonai-code/` — Terminal CLI (`run`, `chat`, `code`, Typer, runtime, LLM)
- `src/praisonai/` — Wrapper (gateway, bots, `framework_adapters/`, integrations)
Core execution modules live in `praisonaiagents`:
- `agent/` — Agent class, handoff, autonomy
- `llm/` — Model runtime with provider routing, rate limiting, failover
- `tools/` — Tool runtime with sandbox, approval, retry
- `memory/` — Memory runtime (in-memory, SQLite, MongoDB, Mem0 adapters)
- `knowledge/` — Knowledge management (indexing, retrieval, chunking)
- `workflows/` — Workflow engine (YAML/SDK-based orchestration)
- `hooks/`, `bus/` — Hook system and event bus
- `mcp/` — MCP protocol support
- `a2a/`, `a2ui/` — Agent-to-agent and agent-to-UI protocols (wrapper/SDK)
Wrapper-only surfaces remain in `src/praisonai/`:
- `gateway/`, `bots/` — Multi-bot orchestration (BotOS)
- `framework_adapters/` — CrewAI, AutoGen, PraisonAI adapters
- `cli/commands/` — Wrapper commands (`bot`, `gateway`, `pairing`, …)
### Multi-SDK Layout
- **Python Core SDK** — `src/praisonai-agents/` (`praisonaiagents`)
- **Python Terminal CLI** — `src/praisonai-code/` (`praisonai-code`)
- **Python Wrapper** — `src/praisonai/` (`praisonai`)
- **TypeScript SDK** — JS/TS runtime (`ts-sdk/`)
- **Rust SDK** — High-performance Rust runtime (`rust-sdk/`)
- **UI** — Web UI (`ui/`)
---
## 4. Layered Architecture
```mermaid
flowchart TB
subgraph UX["Interface Layer"]
CLI[CLI]
SDK[SDK]
API[API Gateway]
end
subgraph Control["Control Plane"]
Compiler[Workflow Compiler]
Policy[Policy Engine]
Orchestrator[Execution Orchestrator]
end
subgraph Runtime["Runtime Plane"]
ModelRT[Model Runtime]
ToolRT[Tool Runtime]
MemoryRT[Memory Runtime]
end
subgraph Observe["Observability Plane"]
EventBus[Run Event Bus]
Metrics[Metrics + Cost]
Replay[Replay Engine]
end
subgraph Data["Data Plane"]
Ledger[Run Ledger]
Checkpoints[Checkpoints]
Artifacts[Artifacts Store]
end
CLI --> API
SDK --> API
API --> Compiler
API --> Policy
Compiler --> Orchestrator
Policy --> Orchestrator
Orchestrator --> ModelRT
Orchestrator --> ToolRT
Orchestrator --> MemoryRT
Orchestrator --> EventBus
EventBus --> Metrics
EventBus --> Replay
Orchestrator --> Ledger
Orchestrator --> Checkpoints
ToolRT --> Artifacts
Replay --> Checkpoints
```
### Interface Layer
- **CLI** — The `praisonai` command-line entry point
- **SDK** — Python library API (`from praisonai import Agent`)
- **API Gateway** — HTTP/WebSocket REST API (`praisonai api` or `a2a`/`a2ui`)
### Control Plane
- **Workflow Compiler** — Converts YAML workflows and SDK-defined graphs into
a normalized `CompiledGraph` with adjacency validation and cycle detection.
- **Policy Engine** — Enforces runtime policies (budget, timeout, approval
gates, capability validation) before and during execution.
- **Execution Orchestrator** — Drives the state machine through graph nodes,
handling retry, fallback, and failure classification.
### Runtime Plane
- **Model Runtime** — Provider abstraction layer. Routes to OpenAI, Anthropic,
Gemini, Ollama, DeepSeek, etc. Handles rate limiting, token tracking, and
automatic failover between providers.
- **Tool Runtime** — Executes tool calls with configurable sandboxing,
approval gates, retry policies, and capability scope validation.
- **Memory Runtime** — Manages session memory, persistent memory stores,
and knowledge retrieval across in-memory, SQLite, and MongoDB backends.
### Observability Plane
- **Run Event Bus** — Structured event streaming for all lifecycle transitions
(START, INPUT, MODEL_CALL, TOOL_CALL, ERROR, RETRY, COMPLETE).
- **Metrics + Cost** — Token counting, cost tracking, and performance metrics.
- **Replay Engine** — Deterministic replay from the last stable checkpoint,
with state hash verification.
### Data Plane
- **Run Ledger** — Persistent record of all runs with state, metrics, and
failure classifications.
- **Checkpoints** — Point-in-time snapshots of run state, memory, and
artifacts for replay and recovery.
- **Artifacts Store** — Tool outputs, generated files, and intermediate
results.
---
## 5. Core Data Contracts
### RunRequest
```text
RunRequest {
run_id: UUID,
workflow_ref: string,
inputs: object,
profile: {safe_mode, budget, timeout, approval_policy},
context: {user_id, workspace_id, env}
}
```
### RunEvent
```text
RunEvent {
ts: ISO8601,
run_id: UUID,
step_id: string,
type: START | INPUT | MODEL_CALL | TOOL_CALL |
ERROR | RETRY | COMPLETE,
payload: object,
cost: {tokens_in, tokens_out, usd},
latency_ms: number
}
```
### Checkpoint
```text
Checkpoint {
run_id: UUID,
step_id: string,
state_hash: string,
memory_snapshot_ref: string,
artifact_refs: string[]
}
```
### Execution Sequence
```text
1) Client submits RunRequest
2) Gateway validates profile + schema
3) Compiler resolves workflow → CompiledGraph
4) Orchestrator starts node execution
5) Node may call Model Runtime or Tool Runtime
6) Every transition emits RunEvent
7) On failure: classifier decides retry/fallback/abort
8) Checkpoint stored after each critical step
9) Final state + artifacts persisted to run ledger
10) Replay can resume from last stable checkpoint
```
---
## 6. Reliability by Design
### Error Taxonomy
All failures are classified into one of five categories for automatic
remediation:
| Category | Description | Default Action |
|----------|-------------|----------------|
| `config` | Invalid configuration or profile | Abort with remediation hint |
| `dependency` | Missing optional module or capability | Degrade / skip optional path |
| `tool` | Tool execution failure | Retry with backoff and policy cap |
| `model` | Model API error or timeout | Fallback to alternate provider/route |
| `infra` | Network or resource exhaustion | Circuit-breaker + replay from checkpoint |
```mermaid
flowchart TD
S[Step Failure] --> T{Failure Type?}
T -->|Config| C1[Abort + config remediation hint]
T -->|Dependency| C2[Degrade capability / skip optional path]
T -->|Tool| C3[Retry with backoff and policy cap]
T -->|Model| C4[Fallback model/provider route]
T -->|Infra| C5[Circuit-breaker + replay from checkpoint]
```
### Hardening Checklist
- **Dependency gates** — Optional modules declared as capabilities;
unavailable capability results in skip/degrade, not crash.
- **Cross-platform adapters** — OS-specific implementations behind a single
interface (e.g., file lock adapter for Windows/Linux/macOS).
- **Idempotent fixtures** — Integration tests use unique tenant/workspace IDs
and guaranteed teardown.
- **Fail-safe output mode** — CLI rendering falls back to ASCII-safe mode on
encoding mismatch.
- **CI parity matrix** — Run smoke workflows across Windows, Linux, and macOS.
### Deterministic Test Pattern
```python
# Integration test pattern — always use unique, traceable IDs
import uuid
def test_agent_workflow():
tenant_id = f"test-{uuid.uuid4().hex[:8]}"
agent = Agent(name=f"agent-{tenant_id}", ...)
result = agent.run("task")
assert result.status == "success"
# teardown is guaranteed via fixture or context manager
```
---
## 7. Observability & Telemetry
### Current Telemetry Stack
PraisonAI already includes:
- **OpenTelemetry integration** — Manual and auto-instrumentation for
traces, metrics, and logs.
- **Token tracking** — Per-call and cumulative token/cost tracking.
- **Performance monitoring** — Real-time dashboards and monitoring views.
- **LangTrace integration** — LangTrace provider for tracing.
- **Run outcomes** — Structured `RunOutcome` objects with status, duration,
token usage, and error classification.
### Planned Enhancements
- **Run Event Bus** — Stream all lifecycle events as structured `RunEvent`
payloads.
- **Failure classifier** — Automatic failure category detection with
remediation hints.
- **Run ledger visualization** — Timeline view of runs with step-by-step
event drill-down.
- **Replay integration** — Checkpoint-based run replay from the
observability dashboard.
---
## 8. Replay & Checkpointing
### Design
```mermaid
sequenceDiagram
participant U as User/Client
participant G as Gateway
participant C as Compiler
participant O as Orchestrator
participant R as Model/Tool Runtime
participant E as Event Bus
participant L as Run Ledger
participant K as Checkpoint Store
U->>G: Submit RunRequest
G->>C: Validate + compile workflow
C-->>O: CompiledGraph
O->>R: Execute next step
R-->>O: Step output / error
O->>E: Emit structured RunEvent
O->>K: Persist checkpoint
O->>L: Persist run state + metrics
O-->>U: Final result / failure summary
```
### Current State
- Basic run outcome persistence exists
- Session persistence for bot/multi-turn conversations
- Token and cost tracking per run
### Roadmap
- Full checkpoint creation at configurable step granularity
- State hash verification for deterministic replay
- Replay API endpoint (`POST /runs/{id}/replay`)
- Checkpoint pruning and retention policy
---
## 9. Implementation Roadmap
### Completed (C7 / C7.1 — v4.6.110)
| Project | Description | Status |
|---------|-------------|--------|
| C7 hot path | Standalone `praisonai-code run/chat/code` without wrapper import | **Complete** |
| C7.1 boundaries | Three-tier ownership, `_wrapper_bridge`, import gates | **Complete** |
| CI parity | Smoke standalone block + pre-existing test fixes (#2560) | **Complete** |
### Quarter 1: Trust Foundation (Q3 2026)
| Priority | Project | Description | Status |
|----------|---------|-------------|--------|
| P0 | Reliability Core | Cross-platform hardening, optional-dep gating, deterministic fixtures | In progress |
| P0 | Doctor Auto-Fix | Automated environment diagnosis and repairs | Planned |
| P1 | Golden-Path CLI | Single canonical flow: init → run → test → deploy | Planned |
| P1 | CI Parity Matrix | Cross-platform smoke tests in CI pipeline | Planned |
### Quarter 2: Operational Excellence (Q4 2026)
| Priority | Project | Description | Status |
|----------|---------|-------------|--------|
| P1 | Trace + Replay | Run timeline, checkpoint replay, failure classifier | Planned |
| P1 | Failure Classifier | Automatic failure category detection with remediation | Planned |
| P2 | Graph Studio UX | Visual deterministic orchestration editor/inspector | Planned |
| P2 | Safe Production Profiles | Policy presets: guardrails, approvals, cost/time caps | Planned |
```mermaid
gantt
title PraisonAI Architecture Program (2 Quarters)
dateFormat YYYY-MM-DD
section Quarter 1
Reliability hardening + parity CI :a1, 2026-07-01, 45d
Deterministic test isolation :a2, 2026-07-10, 50d
Doctor Auto-Fix MVP :a3, 2026-07-20, 40d
Golden-path CLI :a4, 2026-08-01, 45d
section Quarter 2
Trace bus + run ledger :b1, 2026-10-01, 45d
Replay engine :b2, 2026-10-15, 45d
Failure classifier :b3, 2026-11-01, 30d
Graph UX beta + Safe profiles :b4, 2026-11-10, 50d
```
---
## 10. Success Metrics
### KPI Targets
| KPI | Target | Primary Workstream |
|-----|--------|-------------------|
| Time-to-first-successful run | 30-40% reduction | Golden-path CLI + Doctor Auto-Fix |
| Cross-platform issue rate | 50% reduction | Reliability Core + adapter layer |
| Incident triage time | 40% reduction | Trace + Replay + failure classifier |
| CI confidence | Flaky test rate <2% | Deterministic tests + fixture isolation |
### Governance
- **Architecture Review Council** — Reviews runtime-contract changes
- **Release Quality Gate** — Platform matrix + deterministic test thresholds
- **Monthly telemetry review** — DX, reliability, adoption trends
---
*This document is a living reference. Update as the architecture evolves.*
-14
View File
@@ -1,14 +0,0 @@
# Contributing to PraisonAI
Thank you for your interest! Here's how to get started:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request
## Guidelines
- Follow existing code style
- Add tests for new features
- Update documentation as needed
-20
View File
@@ -1,20 +0,0 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+38 -1166
View File
File diff suppressed because it is too large. Load diff
-73
View File
@@ -1,73 +0,0 @@
# Security Policy
PraisonAI takes security seriously. We appreciate responsible disclosure from researchers and users.
## Supported packages
| Package | Registry | Source path |
|---------|----------|-------------|
| `praisonaiagents` | [PyPI](https://pypi.org/project/praisonaiagents/) | `src/praisonai-agents/` |
| `praisonai` | [PyPI](https://pypi.org/project/praisonai/) | `src/praisonai/` |
| `praisonai` | [npm](https://www.npmjs.com/package/praisonai) | `src/praisonai-ts/` |
Report issues against the **specific package** where the vulnerability exists. A platform-only issue does not necessarily affect the core SDK, and vice versa.
### Packages maintained in their own repositories
`praisonai-platform` now lives at
[MervinPraison/PraisonAI-Platform](https://github.com/MervinPraison/PraisonAI-Platform).
Report platform vulnerabilities through
[that repository's advisories](https://github.com/MervinPraison/PraisonAI-Platform/security/advisories/new),
not here — it is versioned and released independently of the SDK.
## In scope
- Authentication, authorisation, and multi-tenant isolation (e.g. workspace boundaries)
- Tool execution, sandbox escape, and code execution paths
- SSRF, injection, and unsafe deserialisation in shipped code
- MCP, gateway, and bot/webhook integrations
- Secrets handling, JWT/session configuration, and default credentials
- Supply-chain issues in published package artefacts
## Out of scope
- Issues in forks or deployments not maintained in [MervinPraison/PraisonAI](https://github.com/MervinPraison/PraisonAI)
- Vulnerabilities requiring physical access or fully compromised operator machines
- Reports against versions no longer published or without a supported upgrade path
- Theoretical issues with no practical impact in documented, supported configurations
- Behaviour covered by an explicit, documented opt-out (e.g. dev-only env vars)
## How to report
**Preferred:** open a **private** security advisory:
[https://github.com/MervinPraison/PraisonAI/security/advisories/new](https://github.com/MervinPraison/PraisonAI/security/advisories/new)
Include:
1. Affected package and version
2. Clear reproduction steps
3. Impact assessment (confidentiality, integrity, availability)
4. Suggested fix (optional)
Please allow reasonable time for triage and a patched release before public disclosure.
## What to expect
1. **Triage** — we validate the report and assign a disposition (`real`, `duplicate`, `wontfix`, etc.)
2. **Fix** — minimal, backward-compatible patch on `main` where appropriate
3. **Release** — patched version published to PyPI/npm **before** the advisory is published
4. **Advisory** — GitHub Security Advisory with reporter credit and CVE where applicable
## Resources
- **Repository:** https://github.com/MervinPraison/PraisonAI
- **Documentation:** https://docs.praison.ai
- **Website:** https://praison.ai
- **Advisories:** https://github.com/MervinPraison/PraisonAI/security/advisories
## Maintainer workflow
Contributors and agents handling audits should follow:
`src/praisonai-agents/.agent/workflows/security-audit.md`
-1089
View File
File diff suppressed because it is too large. Load diff
-10
View File
@@ -1,10 +0,0 @@
# Deployment infra has moved
Deployment assets are no longer at the monorepo root.
| Asset | New location |
|-------|----------------|
| Gateway Helm chart | [`src/praisonai-bot/infra/helm/praisonai-gateway/`](../src/praisonai-bot/infra/helm/praisonai-gateway/) |
| Agents API Helm, Compose, Starters | [`src/praisonai-deploy/infra/`](../src/praisonai-deploy/infra/) |
See [`src/praisonai-deploy/infra/README.md`](../src/praisonai-deploy/infra/README.md).
-59
View File
@@ -1,59 +0,0 @@
# PraisonAI Docker Environment Configuration
# Copy this file to .env and update with your values
# Required: OpenAI API Key
OPENAI_API_KEY=your_openai_api_key_here
# Required: Chainlit Authentication Secret (generate a random string)
CHAINLIT_AUTH_SECRET=your_chainlit_secret_here
# Optional: Additional API Keys
ANTHROPIC_API_KEY=your_anthropic_api_key_here
GOOGLE_API_KEY=your_google_api_key_here
GROQ_API_KEY=your_groq_api_key_here
COHERE_API_KEY=your_cohere_api_key_here
# Bot Tokens (for messaging bot services)
SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
SLACK_APP_TOKEN=xapp-your-slack-app-token
DISCORD_BOT_TOKEN=your-discord-bot-token
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
# Optional: Service Configuration
FLASK_ENV=development
CHAINLIT_HOST=0.0.0.0
# Optional: Port Configuration (modify if you have conflicts)
UI_PORT=8082
CHAT_PORT=8083
API_PORT=8080
# Optional: Database Configuration
DATABASE_URL=sqlite:///root/.praison/database.sqlite
# Optional: Logging Configuration
LOG_LEVEL=INFO
PYTHONPATH=/app
# Optional: Development Settings
DEV_MODE=false
DEBUG=false
# Optional: Model Configuration
DEFAULT_MODEL=gpt-4
DEFAULT_TEMPERATURE=0.7
MAX_TOKENS=4000
# Optional: Security Settings
CORS_ORIGINS=http://localhost:3000,http://localhost:8082,http://localhost:8083
RATE_LIMIT=100
# Optional: Feature Flags
ENABLE_CHAT=true
ENABLE_UI=true
ENABLE_API=true
ENABLE_REALTIME=true
# Optional: Volume Paths (for custom mounting)
PRAISON_DATA_PATH=~/.praison
EXAMPLES_PATH=./examples
-46
View File
@@ -1,46 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Create praison config directory
RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
# Note: praisonai is installed from PyPI - remove version pin to use latest available
RUN pip install --no-cache-dir \
flask \
praisonai \
"praisonai[api]" \
gunicorn \
markdown
# Copy application code
COPY . .
# Set environment variables for directory management
ENV PRAISON_CONFIG_DIR=/root/.praison
ENV DOCKER_CONTAINER=true
# Create health check API if api.py doesn't exist
RUN if [ ! -f api.py ]; then \
echo "from flask import Flask\n\
app = Flask(__name__)\n\
\n\
@app.route('/health')\n\
def health():\n\
return {'status': 'healthy'}, 200\n\
\n\
if __name__ == '__main__':\n\
app.run()" > api.py; \
fi
EXPOSE 8080
CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]
-33
View File
@@ -1,33 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Create praison config directory
RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.7.0" \
"praisonai[chat]" \
"embedchain[github,youtube]"
# Copy application code
COPY . .
# Set environment variables for directory management
ENV PRAISON_CONFIG_DIR=/root/.praison
ENV CHAINLIT_CONFIG_DIR=/root/.praison
ENV CHAINLIT_DB_DIR=/root/.praison
ENV DOCKER_CONTAINER=true
# Default command (will be overridden by docker-compose)
CMD ["praisonai", "chat"]
-61
View File
@@ -1,61 +0,0 @@
# PraisonAI 🦞 Claw — All-in-one: dashboard + bots + gateway + agents
#
# Build: docker build -f docker/Dockerfile.claw -t praisonai:claw ..
# Run: docker run -p 8082:8082 -e OPENAI_API_KEY=sk-xxx praisonai:claw
# Health: curl http://localhost:8082/health
#
# Override CMD for other modes:
# Bot: docker run -e ... praisonai:claw praisonai bot telegram
# Gateway: docker run -e ... praisonai:claw praisonai gateway start
# Serve: docker run -e ... praisonai:claw praisonai serve agents.yaml
# Shell: docker run --rm -it praisonai:claw bash
FROM python:3.12-slim AS base
# ── Environment ──────────────────────────────────────────────────────────────
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
AIUI_PORT=8082 \
AIUI_HOST=0.0.0.0 \
AIUI_DATA_DIR=/data
WORKDIR /app
# ── System dependencies ─────────────────────────────────────────────────────
# tini: PID 1 signal handling (graceful shutdown on Fly/Railway/k8s)
# curl: HEALTHCHECK probe
# build-essential: compile native Python wheels (chromadb, etc.)
RUN apt-get update && apt-get install -y --no-install-recommends \
tini \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# ── Install PraisonAI 🦞 ────────────────────────────────────────────────────
# Single command installs: AIUI dashboard, agents SDK (memory/knowledge/llm/mcp),
# bot channels (Telegram/Discord/Slack), gateway, search tools, and more.
RUN pip install --no-cache-dir "praisonai[claw]"
# ── Data volume ──────────────────────────────────────────────────────────────
# Persistence for sessions, config, memory, knowledge stores
RUN mkdir -p /data
VOLUME /data
# ── Entrypoint ───────────────────────────────────────────────────────────────
# Ensures /data exists and forwards all signals via tini
RUN printf '#!/bin/sh\n\
set -e\n\
mkdir -p /data 2>/dev/null || true\n\
exec "$@"\n' > /usr/local/bin/entrypoint.sh \
&& chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 8082
# ── Health check ─────────────────────────────────────────────────────────────
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -f http://localhost:8082/health || exit 1
# ── Run ──────────────────────────────────────────────────────────────────────
# Default: AIUI dashboard via praisonai claw. Override CMD for bot/gateway/serve.
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
CMD ["praisonai", "claw", "--port", "8082"]
-47
View File
@@ -1,47 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
portaudio19-dev \
python3-pyaudio \
curl \
git \
vim \
nano \
&& rm -rf /var/lib/apt/lists/*
# Create praison config directory
RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.7.0" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
"praisonai[code]" \
"embedchain[github,youtube]" \
jupyter \
jupyterlab \
notebook \
pytest \
pytest-asyncio \
mkdocs \
mkdocs-material
# Copy application code
COPY . .
# Set environment variables for directory management
ENV PRAISON_CONFIG_DIR=/root/.praison
ENV CHAINLIT_CONFIG_DIR=/root/.praison
ENV CHAINLIT_DB_DIR=/root/.praison
ENV DOCKER_CONTAINER=true
# Default command (will be overridden by docker-compose)
CMD ["praisonai", "ui"]
-32
View File
@@ -1,32 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Create praison config directory
RUN mkdir -p /root/.praison
# Copy praisonaiagents source
COPY src/praisonai-agents/ /app/praisonaiagents/
WORKDIR /app/praisonaiagents
# Install praisonaiagents with all features (non-editable for production)
RUN pip install --no-cache-dir ".[all]"
# Set environment variables for directory management
ENV PRAISON_CONFIG_DIR=/root/.praison
ENV CHAINLIT_CONFIG_DIR=/root/.praison
ENV CHAINLIT_DB_DIR=/root/.praison
ENV DOCKER_CONTAINER=true
WORKDIR /app
# Default command
CMD ["python", "-c", "import praisonaiagents; print('PraisonAI Agents installed successfully')"]
-25
View File
@@ -1,25 +0,0 @@
# Dockerfile for PraisonAI Serve API
# Deploy to Railway, Render, Fly.io, or any container platform
#
# Build: docker build -f Dockerfile.serve -t praisonai-serve .
# Run: docker run -p 8005:8005 -e OPENAI_API_KEY=sk-xxx praisonai-serve
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
RUN pip install --no-cache-dir praisonai praisonaiagents
# Copy agents configuration
COPY agents.yaml .
# Expose the API port
EXPOSE 8005
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8005/health || exit 1
# Start the API server
CMD ["praisonai", "serve", "agents.yaml", "--host", "0.0.0.0", "--port", "8005"]
-33
View File
@@ -1,33 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Create praison config directory
RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.7.0" \
"praisonai[ui]" \
"praisonai[crewai]"
# Copy application code
COPY . .
# Set environment variables for directory management
ENV PRAISON_CONFIG_DIR=/root/.praison
ENV CHAINLIT_CONFIG_DIR=/root/.praison
ENV CHAINLIT_DB_DIR=/root/.praison
ENV DOCKER_CONTAINER=true
# Default command (will be overridden by docker-compose)
CMD ["praisonai", "ui"]
-265
View File
@@ -1,265 +0,0 @@
# PraisonAI Docker Setup
This directory contains Docker configurations for running PraisonAI services in containerized environments. The setup addresses directory management issues and provides comprehensive multi-service deployment options.
## 🐳 Available Services
### Core Services
- **UI Service** (`port 8082`) - Chainlit-based web interface
- **Chat Service** (`port 8083`) - Dedicated chat interface
- **API Service** (`port 8080`) - REST API endpoint
- **Agents Service** - Standalone PraisonAI Agents runtime
### Docker Files
- `Dockerfile` - Basic API service
- `Dockerfile.ui` - UI service with web interface
- `Dockerfile.chat` - Chat-focused service
- `Dockerfile.dev` - Development environment with tools
- `Dockerfile.praisonaiagents` - Standalone agents framework
- `docker-compose.yml` - Multi-service orchestration
## 🚀 Quick Start
### Single Service
```bash
# Run UI service
docker run -p 8082:8082 -e OPENAI_API_KEY=your_key ghcr.io/mervinpraison/praisonai:ui
# Run Chat service
docker run -p 8083:8083 -e OPENAI_API_KEY=your_key ghcr.io/mervinpraison/praisonai:chat
# Run API service
docker run -p 8080:8080 -e OPENAI_API_KEY=your_key ghcr.io/mervinpraison/praisonai:api
```
### Multi-Service with Docker Compose
```bash
# Create environment file
cat > .env << EOF
OPENAI_API_KEY=your_openai_api_key_here
CHAINLIT_AUTH_SECRET=your_secret_here
EOF
# Start all services
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose down
```
## 📁 Directory Management
### Problem Solved
The original issue was that files like `chainlit.md`, `.chainlit` directory, and `public` folder were cluttering the root directory.
### Solution Implemented
All PraisonAI configuration and runtime files are now stored in `~/.praison/`:
```bash
~/.praison/
├── database.sqlite # Chainlit database
├── chainlit.md # Chainlit configuration
├── .chainlit/ # Chainlit runtime files
└── config/ # PraisonAI configuration
```
### Environment Variables
```bash
PRAISON_CONFIG_DIR=/root/.praison # Main config directory
CHAINLIT_CONFIG_DIR=/root/.praison # Chainlit config location
CHAINLIT_DB_DIR=/root/.praison # Database location
```
## 🔧 Service Configuration
### UI Service (Port 8082)
```yaml
environment:
- CHAINLIT_PORT=8082
- CHAINLIT_HOST=0.0.0.0
- OPENAI_API_KEY=${OPENAI_API_KEY}
- CHAINLIT_AUTH_SECRET=${CHAINLIT_AUTH_SECRET}
```
### Chat Service (Port 8083)
```yaml
environment:
- CHAINLIT_PORT=8083
- CHAINLIT_HOST=0.0.0.0
- OPENAI_API_KEY=${OPENAI_API_KEY}
```
### API Service (Port 8080)
```yaml
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
```
## 🎯 Service Endpoints
| Service | Port | Endpoint | Description |
|---------|------|----------|-------------|
| UI | 8082 | http://localhost:8082 | Web interface |
| Chat | 8083 | http://localhost:8083 | Chat interface |
| API | 8080 | http://localhost:8080 | REST API |
| API Health | 8080 | http://localhost:8080/health | Health check |
## 🔍 Health Checks
All services include health checks:
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:PORT"]
interval: 10s
timeout: 5s
retries: 3
```
## 📦 Package Versions
All Docker images use consistent, up-to-date versions:
- PraisonAI: `>=2.2.95`
- PraisonAI Agents: `>=0.0.92`
- Python: `3.11-slim`
## 🔒 Security Features
- Non-root user execution where possible
- Minimal base image (python:3.11-slim)
- No unnecessary packages installed
- Environment variable-based configuration
- Volume mounting for persistent data
## 🛠 Development
### Development Environment
```bash
# Use development Dockerfile with additional tools
docker build -f Dockerfile.dev -t praisonai:dev .
docker run -it -v $(pwd):/app praisonai:dev bash
```
### Custom Configuration
```bash
# Mount custom config directory
docker run -v ~/.praison:/root/.praison praisonai:ui
```
## 📊 Monitoring
### Docker Compose Monitoring
```bash
# View service status
docker-compose ps
# View resource usage
docker-compose top
# View logs for specific service
docker-compose logs ui
docker-compose logs chat
docker-compose logs api
```
## 🚨 Troubleshooting
### Common Issues
1. **Port conflicts**
```bash
# Check port usage
netstat -tlnp | grep :8082
# Use different ports
docker run -p 9082:8082 praisonai:ui
```
2. **Environment variables not loading**
```bash
# Verify .env file
cat .env
# Set variables directly
docker run -e OPENAI_API_KEY=your_key praisonai:ui
```
3. **Permission issues**
```bash
# Check volume permissions
ls -la ~/.praison/
# Fix permissions
sudo chown -R $(id -u):$(id -g) ~/.praison/
```
4. **Service won't start**
```bash
# Check logs
docker-compose logs service_name
# Restart service
docker-compose restart service_name
```
## 🔄 Updates
### Pulling Latest Images
```bash
# Pull latest images
docker-compose pull
# Restart with new images
docker-compose up -d
```
### Version Pinning
To use specific versions, update the Dockerfile:
```dockerfile
RUN pip install "praisonai==2.2.95" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
### Recommended Production Setup
```yaml
# docker-compose.prod.yml
version: '3.8'
services:
ui:
image: ghcr.io/mervinpraison/praisonai:ui
restart: unless-stopped
environment:
- CHAINLIT_HOST=0.0.0.0
- CHAINLIT_PORT=8082
volumes:
- praison_data:/root/.praison
networks:
- praison_network
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
```
### Load Balancer Configuration
For production environments, consider using nginx or similar:
```nginx
upstream praisonai {
server localhost:8082;
server localhost:8083;
}
server {
listen 80;
location / {
proxy_pass http://praisonai;
}
}
```
This Docker setup provides a clean, organized, and scalable way to deploy PraisonAI services while solving the directory management issues mentioned in the original request.
-15
View File
@@ -1,15 +0,0 @@
# Bot Environment Variables
# Copy to .env and fill in your values
# Required: LLM API Key
OPENAI_API_KEY=your_openai_api_key_here
# Slack Bot (Socket Mode)
SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
SLACK_APP_TOKEN=xapp-your-slack-app-token
# Discord Bot
DISCORD_BOT_TOKEN=your-discord-bot-token
# Telegram Bot
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
-40
View File
@@ -1,40 +0,0 @@
# Dockerfile for PraisonAI Slack Bot
# Secrets are passed at runtime via environment variables
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Create praison config directory
RUN mkdir -p /root/.praison
# Install praisonai-bot with channel dependencies (slim bot image)
RUN pip install --no-cache-dir \
"praisonai-bot[bot]" \
slack-bolt \
slack-sdk
# Set environment variables for directory management
ENV PRAISON_CONFIG_DIR=/root/.praison
ENV DOCKER_CONTAINER=true
# Bot tokens should be passed at runtime (not baked into image)
# SLACK_BOT_TOKEN=xoxb-...
# SLACK_APP_TOKEN=xapp-...
# Optional: Copy custom agent configuration
# COPY agent.yaml /app/agent.yaml
# Expose no ports - Socket Mode doesn't need them
# For webhook mode, expose port 3000
# Default command - tokens from environment
CMD ["praisonai-bot", "bot", "slack"]
-66
View File
@@ -1,66 +0,0 @@
# PraisonAI Bot Docker Deployment
Deploy Slack, Discord, or Telegram bots using Docker.
## Quick Start
```bash
# 1. Copy environment template
cp .env.template .env
# 2. Edit .env with your tokens
nano .env
# 3. Run specific bot
docker compose up slack-bot -d
# Or run all bots
docker compose up -d
```
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `OPENAI_API_KEY` | Yes | LLM API key |
| `SLACK_BOT_TOKEN` | For Slack | Bot token (xoxb-...) |
| `SLACK_APP_TOKEN` | For Slack | App token (xapp-...) |
| `DISCORD_BOT_TOKEN` | For Discord | Bot token |
| `TELEGRAM_BOT_TOKEN` | For Telegram | Bot token |
## Slack Setup
1. Create app at https://api.slack.com/apps
2. Enable **Socket Mode** → Generate App-Level Token with `connections:write`
3. Add **OAuth Scopes**: `chat:write`, `app_mentions:read`, `im:history`
4. Enable **Event Subscriptions** → Subscribe to `app_mention`, `message.im`
5. **Install to Workspace**
6. Copy Bot Token and App Token to `.env`
## Commands
```bash
# Start Slack bot
docker compose up slack-bot -d
# View logs
docker compose logs -f slack-bot
# Stop bot
docker compose down slack-bot
# Rebuild after updates
docker compose build --no-cache slack-bot
```
## Custom Agent Configuration
Mount your agent config:
```yaml
services:
slack-bot:
volumes:
- ./agent.yaml:/app/agent.yaml
command: ["praisonai", "bot", "slack", "--agent", "/app/agent.yaml"]
```
-53
View File
@@ -1,53 +0,0 @@
version: '3.8'
# PraisonAI Bot Services
# Run specific bots: docker compose up slack-bot
# Run all bots: docker compose up
services:
# Slack Bot
slack-bot:
build:
context: ../..
dockerfile: docker/bots/Dockerfile
volumes:
- praison_data:/root/.praison
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}
SLACK_APP_TOKEN: ${SLACK_APP_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai-bot", "bot", "slack" ]
restart: unless-stopped
# Discord Bot
discord-bot:
build:
context: ../..
dockerfile: docker/bots/Dockerfile
volumes:
- praison_data:/root/.praison
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai-bot", "bot", "discord" ]
restart: unless-stopped
# Telegram Bot
telegram-bot:
build:
context: ../..
dockerfile: docker/bots/Dockerfile
volumes:
- praison_data:/root/.praison
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai-bot", "bot", "telegram" ]
restart: unless-stopped
volumes:
praison_data:
driver: local
-23
View File
@@ -1,23 +0,0 @@
# Use an official Python runtime as a parent image
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install PraisonAI with the 'call' extra and ensure it's the latest version
RUN pip install --no-cache-dir --upgrade "praisonai[call]"
# Expose the port the app runs on
EXPOSE 8090
# Run the application
CMD ["praisonai", "call"]
-13
View File
@@ -1,13 +0,0 @@
# Praison AI Call Docker
1. Build the Docker image:
```
docker build -t praisonai-call .
```
2. Run the container:
```
docker run -d -p 8090:8090 praisonai-call -e OPENAI_API_KEY=your_api_key_here
```
Make sure to replace your_api_key_here with your actual OpenAI API key.
Loaded 100 of 5733 files, more files were not shown because too many files have changed in this diff. Show more