mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 01:08:39 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac4eafc3a0 |
No files matched your search
@@ -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 & 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 <cmd></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>=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 -->
|
||||
@@ -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 & 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 & 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 -->
|
||||
@@ -222,12 +222,10 @@ runs:
|
||||
|
||||
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
|
||||
Before implementing anything, answer these questions:
|
||||
- CORE vs WRAPPER vs TOOLS vs PLUGINS ROUTING:
|
||||
- CORE vs WRAPPER vs TOOLS 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).
|
||||
3. Tools (PraisonAI-Tools): Specific integrations, external tools (e.g. SurrealDB, Slack), and community extensions MUST go to the `MervinPraison/PraisonAI-Tools` repository. If the feature is a tool, clone `https://github.com/MervinPraison/PraisonAI-Tools`, implement it there, and create the PR using `gh pr create -R MervinPraison/PraisonAI-Tools`.
|
||||
- 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.
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
name: Install PraisonAI monorepo packages
|
||||
description: Install praisonai-agents, praisonai-code, praisonai-bot, praisonai-train, praisonai-browser, praisonai-mcp, praisonai-sandbox, and praisonai wrapper from local paths (mirrors PyPI eight-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"
|
||||
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 "$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 "$WRAPPER_PKG"
|
||||
pip install -e "${WRAPPER_SUFFIX}"
|
||||
fi
|
||||
@@ -34,17 +34,11 @@ steps:
|
||||
|
||||
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:
|
||||
Fourth, 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.
|
||||
Output: routing decision (Core SDK / Wrapper / Tools / 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
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
@@ -27,109 +27,6 @@ const withRecovery = [
|
||||
];
|
||||
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' },
|
||||
@@ -151,68 +48,9 @@ assert('verdict after head accepted', mg.findMergeGateVerdict(
|
||||
'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(
|
||||
@@ -239,25 +77,23 @@ assert('issues event never blocks', !mg.hasBlockingClaudeRunForPr(
|
||||
'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();
|
||||
// Conflict rebase clears after bot completion + FINAL on HEAD
|
||||
const conflictTrigger = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude this PR has merge conflicts with main. Please rebase',
|
||||
created_at: conflictIso(-30 * 60 * 1000),
|
||||
created_at: '2026-06-26T10:30:38Z',
|
||||
};
|
||||
const rebaseDone = {
|
||||
user: { login: 'praisonai-triage-agent[bot]' },
|
||||
body: 'Rebase complete — PR #2308 onto latest main',
|
||||
created_at: conflictIso(-29 * 60 * 1000),
|
||||
created_at: '2026-06-26T10:30:55Z',
|
||||
};
|
||||
const finalAfterRebase = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude You are the FINAL architecture reviewer.',
|
||||
created_at: conflictIso(-20 * 60 * 1000),
|
||||
created_at: '2026-06-26T10:39:43Z',
|
||||
};
|
||||
const headAfterRebase = conflictIso(-21 * 60 * 1000);
|
||||
const headAfterRebase = '2026-06-26T10:39:25Z';
|
||||
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));
|
||||
|
||||
+13
-242
@@ -7,9 +7,7 @@ const CLAUDE_TRIGGER_LOGINS = ['MervinPraison', 'github-actions[bot]'];
|
||||
const AUTO_ACTORS = CLAUDE_TRIGGER_LOGINS;
|
||||
const CONFLICT_COOLDOWN_MS = 12 * 60 * 60 * 1000;
|
||||
const CLAUDE_ACTIVE_MS = 35 * 60 * 1000;
|
||||
const STALE_FINAL_RECOVERY_WINDOW_MS = 60 * 60 * 1000;
|
||||
const STALE_FINAL_MAX_PER_WINDOW = 2;
|
||||
const PUSH_AFTER_FINAL_DEBOUNCE_MS = 15 * 60 * 1000;
|
||||
const POST_PUSH_BUFFER_MS = 5 * 60 * 1000;
|
||||
const ALLOWED_MERGE_STATES = new Set(['CLEAN', 'UNSTABLE']);
|
||||
const AGENT_PY_MAX_AUTO_LINES = 100;
|
||||
const PR_MAX_AUTO_ADDITIONS = 800;
|
||||
@@ -41,11 +39,6 @@ const BLOCK_LABELS = new Set([
|
||||
'no-auto-merge',
|
||||
'auto-merged-by-gate',
|
||||
]);
|
||||
const MERGE_READY_LABEL = 'pipeline/merge-ready';
|
||||
/** Superseded concurrency runs; must not block merge when real tests passed. */
|
||||
const OPTIONAL_CANCELLED_CHECKS = new Set(['detect-and-trigger']);
|
||||
/** Cancelled smoke/windows after timeout are non-blocking when core shards passed on HEAD. */
|
||||
const OPTIONAL_CANCELLED_WHEN_CORE_GREEN = new Set(['smoke', 'test-windows']);
|
||||
const BOT_REVIEWER_PATTERNS = [
|
||||
'coderabbit',
|
||||
'qodo',
|
||||
@@ -72,12 +65,6 @@ function isClaudeTriggerNoise(c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isClaudeFinalReplyComment(c) {
|
||||
const login = (c.user?.login || '').toLowerCase();
|
||||
if (!login.includes('praisonai-triage')) return false;
|
||||
return (c.body || '').includes('Claude finished');
|
||||
}
|
||||
|
||||
function hasRecentClaudeTrigger(comments, minutes = 35) {
|
||||
const cutoff = Date.now() - minutes * 60 * 1000;
|
||||
return comments.some((c) => {
|
||||
@@ -186,11 +173,6 @@ function isStaleFinalAfterPush(comments, headPushedAt) {
|
||||
);
|
||||
const finalTime = new Date(latestFinal.created_at).getTime();
|
||||
if (headTime <= finalTime + 60000) return false;
|
||||
const claudeRepliedAfterFinal = comments.some((c) => {
|
||||
if (!isClaudeFinalReplyComment(c)) return false;
|
||||
return new Date(c.created_at).getTime() >= finalTime - 60000;
|
||||
});
|
||||
if (claudeRepliedAfterFinal) return false;
|
||||
const claudeSinceHead = comments.some((c) => {
|
||||
if (!CLAUDE_TRIGGER_LOGINS.includes(c.user.login)) return false;
|
||||
if (isClaudeTriggerNoise(c)) return false;
|
||||
@@ -213,55 +195,6 @@ function shouldSkipFinalRecovery(comments, headPushedAt) {
|
||||
return hasRecentClaudeTrigger(comments, 35);
|
||||
}
|
||||
|
||||
function countFinalTriggersSince(comments, sinceMs) {
|
||||
return comments.filter(
|
||||
(c) => isFinalClaudeTriggerComment(c) && new Date(c.created_at).getTime() > sinceMs
|
||||
).length;
|
||||
}
|
||||
|
||||
function isClaudeAutomationLogin(login) {
|
||||
const lower = (login || '').toLowerCase();
|
||||
return lower.includes('praisonai-triage') || lower === 'github-actions[bot]';
|
||||
}
|
||||
|
||||
function isPushSoonAfterLatestFinal(comments, headPushedAt) {
|
||||
if (!headPushedAt) return false;
|
||||
const finals = comments.filter(isFinalClaudeTriggerComment);
|
||||
if (finals.length === 0) return false;
|
||||
const latestFinal = finals.reduce((a, b) =>
|
||||
new Date(a.created_at) > new Date(b.created_at) ? a : b
|
||||
);
|
||||
const finalTime = new Date(latestFinal.created_at).getTime();
|
||||
const headTime = new Date(headPushedAt).getTime();
|
||||
if (headTime <= finalTime) return false;
|
||||
return headTime - finalTime < PUSH_AFTER_FINAL_DEBOUNCE_MS;
|
||||
}
|
||||
|
||||
/** Returns { skip: true, reason } when stale-FINAL recovery should not post. */
|
||||
function shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusherLogin = null) {
|
||||
if (!isStaleFinalAfterPush(comments, headPushedAt)) {
|
||||
return { skip: true, reason: 'not stale' };
|
||||
}
|
||||
if (headPusherLogin && isClaudeAutomationLogin(headPusherLogin)) {
|
||||
return { skip: true, reason: 'head pushed by Claude automation' };
|
||||
}
|
||||
if (isPushSoonAfterLatestFinal(comments, headPushedAt)) {
|
||||
return {
|
||||
skip: true,
|
||||
reason: 'head pushed soon after FINAL (wait for CI / batched fixes)',
|
||||
};
|
||||
}
|
||||
const windowStart = Date.now() - STALE_FINAL_RECOVERY_WINDOW_MS;
|
||||
const finalsInWindow = countFinalTriggersSince(comments, windowStart);
|
||||
if (finalsInWindow >= STALE_FINAL_MAX_PER_WINDOW) {
|
||||
return {
|
||||
skip: true,
|
||||
reason: `stale-FINAL capped (${finalsInWindow} FINAL triggers in last hour)`,
|
||||
};
|
||||
}
|
||||
return { skip: false, reason: '' };
|
||||
}
|
||||
|
||||
function finalClaudeCompletedOnSha(comments, headPushedAt) {
|
||||
if (!hasFinalClaudeReviewTrigger(comments)) return false;
|
||||
if (isStaleFinalAfterPush(comments, headPushedAt)) return false;
|
||||
@@ -269,7 +202,7 @@ function finalClaudeCompletedOnSha(comments, headPushedAt) {
|
||||
}
|
||||
|
||||
const FINAL_CLAUDE_REVIEW_BODY =
|
||||
'@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n6. Repo routing: agent-callable tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins; optional sandbox backends → PraisonAI-Plugins (`praisonai.sandbox` entry point) — request changes if wrongly added to praisonaiagents/\n\n**Phase 2: FIX Valid Issues**\n7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n8. Also independently identify and fix any gaps or issues you find in the changed code — do not rely only on prior reviewer feedback\n9. Push all code fixes directly to THIS branch (do NOT create a new PR)\n10. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n11. If all issues are resolved, approve the PR / close the Issue\n12. If blocking issues remain, request changes / leave clear action items';
|
||||
'@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items';
|
||||
|
||||
async function getMergeState(github, owner, repo, prNumber) {
|
||||
const query = `
|
||||
@@ -310,87 +243,14 @@ async function getMergeState(github, owner, repo, prNumber) {
|
||||
};
|
||||
}
|
||||
|
||||
async function listChecksOnSha(github, owner, repo, sha) {
|
||||
async function allChecksGreenOnSha(github, owner, repo, sha, core) {
|
||||
const { data } = await github.rest.checks.listForRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: sha,
|
||||
per_page: 100,
|
||||
});
|
||||
return (data.check_runs || []).filter((r) => r.head_sha === sha);
|
||||
}
|
||||
|
||||
function listFailedChecksOnSha(runs) {
|
||||
return (runs || []).filter((run) => {
|
||||
if (run.status !== 'completed') return false;
|
||||
return run.conclusion === 'failure';
|
||||
});
|
||||
}
|
||||
|
||||
function isCoreTestRun(run) {
|
||||
const name = run?.name || '';
|
||||
return name === 'test-core' || name.startsWith('test-core ') || name === 'test-core-collect';
|
||||
}
|
||||
|
||||
function coreTestsGreenOnRuns(runs) {
|
||||
const coreRuns = (runs || []).filter(isCoreTestRun);
|
||||
if (coreRuns.length === 0) return false;
|
||||
return coreRuns.every(
|
||||
(run) =>
|
||||
run.status === 'completed' &&
|
||||
['success', 'neutral', 'skipped'].includes(run.conclusion)
|
||||
);
|
||||
}
|
||||
|
||||
function checkConclusionRank(conclusion) {
|
||||
if (conclusion === 'success') return 4;
|
||||
if (conclusion === 'neutral') return 3;
|
||||
if (conclusion === 'skipped') return 2;
|
||||
if (conclusion === 'cancelled') return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function isPendingRun(run) {
|
||||
return !!(run && run.status && run.status !== 'completed');
|
||||
}
|
||||
|
||||
function bestRunsByName(runs) {
|
||||
const byName = new Map();
|
||||
for (const run of runs || []) {
|
||||
if (!run) continue;
|
||||
const existing = byName.get(run.name);
|
||||
if (!existing) {
|
||||
byName.set(run.name, run);
|
||||
continue;
|
||||
}
|
||||
const runPending = isPendingRun(run);
|
||||
const existingPending = isPendingRun(existing);
|
||||
if (existingPending) continue;
|
||||
if (
|
||||
runPending ||
|
||||
checkConclusionRank(run.conclusion) > checkConclusionRank(existing.conclusion)
|
||||
) {
|
||||
byName.set(run.name, run);
|
||||
}
|
||||
}
|
||||
return [...byName.values()];
|
||||
}
|
||||
|
||||
function isAcceptableCheckConclusion(run, runs) {
|
||||
if (['success', 'neutral', 'skipped'].includes(run.conclusion)) return true;
|
||||
if (run.conclusion === 'cancelled' && OPTIONAL_CANCELLED_CHECKS.has(run.name)) return true;
|
||||
if (
|
||||
run.conclusion === 'cancelled' &&
|
||||
OPTIONAL_CANCELLED_WHEN_CORE_GREEN.has(run.name) &&
|
||||
coreTestsGreenOnRuns(runs)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function allChecksGreenOnSha(github, owner, repo, sha, core) {
|
||||
const runs = bestRunsByName(await listChecksOnSha(github, owner, repo, sha));
|
||||
const runs = (data.check_runs || []).filter((r) => r.head_sha === sha);
|
||||
if (runs.length === 0) {
|
||||
core?.info?.(`No check runs on ${sha.slice(0, 7)} — allowing (e.g. docs-only PR)`);
|
||||
return true;
|
||||
@@ -400,17 +260,11 @@ async function allChecksGreenOnSha(github, owner, repo, sha, core) {
|
||||
core?.info?.(`Check pending: ${run.name} (${run.status})`);
|
||||
return false;
|
||||
}
|
||||
if (!isAcceptableCheckConclusion(run, runs)) {
|
||||
const ok = ['success', 'neutral', 'skipped'].includes(run.conclusion);
|
||||
if (!ok) {
|
||||
core?.info?.(`Check failed: ${run.name} (${run.conclusion})`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
run.conclusion === 'cancelled' &&
|
||||
OPTIONAL_CANCELLED_WHEN_CORE_GREEN.has(run.name) &&
|
||||
coreTestsGreenOnRuns(runs)
|
||||
) {
|
||||
core?.info?.(`Ignoring cancelled ${run.name} — test-core green on HEAD`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -749,16 +603,15 @@ async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, op
|
||||
reasons.push('recent merge-conflict @claude');
|
||||
}
|
||||
if (!skipRecentClaudeCooldown && hasRecentClaudeTrigger(ctx.comments, 35)) {
|
||||
const verdictOnHead = findMergeGateVerdict(ctx.comments, null, ctx.headPushedAt) !== null;
|
||||
if (!verdictOnHead) reasons.push('recent @claude within 35min');
|
||||
reasons.push('recent @claude within 35min');
|
||||
}
|
||||
|
||||
if (!skipGlobalClaudeRunCheck && (await hasInProgressClaudeAssistant(github, owner, repo, prNumber))) {
|
||||
reasons.push('claude.yml in progress');
|
||||
}
|
||||
|
||||
const checksOk = await allChecksGreenOnSha(github, owner, repo, ctx.headSha, core);
|
||||
if (!checksOk) reasons.push('CI not green on HEAD');
|
||||
const headTime = new Date(ctx.headPushedAt).getTime();
|
||||
if (Date.now() - headTime < POST_PUSH_BUFFER_MS) reasons.push('post-push buffer (<5min)');
|
||||
|
||||
if (!finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt)) {
|
||||
if (!hasFinalClaudeReviewTrigger(ctx.comments)) {
|
||||
@@ -793,6 +646,9 @@ async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, op
|
||||
const sdkCheckReason = await sdkTestChecksReason(github, owner, repo, ctx.headSha, pullFiles, core);
|
||||
if (sdkCheckReason) reasons.push(sdkCheckReason);
|
||||
|
||||
const checksOk = await allChecksGreenOnSha(github, owner, repo, ctx.headSha, core);
|
||||
if (!checksOk) reasons.push('CI not green on HEAD');
|
||||
|
||||
if (ctx.pr.mergeable === false) reasons.push('not mergeable');
|
||||
|
||||
const ready = reasons.length === 0;
|
||||
@@ -804,67 +660,10 @@ async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, op
|
||||
};
|
||||
}
|
||||
|
||||
async function listPrNumbersForMergeGateScan(github, owner, repo, core) {
|
||||
let mergeReadyIssues;
|
||||
if (typeof github.paginate === 'function') {
|
||||
mergeReadyIssues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
labels: MERGE_READY_LABEL,
|
||||
per_page: 100,
|
||||
});
|
||||
} else {
|
||||
({ data: mergeReadyIssues } = await github.rest.issues.listForRepo({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
labels: MERGE_READY_LABEL,
|
||||
per_page: 100,
|
||||
}));
|
||||
}
|
||||
const mergeReady = mergeReadyIssues
|
||||
.filter((issue) => issue.pull_request)
|
||||
.map((issue) => issue.number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
if (mergeReady.length > 0) {
|
||||
core?.info?.(
|
||||
`Merge gate scan: ${mergeReady.length} ${MERGE_READY_LABEL} PR(s) (skipping unlabelled open PRs)`
|
||||
);
|
||||
return mergeReady;
|
||||
}
|
||||
|
||||
core?.info?.(`No ${MERGE_READY_LABEL} PRs — scanning oldest open PRs`);
|
||||
const prs =
|
||||
typeof github.paginate === 'function'
|
||||
? await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
})
|
||||
: (await github.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
})).data;
|
||||
return prs.map((pr) => pr.number);
|
||||
}
|
||||
|
||||
async function selectMergeGateCandidates(github, owner, repo, prNumbers, maxCandidates, core) {
|
||||
const readyList = [];
|
||||
const skipped = [];
|
||||
for (const num of prNumbers) {
|
||||
if (readyList.length >= maxCandidates) {
|
||||
core?.info?.(`Found ${maxCandidates} candidate(s) — skipping remaining PR scans`);
|
||||
break;
|
||||
}
|
||||
const result = await evaluatePipelineQuiescent(github, owner, repo, num, core);
|
||||
if (result.ready) {
|
||||
readyList.push({ pr_number: num, head_sha: result.headSha });
|
||||
@@ -879,20 +678,12 @@ async function selectMergeGateCandidates(github, owner, repo, prNumbers, maxCand
|
||||
};
|
||||
}
|
||||
|
||||
const AUTOMATED_FALLBACK_MARKER = 'Automated fallback —';
|
||||
|
||||
function isAutomatedFallbackVerdict(body) {
|
||||
return (body || '').includes(AUTOMATED_FALLBACK_MARKER);
|
||||
}
|
||||
|
||||
function findMergeGateVerdict(comments, minCreatedAt = null, headPushedAt = null, options = {}) {
|
||||
const { excludeAutomatedFallback = false } = options || {};
|
||||
function findMergeGateVerdict(comments, minCreatedAt = null, headPushedAt = null) {
|
||||
const minTime = minCreatedAt ? new Date(minCreatedAt).getTime() : 0;
|
||||
const headTime = headPushedAt ? new Date(headPushedAt).getTime() - 60000 : 0;
|
||||
const gateComments = comments
|
||||
.filter((c) => {
|
||||
if (!(c.body || '').includes('MERGE_GATE_VERDICT:')) return false;
|
||||
if (excludeAutomatedFallback && isAutomatedFallbackVerdict(c.body)) return false;
|
||||
const created = new Date(c.created_at).getTime();
|
||||
if (minTime && created < minTime) return false;
|
||||
if (headTime && created < headTime) return false;
|
||||
@@ -911,7 +702,6 @@ module.exports = {
|
||||
AUTO_ACTORS,
|
||||
isFinalClaudeTriggerComment,
|
||||
isClaudeTriggerNoise,
|
||||
isClaudeFinalReplyComment,
|
||||
hasRecentClaudeTrigger,
|
||||
hasRecentConflictComment,
|
||||
isConflictRebaseTriggerComment,
|
||||
@@ -922,15 +712,6 @@ module.exports = {
|
||||
hasFinalClaudeReviewTrigger,
|
||||
finalClaudeCompletedOnSha,
|
||||
getMergeState,
|
||||
OPTIONAL_CANCELLED_CHECKS,
|
||||
OPTIONAL_CANCELLED_WHEN_CORE_GREEN,
|
||||
isCoreTestRun,
|
||||
coreTestsGreenOnRuns,
|
||||
isPendingRun,
|
||||
bestRunsByName,
|
||||
isAcceptableCheckConclusion,
|
||||
listChecksOnSha,
|
||||
listFailedChecksOnSha,
|
||||
allChecksGreenOnSha,
|
||||
hasInProgressClaudeAssistant,
|
||||
claudeRunBlocksPr,
|
||||
@@ -958,19 +739,9 @@ module.exports = {
|
||||
isStaleFinalAfterPush,
|
||||
needsStaleFinalRecovery,
|
||||
shouldSkipFinalRecovery,
|
||||
shouldSkipStaleFinalRecovery,
|
||||
isClaudeAutomationLogin,
|
||||
isPushSoonAfterLatestFinal,
|
||||
countFinalTriggersSince,
|
||||
STALE_FINAL_RECOVERY_WINDOW_MS,
|
||||
STALE_FINAL_MAX_PER_WINDOW,
|
||||
FINAL_CLAUDE_REVIEW_BODY,
|
||||
loadPrContext,
|
||||
evaluatePipelineQuiescent,
|
||||
MERGE_READY_LABEL,
|
||||
listPrNumbersForMergeGateScan,
|
||||
selectMergeGateCandidates,
|
||||
isAutomatedFallbackVerdict,
|
||||
AUTOMATED_FALLBACK_MARKER,
|
||||
findMergeGateVerdict,
|
||||
};
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
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 = [
|
||||
@@ -156,51 +155,11 @@ async function syncPipelineLabels(github, owner, repo, prNumber, core) {
|
||||
}
|
||||
|
||||
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;
|
||||
return { synced: true, stage, blockers, ready: evalResult.ready, reasons: evalResult.reasons };
|
||||
}
|
||||
|
||||
async function syncOpenPullRequests(github, owner, repo, options, core) {
|
||||
const { maxPrs = 20, dispatchMergeGate = true } = options || {};
|
||||
const { maxPrs = 20 } = options || {};
|
||||
await ensurePipelineLabels(github, owner, repo, core);
|
||||
let prs;
|
||||
if (maxPrs <= 100) {
|
||||
@@ -219,28 +178,14 @@ async function syncOpenPullRequests(github, owner, repo, options, core) {
|
||||
});
|
||||
}
|
||||
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 || [],
|
||||
});
|
||||
}
|
||||
await syncPipelineLabels(github, owner, repo, pr.number, core);
|
||||
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'})`);
|
||||
core?.info?.(`Pipeline label sync complete (${synced} PR(s))`);
|
||||
return synced;
|
||||
}
|
||||
|
||||
@@ -254,5 +199,4 @@ module.exports = {
|
||||
ensurePipelineLabels,
|
||||
syncPipelineLabels,
|
||||
syncOpenPullRequests,
|
||||
dispatchMergeGateForOldestReady,
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
@@ -1,57 +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'));
|
||||
|
||||
const noonUtc = new Date('2026-07-08T12:00:00Z');
|
||||
const dayStart = rg.utcDayStart(noonUtc);
|
||||
assert.strictEqual(dayStart.toISOString(), '2026-07-08T00:00:00.000Z');
|
||||
|
||||
(async () => {
|
||||
assert.strictEqual(await rg.pypiVersionExists('praisonaiagents', '0.0.0'), false);
|
||||
|
||||
const mockGithub = {
|
||||
rest: {
|
||||
actions: {
|
||||
listWorkflowRuns: async () => ({
|
||||
data: {
|
||||
workflow_runs: [
|
||||
{
|
||||
conclusion: 'success',
|
||||
created_at: '2026-07-08T09:00:00Z',
|
||||
status: 'completed',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
await rg.hasSuccessfulReleaseToday(mockGithub, 'o', 'r', noonUtc),
|
||||
true
|
||||
);
|
||||
|
||||
const result = await rg.evaluateReleasePreflight(
|
||||
mockGithub, 'o', 'r',
|
||||
{ headSha: 'abc', isCiTrigger: false, bump: 'patch', now: noonUtc },
|
||||
null
|
||||
);
|
||||
assert.ok(result.reasons.some((r) => r.includes('already released today')));
|
||||
|
||||
console.log('ok: bumpPatch');
|
||||
console.log('ok: readVersionsFromTree');
|
||||
console.log('ok: pypiVersionExists missing version');
|
||||
console.log('ok: utcDayStart');
|
||||
console.log('ok: hasSuccessfulReleaseToday');
|
||||
console.log('ok: daily dedupe blocks second release');
|
||||
})();
|
||||
@@ -1,230 +0,0 @@
|
||||
/**
|
||||
* Release gate preflight — path changes, CI SHA, dedupe, PyPI version checks.
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const PACKAGE_PATHS = ['src/praisonai', 'src/praisonai-agents', 'src/praisonai-code'];
|
||||
const ACTIVE_RELEASE_STATUSES = new Set([
|
||||
'queued', 'in_progress', 'waiting', 'pending', 'requested',
|
||||
]);
|
||||
|
||||
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) {
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'pypi-release.yml',
|
||||
per_page: 20,
|
||||
});
|
||||
return runs.data.workflow_runs.some(
|
||||
(r) => ACTIVE_RELEASE_STATUSES.has(r.status) && !r.conclusion
|
||||
);
|
||||
}
|
||||
|
||||
function utcDayStart(now = new Date()) {
|
||||
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||
}
|
||||
|
||||
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
|
||||
const dayStart = utcDayStart(now);
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'pypi-release.yml',
|
||||
status: 'completed',
|
||||
per_page: 30,
|
||||
});
|
||||
return runs.data.workflow_runs.some(
|
||||
(r) => r.conclusion === 'success' && new Date(r.created_at) >= dayStart
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (await hasActiveReleaseRun(github, owner, repo)) {
|
||||
reasons.push('PyPI Release already in progress or awaiting approval');
|
||||
return out;
|
||||
}
|
||||
|
||||
const referenceTime = options.now instanceof Date ? options.now : new Date();
|
||||
if (await hasSuccessfulReleaseToday(github, owner, repo, referenceTime)) {
|
||||
const day = referenceTime.toISOString().slice(0, 10);
|
||||
reasons.push(`already released today (UTC ${day}); max one patch release per day`);
|
||||
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) {
|
||||
reasons.push(`CI not green on HEAD (last green: ${greenSha ? greenSha.slice(0, 7) : 'none'})`);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
out.ready = true;
|
||||
out.reasons = ['ready'];
|
||||
if (core) core.info(`Release preflight passed for ${out.headSha || headSha}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bumpPatch,
|
||||
readVersionsFromTree,
|
||||
pypiVersionExists,
|
||||
hasSuccessfulReleaseToday,
|
||||
utcDayStart,
|
||||
evaluateReleasePreflight,
|
||||
ACTIVE_RELEASE_STATUSES,
|
||||
PACKAGE_PATHS,
|
||||
};
|
||||
@@ -42,5 +42,5 @@ jobs:
|
||||
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'
|
||||
body: '🤖 Auto-triaging with Claude. Analyzing issue and implementing a fix...\n\nClaude will:\n1. Classify this issue\n2. Search codebase for root cause\n3. Implement a minimal fix following AGENTS.md\n4. Create a PR with the changes'
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
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.
|
||||
# Review chain: CodeRabbit/Qodo/Gemini review first → Copilot → Claude (final)
|
||||
# For human PRs: CodeRabbit/Qodo auto-review → this workflow triggers Copilot → then Claude
|
||||
# For bot PRs: CodeRabbit/Qodo skip bots, so we trigger them explicitly first
|
||||
#
|
||||
# IMPORTANT: @copilot ignores bot comments. All @copilot mentions must use
|
||||
# PAT_TOKEN so the comment posts as MervinPraison (a real user).
|
||||
#
|
||||
# Claude trigger runs IN-BAND (same workflow) to avoid GitHub Actions
|
||||
# blocking bot-triggered workflows with action_required approval gates.
|
||||
@@ -13,108 +16,518 @@ on:
|
||||
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
|
||||
# HUMAN PRs: Trigger @copilot AFTER CodeRabbit posts its summary
|
||||
# ================================================================
|
||||
claude-after-prior-reviewers:
|
||||
copilot-after-coderabbit:
|
||||
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'))
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
github.event.comment.user.login == 'coderabbitai[bot]' &&
|
||||
contains(github.event.comment.body, 'summarize by coderabbit')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
pr_number: ${{ steps.trigger.outputs.pr_number }}
|
||||
triggered: ${{ steps.trigger.outputs.triggered }}
|
||||
steps:
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
id: trigger
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
// Check for duplicates — posted by MervinPraison (PAT) or github-actions[bot] (legacy)
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@copilot') &&
|
||||
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
|
||||
);
|
||||
if (alreadyPosted) {
|
||||
console.log('Copilot already triggered, skipping');
|
||||
core.setOutput('triggered', 'false');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini 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, or Gemini flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible'
|
||||
});
|
||||
core.setOutput('triggered', 'true');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
|
||||
# Fallback: Trigger @copilot after Qodo review (if Coderabbit doesn't fire)
|
||||
copilot-after-qodo:
|
||||
if: |
|
||||
github.event_name == 'pull_request_review' &&
|
||||
github.event.review.user.login == 'qodo-code-review[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
pr_number: ${{ steps.trigger.outputs.pr_number }}
|
||||
triggered: ${{ steps.trigger.outputs.triggered }}
|
||||
steps:
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
id: trigger
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@copilot') &&
|
||||
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
|
||||
);
|
||||
if (alreadyPosted) {
|
||||
console.log('Copilot already triggered, skipping');
|
||||
core.setOutput('triggered', 'false');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above first.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused?\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. Suggest specific improvements with code examples where possible'
|
||||
});
|
||||
core.setOutput('triggered', 'true');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
|
||||
# ================================================================
|
||||
# FALLBACK: Claude trigger for cases where Copilot chain fails/skips
|
||||
# Ensures Claude always runs after sufficient review time
|
||||
# ================================================================
|
||||
claude-fallback-timeout:
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for review window (15 min)
|
||||
run: sleep 900
|
||||
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
core.setOutput('comments_count', comments.data.length.toString());
|
||||
|
||||
// Collect all bot reviewer comments for context
|
||||
const botReviews = comments.data.filter(c =>
|
||||
['coderabbitai[bot]', 'qodo-code-review[bot]', 'gemini-code-assist[bot]',
|
||||
'copilot-swe-agent', 'Copilot', 'greptile-apps[bot]'].some(bot =>
|
||||
c.user.login.toLowerCase().includes(bot.toLowerCase())
|
||||
)
|
||||
);
|
||||
core.setOutput('review_count', botReviews.length.toString());
|
||||
|
||||
- name: Aggregate reviews and trigger Claude
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, per_page: 100});
|
||||
const reviews = {coderabbit: [], qodo: [], gemini: [], copilot: [], greptile: [], others: []};
|
||||
comments.data.forEach(c => {
|
||||
const login = c.user.login.toLowerCase();
|
||||
const body = c.body.substring(0, 500);
|
||||
if (login.includes('coderabbit')) reviews.coderabbit.push(body);
|
||||
else if (login.includes('qodo')) reviews.qodo.push(body);
|
||||
else if (login.includes('gemini')) reviews.gemini.push(body);
|
||||
else if (login.includes('copilot')) reviews.copilot.push(body);
|
||||
else if (login.includes('greptile')) reviews.greptile.push(body);
|
||||
else if (c.user.type === 'Bot') reviews.others.push(body);
|
||||
});
|
||||
const summaryParts = [];
|
||||
if (reviews.coderabbit.length) summaryParts.push('CodeRabbit: ' + reviews.coderabbit.length + ' comments');
|
||||
if (reviews.qodo.length) summaryParts.push('Qodo: ' + reviews.qodo.length + ' comments');
|
||||
if (reviews.gemini.length) summaryParts.push('Gemini: ' + reviews.gemini.length + ' comments');
|
||||
if (reviews.copilot.length) summaryParts.push('Copilot: ' + reviews.copilot.length + ' comments');
|
||||
if (reviews.greptile.length) summaryParts.push('Greptile: ' + reviews.greptile.length + ' comments');
|
||||
const reviewSummary = summaryParts.length ? summaryParts.join(' | ') : 'No bot reviews detected';
|
||||
const claudeBody = '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. ' + reviewSummary + '.\n\n' +
|
||||
'Review Context: ' + comments.data.length + ' total comments, ' +
|
||||
(reviews.coderabbit.length + reviews.qodo.length + reviews.gemini.length + reviews.copilot.length + reviews.greptile.length) + ' bot reviews.\n\n' +
|
||||
'Phase 1: Review per AGENTS.md principles (protocol-driven, backward compatible, no perf impact, SDK value gate — no feature bloat).\n' +
|
||||
'Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n' +
|
||||
'Phase 2: FIX valid issues found by prior reviewers.\n' +
|
||||
'Phase 3: Provide final verdict (approve or request changes).';
|
||||
await github.rest.issues.createComment({issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: claudeBody});
|
||||
console.log('Claude fallback triggered on PR #' + context.issue.number + ' with ' + reviewSummary);
|
||||
|
||||
# ================================================================
|
||||
# Claude FINAL review — polls for Copilot's response, then triggers
|
||||
# This runs IN-BAND under the PAT context (MervinPraison), avoiding
|
||||
# the action_required approval gate that blocks bot-triggered workflows.
|
||||
# ================================================================
|
||||
claude-after-copilot:
|
||||
needs: [copilot-after-coderabbit]
|
||||
if: |
|
||||
always() &&
|
||||
needs.copilot-after-coderabbit.result == 'success' &&
|
||||
needs.copilot-after-coderabbit.outputs.triggered == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Post Claude FINAL after prior reviewers
|
||||
- name: Wait for Copilot to respond (poll)
|
||||
id: poll
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
|
||||
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
|
||||
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 pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
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
|
||||
// Poll for Copilot's response — check every 30s for up to 10 minutes
|
||||
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
|
||||
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
|
||||
|
||||
- name: Wait for prior reviewers (30 min)
|
||||
run: sleep 1800
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
|
||||
|
||||
- name: Post Claude FINAL (skip Copilot)
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner,
|
||||
repo,
|
||||
per_page: 50,
|
||||
sort: 'created',
|
||||
direction: 'desc'
|
||||
});
|
||||
|
||||
// Look for Copilot's response (copilot-swe-agent or Copilot)
|
||||
const copilotResponse = comments.data.find(c =>
|
||||
(c.user.login === 'copilot-swe-agent' ||
|
||||
c.user.login === 'Copilot' ||
|
||||
c.user.login.toLowerCase().includes('copilot')) &&
|
||||
c.user.type === 'Bot'
|
||||
);
|
||||
|
||||
if (copilotResponse) {
|
||||
console.log(`Copilot responded at ${copilotResponse.created_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check PR reviews for Copilot
|
||||
const reviews = await github.rest.pulls.listReviews({
|
||||
pull_number: pr,
|
||||
owner,
|
||||
repo
|
||||
});
|
||||
|
||||
const copilotReview = reviews.data.find(r =>
|
||||
r.user.login.toLowerCase().includes('copilot')
|
||||
);
|
||||
|
||||
if (copilotReview) {
|
||||
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
if (i < maxAttempts - 1) {
|
||||
console.log(`Copilot hasn't responded yet, waiting ${delayMs/1000}s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Copilot did not respond within timeout, triggering Claude anyway');
|
||||
core.setOutput('copilot_responded', 'timeout');
|
||||
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Trigger Claude final review
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
console.log(`Claude triggered on PR #${pr}`);
|
||||
|
||||
- name: Sync pipeline status labels
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
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 }
|
||||
);
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
|
||||
# ================================================================
|
||||
# Recovery: post FINAL @claude after push or when chain missed
|
||||
# Claude after Qodo fallback path (same polling approach)
|
||||
# ================================================================
|
||||
claude-after-copilot-qodo:
|
||||
needs: [copilot-after-qodo]
|
||||
if: |
|
||||
always() &&
|
||||
needs.copilot-after-qodo.result == 'success' &&
|
||||
needs.copilot-after-qodo.outputs.triggered == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Wait for Copilot to respond (poll)
|
||||
id: poll
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
|
||||
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
|
||||
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr, owner, repo,
|
||||
per_page: 50, sort: 'created', direction: 'desc'
|
||||
});
|
||||
const copilotResponse = comments.data.find(c =>
|
||||
(c.user.login === 'copilot-swe-agent' ||
|
||||
c.user.login === 'Copilot' ||
|
||||
c.user.login.toLowerCase().includes('copilot')) &&
|
||||
c.user.type === 'Bot'
|
||||
);
|
||||
if (copilotResponse) {
|
||||
console.log(`Copilot responded at ${copilotResponse.created_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
const reviews = await github.rest.pulls.listReviews({
|
||||
pull_number: pr, owner, repo
|
||||
});
|
||||
const copilotReview = reviews.data.find(r =>
|
||||
r.user.login.toLowerCase().includes('copilot')
|
||||
);
|
||||
if (copilotReview) {
|
||||
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
if (i < maxAttempts - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
console.log('Copilot did not respond within timeout, triggering Claude anyway');
|
||||
core.setOutput('copilot_responded', 'timeout');
|
||||
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Trigger Claude final review
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
console.log(`Claude triggered on PR #${pr}`);
|
||||
|
||||
- name: Sync pipeline status labels
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
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 = parseInt(process.env.PR_NUMBER, 10);
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
|
||||
# ================================================================
|
||||
# Claude after Gemini — restores the trigger lost from the deleted
|
||||
# chain-claude-after-copilot.yml workflow.
|
||||
# Fires when Gemini posts a review comment on a PR or issue.
|
||||
# ================================================================
|
||||
claude-after-gemini:
|
||||
if: |
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.user.login == 'gemini-code-assist[bot]' ||
|
||||
contains(github.event.comment.body, 'Review completed by Gemini CLI') ||
|
||||
contains(github.event.comment.body, 'The fix is ready for review. Please test')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Post Claude analysis request (Lead Engineer)
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the Lead Engineer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL analysis and reviews above carefully (Gemini, CodeRabbit, Qodo, Copilot, etc).\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
|
||||
# ================================================================
|
||||
# Recovery: post FINAL @claude if merge-conflict comment blocked the chain
|
||||
# (e.g. conflict fired before claude-after-copilot could post review prompt)
|
||||
# ================================================================
|
||||
claude-review-recovery:
|
||||
if: |
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'synchronize'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -130,7 +543,6 @@ jobs:
|
||||
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;
|
||||
@@ -138,7 +550,6 @@ jobs:
|
||||
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);
|
||||
@@ -148,24 +559,11 @@ jobs:
|
||||
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}`);
|
||||
await github.rest.issues.createComment({ issue_number: pr, owner, repo, body: finalBody });
|
||||
core.info(`Posted stale-FINAL recovery @claude on PR #${pr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -174,10 +572,18 @@ jobs:
|
||||
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}`);
|
||||
const hasReviewerActivity = comments.some(c => {
|
||||
const login = c.user.login.toLowerCase();
|
||||
return login.includes('coderabbit') || login.includes('copilot') ||
|
||||
login.includes('greptile') || login.includes('qodo') || login.includes('gemini');
|
||||
});
|
||||
if (!hasReviewerActivity) {
|
||||
core.info('No bot reviewer activity yet, skipping recovery');
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({ issue_number: pr, owner, repo, body: finalBody });
|
||||
core.info(`Posted missing FINAL Claude review on PR #${pr}`);
|
||||
|
||||
- name: Sync pipeline status labels
|
||||
uses: actions/github-script@v7
|
||||
@@ -190,8 +596,8 @@ jobs:
|
||||
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:
|
||||
# Scheduled: re-post @claude when FINAL is stale (Claude pushed after last FINAL trigger)
|
||||
stale-final-recovery-scheduled:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -200,14 +606,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Post missing/stale FINAL @claude on open PRs
|
||||
- name: Re-post stale FINAL @claude on open internal 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, {
|
||||
@@ -216,43 +621,28 @@ jobs:
|
||||
let posted = 0;
|
||||
for (const pr of prs) {
|
||||
if (pr.draft) continue;
|
||||
if (pr.head?.repo?.full_name && pr.head.repo.full_name !== `${owner}/${repo}`) 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.needsStaleFinalRecovery(comments, headPushedAt)) continue;
|
||||
if (mergeGate.hasRecentClaudeTrigger(comments, 10)) {
|
||||
core.info(`PR #${pr.number}: @claude within 10min — skip`);
|
||||
core.info(`PR #${pr.number}: stale FINAL but @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;
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr.number, owner, repo,
|
||||
body: mergeGate.FINAL_CLAUDE_REVIEW_BODY,
|
||||
});
|
||||
posted++;
|
||||
core.info(`Posted scheduled stale-FINAL recovery on PR #${pr.number}`);
|
||||
if (posted >= 5) break;
|
||||
}
|
||||
core.info(`Missing/stale Claude FINAL sweep complete (${posted} posted)`);
|
||||
core.info(`Scheduled stale-FINAL recovery complete (${posted} posted)`);
|
||||
|
||||
# ================================================================
|
||||
# BOT PRs: kick CodeRabbit + Qodo (Greptile may follow on its own)
|
||||
# BOT PRs: CodeRabbit/Qodo skip bot-authored PRs by default.
|
||||
# Trigger them explicitly. Do NOT trigger Copilot here — it will
|
||||
# be triggered by copilot-after-coderabbit/qodo when they finish.
|
||||
# ================================================================
|
||||
bot-pr-trigger-reviews:
|
||||
if: |
|
||||
@@ -291,4 +681,4 @@ jobs:
|
||||
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);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
@@ -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();
|
||||
@@ -5,8 +5,7 @@ name: Claude PR Merge Gate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Scan for merge-ready PRs every 10 minutes (assess runs only when candidates exist).
|
||||
- cron: '*/10 * * * *'
|
||||
- cron: '0 2,8,14,20 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
@@ -34,11 +33,11 @@ on:
|
||||
|
||||
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
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
MAX_CANDIDATES: 3
|
||||
MAX_CANDIDATES: 5
|
||||
DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
|
||||
|
||||
permissions:
|
||||
@@ -56,22 +55,15 @@ jobs:
|
||||
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
|
||||
env:
|
||||
PAT: ${{ secrets.GH_TOKEN }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
@@ -79,6 +71,11 @@ jobs:
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
if (!process.env.PAT) {
|
||||
core.setFailed('GH_TOKEN secret is required for merge gate auto-dispatch (needs repo + workflow scope).');
|
||||
return;
|
||||
}
|
||||
|
||||
if (wr.conclusion !== 'success') {
|
||||
core.info(`Skip dispatch: ${wr.name} conclusion=${wr.conclusion}`);
|
||||
return;
|
||||
@@ -123,26 +120,17 @@ jobs:
|
||||
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 }}
|
||||
github-token: ${{ env.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
@@ -167,9 +155,10 @@ jobs:
|
||||
skipped.push({ pr: n, reasons: result.reasons });
|
||||
}
|
||||
} else {
|
||||
const prNumbers = await mergeGate.listPrNumbersForMergeGateScan(
|
||||
github, owner, repo, core
|
||||
);
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner, repo, state: 'open', per_page: 100, sort: 'created', direction: 'asc',
|
||||
});
|
||||
const prNumbers = prs.map((p) => p.number);
|
||||
const selected = await mergeGate.selectMergeGateCandidates(
|
||||
github, owner, repo, prNumbers, maxCandidates, core
|
||||
);
|
||||
@@ -206,7 +195,7 @@ jobs:
|
||||
});
|
||||
}
|
||||
|
||||
await ps.syncOpenPullRequests(github, owner, repo, { maxPrs: 10 }, core);
|
||||
await ps.syncOpenPullRequests(github, owner, repo, { maxPrs: 20 }, core);
|
||||
|
||||
claude-assess:
|
||||
needs: scan-candidates
|
||||
@@ -240,7 +229,7 @@ jobs:
|
||||
id: mark_active
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
github-token: ${{ env.GH_TOKEN }}
|
||||
script: |
|
||||
core.setOutput('started_at', new Date().toISOString());
|
||||
await github.rest.issues.addLabels({
|
||||
@@ -262,6 +251,7 @@ jobs:
|
||||
|
||||
- name: Claude merge gate assessment (read-only)
|
||||
id: assess
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/claude-code-action
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
@@ -288,13 +278,12 @@ jobs:
|
||||
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.
|
||||
3. Confirm SDK value gate: change strengthens praisonaiagents (no scope creep).
|
||||
4. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
|
||||
5. If agent.py adds >100 lines or new Agent __init__ params, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
|
||||
6. 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.
|
||||
7. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
|
||||
8. 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: |
|
||||
@@ -309,12 +298,11 @@ jobs:
|
||||
Read
|
||||
timeout_minutes: 15
|
||||
|
||||
- name: Post fallback BLOCK if Opus did not comment
|
||||
- name: Post fallback verdict if Claude did not comment
|
||||
id: fallback_verdict
|
||||
if: steps.assess.outcome != 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
github-token: ${{ env.GH_TOKEN }}
|
||||
script: |
|
||||
const mergeGate = require('/tmp/merge-gate.js');
|
||||
const owner = context.repo.owner;
|
||||
@@ -323,24 +311,24 @@ jobs:
|
||||
|
||||
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 }
|
||||
);
|
||||
const existing = mergeGate.findMergeGateVerdict(ctx.comments, minCreatedAt, ctx.headPushedAt);
|
||||
if (existing) {
|
||||
core.info(`Opus verdict already posted: ${existing}`);
|
||||
core.info(`Verdict already posted: ${existing}`);
|
||||
core.setOutput('verdict', existing);
|
||||
return;
|
||||
}
|
||||
|
||||
const check = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core, { forMergeStep: true }
|
||||
);
|
||||
const verdict = check.ready ? 'APPROVE' : 'BLOCK';
|
||||
const lines = [
|
||||
'MERGE_GATE_VERDICT: BLOCK',
|
||||
`MERGE_GATE_VERDICT: ${verdict}`,
|
||||
'',
|
||||
'Automated fallback — Opus merge gate assessment did not complete.',
|
||||
'Automated fallback — Claude assess did not post a verdict comment (e.g. GitHub MCP unavailable).',
|
||||
];
|
||||
if (check.ready) {
|
||||
lines.push('Deterministic gates passed, but Opus MERGE_GATE_VERDICT is required before merge.');
|
||||
lines.push('All deterministic merge gates passed.');
|
||||
} else {
|
||||
lines.push('Blockers:', ...check.reasons.map((r) => `- ${r}`));
|
||||
}
|
||||
@@ -348,12 +336,14 @@ jobs:
|
||||
owner, repo, issue_number: prNumber,
|
||||
body: lines.join('\n'),
|
||||
});
|
||||
core.info('Posted fallback BLOCK — Opus verdict required');
|
||||
core.info(`Posted fallback verdict: ${verdict}`);
|
||||
core.setOutput('verdict', verdict);
|
||||
|
||||
- name: Require Opus MERGE_GATE_VERDICT
|
||||
- name: Verify verdict posted
|
||||
id: verdict
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
github-token: ${{ env.GH_TOKEN }}
|
||||
script: |
|
||||
const mergeGate = require('/tmp/merge-gate.js');
|
||||
const comments = await mergeGate.listAllComments(
|
||||
@@ -363,23 +353,15 @@ jobs:
|
||||
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);
|
||||
const verdict = mergeGate.findMergeGateVerdict(comments, minCreatedAt, ctx.headPushedAt);
|
||||
core.setOutput('verdict', verdict || 'NONE');
|
||||
if (!verdict) core.warning('No recent MERGE_GATE_VERDICT comment found after assessment');
|
||||
|
||||
- name: Remove merge gate active label
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
github-token: ${{ env.GH_TOKEN }}
|
||||
script: |
|
||||
const prNumber = ${{ matrix.pr_number }};
|
||||
try {
|
||||
@@ -413,25 +395,16 @@ jobs:
|
||||
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 }}
|
||||
github-token: ${{ env.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
@@ -454,9 +427,7 @@ jobs:
|
||||
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 }
|
||||
);
|
||||
const verdict = mergeGate.findMergeGateVerdict(comments, verdictMin, ctx.headPushedAt);
|
||||
if (verdict !== 'APPROVE') {
|
||||
core.info(`Skip merge: verdict=${verdict || 'NONE'}`);
|
||||
await removeLabels();
|
||||
@@ -479,38 +450,9 @@ jobs:
|
||||
}
|
||||
|
||||
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.pulls.merge({
|
||||
owner, repo, pull_number: prNumber, merge_method: method,
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: prNumber, labels: ['auto-merged-by-gate'],
|
||||
|
||||
@@ -49,14 +49,12 @@ jobs:
|
||||
// --- Label triage ---
|
||||
const labels = [];
|
||||
|
||||
// Kind labels (bug before "how to" — bug reports often include "## How to fix")
|
||||
// Kind labels
|
||||
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'))) {
|
||||
} else if (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')) {
|
||||
} else if (content.includes('question') || content.includes('how do i') || content.includes('how to')) {
|
||||
labels.push('question');
|
||||
}
|
||||
|
||||
@@ -98,21 +96,13 @@ jobs:
|
||||
'- 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).'
|
||||
'_A maintainer can trigger deeper analysis by commenting `@claude` on this issue._'
|
||||
].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) {
|
||||
// --- For owner: add 'claude' label to trigger the code-fix job ---
|
||||
if (isOwner) {
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
@@ -122,43 +112,27 @@ jobs:
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# Job 2: Full Claude code-fix
|
||||
# Job 2: Full Claude code-fix (owner/collaborator only)
|
||||
# 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)
|
||||
# Allow: human users, github-actions[bot] (auto-comments/labels)
|
||||
# Block: dependabot, cursor, renovate, other bots
|
||||
# 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 != 'issue_comment' || contains(github.event.comment.body, '@claude')) &&
|
||||
(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 == '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
|
||||
@@ -294,19 +268,17 @@ jobs:
|
||||
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.
|
||||
3. Tools (PraisonAI-Tools): Capabilities Agents actively call during execution (e.g. SurrealDB tool, Slack tool).
|
||||
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.
|
||||
5. Plugins (PraisonAI-Plugins): Framework lifecycle extensions replacing systemic behavior (e.g. tracing, logging, metrics, hooks, guardrails).
|
||||
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.
|
||||
- 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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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.
|
||||
# When install.sh or install.ps1 change on main, trigger praison.ai CI which
|
||||
# syncs scripts at build time and deploys to Azure AKS (existing azure-pipeline.yaml).
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -19,59 +19,34 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-to-hestia:
|
||||
trigger-website-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production-hestia
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sync install scripts to Hestia
|
||||
- name: Trigger praison.ai CI
|
||||
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 }}
|
||||
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
|
||||
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."
|
||||
if [ -z "${GH_TOKEN:-}" ]; then
|
||||
echo "SECRETS_ADMIN_PAT is not configured in this repository"
|
||||
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
|
||||
gh workflow run CI \
|
||||
--repo MervinPraison/praison.ai \
|
||||
--ref master
|
||||
|
||||
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 "Triggered praison.ai CI for PraisonAI commit ${SOURCE_SHA}"
|
||||
echo "Monitor: https://github.com/MervinPraison/praison.ai/actions/workflows/azure-pipeline.yaml"
|
||||
|
||||
echo "Deployed install scripts for commit ${SOURCE_SHA}"
|
||||
echo "Verify: curl -fsSL https://praison.ai/install.ps1 | head -5"
|
||||
sleep 10
|
||||
RUN_URL=$(gh run list \
|
||||
--repo MervinPraison/praison.ai \
|
||||
--workflow CI \
|
||||
--limit 1 \
|
||||
--json url,status,createdAt \
|
||||
--jq '.[0].url' 2>/dev/null || true)
|
||||
if [ -n "${RUN_URL}" ]; then
|
||||
echo "Latest praison.ai CI run: ${RUN_URL}"
|
||||
fi
|
||||
@@ -7,18 +7,12 @@ on:
|
||||
- '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:
|
||||
@@ -46,16 +40,16 @@ jobs:
|
||||
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
|
||||
python -m pip install --upgrade pip
|
||||
# Install PraisonAI packages in development mode
|
||||
cd PraisonAI/src/praisonai-agents
|
||||
pip install -e .
|
||||
cd ../praisonai
|
||||
pip install -e ".[ui,dev]"
|
||||
# Install PraisonAIUI in development mode
|
||||
cd ../../../PraisonAIUI
|
||||
pip install -e .
|
||||
pip install pytest pytest-cov
|
||||
|
||||
- name: Verify versions
|
||||
run: |
|
||||
@@ -143,14 +137,13 @@ jobs:
|
||||
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
|
||||
python -m pip install --upgrade pip
|
||||
cd PraisonAI/src/praisonai-agents
|
||||
pip install -e .
|
||||
cd ../praisonai
|
||||
pip install -e ".[ui,dev]"
|
||||
cd ../../../PraisonAIUI
|
||||
pip install -e .
|
||||
|
||||
- name: Run agentic integration test
|
||||
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
- cron: '0 */6 * * *'
|
||||
|
||||
concurrency:
|
||||
group: merge-conflict-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
group: merge-conflict-scan-${{ github.repository }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -44,7 +44,6 @@ jobs:
|
||||
'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));
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
name: Nightly Release Gate
|
||||
|
||||
# Preflight before PyPI Release (patch bump).
|
||||
# Triggers: Core Tests success on main (workflow_run) or nightly cron at 00:00 UTC.
|
||||
# Dedupe: max one successful patch release per UTC day (see release-gate.js).
|
||||
# Preflight checks before dispatching PyPI Release (patch bump).
|
||||
# Runs nightly at 00:00 UTC (midnight).
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -14,13 +13,6 @@ on:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Core Tests
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -32,87 +24,83 @@ concurrency:
|
||||
|
||||
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, 'Bump praisonai')
|
||||
)
|
||||
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
|
||||
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
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ steps.ctx.outputs.checkout_sha }}
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch main and tags
|
||||
run: git fetch --tags origin main
|
||||
|
||||
- name: Preflight — path changes, CI SHA, dedupe
|
||||
- name: Preflight — path changes and CI green
|
||||
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';
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
const result = await rg.evaluateReleasePreflight(
|
||||
github, owner, repo,
|
||||
{ headSha, isCiTrigger: isCi, bump: 'patch' },
|
||||
core
|
||||
);
|
||||
git fetch --tags origin
|
||||
|
||||
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 || '');
|
||||
LAST_TAG=$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)
|
||||
if [ -z "$LAST_TAG" ]; then
|
||||
echo "No v* tag found — skip automated release (first release must be manual)"
|
||||
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
||||
echo "reason=no_tag" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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();
|
||||
CHANGED=$(git diff --name-only "${LAST_TAG}" HEAD -- \
|
||||
src/praisonai \
|
||||
src/praisonai-agents || true)
|
||||
|
||||
if (!result.ready) {
|
||||
core.info(`Skip release: ${result.reasons.join('; ')}`);
|
||||
}
|
||||
if [ -z "$CHANGED" ]; then
|
||||
echo "No changes in src/praisonai or src/praisonai-agents since ${LAST_TAG} — skip"
|
||||
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
||||
echo "reason=no_path_changes" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changes since ${LAST_TAG}:"
|
||||
echo "$CHANGED"
|
||||
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
echo "main HEAD: ${HEAD_SHA}"
|
||||
|
||||
GREEN_SHA=$(gh run list \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--workflow "Core Tests" \
|
||||
--branch main \
|
||||
--status success \
|
||||
--json headSha \
|
||||
--jq '.[0].headSha // empty')
|
||||
|
||||
if [ "$GREEN_SHA" != "$HEAD_SHA" ]; then
|
||||
echo "main@${HEAD_SHA} is not covered by a green Core Tests run (last green: ${GREEN_SHA:-none}) — skip"
|
||||
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
||||
echo "reason=ci_not_green" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IN_PROGRESS=$(gh run list \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--workflow "Core Tests" \
|
||||
--branch main \
|
||||
--json headSha,status \
|
||||
--jq "[.[] | select(.headSha == \"${HEAD_SHA}\" and .status == \"in_progress\")] | length")
|
||||
|
||||
if [ "${IN_PROGRESS:-0}" != "0" ]; then
|
||||
echo "Core Tests still in progress on ${HEAD_SHA} — skip"
|
||||
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
||||
echo "reason=ci_in_progress" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Preflight passed — ready to dispatch patch release"
|
||||
echo "should_release=true" >> "$GITHUB_OUTPUT"
|
||||
echo "reason=ready" >> "$GITHUB_OUTPUT"
|
||||
echo "last_tag=${LAST_TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dry run summary
|
||||
if: inputs.dry_run == true
|
||||
@@ -120,6 +108,9 @@ jobs:
|
||||
echo "Dry run — preflight only, no pypi-release dispatch."
|
||||
echo "should_release=${{ steps.preflight.outputs.should_release }}"
|
||||
echo "reason=${{ steps.preflight.outputs.reason }}"
|
||||
if [ "${{ steps.preflight.outputs.should_release }}" = "true" ]; then
|
||||
echo "Would dispatch: pypi-release.yml bump=patch source=nightly"
|
||||
fi
|
||||
|
||||
- name: Dispatch PyPI Release (patch)
|
||||
if: steps.preflight.outputs.should_release == 'true' && inputs.dry_run != true
|
||||
@@ -129,12 +120,7 @@ jobs:
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--ref main \
|
||||
-f bump=patch \
|
||||
-f source=${{ steps.ctx.outputs.source }} \
|
||||
-f trigger_sha=${{ steps.preflight.outputs.head_sha }} \
|
||||
-f source=nightly \
|
||||
-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
|
||||
echo "Dispatched pypi-release.yml (patch, source=nightly)"
|
||||
echo "Approve the pypi environment when prompted."
|
||||
@@ -4,14 +4,12 @@ name: Pipeline Status Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/10 * * * *'
|
||||
- cron: '*/15 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
issues: write
|
||||
actions: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
@@ -21,22 +19,13 @@ jobs:
|
||||
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 }}
|
||||
github-token: ${{ secrets.GH_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
|
||||
github, context.repo.owner, context.repo.repo, { maxPrs: 30 }, core
|
||||
);
|
||||
@@ -49,12 +49,10 @@ jobs:
|
||||
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]"
|
||||
run: |
|
||||
pip install uv
|
||||
cd src/praisonai-agents && uv pip install --system -e ".[all]"
|
||||
cd ../praisonai && uv pip install --system -e ".[all]"
|
||||
|
||||
- name: Run PraisonAI Issue Triage
|
||||
env:
|
||||
|
||||
@@ -16,20 +16,16 @@ 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'))
|
||||
)
|
||||
(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
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
name: PyPI Release
|
||||
|
||||
# Environments (Settings → Environments):
|
||||
# pypi-auto — patch releases from ci/nightly (no reviewers, main branch only)
|
||||
# pypi — manual workflow_dispatch and major/minor (required reviewers)
|
||||
# Requires a GitHub Environment named "pypi" (Settings → Environments → pypi)
|
||||
# with required reviewers before secrets are exposed to this job.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: 'Version bump type for all packages (agents, code, wrapper)'
|
||||
description: 'Version bump type for both packages'
|
||||
required: true
|
||||
default: patch
|
||||
type: choice
|
||||
@@ -27,37 +26,7 @@ on:
|
||||
default: false
|
||||
type: boolean
|
||||
skip_wrapper:
|
||||
description: 'Skip wrapper publish (agents and/or code only)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_code:
|
||||
description: 'Skip praisonai-code publish (recovery if already on PyPI)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_bot:
|
||||
description: 'Skip praisonai-bot publish (recovery if already on PyPI)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_train:
|
||||
description: 'Skip praisonai-train publish (recovery if already on PyPI)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_browser:
|
||||
description: 'Skip praisonai-browser publish (recovery if already on PyPI)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_mcp:
|
||||
description: 'Skip praisonai-mcp publish (recovery if already on PyPI)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_sandbox:
|
||||
description: 'Skip praisonai-sandbox publish (recovery if already on PyPI)'
|
||||
description: 'Publish agents only'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -71,36 +40,6 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
code_version:
|
||||
description: 'Override praisonai-code version (empty = auto bump from pyproject.toml)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
bot_version:
|
||||
description: 'Override praisonai-bot version (empty = auto bump from pyproject.toml)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
train_version:
|
||||
description: 'Override praisonai-train version (empty = auto bump from pyproject.toml)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
browser_version:
|
||||
description: 'Override praisonai-browser version (empty = auto bump from pyproject.toml)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
mcp_version:
|
||||
description: 'Override praisonai-mcp version (empty = auto bump from pyproject.toml)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
sandbox_version:
|
||||
description: 'Override praisonai-sandbox version (empty = auto bump from pyproject.toml)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
source:
|
||||
description: 'Trigger source (audit trail)'
|
||||
required: false
|
||||
@@ -109,12 +48,6 @@ on:
|
||||
options:
|
||||
- manual
|
||||
- nightly
|
||||
- ci
|
||||
trigger_sha:
|
||||
description: 'Main commit SHA that passed release gate (audit/dedupe)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -127,12 +60,7 @@ jobs:
|
||||
release:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
environment: >-
|
||||
${{
|
||||
(inputs.source == 'ci' || inputs.source == 'nightly') &&
|
||||
inputs.bump == 'patch' &&
|
||||
'pypi-auto' || 'pypi'
|
||||
}}
|
||||
environment: pypi
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN || secrets.PYPI_API_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
@@ -161,10 +89,8 @@ jobs:
|
||||
- name: Release summary
|
||||
run: |
|
||||
echo "Source: ${{ inputs.source }}"
|
||||
echo "Trigger SHA: ${{ inputs.trigger_sha }}"
|
||||
echo "Bump: ${{ inputs.bump }}"
|
||||
echo "Dry run: ${{ inputs.dry_run }}"
|
||||
echo "Environment: ${{ (inputs.source == 'ci' || inputs.source == 'nightly') && inputs.bump == 'patch' && 'pypi-auto' || 'pypi' }}"
|
||||
|
||||
- name: Compute release versions
|
||||
id: versions
|
||||
@@ -172,19 +98,7 @@ jobs:
|
||||
BUMP: ${{ inputs.bump }}
|
||||
AGENTS_OVERRIDE: ${{ inputs.agents_version }}
|
||||
WRAPPER_OVERRIDE: ${{ inputs.wrapper_version }}
|
||||
CODE_OVERRIDE: ${{ inputs.code_version }}
|
||||
BOT_OVERRIDE: ${{ inputs.bot_version }}
|
||||
TRAIN_OVERRIDE: ${{ inputs.train_version }}
|
||||
BROWSER_OVERRIDE: ${{ inputs.browser_version }}
|
||||
MCP_OVERRIDE: ${{ inputs.mcp_version }}
|
||||
SANDBOX_OVERRIDE: ${{ inputs.sandbox_version }}
|
||||
SKIP_AGENTS: ${{ inputs.skip_agents }}
|
||||
SKIP_CODE: ${{ inputs.skip_code }}
|
||||
SKIP_BOT: ${{ inputs.skip_bot }}
|
||||
SKIP_TRAIN: ${{ inputs.skip_train }}
|
||||
SKIP_BROWSER: ${{ inputs.skip_browser }}
|
||||
SKIP_MCP: ${{ inputs.skip_mcp }}
|
||||
SKIP_SANDBOX: ${{ inputs.skip_sandbox }}
|
||||
run: |
|
||||
python <<'PY'
|
||||
import os
|
||||
@@ -215,20 +129,8 @@ jobs:
|
||||
|
||||
agents_override = os.environ.get("AGENTS_OVERRIDE", "").strip()
|
||||
wrapper_override = os.environ.get("WRAPPER_OVERRIDE", "").strip()
|
||||
code_override = os.environ.get("CODE_OVERRIDE", "").strip()
|
||||
bot_override = os.environ.get("BOT_OVERRIDE", "").strip()
|
||||
train_override = os.environ.get("TRAIN_OVERRIDE", "").strip()
|
||||
browser_override = os.environ.get("BROWSER_OVERRIDE", "").strip()
|
||||
mcp_override = os.environ.get("MCP_OVERRIDE", "").strip()
|
||||
sandbox_override = os.environ.get("SANDBOX_OVERRIDE", "").strip()
|
||||
validate_version("agents_version", agents_override)
|
||||
validate_version("wrapper_version", wrapper_override)
|
||||
validate_version("code_version", code_override)
|
||||
validate_version("bot_version", bot_override)
|
||||
validate_version("train_version", train_override)
|
||||
validate_version("browser_version", browser_override)
|
||||
validate_version("mcp_version", mcp_override)
|
||||
validate_version("sandbox_version", sandbox_override)
|
||||
|
||||
agents_pyproject = root / "src/praisonai-agents/pyproject.toml"
|
||||
agents_content = agents_pyproject.read_text()
|
||||
@@ -237,54 +139,9 @@ jobs:
|
||||
print("Could not read agents version from pyproject.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_agents = agents_match.group(1)
|
||||
|
||||
code_pyproject = root / "src/praisonai-code/pyproject.toml"
|
||||
code_content = code_pyproject.read_text()
|
||||
code_match = re.search(r'^version\s*=\s*"([^"]+)"', code_content, re.MULTILINE)
|
||||
if not code_match:
|
||||
print("Could not read praisonai-code version from pyproject.toml", file=sys.stderr)
|
||||
if not SEMVER.match(current_agents):
|
||||
print(f"Invalid current agents version: {current_agents}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_code = code_match.group(1)
|
||||
|
||||
bot_pyproject = root / "src/praisonai-bot/pyproject.toml"
|
||||
bot_content = bot_pyproject.read_text()
|
||||
bot_match = re.search(r'^version\s*=\s*"([^"]+)"', bot_content, re.MULTILINE)
|
||||
if not bot_match:
|
||||
print("Could not read praisonai-bot version from pyproject.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_bot = bot_match.group(1)
|
||||
|
||||
train_pyproject = root / "src/praisonai-train/pyproject.toml"
|
||||
train_content = train_pyproject.read_text()
|
||||
train_match = re.search(r'^version\s*=\s*"([^"]+)"', train_content, re.MULTILINE)
|
||||
if not train_match:
|
||||
print("Could not read praisonai-train version from pyproject.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_train = train_match.group(1)
|
||||
|
||||
browser_pyproject = root / "src/praisonai-browser/pyproject.toml"
|
||||
browser_content = browser_pyproject.read_text()
|
||||
browser_match = re.search(r'^version\s*=\s*"([^"]+)"', browser_content, re.MULTILINE)
|
||||
if not browser_match:
|
||||
print("Could not read praisonai-browser version from pyproject.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_browser = browser_match.group(1)
|
||||
|
||||
mcp_pyproject = root / "src/praisonai-mcp/pyproject.toml"
|
||||
mcp_content = mcp_pyproject.read_text()
|
||||
mcp_match = re.search(r'^version\s*=\s*"([^"]+)"', mcp_content, re.MULTILINE)
|
||||
if not mcp_match:
|
||||
print("Could not read praisonai-mcp version from pyproject.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_mcp = mcp_match.group(1)
|
||||
|
||||
sandbox_pyproject = root / "src/praisonai-sandbox/pyproject.toml"
|
||||
sandbox_content = sandbox_pyproject.read_text()
|
||||
sandbox_match = re.search(r'^version\s*=\s*"([^"]+)"', sandbox_content, re.MULTILINE)
|
||||
if not sandbox_match:
|
||||
print("Could not read praisonai-sandbox version from pyproject.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_sandbox = sandbox_match.group(1)
|
||||
|
||||
wrapper_version_py = root / "src/praisonai/praisonai/version.py"
|
||||
wrapper_content = wrapper_version_py.read_text()
|
||||
@@ -293,28 +150,11 @@ jobs:
|
||||
print("Could not read wrapper version from version.py", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_wrapper = wrapper_match.group(1)
|
||||
|
||||
for label, value in (
|
||||
("agents", current_agents),
|
||||
("code", current_code),
|
||||
("bot", current_bot),
|
||||
("train", current_train),
|
||||
("browser", current_browser),
|
||||
("mcp", current_mcp),
|
||||
("sandbox", current_sandbox),
|
||||
("wrapper", current_wrapper),
|
||||
):
|
||||
if not SEMVER.match(value):
|
||||
print(f"Invalid current {label} version: {value}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not SEMVER.match(current_wrapper):
|
||||
print(f"Invalid current wrapper version: {current_wrapper}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
skip_agents = os.environ.get("SKIP_AGENTS", "false").lower() == "true"
|
||||
skip_code = os.environ.get("SKIP_CODE", "false").lower() == "true"
|
||||
skip_bot = os.environ.get("SKIP_BOT", "false").lower() == "true"
|
||||
skip_train = os.environ.get("SKIP_TRAIN", "false").lower() == "true"
|
||||
skip_browser = os.environ.get("SKIP_BROWSER", "false").lower() == "true"
|
||||
skip_mcp = os.environ.get("SKIP_MCP", "false").lower() == "true"
|
||||
skip_sandbox = os.environ.get("SKIP_SANDBOX", "false").lower() == "true"
|
||||
|
||||
if agents_override:
|
||||
agents_version = agents_override
|
||||
@@ -323,48 +163,6 @@ jobs:
|
||||
else:
|
||||
agents_version = bump_version(current_agents)
|
||||
|
||||
if code_override:
|
||||
code_version = code_override
|
||||
elif skip_code:
|
||||
code_version = current_code
|
||||
else:
|
||||
code_version = bump_version(current_code)
|
||||
|
||||
if bot_override:
|
||||
bot_version = bot_override
|
||||
elif skip_bot:
|
||||
bot_version = current_bot
|
||||
else:
|
||||
bot_version = bump_version(current_bot)
|
||||
|
||||
if train_override:
|
||||
train_version = train_override
|
||||
elif skip_train:
|
||||
train_version = current_train
|
||||
else:
|
||||
train_version = bump_version(current_train)
|
||||
|
||||
if browser_override:
|
||||
browser_version = browser_override
|
||||
elif skip_browser:
|
||||
browser_version = current_browser
|
||||
else:
|
||||
browser_version = bump_version(current_browser)
|
||||
|
||||
if mcp_override:
|
||||
mcp_version = mcp_override
|
||||
elif skip_mcp:
|
||||
mcp_version = current_mcp
|
||||
else:
|
||||
mcp_version = bump_version(current_mcp)
|
||||
|
||||
if sandbox_override:
|
||||
sandbox_version = sandbox_override
|
||||
elif skip_sandbox:
|
||||
sandbox_version = current_sandbox
|
||||
else:
|
||||
sandbox_version = bump_version(current_sandbox)
|
||||
|
||||
if wrapper_override:
|
||||
wrapper_version = wrapper_override
|
||||
else:
|
||||
@@ -373,151 +171,32 @@ jobs:
|
||||
github_output = os.environ["GITHUB_OUTPUT"]
|
||||
with open(github_output, "a", encoding="utf-8") as fh:
|
||||
fh.write(f"agents_version={agents_version}\n")
|
||||
fh.write(f"code_version={code_version}\n")
|
||||
fh.write(f"bot_version={bot_version}\n")
|
||||
fh.write(f"train_version={train_version}\n")
|
||||
fh.write(f"browser_version={browser_version}\n")
|
||||
fh.write(f"mcp_version={mcp_version}\n")
|
||||
fh.write(f"sandbox_version={sandbox_version}\n")
|
||||
fh.write(f"wrapper_version={wrapper_version}\n")
|
||||
fh.write(f"current_agents={current_agents}\n")
|
||||
fh.write(f"current_code={current_code}\n")
|
||||
fh.write(f"current_bot={current_bot}\n")
|
||||
fh.write(f"current_train={current_train}\n")
|
||||
fh.write(f"current_browser={current_browser}\n")
|
||||
fh.write(f"current_mcp={current_mcp}\n")
|
||||
fh.write(f"current_sandbox={current_sandbox}\n")
|
||||
fh.write(f"current_wrapper={current_wrapper}\n")
|
||||
|
||||
print(f"Agents: {current_agents} -> {agents_version}")
|
||||
print(f"Code: {current_code} -> {code_version}")
|
||||
print(f"Bot: {current_bot} -> {bot_version}")
|
||||
print(f"Train: {current_train} -> {train_version}")
|
||||
print(f"Browser: {current_browser} -> {browser_version}")
|
||||
print(f"MCP: {current_mcp} -> {mcp_version}")
|
||||
print(f"Sandbox: {current_sandbox} -> {sandbox_version}")
|
||||
print(f"Wrapper: {current_wrapper} -> {wrapper_version}")
|
||||
PY
|
||||
|
||||
- name: Detect existing PyPI versions
|
||||
id: pypi_exists
|
||||
if: inputs.dry_run != true
|
||||
env:
|
||||
AGENTS_VERSION: ${{ steps.versions.outputs.agents_version }}
|
||||
CODE_VERSION: ${{ steps.versions.outputs.code_version }}
|
||||
BOT_VERSION: ${{ steps.versions.outputs.bot_version }}
|
||||
TRAIN_VERSION: ${{ steps.versions.outputs.train_version }}
|
||||
BROWSER_VERSION: ${{ steps.versions.outputs.browser_version }}
|
||||
MCP_VERSION: ${{ steps.versions.outputs.mcp_version }}
|
||||
SANDBOX_VERSION: ${{ steps.versions.outputs.sandbox_version }}
|
||||
WRAPPER_VERSION: ${{ steps.versions.outputs.wrapper_version }}
|
||||
INPUT_SKIP_AGENTS: ${{ inputs.skip_agents }}
|
||||
INPUT_SKIP_CODE: ${{ inputs.skip_code }}
|
||||
INPUT_SKIP_BOT: ${{ inputs.skip_bot }}
|
||||
INPUT_SKIP_TRAIN: ${{ inputs.skip_train }}
|
||||
INPUT_SKIP_BROWSER: ${{ inputs.skip_browser }}
|
||||
INPUT_SKIP_MCP: ${{ inputs.skip_mcp }}
|
||||
INPUT_SKIP_SANDBOX: ${{ inputs.skip_sandbox }}
|
||||
INPUT_SKIP_WRAPPER: ${{ inputs.skip_wrapper }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
skip_agents="${INPUT_SKIP_AGENTS}"
|
||||
skip_code="${INPUT_SKIP_CODE}"
|
||||
skip_bot="${INPUT_SKIP_BOT}"
|
||||
skip_train="${INPUT_SKIP_TRAIN}"
|
||||
skip_browser="${INPUT_SKIP_BROWSER}"
|
||||
skip_mcp="${INPUT_SKIP_MCP}"
|
||||
skip_sandbox="${INPUT_SKIP_SANDBOX}"
|
||||
skip_wrapper="${INPUT_SKIP_WRAPPER}"
|
||||
if [ "$skip_agents" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonaiagents/${AGENTS_VERSION}/json" >/dev/null && skip_agents=true || true
|
||||
fi
|
||||
if [ "$skip_code" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai-code/${CODE_VERSION}/json" >/dev/null && skip_code=true || true
|
||||
fi
|
||||
if [ "$skip_bot" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai-bot/${BOT_VERSION}/json" >/dev/null && skip_bot=true || true
|
||||
fi
|
||||
if [ "$skip_train" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai-train/${TRAIN_VERSION}/json" >/dev/null && skip_train=true || true
|
||||
fi
|
||||
if [ "$skip_browser" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai-browser/${BROWSER_VERSION}/json" >/dev/null && skip_browser=true || true
|
||||
fi
|
||||
if [ "$skip_mcp" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai-mcp/${MCP_VERSION}/json" >/dev/null && skip_mcp=true || true
|
||||
fi
|
||||
if [ "$skip_sandbox" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai-sandbox/${SANDBOX_VERSION}/json" >/dev/null && skip_sandbox=true || true
|
||||
fi
|
||||
if [ "$skip_wrapper" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai/${WRAPPER_VERSION}/json" >/dev/null && skip_wrapper=true || true
|
||||
fi
|
||||
echo "skip_agents=${skip_agents}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_code=${skip_code}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_bot=${skip_bot}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_train=${skip_train}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_browser=${skip_browser}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_mcp=${skip_mcp}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_sandbox=${skip_sandbox}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_wrapper=${skip_wrapper}" >> "$GITHUB_OUTPUT"
|
||||
if [ "$skip_agents" = "true" ] && [ "$skip_code" = "true" ] && [ "$skip_bot" = "true" ] && [ "$skip_train" = "true" ] && [ "$skip_browser" = "true" ] && [ "$skip_mcp" = "true" ] && [ "$skip_sandbox" = "true" ] && [ "$skip_wrapper" = "true" ]; then
|
||||
echo "All target versions already on PyPI — nothing to publish."
|
||||
fi
|
||||
|
||||
- name: Exit if already published
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
steps.pypi_exists.outputs.skip_agents == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_code == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_bot == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_train == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_browser == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_mcp == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_sandbox == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_wrapper == 'true'
|
||||
run: echo "Idempotent skip — target versions already on PyPI."
|
||||
|
||||
- name: Dry run summary
|
||||
if: inputs.dry_run
|
||||
run: |
|
||||
echo "Dry run — no publish, commit, or push."
|
||||
echo "Agents version: ${{ steps.versions.outputs.agents_version }}"
|
||||
echo "Code version: ${{ steps.versions.outputs.code_version }}"
|
||||
echo "Bot version: ${{ steps.versions.outputs.bot_version }}"
|
||||
echo "Train version: ${{ steps.versions.outputs.train_version }}"
|
||||
echo "Browser version: ${{ steps.versions.outputs.browser_version }}"
|
||||
echo "MCP version: ${{ steps.versions.outputs.mcp_version }}"
|
||||
echo "Wrapper version: ${{ steps.versions.outputs.wrapper_version }}"
|
||||
echo ""
|
||||
if [ "${{ inputs.skip_agents }}" != "true" ]; then
|
||||
echo "Would publish praisonaiagents ${{ steps.versions.outputs.agents_version }}"
|
||||
fi
|
||||
if [ "${{ inputs.skip_code }}" != "true" ]; then
|
||||
echo "Would publish praisonai-code ${{ steps.versions.outputs.code_version }}"
|
||||
fi
|
||||
if [ "${{ inputs.skip_bot }}" != "true" ]; then
|
||||
echo "Would publish praisonai-bot ${{ steps.versions.outputs.bot_version }}"
|
||||
fi
|
||||
if [ "${{ inputs.skip_train }}" != "true" ]; then
|
||||
echo "Would publish praisonai-train ${{ steps.versions.outputs.train_version }}"
|
||||
fi
|
||||
if [ "${{ inputs.skip_browser }}" != "true" ]; then
|
||||
echo "Would publish praisonai-browser ${{ steps.versions.outputs.browser_version }}"
|
||||
fi
|
||||
if [ "${{ inputs.skip_mcp }}" != "true" ]; then
|
||||
echo "Would publish praisonai-mcp ${{ steps.versions.outputs.mcp_version }}"
|
||||
echo "Would commit src/praisonai-agents/pyproject.toml and uv.lock"
|
||||
fi
|
||||
if [ "${{ inputs.skip_wrapper }}" != "true" ]; then
|
||||
echo "Would run bump_and_release.py ${{ steps.versions.outputs.wrapper_version }} --agents ${{ steps.versions.outputs.agents_version }} --code-pin ${{ steps.versions.outputs.code_version }} --bot-pin ${{ steps.versions.outputs.bot_version }} --train-pin ${{ steps.versions.outputs.train_version }} --browser-pin ${{ steps.versions.outputs.browser_version }} --mcp-pin ${{ steps.versions.outputs.mcp_version }}"
|
||||
echo "Would run bump_and_release.py ${{ steps.versions.outputs.wrapper_version }} --agents ${{ steps.versions.outputs.agents_version }}"
|
||||
echo "Would uv publish praisonai ${{ steps.versions.outputs.wrapper_version }}"
|
||||
fi
|
||||
|
||||
- name: Publish praisonaiagents
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_agents != true &&
|
||||
steps.pypi_exists.outputs.skip_agents != 'true'
|
||||
if: inputs.dry_run != true && inputs.skip_agents != true
|
||||
working-directory: src/praisonai-agents
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.agents_version }}
|
||||
@@ -540,10 +219,7 @@ jobs:
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonaiagents on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_agents != true &&
|
||||
steps.pypi_exists.outputs.skip_agents != 'true'
|
||||
if: inputs.dry_run != true && inputs.skip_agents != true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonaiagents"
|
||||
@@ -567,10 +243,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Commit agents version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_agents != true &&
|
||||
steps.pypi_exists.outputs.skip_agents != 'true'
|
||||
if: inputs.dry_run != true && inputs.skip_agents != true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-agents/pyproject.toml src/praisonai-agents/uv.lock
|
||||
@@ -579,456 +252,21 @@ jobs:
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Publish praisonai-code
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_code != true &&
|
||||
steps.pypi_exists.outputs.skip_code != 'true'
|
||||
working-directory: src/praisonai-code
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.code_version }}
|
||||
CURRENT: ${{ steps.versions.outputs.current_code }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
new = os.environ["NEW_VERSION"]
|
||||
current = os.environ["CURRENT"]
|
||||
pyproject = Path("pyproject.toml")
|
||||
content = pyproject.read_text()
|
||||
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
|
||||
init_py = Path("praisonai_code/__init__.py")
|
||||
init_content = init_py.read_text()
|
||||
init_py.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new}"', init_content, count=1))
|
||||
PY
|
||||
|
||||
rm -rf dist
|
||||
uv lock
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonai-code on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_code != true &&
|
||||
steps.pypi_exists.outputs.skip_code != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai-code"
|
||||
VERSION="${{ steps.versions.outputs.code_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Commit praisonai-code version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_code != true &&
|
||||
steps.pypi_exists.outputs.skip_code != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-code/pyproject.toml src/praisonai-code/uv.lock src/praisonai-code/praisonai_code/__init__.py
|
||||
git diff --cached --quiet && { echo "No code files to commit"; exit 0; }
|
||||
git commit -m "Bump praisonai-code to ${{ steps.versions.outputs.code_version }}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Publish praisonai-bot
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_bot != true &&
|
||||
steps.pypi_exists.outputs.skip_bot != 'true'
|
||||
working-directory: src/praisonai-bot
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.bot_version }}
|
||||
CURRENT: ${{ steps.versions.outputs.current_bot }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
new = os.environ["NEW_VERSION"]
|
||||
current = os.environ["CURRENT"]
|
||||
pyproject = Path("pyproject.toml")
|
||||
content = pyproject.read_text()
|
||||
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
|
||||
version_py = Path("praisonai_bot/_version.py")
|
||||
version_content = version_py.read_text()
|
||||
version_py.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new}"', version_content, count=1))
|
||||
PY
|
||||
|
||||
rm -rf dist
|
||||
uv lock
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonai-bot on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_bot != true &&
|
||||
steps.pypi_exists.outputs.skip_bot != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai-bot"
|
||||
VERSION="${{ steps.versions.outputs.bot_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Commit praisonai-bot version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_bot != true &&
|
||||
steps.pypi_exists.outputs.skip_bot != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-bot/pyproject.toml src/praisonai-bot/uv.lock src/praisonai-bot/praisonai_bot/_version.py
|
||||
git diff --cached --quiet && { echo "No bot files to commit"; exit 0; }
|
||||
git commit -m "Bump praisonai-bot to ${{ steps.versions.outputs.bot_version }}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Publish praisonai-train
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_train != true &&
|
||||
steps.pypi_exists.outputs.skip_train != 'true'
|
||||
working-directory: src/praisonai-train
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.train_version }}
|
||||
CURRENT: ${{ steps.versions.outputs.current_train }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
new = os.environ["NEW_VERSION"]
|
||||
current = os.environ["CURRENT"]
|
||||
pyproject = Path("pyproject.toml")
|
||||
content = pyproject.read_text()
|
||||
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
|
||||
version_py = Path("praisonai_train/_version.py")
|
||||
version_content = version_py.read_text()
|
||||
version_py.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new}"', version_content, count=1))
|
||||
PY
|
||||
|
||||
rm -rf dist
|
||||
uv lock
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonai-train on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_train != true &&
|
||||
steps.pypi_exists.outputs.skip_train != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai-train"
|
||||
VERSION="${{ steps.versions.outputs.train_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Commit praisonai-train version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_train != true &&
|
||||
steps.pypi_exists.outputs.skip_train != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-train/pyproject.toml src/praisonai-train/uv.lock src/praisonai-train/praisonai_train/_version.py
|
||||
git diff --cached --quiet && { echo "No train files to commit"; exit 0; }
|
||||
git commit -m "Bump praisonai-train to ${{ steps.versions.outputs.train_version }}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Publish praisonai-browser
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_browser != true &&
|
||||
steps.pypi_exists.outputs.skip_browser != 'true'
|
||||
working-directory: src/praisonai-browser
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.browser_version }}
|
||||
CURRENT: ${{ steps.versions.outputs.current_browser }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
new = os.environ["NEW_VERSION"]
|
||||
current = os.environ["CURRENT"]
|
||||
pyproject = Path("pyproject.toml")
|
||||
content = pyproject.read_text()
|
||||
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
|
||||
version_py = Path("praisonai_browser/_version.py")
|
||||
version_content = version_py.read_text()
|
||||
version_py.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new}"', version_content, count=1))
|
||||
PY
|
||||
|
||||
rm -rf dist
|
||||
uv lock
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonai-browser on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_browser != true &&
|
||||
steps.pypi_exists.outputs.skip_browser != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai-browser"
|
||||
VERSION="${{ steps.versions.outputs.browser_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Commit praisonai-browser version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_browser != true &&
|
||||
steps.pypi_exists.outputs.skip_browser != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-browser/pyproject.toml src/praisonai-browser/uv.lock src/praisonai-browser/praisonai_browser/_version.py
|
||||
git diff --cached --quiet && { echo "No browser files to commit"; exit 0; }
|
||||
git commit -m "Bump praisonai-browser to ${{ steps.versions.outputs.browser_version }}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Publish praisonai-mcp
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_mcp != true &&
|
||||
steps.pypi_exists.outputs.skip_mcp != 'true'
|
||||
working-directory: src/praisonai-mcp
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.mcp_version }}
|
||||
CURRENT: ${{ steps.versions.outputs.current_mcp }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
new = os.environ["NEW_VERSION"]
|
||||
current = os.environ["CURRENT"]
|
||||
pyproject = Path("pyproject.toml")
|
||||
content = pyproject.read_text()
|
||||
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
|
||||
version_py = Path("praisonai_mcp/_version.py")
|
||||
version_content = version_py.read_text()
|
||||
version_py.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new}"', version_content, count=1))
|
||||
PY
|
||||
|
||||
rm -rf dist
|
||||
uv lock
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonai-mcp on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_mcp != true &&
|
||||
steps.pypi_exists.outputs.skip_mcp != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai-mcp"
|
||||
VERSION="${{ steps.versions.outputs.mcp_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Commit praisonai-mcp version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_mcp != true &&
|
||||
steps.pypi_exists.outputs.skip_mcp != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-mcp/pyproject.toml src/praisonai-mcp/uv.lock src/praisonai-mcp/praisonai_mcp/_version.py
|
||||
git diff --cached --quiet && { echo "No mcp files to commit"; exit 0; }
|
||||
git commit -m "Bump praisonai-mcp to ${{ steps.versions.outputs.mcp_version }}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Publish praisonai-sandbox
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_sandbox != true &&
|
||||
steps.pypi_exists.outputs.skip_sandbox != 'true'
|
||||
working-directory: src/praisonai-sandbox
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.sandbox_version }}
|
||||
CURRENT: ${{ steps.versions.outputs.current_sandbox }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
new = os.environ["NEW_VERSION"]
|
||||
current = os.environ["CURRENT"]
|
||||
pyproject = Path("pyproject.toml")
|
||||
content = pyproject.read_text()
|
||||
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
|
||||
version_py = Path("praisonai_sandbox/_version.py")
|
||||
version_content = version_py.read_text()
|
||||
version_py.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new}"', version_content, count=1))
|
||||
PY
|
||||
|
||||
rm -rf dist
|
||||
uv lock
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonai-sandbox on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_sandbox != true &&
|
||||
steps.pypi_exists.outputs.skip_sandbox != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai-sandbox"
|
||||
VERSION="${{ steps.versions.outputs.sandbox_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Commit praisonai-sandbox version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_sandbox != true &&
|
||||
steps.pypi_exists.outputs.skip_sandbox != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-sandbox/pyproject.toml src/praisonai-sandbox/uv.lock src/praisonai-sandbox/praisonai_sandbox/_version.py
|
||||
git diff --cached --quiet && { echo "No sandbox files to commit"; exit 0; }
|
||||
git commit -m "Bump praisonai-sandbox to ${{ steps.versions.outputs.sandbox_version }}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Bump and release wrapper
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_wrapper != true &&
|
||||
steps.pypi_exists.outputs.skip_wrapper != 'true'
|
||||
if: inputs.dry_run != true && inputs.skip_wrapper != true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python src/praisonai/scripts/bump_and_release.py "${{ steps.versions.outputs.wrapper_version }}" \
|
||||
--agents "${{ steps.versions.outputs.agents_version }}" \
|
||||
--code-pin "${{ steps.versions.outputs.code_version }}" \
|
||||
--bot-pin "${{ steps.versions.outputs.bot_version }}" \
|
||||
--train-pin "${{ steps.versions.outputs.train_version }}" \
|
||||
--browser-pin "${{ steps.versions.outputs.browser_version }}" \
|
||||
--mcp-pin "${{ steps.versions.outputs.mcp_version }}" \
|
||||
--sandbox-pin "${{ steps.versions.outputs.sandbox_version }}" \
|
||||
--wait --max-wait 600 --no-add-all
|
||||
|
||||
- name: Publish praisonai wrapper to PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_wrapper != true &&
|
||||
steps.pypi_exists.outputs.skip_wrapper != 'true'
|
||||
if: inputs.dry_run != true && inputs.skip_wrapper != true
|
||||
working-directory: src/praisonai
|
||||
run: uv publish
|
||||
|
||||
- name: Wait for praisonai on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_wrapper != true &&
|
||||
steps.pypi_exists.outputs.skip_wrapper != 'true'
|
||||
if: inputs.dry_run != true && inputs.skip_wrapper != true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai"
|
||||
@@ -1056,42 +294,12 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AGENTS="${{ steps.versions.outputs.agents_version }}"
|
||||
CODE="${{ steps.versions.outputs.code_version }}"
|
||||
BOT="${{ steps.versions.outputs.bot_version }}"
|
||||
TRAIN="${{ steps.versions.outputs.train_version }}"
|
||||
BROWSER="${{ steps.versions.outputs.browser_version }}"
|
||||
MCP="${{ steps.versions.outputs.mcp_version }}"
|
||||
SANDBOX="${{ steps.versions.outputs.sandbox_version }}"
|
||||
WRAPPER="${{ steps.versions.outputs.wrapper_version }}"
|
||||
|
||||
if [ "${{ inputs.skip_agents }}" != "true" ]; then
|
||||
echo "✅ praisonaiagents==${AGENTS}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_code }}" != "true" ]; then
|
||||
echo "✅ praisonai-code==${CODE}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_bot }}" != "true" ]; then
|
||||
echo "✅ praisonai-bot==${BOT}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_train }}" != "true" ]; then
|
||||
echo "✅ praisonai-train==${TRAIN}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_browser }}" != "true" ]; then
|
||||
echo "✅ praisonai-browser==${BROWSER}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_mcp }}" != "true" ]; then
|
||||
echo "✅ praisonai-mcp==${MCP}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_sandbox }}" != "true" ]; then
|
||||
echo "✅ praisonai-sandbox==${SANDBOX}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_wrapper }}" != "true" ]; then
|
||||
echo "✅ praisonai==${WRAPPER}"
|
||||
fi
|
||||
@@ -30,12 +30,10 @@ jobs:
|
||||
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: |
|
||||
cd src/praisonai
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
python -m pip install duckduckgo_search pytest pytest-asyncio pytest-cov
|
||||
python -m pip install "praisonaiagents[knowledge]"
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
name: Sync Claude Secrets
|
||||
name: Sync Claude Secrets to PraisonAIUI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_repo:
|
||||
description: 'Destination repo (owner/name)'
|
||||
required: true
|
||||
default: 'MervinPraison/PraisonAIUI'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -16,10 +10,10 @@ jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync Claude secrets
|
||||
- name: Sync secrets to PraisonAIUI
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
|
||||
TARGET: ${{ inputs.target_repo }}
|
||||
TARGET: MervinPraison/PraisonAIUI
|
||||
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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -53,13 +53,12 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov pytest-benchmark pytest-timeout
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
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: |
|
||||
|
||||
+52
-268
@@ -7,284 +7,68 @@ on:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-core.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- '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: subdirs
|
||||
paths: >-
|
||||
tests/unit/scheduler/
|
||||
tests/unit/integrations/
|
||||
tests/unit/deploy/
|
||||
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-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
|
||||
|
||||
test-core:
|
||||
name: test-core
|
||||
needs: [test-core-collect, test-core-unit, test-core-integration]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Report Core Tests result
|
||||
shell: bash
|
||||
- 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: |
|
||||
set -euo pipefail
|
||||
collect='${{ needs.test-core-collect.result }}'
|
||||
unit='${{ needs.test-core-unit.result }}'
|
||||
integration='${{ needs.test-core-integration.result }}'
|
||||
echo "collect=$collect unit=$unit integration=$integration"
|
||||
if [ "$collect" != "success" ] || [ "$unit" != "success" ] || [ "$integration" != "success" ]; then
|
||||
echo "Core Tests failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "Core Tests passed"
|
||||
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 ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
- 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: Run Unit Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings \
|
||||
--cov=praisonai --cov-report=xml --cov-branch \
|
||||
--timeout=60 || echo "Some unit tests failed"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings \
|
||||
--timeout=60 \
|
||||
--ignore=tests/integration/test_tracker_complex_tasks.py \
|
||||
--ignore=tests/integration/test_serve_integration.py \
|
||||
|| echo "Some integration tests failed"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN || '' }}
|
||||
files: src/praisonai/coverage.xml
|
||||
flags: core-tests
|
||||
fail_ci_if_error: false
|
||||
@@ -29,12 +29,11 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
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: |
|
||||
@@ -96,12 +95,11 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system pytest pytest-benchmark
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
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: |
|
||||
|
||||
@@ -49,12 +49,10 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
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: |
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
# ============================================
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 12
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -64,216 +64,28 @@ jobs:
|
||||
- name: Install minimal dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
# Install praisonai-agents with all deps (required for tests)
|
||||
cd src/praisonai-agents
|
||||
pip install -e ".[all]"
|
||||
cd ../praisonai
|
||||
pip install -e ".[chat,code]"
|
||||
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 \
|
||||
-q --tb=line --timeout=30 --maxfail=5
|
||||
# Run only CLI tests for smoke - fast and critical path
|
||||
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: 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: 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
|
||||
python scripts/audit_hybrid_modules.py
|
||||
--maxfail=5 \
|
||||
|| echo "Some smoke tests failed"
|
||||
|
||||
- name: Smoke Test Summary
|
||||
if: always()
|
||||
@@ -286,7 +98,7 @@ jobs:
|
||||
main:
|
||||
needs: smoke
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.11']
|
||||
@@ -312,37 +124,36 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
# Install praisonai-agents with all deps (required for tests)
|
||||
cd src/praisonai-agents
|
||||
pip install -e ".[all]"
|
||||
cd ../praisonai
|
||||
pip install -e ".[crewai,autogen]"
|
||||
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" \
|
||||
python -m pytest tests/unit/ tests/integration/ \
|
||||
-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" \
|
||||
-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 \
|
||||
--ignore=tests/unit/api \
|
||||
--cov=praisonai \
|
||||
--cov-report=xml \
|
||||
-n 2 --dist loadfile \
|
||||
--maxfail=10
|
||||
-n 4 \
|
||||
--maxfail=10 \
|
||||
|| echo "Some tests failed"
|
||||
|
||||
- name: Upload coverage
|
||||
if: matrix.python-version == '3.11'
|
||||
@@ -399,12 +210,11 @@ jobs:
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e ".[all]"
|
||||
cd ../praisonai
|
||||
pip install -e ".[openai]"
|
||||
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'
|
||||
@@ -463,12 +273,11 @@ jobs:
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e ".[all]"
|
||||
cd ../praisonai
|
||||
pip install -e ".[anthropic]"
|
||||
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'
|
||||
@@ -523,12 +332,11 @@ jobs:
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e ".[all]"
|
||||
cd ../praisonai
|
||||
pip install -e ".[google]"
|
||||
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'
|
||||
@@ -583,11 +391,11 @@ jobs:
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e ".[all]"
|
||||
cd ../praisonai
|
||||
pip install -e .
|
||||
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'
|
||||
@@ -642,11 +450,11 @@ jobs:
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e ".[all]"
|
||||
cd ../praisonai
|
||||
pip install -e .
|
||||
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'
|
||||
@@ -701,12 +509,11 @@ jobs:
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e ".[all]"
|
||||
cd ../praisonai
|
||||
pip install -e ".[cohere]"
|
||||
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'
|
||||
@@ -728,76 +535,6 @@ jobs:
|
||||
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/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
|
||||
# ============================================
|
||||
|
||||
@@ -61,12 +61,11 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
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: |
|
||||
|
||||
@@ -7,20 +7,18 @@ on:
|
||||
- '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
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_MODEL_NAME: gpt-4o-mini
|
||||
@@ -35,10 +33,18 @@ jobs:
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install monorepo packages
|
||||
run: python -m pip install --upgrade pip
|
||||
- name: Install praisonaiagents
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e .
|
||||
shell: bash
|
||||
|
||||
- name: Install praisonai wrapper
|
||||
run: |
|
||||
cd src/praisonai
|
||||
pip install -e .
|
||||
shell: bash
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
|
||||
# ==================== CORE IMPORTS ====================
|
||||
- name: Test core imports
|
||||
@@ -176,7 +182,6 @@ jobs:
|
||||
echo "Testing both entry point methods..."
|
||||
python -m praisonai --version
|
||||
praisonai --version
|
||||
praisonai-bot gateway --help
|
||||
|
||||
- name: Test doctor packaging command
|
||||
run: |
|
||||
|
||||
-11
@@ -22,22 +22,11 @@ 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/
|
||||
|
||||
.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
|
||||
|
||||
|
||||
@@ -7,9 +7,4 @@ You are working on the PraisonAI project.
|
||||
- Be concise and helpful in responses
|
||||
- Test implementation thoroughly
|
||||
- Ensure backward compatibility with existing APIs
|
||||
- Follow protocol-driven design across the eight 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/`, 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)
|
||||
- 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.
|
||||
- Follow protocol-driven design: core protocols in praisonaiagents/, heavy implementations in praisonai/
|
||||
+35
-172
@@ -1,27 +1,24 @@
|
||||
# PraisonAI Architecture
|
||||
|
||||
> **Last updated:** 2026-07-17 (C13 — eight-package tiered model)
|
||||
> **Last updated:** 2026-06-24
|
||||
>
|
||||
> 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.
|
||||
> Covers system design, runtime architecture, data contracts, reliability,
|
||||
> observability, and the 2-quarter road map.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#1-executive-summary)
|
||||
2. [Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13)](#2-python-tiered-package-model-c71--c9--c10--c11--c12--c13)
|
||||
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
|
||||
2. [System Overview](#2-system-overview)
|
||||
3. [Layered Architecture](#3-layered-architecture)
|
||||
4. [Core Data Contracts](#4-core-data-contracts)
|
||||
5. [Reliability by Design](#5-reliability-by-design)
|
||||
6. [Observability & Telemetry](#6-observability--telemetry)
|
||||
7. [Replay & Checkpointing](#7-replay--checkpointing)
|
||||
8. [Implementation Roadmap](#8-implementation-roadmap)
|
||||
9. [Success Metrics](#9-success-metrics)
|
||||
|
||||
---
|
||||
|
||||
@@ -51,124 +48,7 @@ Orchestration + Observability** core to unlock adoption and trust:
|
||||
|
||||
---
|
||||
|
||||
## 2. Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13)
|
||||
|
||||
**Release:** v4.6.110+ · `praisonaiagents` · `praisonai-code` · `praisonai-bot` · `praisonai-train` · `praisonai-browser` · `praisonai-mcp` · `praisonai-sandbox` · `praisonai`
|
||||
|
||||
The Python monorepo publishes eight 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`.
|
||||
|
||||
```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]
|
||||
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"]
|
||||
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 --> 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 _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`
|
||||
|
||||
**Backward compatibility:** `praisonai.bots`, `praisonai.gateway`, `praisonai.train`,
|
||||
`praisonai.browser`, `praisonai.mcp_server`, `praisonai.sandbox`, and related CLI paths remain as
|
||||
`alias_package` shims to `praisonai_bot.*` / `praisonai_train.*` /
|
||||
`praisonai_browser.*` / `praisonai_mcp.*` / `praisonai_sandbox.*`.
|
||||
|
||||
| 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` |
|
||||
| 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`) |
|
||||
| 3 | `src/praisonai/` | `framework_adapters/`, serve, dashboard, async jobs API | — |
|
||||
|
||||
**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
|
||||
## 2. System Overview
|
||||
|
||||
```
|
||||
User/SDK/CLI
|
||||
@@ -195,42 +75,33 @@ User/SDK/CLI
|
||||
|
||||
### Existing Architecture (Python SDK)
|
||||
|
||||
The Python SDK is organised as three publishable tiers (see [§2](#2-python-three-tier-package-model-c71)):
|
||||
The Python SDK is the primary runtime and is organised as follows:
|
||||
|
||||
- `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`, …)
|
||||
- `src/praisonai/` — Core runtime and agent abstractions
|
||||
- `agents/` — Agent types (chat, code, realtime, audio, vision, etc.)
|
||||
- `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)
|
||||
- `workflow/` — Workflow engine (YAML/SDK-based orchestration)
|
||||
- `telemetry/` — Observability, monitoring, token tracking
|
||||
- `reliability/` — Error classification, circuit breaker, failover
|
||||
- `skill/` — Plugin/skill system
|
||||
- `loop_detection/` — Doom loop detection and prevention
|
||||
- `mcp/` — MCP protocol support
|
||||
- `a2a/`, `a2ui/` — Agent-to-agent and agent-to-UI protocols
|
||||
|
||||
### Multi-SDK Layout
|
||||
|
||||
- **Python Core SDK** — `src/praisonai-agents/` (`praisonaiagents`)
|
||||
- **Python Terminal CLI** — `src/praisonai-code/` (`praisonai-code`)
|
||||
- **Python Wrapper** — `src/praisonai/` (`praisonai`)
|
||||
- **Python SDK** — Primary runtime (`src/praisonai/`)
|
||||
- **TypeScript SDK** — JS/TS runtime (`ts-sdk/`)
|
||||
- **Rust SDK** — High-performance Rust runtime (`rust-sdk/`)
|
||||
- **CLI** — Command-line interface (`cli/`)
|
||||
- **UI** — Web UI (`ui/`)
|
||||
|
||||
---
|
||||
|
||||
## 4. Layered Architecture
|
||||
## 3. Layered Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
@@ -326,7 +197,7 @@ flowchart TB
|
||||
|
||||
---
|
||||
|
||||
## 5. Core Data Contracts
|
||||
## 4. Core Data Contracts
|
||||
|
||||
### RunRequest
|
||||
|
||||
@@ -384,7 +255,7 @@ Checkpoint {
|
||||
|
||||
---
|
||||
|
||||
## 6. Reliability by Design
|
||||
## 5. Reliability by Design
|
||||
|
||||
### Error Taxonomy
|
||||
|
||||
@@ -437,7 +308,7 @@ def test_agent_workflow():
|
||||
|
||||
---
|
||||
|
||||
## 7. Observability & Telemetry
|
||||
## 6. Observability & Telemetry
|
||||
|
||||
### Current Telemetry Stack
|
||||
|
||||
@@ -464,7 +335,7 @@ PraisonAI already includes:
|
||||
|
||||
---
|
||||
|
||||
## 8. Replay & Checkpointing
|
||||
## 7. Replay & Checkpointing
|
||||
|
||||
### Design
|
||||
|
||||
@@ -505,15 +376,7 @@ sequenceDiagram
|
||||
|
||||
---
|
||||
|
||||
## 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** |
|
||||
## 8. Implementation Roadmap
|
||||
|
||||
### Quarter 1: Trust Foundation (Q3 2026)
|
||||
|
||||
@@ -551,7 +414,7 @@ gantt
|
||||
|
||||
---
|
||||
|
||||
## 10. Success Metrics
|
||||
## 9. Success Metrics
|
||||
|
||||
### KPI Targets
|
||||
|
||||
|
||||
@@ -568,7 +568,7 @@ praisonai agents.yaml
|
||||
| Query Rewriter Agent | [Example](examples/python/agents/query-rewriter-agent.py) | [📖](https://docs.praison.ai/docs/agents/query-rewriter) |
|
||||
| Native Web Search | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/docs/agents/websearch) |
|
||||
| Built-in Search Tools | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/docs/tools/tavily) |
|
||||
| Unified Web Search | [Example](examples/python/web_search_example.py) | [📖](https://docs.praison.ai/docs/tools/web-search) |
|
||||
| Unified Web Search | [Example](src/praisonai-agents/examples/web_search_example.py) | [📖](https://docs.praison.ai/docs/tools/web-search) |
|
||||
| Web Fetch (Anthropic) | [Example](examples/python/agents/web-fetch-agent.py) | [📖](https://docs.praison.ai/docs/features/model-capabilities) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -18,7 +18,6 @@ Methods:
|
||||
* <code title="class HandoffResult">HandoffResult.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_outcome</a>(outcome: AgentRunOutcome, target_agent: Optional[str] = None, source_agent: Optional[str] = None, handoff_depth: int = 0) -> 'HandoffResult'</code>
|
||||
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">json</a>() -> Optional[str]</code>
|
||||
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">to_dict</a>() -> dict</code>
|
||||
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">is_multimodal</a>() -> bool</code>
|
||||
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
|
||||
# Agents
|
||||
@@ -61,7 +60,6 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to_async</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">is_closed</a>() -> bool</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">last_stop_reason</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">list_mcp_servers</a>() -> List[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_instance</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_instance</a>(value)</code>
|
||||
@@ -81,7 +79,7 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous_async</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str = '', threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False, goal: Optional[str] = None, goal_criteria: Optional[Any] = None, judge_model: Optional[str] = None) -> 'EvaluationLoopResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until_async</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', action: str = 'add', **kwargs: Any) -> None</code>
|
||||
@@ -192,7 +190,7 @@ Methods:
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_single_file_plugins</a>() -> int</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_all</a>() -> Dict[str, Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool_definitions</a>(permission_resolver: Optional[Callable[[str], bool]] = None) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool_definitions</a>() -> List[Dict[str, Any]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_trust_level</a>(name: str) -> Optional[str]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_available_tools</a>(context: Optional[Dict[str, Any]] = None, ttl_seconds: Optional[float] = None) -> List[Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_base_tools</a>() -> List[BaseTool]</code>
|
||||
@@ -232,6 +230,13 @@ Methods:
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">repeat</a>(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None, max_iterations: int = 10) -> Repeat</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">route</a>(routes: Dict[str, List], default: Optional[List] = None) -> Route</code>
|
||||
|
||||
# DB
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import db
|
||||
```
|
||||
|
||||
# Memory
|
||||
|
||||
Types:
|
||||
@@ -477,27 +482,20 @@ Methods:
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_scripts</a>(skill: LoadedSkill) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate</a>(skill: LoadedSkill) -> bool</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate_by_name</a>(name: str) -> bool</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_bundle</a>(manifest: 'BundleManifest') -> None</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill</a>(skill_path: str) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill_by_name</a>(name: str, skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">approve</a>(identifier: str) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">archive_skill</a>(name: str) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">bundle_names</a>() -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">bundles</a>() -> List['BundleManifest']</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">clear</a>() -> None</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">create</a>(name: str, content: str, category: Optional[str] = None, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">create_skill</a>(name: str, content: str, category: str = None, agent_created: bool = True, propose: Optional[bool] = None, base_dir: Optional[str] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">create_skill</a>(name: str, content: str, category: str = None, agent_created: bool = True, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">delete</a>(name: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">delete_skill</a>(name: str, hard: bool = False, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">discover</a>(skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> int</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">discover_bundles</a>(skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> int</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">edit</a>(name: str, content: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">edit_skill</a>(name: str, content: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_allowed_tools</a>(name: str) -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_available_skills</a>() -> List[SkillMetadata]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_available_skills_by_state</a>(state: SkillState) -> List[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_bundle</a>(name: str) -> Optional['BundleManifest']</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_bundle_instructions</a>() -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_instructions</a>(name: str) -> Optional[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skill</a>(name: str) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skills_diagnostics</a>() -> Dict[str, ValidationResult]</code>
|
||||
@@ -511,7 +509,6 @@ Methods:
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">reject</a>(identifier: str) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">remove_file</a>(name: str, file_path: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">remove_skill_file</a>(name: str, file_path: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">resolve</a>(selectors: List[str]) -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">restore_skill</a>(name: str, skill_dir: Optional[str] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">rollback_skill</a>(name: str) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skill_names</a>() -> List[str]</code>
|
||||
@@ -564,13 +561,9 @@ from praisonaiagents import MCP
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">apply_tool_filters</a>(allowed_tools: Optional[List[str]] = None, disabled_tools: Optional[List[str]] = None) -> 'MCP'</code>
|
||||
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_prompts</a>() -> List[dict]</code>
|
||||
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_resources</a>() -> List[dict]</code>
|
||||
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_tools</a>() -> List[Callable]</code>
|
||||
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">shutdown</a>()</code>
|
||||
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">to_openai_tool</a>()</code>
|
||||
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">with_tool_prefix</a>(prefix: str) -> 'MCP'</code>
|
||||
|
||||
# Telemetry
|
||||
|
||||
@@ -662,8 +655,6 @@ Methods:
|
||||
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/autonomy.py">from_dict</a>(data: Dict[str, Any]) -> 'AutonomyConfig'</code>
|
||||
* <code title="class CachingConfig">CachingConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">from_dict</a>(data: Dict[str, Any]) -> 'ExecutionConfig'</code>
|
||||
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">resolved_max_steps</a>() -> int</code>
|
||||
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">resolved_max_tool_calls</a>() -> int</code>
|
||||
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class HooksConfig">HooksConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
@@ -813,7 +804,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, CloudProvider, DB, Deploy, DeployConfig, DeployType, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, __version__, arun, run
|
||||
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, CloudProvider, DB, Deploy, DeployConfig, DeployType, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, PraisonAI, __version__, arun, run, run_integrated_gateway
|
||||
```
|
||||
|
||||
# CLI
|
||||
@@ -821,14 +812,81 @@ from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, CloudProv
|
||||
Methods:
|
||||
|
||||
* <code title="cli">praisonai <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai acp acp-main <a href="./src/praisonai/praisonai/cli/commands/acp.py">--help</a></code>
|
||||
* <code title="cli">praisonai agent list <a href="./src/praisonai/praisonai/cli/commands/agent.py">--help</a></code>
|
||||
* <code title="cli">praisonai agent show <a href="./src/praisonai/praisonai/cli/commands/agent.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents create <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents info <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents list <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai app app <a href="./src/praisonai/praisonai/cli/commands/app.py">--help</a></code>
|
||||
* <code title="cli">praisonai attach attach-main <a href="./src/praisonai/praisonai/cli/commands/attach.py">--help</a></code>
|
||||
* <code title="cli">praisonai audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.py">--help</a></code>
|
||||
* <code title="cli">praisonai auth list <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
|
||||
* <code title="cli">praisonai auth login <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
|
||||
* <code title="cli">praisonai auth logout <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
|
||||
* <code title="cli">praisonai auth status <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch batch-run <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch list <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch report <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch stats <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark agent <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark benchmark-callback <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark cli <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark compare <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark litellm <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark profile <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark sdk <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark workflow <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot agentmail <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot bot-callback <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot discord <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot email <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot install-daemon <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot linear <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot list <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot purge <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot replay <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot slack <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot start <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot telegram <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai bot whatsapp <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser browser-callback <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser click <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser navigate <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser open <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser profiles <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser screenshot <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser snapshot <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser status <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai browser type <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
|
||||
* <code title="cli">praisonai call call-main <a href="./src/praisonai/praisonai/cli/commands/call.py">--help</a></code>
|
||||
* <code title="cli">praisonai chat <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai chat chat-main <a href="./src/praisonai/praisonai/cli/commands/chat.py">--help</a></code>
|
||||
* <code title="cli">praisonai checkpoint delete <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
|
||||
* <code title="cli">praisonai checkpoint diff <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
|
||||
* <code title="cli">praisonai checkpoint list <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
|
||||
* <code title="cli">praisonai checkpoint restore <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
|
||||
* <code title="cli">praisonai checkpoint save <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
|
||||
* <code title="cli">praisonai claw claw <a href="./src/praisonai/praisonai/cli/commands/claw.py">--help</a></code>
|
||||
* <code title="cli">praisonai code <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai code code-main <a href="./src/praisonai/praisonai/cli/commands/code.py">--help</a></code>
|
||||
* <code title="cli">praisonai command list <a href="./src/praisonai/praisonai/cli/commands/command.py">--help</a></code>
|
||||
* <code title="cli">praisonai command show <a href="./src/praisonai/praisonai/cli/commands/command.py">--help</a></code>
|
||||
* <code title="cli">praisonai commit commit-main <a href="./src/praisonai/praisonai/cli/commands/commit.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion bash <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion completion-callback <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion fish <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion zsh <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai config doctor <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config env <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config get <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config list <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config path <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config reset <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config set <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config show <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config sources <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config validate <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai context add <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context clear <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context compact <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
@@ -838,7 +896,22 @@ Methods:
|
||||
* <code title="cli">praisonai context show <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context stats <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context tail <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai daemon daemon-main <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
|
||||
* <code title="cli">praisonai daemon start <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
|
||||
* <code title="cli">praisonai daemon status <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
|
||||
* <code title="cli">praisonai daemon stop <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
|
||||
* <code title="cli">praisonai dashboard unified <a href="./src/praisonai/praisonai/cli/commands/dashboard.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug acp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug debug-callback <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug interactive <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug lsp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug trace <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy aws <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy azure <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy docker <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy gcp <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai diag diag-callback <a href="./src/praisonai/praisonai/cli/commands/diag.py">--help</a></code>
|
||||
* <code title="cli">praisonai diag export <a href="./src/praisonai/praisonai/cli/commands/diag.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs api-md <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs generate <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs list <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
@@ -847,6 +920,29 @@ Methods:
|
||||
* <code title="cli">praisonai docs run-all <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs serve <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs stats <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor cleanup <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor docker <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor env <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor llm-providers <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor mcp <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor memory-store <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor metadata-store <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor network <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor packaging <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor performance <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor runtime <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor selftest <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor skills <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor troubleshoot <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai endpoints list <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
|
||||
* <code title="cli">praisonai endpoints test <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
|
||||
* <code title="cli">praisonai environment check <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
|
||||
* <code title="cli">praisonai environment doctor <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
|
||||
* <code title="cli">praisonai environment view <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
|
||||
* <code title="cli">praisonai eval accuracy <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
|
||||
* <code title="cli">praisonai eval judge <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
|
||||
* <code title="cli">praisonai eval list-judges <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
|
||||
* <code title="cli">praisonai eval performance <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples find <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples info <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples list <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
@@ -859,6 +955,40 @@ Methods:
|
||||
* <code title="cli">praisonai flow import <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
|
||||
* <code title="cli">praisonai flow list <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
|
||||
* <code title="cli">praisonai flow version <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway channels <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway gateway-callback <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway install <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway logs <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway mint-link <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway pause <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway reconnect <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway resume <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway stop <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway uninstall <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai github triage <a href="./src/praisonai/praisonai/cli/commands/github.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks list <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks remove <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai identity import <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
|
||||
* <code title="cli">praisonai identity link <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
|
||||
* <code title="cli">praisonai identity list <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
|
||||
* <code title="cli">praisonai identity unlink <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
|
||||
* <code title="cli">praisonai init init <a href="./src/praisonai/praisonai/cli/commands/init.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban block <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban boards <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban comment <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban complete <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban create <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban dispatch <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban link <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban list <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban move <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban reclaim <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban show <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai kanban unblock <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge add <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge index <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge list <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
@@ -876,6 +1006,13 @@ Methods:
|
||||
* <code title="cli">praisonai langfuse test <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse traces <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse version <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai loop help <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
|
||||
* <code title="cli">praisonai loop loop-main <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp logs <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp lsp-callback <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp start <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp status <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp stop <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed delete <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed get <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed list <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
@@ -887,6 +1024,26 @@ Methods:
|
||||
* <code title="cli">praisonai managed save <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed show <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed update <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp add <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp auth <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp describe <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp list <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp logout <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp mcp-callback <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp remove <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp run <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp status <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp sync <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp test <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp tools <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory add <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory clear <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory search <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory show <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory status <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai models describe <a href="./src/praisonai/praisonai/cli/commands/models.py">--help</a></code>
|
||||
* <code title="cli">praisonai models list <a href="./src/praisonai/praisonai/cli/commands/models.py">--help</a></code>
|
||||
* <code title="cli">praisonai models validate <a href="./src/praisonai/praisonai/cli/commands/models.py">--help</a></code>
|
||||
* <code title="cli">praisonai n8n export <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
|
||||
* <code title="cli">praisonai n8n import <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
|
||||
* <code title="cli">praisonai n8n list <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
|
||||
@@ -894,6 +1051,38 @@ Methods:
|
||||
* <code title="cli">praisonai n8n pull <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
|
||||
* <code title="cli">praisonai n8n push <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
|
||||
* <code title="cli">praisonai n8n test <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
|
||||
* <code title="cli">praisonai obs obs-fallback <a href="./src/praisonai/praisonai/cli/commands/obs.py">--help</a></code>
|
||||
* <code title="cli">praisonai onboard onboard-callback <a href="./src/praisonai/praisonai/cli/commands/onboard.py">--help</a></code>
|
||||
* <code title="cli">praisonai package install <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai package list <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai package uninstall <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai pairing approve <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
|
||||
* <code title="cli">praisonai pairing clear <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
|
||||
* <code title="cli">praisonai pairing list <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
|
||||
* <code title="cli">praisonai pairing revoke <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
|
||||
* <code title="cli">praisonai paths show <a href="./src/praisonai/praisonai/cli/commands/paths.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions allow <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions ask <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions deny <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions export <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions import-rules <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions list <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions remove <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions reset <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins create <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins disable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins discover <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins doctor <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins enable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins info <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins install <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins list <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins plugins-callback <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins remove <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai port check <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
|
||||
* <code title="cli">praisonai port kill <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
|
||||
* <code title="cli">praisonai port list <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
|
||||
* <code title="cli">praisonai port port-callback <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile imports <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile optimize <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile profile-callback <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
@@ -901,6 +1090,8 @@ Methods:
|
||||
* <code title="cli">praisonai profile snapshot <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile startup <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile suite <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai publish publish-main <a href="./src/praisonai/praisonai/cli/commands/publish.py">--help</a></code>
|
||||
* <code title="cli">praisonai publish pypi <a href="./src/praisonai/praisonai/cli/commands/publish.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag chat <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag eval <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag index <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
@@ -916,6 +1107,8 @@ Methods:
|
||||
* <code title="cli">praisonai recipe optimize <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe run <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe serve <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai registry list <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
|
||||
* <code title="cli">praisonai registry serve <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
|
||||
* <code title="cli">praisonai replay cleanup <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
|
||||
* <code title="cli">praisonai replay context <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
|
||||
* <code title="cli">praisonai replay dashboard <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
|
||||
@@ -923,7 +1116,20 @@ Methods:
|
||||
* <code title="cli">praisonai replay flow <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
|
||||
* <code title="cli">praisonai replay list <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
|
||||
* <code title="cli">praisonai replay show <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
|
||||
* <code title="cli">praisonai research research-main <a href="./src/praisonai/praisonai/cli/commands/research.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval index <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval query <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval search <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules add <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules clear <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules list <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai run <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai run run-main <a href="./src/praisonai/praisonai/cli/commands/run.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox explain <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox list <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox recreate <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox sandbox-callback <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox status <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule add <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule blueprint <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule blueprint-list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
@@ -940,13 +1146,92 @@ Methods:
|
||||
* <code title="cli">praisonai schedule suggestion-dismiss <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule suggestion-propose <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule suggestions <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve a2a <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve a2u <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve acp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve agents <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve docs <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve gateway <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve lsp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve mcp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve openai <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve rag <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve recipe <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve registry <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve scheduler <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve serve-callback <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve start <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve status <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve stop <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve ui <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve ui-gateway <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve unified <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai session delete <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session export <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session import <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session list <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session resume <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session show <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai setup config <a href="./src/praisonai/praisonai/cli/commands/setup.py">--help</a></code>
|
||||
* <code title="cli">praisonai setup reset <a href="./src/praisonai/praisonai/cli/commands/setup.py">--help</a></code>
|
||||
* <code title="cli">praisonai setup setup-callback <a href="./src/praisonai/praisonai/cli/commands/setup.py">--help</a></code>
|
||||
* <code title="cli">praisonai setup wizard <a href="./src/praisonai/praisonai/cli/commands/setup.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills check <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills create <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills eligible <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills info <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills install <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills learn <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills list <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills search <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills validate <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise check <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise fix <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise init <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise report <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai templates create <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
|
||||
* <code title="cli">praisonai templates list <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
|
||||
* <code title="cli">praisonai test info <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai test interactive <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai test run <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo add <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo done <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo list <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools info <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools list <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools test <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools validate <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces disable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces enable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces list <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces status <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai tracker batch <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
|
||||
* <code title="cli">praisonai tracker judge <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
|
||||
* <code title="cli">praisonai tracker run <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
|
||||
* <code title="cli">praisonai tracker tools <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
|
||||
* <code title="cli">praisonai tracker tracker-main <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
|
||||
* <code title="cli">praisonai train agents <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai train apply <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai train list <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai train llm <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai train show <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai train train-callback <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui agents <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui bot <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui realtime <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui ui <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai up logs <a href="./src/praisonai/praisonai/cli/commands/up.py">--help</a></code>
|
||||
* <code title="cli">praisonai up status <a href="./src/praisonai/praisonai/cli/commands/up.py">--help</a></code>
|
||||
* <code title="cli">praisonai up up-start <a href="./src/praisonai/praisonai/cli/commands/up.py">--help</a></code>
|
||||
* <code title="cli">praisonai validate check <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
|
||||
* <code title="cli">praisonai validate schema <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
|
||||
* <code title="cli">praisonai validate validate <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
|
||||
* <code title="cli">praisonai version check <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai version show <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai version version-callback <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow create <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow list <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow run <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
|
||||
# TypeScript
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.155" \
|
||||
"praisonai>=4.6.80" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.155" \
|
||||
"praisonai>=4.6.80" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.155" \
|
||||
"praisonai>=4.6.80" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Create praison config directory
|
||||
RUN mkdir -p /root/.praison
|
||||
|
||||
# Install praisonai-bot with channel dependencies (slim bot image)
|
||||
# Install praisonai with bot dependencies
|
||||
RUN pip install --no-cache-dir \
|
||||
"praisonai-bot[bot]" \
|
||||
praisonai \
|
||||
slack-bolt \
|
||||
slack-sdk
|
||||
slack-sdk \
|
||||
"discord.py>=2.0.0" \
|
||||
"python-telegram-bot>=20.0"
|
||||
|
||||
# Set environment variables for directory management
|
||||
ENV PRAISON_CONFIG_DIR=/root/.praison
|
||||
@@ -37,4 +39,4 @@ ENV DOCKER_CONTAINER=true
|
||||
# For webhook mode, expose port 3000
|
||||
|
||||
# Default command - tokens from environment
|
||||
CMD ["praisonai-bot", "bot", "slack"]
|
||||
CMD ["praisonai", "bot", "slack"]
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}
|
||||
SLACK_APP_TOKEN: ${SLACK_APP_TOKEN}
|
||||
PRAISON_CONFIG_DIR: /root/.praison
|
||||
command: [ "praisonai-bot", "bot", "slack" ]
|
||||
command: [ "praisonai", "bot", "slack" ]
|
||||
restart: unless-stopped
|
||||
|
||||
# Discord Bot
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
|
||||
PRAISON_CONFIG_DIR: /root/.praison
|
||||
command: [ "praisonai-bot", "bot", "discord" ]
|
||||
command: [ "praisonai", "bot", "discord" ]
|
||||
restart: unless-stopped
|
||||
|
||||
# Telegram Bot
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||
PRAISON_CONFIG_DIR: /root/.praison
|
||||
command: [ "praisonai-bot", "bot", "telegram" ]
|
||||
command: [ "praisonai", "bot", "telegram" ]
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
Whitespace-only changes.
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
HTTP Approval Example
|
||||
=====================
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Slack Approval Example
|
||||
======================
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Completions Capability Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Embeddings Capability Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Images Capability Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Moderations Capability Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Energy Industry Template
|
||||
========================
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Transportation Industry Template
|
||||
================================
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CI Integration Example - Using Doctor in CI/CD pipelines
|
||||
@@ -20,9 +19,7 @@ def run_doctor_ci():
|
||||
[sys.executable, "-m", "praisonai", "doctor", "ci"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=120,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
# Parse JSON output
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Agent-centric Embedding Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Endpoints Example
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fixed example demonstrating proper guardrail usage with PraisonAI Agents.
|
||||
|
||||
@@ -149,28 +149,21 @@ def main():
|
||||
|
||||
print(f"\nOriginal document length: {len(content)} characters")
|
||||
|
||||
chunks = [{"text": content, "metadata": {"source": doc_path}}]
|
||||
target_tokens = 2000
|
||||
|
||||
# Compress with different ratios
|
||||
for ratio in [0.3, 0.5, 0.7]:
|
||||
compressor = ContextCompressor(verbose=False)
|
||||
result = compressor.compress(
|
||||
chunks,
|
||||
query="security verification code",
|
||||
target_tokens=target_tokens,
|
||||
compression_ratio=ratio,
|
||||
compressor = ContextCompressor(
|
||||
max_tokens=2000,
|
||||
target_ratio=ratio,
|
||||
)
|
||||
result = compressor.compress([content], query="security verification code")
|
||||
|
||||
print(f"\nCompression ratio {ratio}:")
|
||||
print(f" Original tokens: {result.original_tokens}")
|
||||
print(f" Compressed tokens: {result.compressed_tokens}")
|
||||
print(f" Actual ratio: {result.compression_ratio:.2f}")
|
||||
print(f" Actual ratio: {result.compressed_tokens / max(result.original_tokens, 1):.2f}")
|
||||
|
||||
# Check if secret code is preserved
|
||||
compressed_text = " ".join(
|
||||
c.get("text", "") for c in result.chunks
|
||||
) if result.chunks else ""
|
||||
compressed_text = " ".join(result.chunks) if result.chunks else ""
|
||||
if SECRET_CODE in compressed_text:
|
||||
print(f" ✅ Secret code preserved in compressed output")
|
||||
else:
|
||||
|
||||
@@ -65,6 +65,7 @@ def main():
|
||||
name="Policy_Expert",
|
||||
instructions="You are a policy expert.",
|
||||
knowledge=[temp_dir],
|
||||
agent_id="policy_agent_v1", # Knowledge scoped to this agent
|
||||
)
|
||||
|
||||
response = shared_agent.chat("What are the expense report rules?")
|
||||
@@ -80,6 +81,7 @@ def main():
|
||||
instructions="You are a personal HR assistant.",
|
||||
knowledge=[temp_dir],
|
||||
memory={"user_id": "user_bob"},
|
||||
agent_id="hr_bot_v2",
|
||||
)
|
||||
|
||||
response = combined_agent.chat("What training is required?")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + ClickHouse Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + MongoDB Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + MySQL Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + PostgreSQL Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + Redis Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + SQLite Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Custom Tools MCP Server Example
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP Stream MCP Server Example
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP Client Example
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""Multi-Agent Shared Session Demo"""
|
||||
from praisonaiagents import Agent
|
||||
from praisonai.persistence import create_conversation_store
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Langfuse Integration Example (Updated for TraceSinkProtocol)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""MLflow Local Observability Demo (no API key needed)"""
|
||||
import mlflow
|
||||
import time
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
ClickHouse — Persistence Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Minimal Agent with Database Persistence
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
MongoDB StateStore — Full Persistence Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
MySQL ConversationStore — Full Persistence Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
PostgreSQL ConversationStore — Full Persistence Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
PostgreSQL with Runs and Traces
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Redis StateStore — Full Persistence Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Session Resume Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Redis State Store - Agent-First Example
|
||||
|
||||
@@ -14,8 +13,7 @@ Expected Output:
|
||||
Agent responds with state persisted to Redis
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.db import db
|
||||
from praisonaiagents import Agent, db
|
||||
|
||||
print("=== Redis State Store (Agent-First) ===")
|
||||
|
||||
@@ -28,7 +26,7 @@ my_db = db(
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
memory=my_db,
|
||||
db=my_db,
|
||||
session_id="redis-state-example"
|
||||
)
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def demonstrate_collision_problem():
|
||||
_reset_default_registry()
|
||||
|
||||
# Agent 1 registers "custom" provider
|
||||
register_llm_provider("custom", Agent1Provider, override=True)
|
||||
register_llm_provider("custom", Agent1Provider)
|
||||
print("Agent 1 registered 'custom' provider")
|
||||
|
||||
# Agent 2 tries to register same name - ERROR!
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
PraisonAI A2A Server Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Agent-Centric Autonomy Example.
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
AutoAgents Workflow Patterns Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Context Agent Example - Basic Usage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
# Uses removed praisonaiagents.tools exports; pending example refresh
|
||||
from praisonaiagents import Agent, Tools
|
||||
from praisonaiagents.tools import read_csv, read_excel, write_csv, write_excel, filter_data, get_summary, group_by, pivot_table
|
||||
import os
|
||||
@@ -9,4 +7,4 @@ agent.start(f"""
|
||||
Read the data from the csv file {os.path.join(os.path.dirname(__file__), "tesla-stock-price.csv")}
|
||||
Analyse the data and give me the insights
|
||||
read_csv to read the file
|
||||
""")
|
||||
""")
|
||||
@@ -1,7 +1,5 @@
|
||||
# praisonai: skip=true
|
||||
# Uses removed praisonaiagents.tools exports; pending example refresh
|
||||
from praisonaiagents import Agent, Tools
|
||||
from praisonaiagents.tools import get_stock_price, get_stock_info, get_historical_data
|
||||
|
||||
agent = Agent(instructions="You are a Research Agent", tools=[get_stock_price, get_stock_info, get_historical_data])
|
||||
agent.start("Understand current stock price and historical data of Apple and Google. Tell me if I can invest in them")
|
||||
agent.start("Understand current stock price and historical data of Apple and Google. Tell me if I can invest in them")
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Math Agent Example
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ def example_auto_agents_multi_provider():
|
||||
from praisonaiagents.agents import AutoAgents
|
||||
|
||||
# Create AutoAgents that will automatically assign appropriate models
|
||||
auto_agents = AutoAgents(
|
||||
auto_agents = AutoAgentTeam(
|
||||
instructions="Create a market research report on electric vehicles. Include data analysis, competitor analysis, and future projections.",
|
||||
max_agents=3,
|
||||
llm="gpt-4o-mini", # Default model for agent generation
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
# Uses removed praisonaiagents.tools exports; pending example refresh
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.tools import wiki_search, wiki_summary, wiki_page, wiki_random, wiki_language
|
||||
|
||||
@@ -15,4 +13,4 @@ agent.start(
|
||||
"First search the history of AI"
|
||||
"Read the page of the history of AI"
|
||||
"Get the summary of the page"
|
||||
)
|
||||
)
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
from praisonaiagents import Agent, MCP
|
||||
|
||||
qa_agent = Agent(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
from praisonaiagents import Agent, AgentTeam, Tools
|
||||
|
||||
research_agent = Agent(name="Research", instructions="You are a research agent to search internet about AI 2024", tools=[Tools.internet_search])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
from praisonaiagents import Agent, AgentTeam, Tools
|
||||
|
||||
research_agent = Agent(name="Research", instructions="You are a research agent to search internet about AI 2024", tools=[Tools.internet_search])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Secondary Market Research FastAPI Application
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
from praisonaiagents import Agent, MCP
|
||||
|
||||
search_agent = Agent(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
from praisonaiagents import Agent, AgentTeam
|
||||
from duckduckgo_search import DDGS
|
||||
|
||||
|
||||
@@ -1,34 +1,5 @@
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
def _wait_until_ready(host: str, port: int, timeout: float = 15.0) -> bool:
|
||||
"""Poll the MCP server port until it accepts connections or times out."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(1.0)
|
||||
if sock.connect_ex((host, port)) == 0:
|
||||
return True
|
||||
time.sleep(0.25)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host, port = "127.0.0.1", 8080
|
||||
agent = Agent(name="TweetAgent", instructions="Create a Tweet based on the topic provided")
|
||||
server = threading.Thread(
|
||||
target=lambda: agent.launch(port=port, protocol="mcp", path="/mcp"),
|
||||
daemon=True,
|
||||
)
|
||||
server.start()
|
||||
|
||||
if _wait_until_ready(host, port):
|
||||
print(f"MCP server started on http://{host}:{port}/mcp")
|
||||
else:
|
||||
print(f"MCP server failed to start on http://{host}:{port}/mcp", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
agent.launch(port=8080, protocol="mcp", path="/")
|
||||
Loaded 100 of 1947 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user