mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 03:31:28 -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,53 +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'));
|
||||
|
||||
// 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' },
|
||||
@@ -95,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(
|
||||
@@ -183,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));
|
||||
|
||||
+12
-167
@@ -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,9 +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']);
|
||||
const BOT_REVIEWER_PATTERNS = [
|
||||
'coderabbit',
|
||||
'qodo',
|
||||
@@ -70,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) => {
|
||||
@@ -184,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;
|
||||
@@ -211,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;
|
||||
@@ -267,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 = `
|
||||
@@ -308,25 +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';
|
||||
});
|
||||
}
|
||||
|
||||
async function allChecksGreenOnSha(github, owner, repo, sha, core) {
|
||||
const runs = 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;
|
||||
@@ -336,9 +260,7 @@ async function allChecksGreenOnSha(github, owner, repo, sha, core) {
|
||||
core?.info?.(`Check pending: ${run.name} (${run.status})`);
|
||||
return false;
|
||||
}
|
||||
const ok =
|
||||
['success', 'neutral', 'skipped'].includes(run.conclusion) ||
|
||||
(run.conclusion === 'cancelled' && OPTIONAL_CANCELLED_CHECKS.has(run.name));
|
||||
const ok = ['success', 'neutral', 'skipped'].includes(run.conclusion);
|
||||
if (!ok) {
|
||||
core?.info?.(`Check failed: ${run.name} (${run.conclusion})`);
|
||||
return false;
|
||||
@@ -681,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)) {
|
||||
@@ -725,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;
|
||||
@@ -736,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 });
|
||||
@@ -811,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;
|
||||
@@ -843,7 +702,6 @@ module.exports = {
|
||||
AUTO_ACTORS,
|
||||
isFinalClaudeTriggerComment,
|
||||
isClaudeTriggerNoise,
|
||||
isClaudeFinalReplyComment,
|
||||
hasRecentClaudeTrigger,
|
||||
hasRecentConflictComment,
|
||||
isConflictRebaseTriggerComment,
|
||||
@@ -854,9 +712,6 @@ module.exports = {
|
||||
hasFinalClaudeReviewTrigger,
|
||||
finalClaudeCompletedOnSha,
|
||||
getMergeState,
|
||||
OPTIONAL_CANCELLED_CHECKS,
|
||||
listChecksOnSha,
|
||||
listFailedChecksOnSha,
|
||||
allChecksGreenOnSha,
|
||||
hasInProgressClaudeAssistant,
|
||||
claudeRunBlocksPr,
|
||||
@@ -884,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'],
|
||||
|
||||
@@ -52,11 +52,9 @@ jobs:
|
||||
// Kind labels
|
||||
if (content.includes('feature request') || content.includes('[feature')) {
|
||||
labels.push('enhancement');
|
||||
} else if (title.includes('[question]') || content.includes('how do i') || content.includes('how to')) {
|
||||
labels.push('question');
|
||||
} else if (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback')) {
|
||||
labels.push('bug');
|
||||
} else if (content.includes('question')) {
|
||||
} else if (content.includes('question') || content.includes('how do i') || content.includes('how to')) {
|
||||
labels.push('question');
|
||||
}
|
||||
|
||||
@@ -98,18 +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,
|
||||
@@ -119,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
|
||||
@@ -291,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 }}
|
||||
|
||||
@@ -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: 8
|
||||
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,14 +7,12 @@ 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:
|
||||
@@ -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: |
|
||||
|
||||
@@ -22,8 +22,6 @@ 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
|
||||
|
||||
@@ -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>
|
||||
@@ -484,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>
|
||||
@@ -518,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>
|
||||
@@ -571,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
|
||||
|
||||
@@ -669,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>
|
||||
@@ -820,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
|
||||
@@ -828,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>
|
||||
@@ -845,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>
|
||||
@@ -854,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>
|
||||
@@ -866,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>
|
||||
@@ -883,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>
|
||||
@@ -894,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>
|
||||
@@ -901,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>
|
||||
@@ -908,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>
|
||||
@@ -923,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>
|
||||
@@ -930,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>
|
||||
@@ -947,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:
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"""
|
||||
Standalone gateway example (praisonai-bot first).
|
||||
|
||||
Install:
|
||||
pip install praisonaiagents "praisonai-bot[gateway]"
|
||||
|
||||
Set credentials:
|
||||
export OPENAI_API_KEY=your_key
|
||||
|
||||
Run:
|
||||
python bot_standalone_example.py
|
||||
|
||||
For Telegram/Discord/Slack channels, use the CLI after onboarding:
|
||||
praisonai-bot onboard
|
||||
praisonai-bot bot start --platform telegram
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.gateway import GatewayConfig
|
||||
from praisonai_bot.gateway import WebSocketGateway
|
||||
|
||||
assistant = Agent(name="assistant", instructions="You are a helpful assistant.")
|
||||
|
||||
config = GatewayConfig(host="127.0.0.1", port=8765)
|
||||
gateway = WebSocketGateway(config=config)
|
||||
gateway.register_agent(assistant, agent_id="assistant")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Gateway: ws://127.0.0.1:8765 health: http://127.0.0.1:8765/health")
|
||||
print("CLI equivalent: praisonai-bot gateway start --host 127.0.0.1 --port 8765")
|
||||
asyncio.run(gateway.start())
|
||||
@@ -1,56 +0,0 @@
|
||||
"""
|
||||
Loop Health Evaluation Example
|
||||
|
||||
Demonstrates ``LoopEvaluator``, which scores loop *health* (convergence,
|
||||
wasted iterations, doom-loop guard events) on top of an ``EvaluationLoop`` run.
|
||||
|
||||
Where ``EvaluationLoop`` tells you *what* score the agent reached, the
|
||||
``LoopEvaluator`` tells you *how healthy* the loop was getting there:
|
||||
- Did it converge, and in how many iterations?
|
||||
- Were iterations wasted after the threshold was already met?
|
||||
- Did a doom-loop safety guard fire?
|
||||
|
||||
Usage:
|
||||
pip install praisonaiagents
|
||||
export OPENAI_API_KEY=your-key
|
||||
python loop_health_eval.py
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.eval import EvaluationLoop, LoopEvaluator
|
||||
|
||||
|
||||
def main():
|
||||
agent = Agent(
|
||||
name="writer",
|
||||
instructions="You are a technical writer. Write clear, accurate explanations.",
|
||||
)
|
||||
|
||||
loop = EvaluationLoop(
|
||||
agent=agent,
|
||||
criteria="Explanation is clear, accurate, and complete.",
|
||||
threshold=8.0,
|
||||
max_iterations=5,
|
||||
)
|
||||
|
||||
# 1. Run the quality loop (existing behaviour).
|
||||
loop_result = loop.run("Explain how HTTPS works.")
|
||||
loop_result.print_summary()
|
||||
|
||||
# 2. Collect any structured guard events emitted during the run.
|
||||
# In a real setup these would come from doom_loop.py / loop_detection
|
||||
# plugins. Here we pass an empty list (no guards fired).
|
||||
guard_events = []
|
||||
|
||||
# 3. Score loop health.
|
||||
health = LoopEvaluator(threshold=8.0).run(loop_result, guard_events=guard_events)
|
||||
health.print_summary()
|
||||
|
||||
if not health.success:
|
||||
print("\n⚠️ Loop was UNHEALTHY:")
|
||||
print(f" {health.reasoning}")
|
||||
else:
|
||||
print("\n✅ Loop was healthy.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Example third-party framework adapter (entry-point template)."""
|
||||
|
||||
from praisonaiagents.frameworks.base import BaseFrameworkAdapter
|
||||
|
||||
|
||||
class MyFrameworkAdapter(BaseFrameworkAdapter):
|
||||
name = "my_framework"
|
||||
install_hint = "pip install my-framework-bridge"
|
||||
requires_tools_extra = False
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return True
|
||||
|
||||
def run(
|
||||
self,
|
||||
config,
|
||||
llm_config,
|
||||
topic,
|
||||
*,
|
||||
tools_dict=None,
|
||||
agent_callback=None,
|
||||
task_callback=None,
|
||||
cli_config=None,
|
||||
) -> str:
|
||||
return f"my_framework ran: {topic}"
|
||||
@@ -1,14 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "my-framework-bridge"
|
||||
version = "0.0.1"
|
||||
dependencies = ["praisonaiagents"]
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["my_framework_adapter"]
|
||||
|
||||
[project.entry-points."praisonai.framework_adapters"]
|
||||
my_framework = "my_framework_adapter:MyFrameworkAdapter"
|
||||
@@ -132,7 +132,6 @@ if __name__ == "__main__":
|
||||
demonstrate_result_handling()
|
||||
|
||||
print("To run code in sandbox via CLI:")
|
||||
print(" praisonai sandbox run --code \"print('Hello, World!')\"")
|
||||
print(" praisonai sandbox run --file script.py --type docker")
|
||||
print(" praisonai sandbox run \"print('Hello, World!')\"")
|
||||
print(" praisonai sandbox run --file script.py")
|
||||
print(" praisonai sandbox shell")
|
||||
print(" praisonai sandbox backends")
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Simple test script for 24/7 Agent Scheduler.
|
||||
Simple test script for 24/7 Agent Scheduler
|
||||
|
||||
This script demonstrates the basic usage of AgentScheduler
|
||||
for running agents periodically.
|
||||
@@ -8,56 +8,42 @@ Usage:
|
||||
python simple_test.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../.."))
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..'))
|
||||
|
||||
from unittest.mock import Mock
|
||||
from praisonai.scheduler import AgentScheduler
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Create a mock agent
|
||||
#
|
||||
# The scheduler now supports both asynchronous (astart) and synchronous
|
||||
# (start) execution. This example exposes both methods so either dispatch
|
||||
# path can be exercised.
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
mock_agent = Mock(spec=["name", "start", "astart"])
|
||||
# Create a mock agent for testing
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
mock_agent.astart = AsyncMock(
|
||||
return_value="Test execution successful!"
|
||||
)
|
||||
mock_agent.start = Mock(
|
||||
return_value="Test execution successful!"
|
||||
)
|
||||
mock_agent.start = Mock(return_value="Test execution successful!")
|
||||
|
||||
# Create scheduler
|
||||
scheduler = AgentScheduler(
|
||||
agent=mock_agent,
|
||||
task="Test task execution",
|
||||
task="Test task execution"
|
||||
)
|
||||
|
||||
print("🤖 Testing AgentScheduler")
|
||||
print("=" * 60)
|
||||
|
||||
# Execute once
|
||||
# Test 1: Execute once
|
||||
print("\n✅ Test 1: Execute once")
|
||||
result = scheduler.execute_once()
|
||||
print(f"Result: {result}")
|
||||
|
||||
# Statistics
|
||||
# Test 2: Get stats
|
||||
print("\n✅ Test 2: Get statistics")
|
||||
stats = scheduler.get_stats()
|
||||
print(f"Stats: {stats}")
|
||||
|
||||
# Periodic execution
|
||||
# Test 3: Start scheduler (will run every 5 seconds)
|
||||
print("\n✅ Test 3: Start scheduler (*/5s)")
|
||||
scheduler.start("*/5s", run_immediately=True)
|
||||
|
||||
import time
|
||||
print("Running for 12 seconds...")
|
||||
time.sleep(12)
|
||||
|
||||
@@ -65,10 +51,11 @@ time.sleep(12)
|
||||
print("\n✅ Test 4: Stop scheduler")
|
||||
scheduler.stop()
|
||||
|
||||
# Final statistics
|
||||
# Final stats
|
||||
print("\n📊 Final Statistics:")
|
||||
for key, value in scheduler.get_stats().items():
|
||||
final_stats = scheduler.get_stats()
|
||||
for key, value in final_stats.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print("\n✅ All tests passed!")
|
||||
print("=" * 60)
|
||||
print("=" * 60)
|
||||
@@ -1,106 +0,0 @@
|
||||
"""Use Xquik tweet search as a PraisonAI custom tool.
|
||||
|
||||
Set XQUIK_API_KEY before running this example. The tool calls the public
|
||||
Xquik tweet search endpoint and returns compact tweet summaries for the agent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from praisonaiagents import Agent, tool
|
||||
|
||||
|
||||
XQUIK_SEARCH_URL = "https://xquik.com/api/v1/x/tweets/search"
|
||||
|
||||
|
||||
def _xquik_api_key_available() -> tuple[bool, str]:
|
||||
return bool(os.environ.get("XQUIK_API_KEY")), "Set XQUIK_API_KEY before using search_x_posts."
|
||||
|
||||
|
||||
def _as_record(value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
return {}
|
||||
|
||||
|
||||
def _as_text(value: Any) -> str:
|
||||
if isinstance(value, str):
|
||||
return " ".join(value.split())
|
||||
if value is None:
|
||||
return ""
|
||||
return " ".join(str(value).split())
|
||||
|
||||
|
||||
def _tweet_line(tweet: dict[str, Any]) -> str:
|
||||
author_record = _as_record(tweet.get("author") or tweet.get("user"))
|
||||
author = _as_text(
|
||||
tweet.get("author_username")
|
||||
or tweet.get("username")
|
||||
or author_record.get("username")
|
||||
or author_record.get("screen_name")
|
||||
or author_record.get("handle")
|
||||
)
|
||||
text = _as_text(tweet.get("text") or tweet.get("full_text") or tweet.get("content"))
|
||||
tweet_id = _as_text(tweet.get("id") or tweet.get("tweet_id") or tweet.get("tweetId"))
|
||||
prefix = f"@{author}: " if author else ""
|
||||
suffix = f" ({tweet_id})" if tweet_id else ""
|
||||
return f"{prefix}{text}{suffix}".strip()
|
||||
|
||||
|
||||
def _bounded_limit(value: Any) -> int:
|
||||
try:
|
||||
parsed = int(float(value))
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return 5
|
||||
return max(1, min(parsed, 10))
|
||||
|
||||
|
||||
@tool(availability=_xquik_api_key_available)
|
||||
def search_x_posts(query: str, limit: int = 5) -> str:
|
||||
"""Search recent public X posts with Xquik."""
|
||||
api_key = os.environ.get("XQUIK_API_KEY")
|
||||
if not api_key:
|
||||
return "Set XQUIK_API_KEY before using search_x_posts."
|
||||
|
||||
query_text = _as_text(query)
|
||||
if not query_text:
|
||||
return "Provide a non-empty query before using search_x_posts."
|
||||
|
||||
bounded_limit = _bounded_limit(limit)
|
||||
params = {"q": query_text, "limit": bounded_limit, "queryType": "Latest"}
|
||||
url = f"{XQUIK_SEARCH_URL}?{urlencode(params)}"
|
||||
request = Request(
|
||||
url,
|
||||
headers={"X-API-Key": api_key, "Accept": "application/json"},
|
||||
)
|
||||
|
||||
try:
|
||||
with urlopen(request, timeout=20) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
except HTTPError as error:
|
||||
return f"Xquik returned HTTP {error.code}."
|
||||
except (URLError, TimeoutError, json.JSONDecodeError) as error:
|
||||
return f"Xquik search failed: {error.__class__.__name__}."
|
||||
|
||||
tweets = _as_record(payload).get("tweets")
|
||||
if not isinstance(tweets, list) or not tweets:
|
||||
return "No posts found."
|
||||
|
||||
lines = [_tweet_line(_as_record(tweet)) for tweet in tweets[:bounded_limit]]
|
||||
return "\n".join(line for line in lines if line) or "No readable posts found."
|
||||
|
||||
|
||||
agent = Agent(
|
||||
instructions="You find recent public X posts and summarize the useful patterns.",
|
||||
tools=[search_x_posts],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent.start("Search X for recent AI agent framework discussions.")
|
||||
@@ -1,18 +0,0 @@
|
||||
# Standalone bot + gateway (praisonai-bot first)
|
||||
#
|
||||
# pip install praisonaiagents "praisonai-bot[gateway,bot]"
|
||||
# export OPENAI_API_KEY=...
|
||||
# praisonai-bot gateway start --host 127.0.0.1 --port 8765
|
||||
|
||||
agent:
|
||||
name: assistant
|
||||
instructions: You are a helpful assistant.
|
||||
llm: gpt-4o-mini
|
||||
|
||||
gateway:
|
||||
host: 127.0.0.1
|
||||
port: 8765
|
||||
|
||||
platforms:
|
||||
telegram:
|
||||
token: ${TELEGRAM_BOT_TOKEN}
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
#!/bin/sh
|
||||
# PraisonAI one-line standalone installer.
|
||||
#
|
||||
# curl -fsSL https://raw.githubusercontent.com/MervinPraison/PraisonAI/main/install.sh | sh
|
||||
#
|
||||
# Installs PraisonAI into a dedicated, isolated environment (via `uv tool` or
|
||||
# `pipx`, auto-detecting/bootstrapping the runtime) and exposes a global
|
||||
# `praisonai` command on PATH. Idempotent and safe to re-run.
|
||||
#
|
||||
# Options (environment variables):
|
||||
# PRAISONAI_VERSION=x.y.z Pin a specific version (default: latest).
|
||||
# PRAISONAI_INSTALLER=uv|pipx Force a specific tool manager.
|
||||
# PRAISONAI_NONINTERACTIVE=1 Never prompt; suitable for CI.
|
||||
#
|
||||
# `pip install praisonai` remains fully supported for library/embedded use;
|
||||
# this installer only manages the standalone CLI binary.
|
||||
|
||||
set -eu
|
||||
|
||||
PACKAGE="praisonai"
|
||||
VERSION="${PRAISONAI_VERSION:-}"
|
||||
FORCED_INSTALLER="${PRAISONAI_INSTALLER:-}"
|
||||
NONINTERACTIVE="${PRAISONAI_NONINTERACTIVE:-}"
|
||||
|
||||
info() { printf '\033[0;36m==>\033[0m %s\n' "$1"; }
|
||||
warn() { printf '\033[0;33mwarning:\033[0m %s\n' "$1" >&2; }
|
||||
err() { printf '\033[0;31merror:\033[0m %s\n' "$1" >&2; }
|
||||
|
||||
has() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
detect_platform() {
|
||||
os="$(uname -s 2>/dev/null || echo unknown)"
|
||||
arch="$(uname -m 2>/dev/null || echo unknown)"
|
||||
info "Detected platform: ${os} ${arch}"
|
||||
}
|
||||
|
||||
# Append an export line to a single rc file, idempotently. Returns 0 on success.
|
||||
_append_path_line() {
|
||||
rc="$1"
|
||||
line="$2"
|
||||
[ -n "${rc}" ] || return 1
|
||||
if [ -f "${rc}" ] && grep -Fq "${bindir}" "${rc}" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
printf '\n# Added by PraisonAI installer\n%s\n' "${line}" >>"${rc}" 2>/dev/null
|
||||
}
|
||||
|
||||
wire_path_dispatch() {
|
||||
bindir="$1"
|
||||
shell_name="$(basename "${SHELL:-sh}")"
|
||||
updated=""
|
||||
|
||||
case "${shell_name}" in
|
||||
zsh)
|
||||
line="export PATH=\"${bindir}:\$PATH\""
|
||||
_append_path_line "${HOME}/.zshrc" "${line}" && updated="${HOME}/.zshrc"
|
||||
;;
|
||||
fish)
|
||||
mkdir -p "${HOME}/.config/fish" 2>/dev/null || true
|
||||
rc="${HOME}/.config/fish/config.fish"
|
||||
# `fish_add_path` is unavailable on older fish; guard it and provide a
|
||||
# POSIX-safe fallback so PATH is wired regardless of fish version.
|
||||
line="if type -q fish_add_path; fish_add_path ${bindir}; else; set -gx PATH ${bindir} \$PATH; end"
|
||||
_append_path_line "${rc}" "${line}" && updated="${rc}"
|
||||
;;
|
||||
bash)
|
||||
# Interactive non-login shells read ~/.bashrc; login shells read the
|
||||
# first existing of ~/.bash_profile, ~/.bash_login, ~/.profile. Wire the
|
||||
# interactive rc and whichever login profile applies so both work.
|
||||
line="export PATH=\"${bindir}:\$PATH\""
|
||||
_append_path_line "${HOME}/.bashrc" "${line}" && updated="${HOME}/.bashrc"
|
||||
if [ -f "${HOME}/.bash_profile" ]; then
|
||||
login_rc="${HOME}/.bash_profile"
|
||||
elif [ -f "${HOME}/.bash_login" ]; then
|
||||
login_rc="${HOME}/.bash_login"
|
||||
else
|
||||
login_rc="${HOME}/.profile"
|
||||
fi
|
||||
_append_path_line "${login_rc}" "${line}" \
|
||||
&& updated="${updated:+${updated} }${login_rc}"
|
||||
;;
|
||||
*)
|
||||
line="export PATH=\"${bindir}:\$PATH\""
|
||||
_append_path_line "${HOME}/.profile" "${line}" && updated="${HOME}/.profile"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "${updated}" ]; then
|
||||
info "Added ${bindir} to PATH in ${updated} (restart your shell to apply)."
|
||||
else
|
||||
warn "Could not update your shell profile; add ${bindir} to your PATH manually."
|
||||
fi
|
||||
}
|
||||
|
||||
# Add a directory to PATH in the user's shell rc file(s), idempotently.
|
||||
wire_path() {
|
||||
bindir="$1"
|
||||
case ":${PATH}:" in
|
||||
*":${bindir}:"*) return 0 ;;
|
||||
esac
|
||||
wire_path_dispatch "${bindir}"
|
||||
}
|
||||
|
||||
spec() {
|
||||
if [ -n "${VERSION}" ]; then
|
||||
printf '%s==%s' "${PACKAGE}" "${VERSION}"
|
||||
else
|
||||
printf '%s' "${PACKAGE}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_with_uv() {
|
||||
info "Installing ${PACKAGE} with 'uv tool'..."
|
||||
uv tool install --force "$(spec)"
|
||||
wire_path "${HOME}/.local/bin"
|
||||
}
|
||||
|
||||
install_with_pipx() {
|
||||
info "Installing ${PACKAGE} with pipx..."
|
||||
pipx install --force "$(spec)"
|
||||
pipx ensurepath >/dev/null 2>&1 || true
|
||||
wire_path "${HOME}/.local/bin"
|
||||
}
|
||||
|
||||
bootstrap_uv() {
|
||||
if [ -n "${NONINTERACTIVE}" ] || [ ! -t 0 ]; then
|
||||
info "Bootstrapping uv (isolated Python tool manager)..."
|
||||
else
|
||||
info "No uv/pipx found; bootstrapping uv..."
|
||||
fi
|
||||
# Download the installer to a file first so a failed/empty download (which a
|
||||
# piped `curl | sh` can silently swallow) surfaces here with a useful message
|
||||
# instead of a later confusing `uv: command not found`.
|
||||
tmp_installer="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/uv-install.$$.sh")"
|
||||
if ! curl -fsSL https://astral.sh/uv/install.sh -o "${tmp_installer}" \
|
||||
|| [ ! -s "${tmp_installer}" ]; then
|
||||
rm -f "${tmp_installer}" 2>/dev/null || true
|
||||
err "Failed to download the uv bootstrap installer (network/DNS issue?)."
|
||||
err "Install uv or pipx manually, then re-run this installer:"
|
||||
err " https://docs.astral.sh/uv/getting-started/installation/"
|
||||
exit 1
|
||||
fi
|
||||
sh "${tmp_installer}"
|
||||
rm -f "${tmp_installer}" 2>/dev/null || true
|
||||
|
||||
# uv installs to ~/.local/bin or ~/.cargo/bin depending on platform.
|
||||
if [ -x "${HOME}/.local/bin/uv" ]; then
|
||||
PATH="${HOME}/.local/bin:${PATH}"
|
||||
elif [ -x "${HOME}/.cargo/bin/uv" ]; then
|
||||
PATH="${HOME}/.cargo/bin:${PATH}"
|
||||
fi
|
||||
export PATH
|
||||
|
||||
# Verify the bootstrap actually produced a working `uv`; otherwise fail loudly
|
||||
# instead of proceeding to `uv tool install` with no uv on PATH.
|
||||
if ! has uv; then
|
||||
err "uv bootstrap completed but 'uv' is not on PATH."
|
||||
err "Open a new shell or install uv/pipx manually, then re-run this installer."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
detect_platform
|
||||
|
||||
if ! has curl; then
|
||||
err "curl is required to run this installer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${FORCED_INSTALLER}" in
|
||||
uv) has uv || bootstrap_uv; install_with_uv ;;
|
||||
pipx) if has pipx; then install_with_pipx; else err "pipx requested but not installed."; exit 1; fi ;;
|
||||
"")
|
||||
if has uv; then
|
||||
install_with_uv
|
||||
elif has pipx; then
|
||||
install_with_pipx
|
||||
else
|
||||
bootstrap_uv
|
||||
install_with_uv
|
||||
fi
|
||||
;;
|
||||
*) err "Unknown PRAISONAI_INSTALLER='${FORCED_INSTALLER}' (use uv or pipx)."; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo
|
||||
info "PraisonAI installed. Get started with:"
|
||||
echo " praisonai setup"
|
||||
echo
|
||||
info "Manage it later with:"
|
||||
echo " praisonai upgrade # update in place"
|
||||
echo " praisonai upgrade --check"
|
||||
echo " praisonai uninstall # remove cleanly"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fail if praisonai_bot production code imports praisonai.* at module level (outside allowlist).
|
||||
set -euo pipefail
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ROOT="src/praisonai-bot/praisonai_bot"
|
||||
ALLOWLIST="scripts/c9_wrapper_import_allowlist.txt"
|
||||
RE='^(from praisonai\.|import praisonai\.)'
|
||||
FAIL=0
|
||||
while IFS= read -r f; do
|
||||
rel="${f#"$ROOT/"}"
|
||||
if printf '%s\n' "$(sed -E 's/[[:space:]]*#.*$//;/^[[:space:]]*$/d' "$ALLOWLIST")" | grep -Fxq "$rel"; then
|
||||
continue
|
||||
fi
|
||||
if grep -E "$RE" "$f" 2>/dev/null | grep -v '^[[:space:]]*#'; then
|
||||
echo "FAIL: module-level praisonai import in $rel"
|
||||
FAIL=1
|
||||
fi
|
||||
done < <(find "$ROOT" -name '*.py' -type f)
|
||||
[ "$FAIL" -eq 0 ] && echo "audit_bot_wrapper_imports ok" || exit 1
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assert critical C8 hybrid import paths resolve (wrapper + code tiers)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
|
||||
def _check(label: str, import_fn) -> None:
|
||||
try:
|
||||
import_fn()
|
||||
except Exception as exc:
|
||||
print(f"FAIL {label}: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1) from exc
|
||||
print(f"OK {label}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
_check(
|
||||
"praisonai.cli.features.tui.app.TUIApp",
|
||||
lambda: importlib.import_module("praisonai.cli.features.tui.app").TUIApp,
|
||||
)
|
||||
_check(
|
||||
"praisonai.cli.features.tools.ToolsHandler",
|
||||
lambda: importlib.import_module("praisonai.cli.features.tools").ToolsHandler,
|
||||
)
|
||||
_check(
|
||||
"praisonai.cli.features.workflow.WorkflowHandler",
|
||||
lambda: importlib.import_module("praisonai.cli.features.workflow").WorkflowHandler,
|
||||
)
|
||||
_check(
|
||||
"praisonai.cli.interactive.core.InteractiveCore",
|
||||
lambda: importlib.import_module("praisonai.cli.interactive.core").InteractiveCore,
|
||||
)
|
||||
_check(
|
||||
"praisonai_code.cli.interactive.async_tui.AsyncTUI",
|
||||
lambda: importlib.import_module("praisonai_code.cli.interactive.async_tui").AsyncTUI,
|
||||
)
|
||||
_check(
|
||||
"praisonai_code.cli.features.workflow.WorkflowHandler",
|
||||
lambda: importlib.import_module("praisonai_code.cli.features.workflow").WorkflowHandler,
|
||||
)
|
||||
print("All hybrid module paths importable.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +0,0 @@
|
||||
# Files allowed to contain lazy ``from praisonai.*`` imports in praisonai-code.
|
||||
# C8 complete — reverse imports eliminated; bridge-only access remains.
|
||||
# Regenerate counts: bash scripts/check_c7_imports.sh
|
||||
#
|
||||
# If a new direct import is unavoidable, add path below after Architecture Review.
|
||||
@@ -1,553 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""C8.4 — mechanical main.py decomposition into legacy modules."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
MAIN = REPO / "src/praisonai-code/praisonai_code/cli/main.py"
|
||||
CODE_LEGACY = REPO / "src/praisonai-code/praisonai_code/cli/legacy"
|
||||
WRAP_LEGACY = REPO / "src/praisonai/praisonai/cli/legacy"
|
||||
PRAISON_AI = CODE_LEGACY / "praison_ai.py"
|
||||
THIN_MAIN = MAIN
|
||||
|
||||
# (start, end) 1-indexed inclusive
|
||||
RANGES = {
|
||||
"env_security.py": (45, 92),
|
||||
"subcommand_handlers.py": [
|
||||
(2342, 2518), # memory
|
||||
(2520, 2631), # rules
|
||||
(3310, 3392), # hooks
|
||||
(3394, 3479), # knowledge (stop before session)
|
||||
(3535, 3647), # docs
|
||||
(3649, 3808), # mcp
|
||||
(3810, 3907), # sensitive patterns + check + clean_commit
|
||||
(3909, 4119), # commit
|
||||
(5761, 5803), # context
|
||||
(5805, 6010), # research
|
||||
],
|
||||
"workflow_commands.py": (2633, 3308),
|
||||
"direct_prompt.py": [
|
||||
(2087, 2340),
|
||||
(4121, 4148), # _save_output
|
||||
(4421, 5385),
|
||||
],
|
||||
"dispatch/argparse_builder.py": (1074, 1359),
|
||||
"interactive_legacy.py": (6012, 7486),
|
||||
}
|
||||
|
||||
# Lines to remove from praison_ai (replaced by delegation)
|
||||
EXTRACT_LINES = set()
|
||||
for spec in RANGES.values():
|
||||
blocks = spec if isinstance(spec, list) else [spec]
|
||||
for start, end in blocks:
|
||||
EXTRACT_LINES.update(range(start, end + 1))
|
||||
|
||||
DELEGATE_METHODS = {
|
||||
"subcommand_handlers": [
|
||||
"handle_memory_command",
|
||||
"handle_rules_command",
|
||||
"handle_hooks_command",
|
||||
"handle_knowledge_command",
|
||||
"handle_docs_command",
|
||||
"handle_mcp_command",
|
||||
"handle_commit_command",
|
||||
"handle_context_command",
|
||||
"handle_research_command",
|
||||
"_check_sensitive_content",
|
||||
"_clean_commit_message",
|
||||
"_get_sensitive_patterns",
|
||||
],
|
||||
"workflow_commands": [
|
||||
"handle_workflow_command",
|
||||
"_run_yaml_workflow",
|
||||
"_validate_yaml_workflow",
|
||||
"_get_canonical_suggestions",
|
||||
"_create_workflow_from_template",
|
||||
"_auto_generate_workflow",
|
||||
],
|
||||
"direct_prompt": [
|
||||
"_rewrite_query",
|
||||
"_rewrite_query_if_enabled",
|
||||
"_expand_prompt",
|
||||
"_expand_prompt_if_enabled",
|
||||
"_load_tools",
|
||||
"_load_toolsets",
|
||||
"_save_output",
|
||||
"handle_direct_prompt",
|
||||
],
|
||||
"interactive_legacy": [
|
||||
"_start_interactive_mode",
|
||||
"_load_interactive_tools",
|
||||
"_run_chat_mode",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def read_lines() -> list[str]:
|
||||
return MAIN.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
|
||||
|
||||
def dedent_block(lines: list[str], spaces: int = 4) -> list[str]:
|
||||
out: list[str] = []
|
||||
prefix = " " * spaces
|
||||
for line in lines:
|
||||
if line.startswith(prefix):
|
||||
out.append(line[spaces:])
|
||||
else:
|
||||
out.append(line)
|
||||
return out
|
||||
|
||||
|
||||
def extract_range(all_lines: list[str], start: int, end: int) -> list[str]:
|
||||
return dedent_block(all_lines[start - 1 : end])
|
||||
|
||||
|
||||
def transform_sensitive_patterns(lines: list[str]) -> list[str]:
|
||||
text = "".join(lines)
|
||||
text = text.replace("cls._SENSITIVE_PATTERNS", "_SENSITIVE_PATTERNS")
|
||||
text = re.sub(
|
||||
r"@classmethod\s*\n\s*def _get_sensitive_patterns\(cls\):",
|
||||
"def _get_sensitive_patterns():",
|
||||
text,
|
||||
)
|
||||
text = text.replace(" _SENSITIVE_PATTERNS = None\n", "_SENSITIVE_PATTERNS = None\n")
|
||||
text = text.replace(" _SENSITIVE_FILES = ", "_SENSITIVE_FILES = ")
|
||||
text = text.replace(" _SENSITIVE_EXTENSIONS = ", "_SENSITIVE_EXTENSIONS = ")
|
||||
text = text.replace("self._get_sensitive_patterns()", "_get_sensitive_patterns()")
|
||||
text = re.sub(
|
||||
r"@staticmethod\s*\n\s*def _clean_commit_message",
|
||||
"def _clean_commit_message",
|
||||
text,
|
||||
)
|
||||
return text.splitlines(keepends=True)
|
||||
|
||||
|
||||
def module_header(doc: str, extra_imports: str = "") -> str:
|
||||
return textwrap.dedent(
|
||||
f'''\
|
||||
"""{doc}"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
from rich import print
|
||||
{extra_imports}
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
def write_env_security(all_lines: list[str]) -> None:
|
||||
body = all_lines[44:92] # module-level — no dedent
|
||||
header = '"""Environment variable security (C8.4)."""\n\nfrom __future__ import annotations\n\nfrom dotenv import load_dotenv\n\n'
|
||||
path = CODE_LEGACY / "env_security.py"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(header + "".join(body), encoding="utf-8")
|
||||
|
||||
|
||||
def write_subcommand_handlers(all_lines: list[str]) -> None:
|
||||
parts: list[str] = []
|
||||
for start, end in RANGES["subcommand_handlers.py"]: # type: ignore[index]
|
||||
chunk = extract_range(all_lines, start, end)
|
||||
if start == 3810:
|
||||
chunk = transform_sensitive_patterns(chunk)
|
||||
parts.append("".join(chunk))
|
||||
content = module_header("CLI subcommand handlers (C8.4).") + "\n".join(parts)
|
||||
path = WRAP_LEGACY / "subcommand_handlers.py"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def write_workflow_commands(all_lines: list[str]) -> None:
|
||||
start, end = RANGES["workflow_commands.py"] # type: ignore[misc]
|
||||
body = extract_range(all_lines, start, end)
|
||||
content = module_header("Workflow CLI commands (C8.4).") + "".join(body)
|
||||
path = WRAP_LEGACY / "workflow_commands.py"
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def write_direct_prompt(all_lines: list[str]) -> None:
|
||||
parts: list[str] = []
|
||||
for start, end in RANGES["direct_prompt.py"]: # type: ignore[index]
|
||||
parts.append("".join(extract_range(all_lines, start, end)))
|
||||
content = module_header("Direct prompt execution (C8.4).") + "\n".join(parts)
|
||||
path = WRAP_LEGACY / "direct_prompt.py"
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def write_argparse_builder(all_lines: list[str]) -> None:
|
||||
body = dedent_block(all_lines[1073:1359], spaces=8)
|
||||
fn = textwrap.dedent(
|
||||
'''\
|
||||
|
||||
def build_argument_parser(in_test_env: bool):
|
||||
"""Build ArgumentParser and parse argv. Returns (args, unknown_args, special_commands)."""
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy', 'templates', 'recipe', 'endpoints', 'audio', 'embed', 'embedding', 'images', 'moderate', 'files', 'batches', 'vector-stores', 'rerank', 'ocr', 'assistants', 'fine-tuning', 'completions', 'messages', 'guardrails', 'rag', 'videos', 'a2a', 'containers', 'passthrough', 'responses', 'search', 'realtime-api', 'doctor', 'registry', 'package', 'install', 'uninstall', 'acp', 'debug', 'lsp', 'diag', 'browser', 'replay', 'bot', 'gateway', 'sandbox', 'wizard', 'migrate', 'security', 'persistence', 'paths', 'claw', 'github', 'managed', 'flow', 'dashboard', 'backends', 'audit']
|
||||
|
||||
'''
|
||||
)
|
||||
# Replace first two lines (special_commands + blank) from body — body starts with special_commands
|
||||
body_text = "".join(body)
|
||||
body_text = re.sub(
|
||||
r"^\s*special_commands = \[.*?\]\s*\n\s*\n",
|
||||
"",
|
||||
body_text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
indented = textwrap.indent(body_text, " ")
|
||||
parse_tail = textwrap.indent(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
if in_test_env:
|
||||
args, unknown_args = parser.parse_known_args([])
|
||||
else:
|
||||
args, unknown_args = parser.parse_known_args()
|
||||
return args, unknown_args, special_commands
|
||||
"""
|
||||
),
|
||||
" ",
|
||||
)
|
||||
extra = "from praisonai.cli.legacy.framework_run import fw_registry_module as _fw_registry_module\n"
|
||||
content = (
|
||||
module_header("ArgumentParser schema for legacy CLI (C8.4).", extra)
|
||||
+ fn
|
||||
+ indented
|
||||
+ parse_tail
|
||||
)
|
||||
path = WRAP_LEGACY / "dispatch" / "argparse_builder.py"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def write_interactive_legacy(all_lines: list[str]) -> None:
|
||||
start, end = RANGES["interactive_legacy.py"] # type: ignore[misc]
|
||||
body = extract_range(all_lines, start, end)
|
||||
content = module_header("Interactive REPL/TUI legacy path (C8.4).") + "".join(body)
|
||||
path = WRAP_LEGACY / "interactive_legacy.py"
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def make_delegate(module_suffix: str, method: str) -> str:
|
||||
mod = f"praisonai.cli.legacy.{module_suffix}"
|
||||
return (
|
||||
f" def {method}(self, *args, **kwargs):\n"
|
||||
f' from praisonai_code._wrapper_bridge import import_wrapper_module\n'
|
||||
f' _mod = import_wrapper_module("{mod}")\n'
|
||||
f' return getattr(_mod, "{method}")(self, *args, **kwargs)\n\n'
|
||||
)
|
||||
|
||||
|
||||
def write_praison_ai(all_lines: list[str]) -> None:
|
||||
kept: list[str] = []
|
||||
for i, line in enumerate(all_lines):
|
||||
line_no = i + 1
|
||||
if line_no < 347:
|
||||
continue
|
||||
if line_no in EXTRACT_LINES:
|
||||
continue
|
||||
kept.append(line)
|
||||
|
||||
text = "".join(kept)
|
||||
|
||||
# Replace argparse builder block inside parse_args
|
||||
text = re.sub(
|
||||
r" # Define special commands\n special_commands = \[.*?"
|
||||
r" args, unknown_args = parser\.parse_known_args\(\)\n\n",
|
||||
" from praisonai_code._wrapper_bridge import import_wrapper_module\n"
|
||||
" _ab = import_wrapper_module(\"praisonai.cli.legacy.dispatch.argparse_builder\")\n"
|
||||
" args, unknown_args, special_commands = _ab.build_argument_parser(in_test_env)\n\n",
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# Remove dead branches in special_commands block (already handled earlier)
|
||||
text = re.sub(
|
||||
r"\n elif args\.command == 'call':\n"
|
||||
r" import importlib\.util\n.*?"
|
||||
r" sys\.exit\(0\)\n",
|
||||
"\n",
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
text = re.sub(
|
||||
r"\n elif args\.command == 'serve':\n"
|
||||
r" # Serve command.*?\n sys\.exit\(exit_code\)\n",
|
||||
"\n",
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
text = re.sub(
|
||||
r"\n elif args\.command == 'replay':\n"
|
||||
r" # Replay command.*?\n sys\.exit\(0\)\n",
|
||||
"\n",
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
delegates = ""
|
||||
for mod, methods in DELEGATE_METHODS.items():
|
||||
for method in methods:
|
||||
delegates += make_delegate(mod, method)
|
||||
|
||||
# Insert delegates at end of PraisonAI class (before trailing module-level code)
|
||||
text = text.rstrip() + "\n\n" + delegates
|
||||
|
||||
header = textwrap.dedent(
|
||||
'''\
|
||||
"""PraisonAI CLI class — C8.4 legacy shell."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import warnings
|
||||
import os
|
||||
import json
|
||||
import yaml
|
||||
import time
|
||||
from rich import print
|
||||
from dotenv import load_dotenv
|
||||
import shutil
|
||||
import subprocess
|
||||
import logging
|
||||
import importlib
|
||||
|
||||
from praisonai_code._version import get_package_version
|
||||
from praisonai_code._framework_availability import is_available
|
||||
from praisonai_code._logging import configure_cli_logging
|
||||
from .legacy.env_security import (
|
||||
_BLOCKED_ENV_KEYS,
|
||||
_BLOCKED_ENV_KEYS_UPPER,
|
||||
_load_env_once,
|
||||
_validate_env_key,
|
||||
)
|
||||
|
||||
__version__ = get_package_version()
|
||||
|
||||
if os.environ.get('LOGLEVEL', 'INFO').upper() != 'DEBUG':
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=".*found in sys.modules after import of package.*",
|
||||
category=RuntimeWarning
|
||||
)
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
helpers = "".join(all_lines[94:173]) + "".join(all_lines[228:346])
|
||||
helpers = helpers.replace(
|
||||
'return import_wrapper_module("praisonai.inbuilt_tools")',
|
||||
'return import_wrapper_module("praisonai.cli.legacy.inbuilt_tools")',
|
||||
)
|
||||
avail_section = "".join(all_lines[174:227])
|
||||
|
||||
praison_content = header + helpers + avail_section + text
|
||||
PRAISON_AI.parent.mkdir(parents=True, exist_ok=True)
|
||||
PRAISON_AI.write_text(praison_content, encoding="utf-8")
|
||||
|
||||
|
||||
def write_thin_main() -> None:
|
||||
content = textwrap.dedent(
|
||||
'''\
|
||||
# praisonai/cli/main.py — thin re-export layer (C8.4)
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import warnings
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import importlib
|
||||
|
||||
from ._warnings import install_warning_filters, _uninstall_warning_filters, _SUPPRESSED_PATTERNS
|
||||
from praisonai_code._version import get_package_version
|
||||
|
||||
__version__ = get_package_version()
|
||||
|
||||
if os.environ.get('LOGLEVEL', 'INFO').upper() != 'DEBUG':
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=".*found in sys.modules after import of package.*",
|
||||
category=RuntimeWarning
|
||||
)
|
||||
|
||||
from .legacy.env_security import (
|
||||
_BLOCKED_ENV_KEYS,
|
||||
_BLOCKED_ENV_KEYS_UPPER,
|
||||
_load_env_once,
|
||||
_validate_env_key,
|
||||
)
|
||||
from .legacy.praison_ai import PraisonAI
|
||||
|
||||
from praisonai_code._framework_availability import is_available
|
||||
|
||||
_AVAILABILITY_FLAGS = {
|
||||
"GRADIO_AVAILABLE": "gradio",
|
||||
"CREWAI_AVAILABLE": "crewai",
|
||||
"AUTOGEN_AVAILABLE": "autogen",
|
||||
"PRAISONAI_AVAILABLE": "praisonaiagents",
|
||||
"TRAIN_AVAILABLE": "unsloth",
|
||||
}
|
||||
|
||||
|
||||
def _compute_availability_flag(name):
|
||||
if name in _AVAILABILITY_FLAGS:
|
||||
return is_available(_AVAILABILITY_FLAGS[name])
|
||||
if name == "CALL_MODULE_AVAILABLE":
|
||||
try:
|
||||
import importlib.util
|
||||
return importlib.util.find_spec("praisonai.api.call") is not None
|
||||
except (ModuleNotFoundError, AttributeError):
|
||||
return False
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
def _ensure_availability_flags():
|
||||
g = globals()
|
||||
for flag in (*_AVAILABILITY_FLAGS, "CALL_MODULE_AVAILABLE"):
|
||||
if flag not in g:
|
||||
g[flag] = _compute_availability_flag(flag)
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name in _AVAILABILITY_FLAGS or name == "CALL_MODULE_AVAILABLE":
|
||||
value = _compute_availability_flag(name)
|
||||
globals()[name] = value
|
||||
return value
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
def _get_inbuilt_tools():
|
||||
from praisonai_code._wrapper_bridge import import_wrapper_module
|
||||
return import_wrapper_module("praisonai.cli.legacy.inbuilt_tools")
|
||||
|
||||
|
||||
def _get_generate_config():
|
||||
from praisonai_code._wrapper_bridge import import_wrapper_module
|
||||
_mod = import_wrapper_module("praisonai.inc.config")
|
||||
return getattr(_mod, "generate_config")
|
||||
|
||||
|
||||
def _get_auto_generator():
|
||||
from praisonai_code._wrapper_bridge import import_wrapper_module
|
||||
_mod = import_wrapper_module("praisonai.auto")
|
||||
return getattr(_mod, "AutoGenerator")
|
||||
|
||||
|
||||
def _get_agents_generator():
|
||||
from praisonai_code._wrapper_bridge import import_wrapper_module
|
||||
_mod = import_wrapper_module("praisonai.agents_generator")
|
||||
return getattr(_mod, "AgentsGenerator")
|
||||
|
||||
|
||||
def _get_call_module():
|
||||
import importlib.util
|
||||
if not importlib.util.find_spec("praisonai.api.call"):
|
||||
raise ImportError(
|
||||
'Call feature is not installed. Install with: pip install "praisonai[call]"'
|
||||
)
|
||||
from praisonai_code._wrapper_bridge import import_wrapper_module
|
||||
_mod = import_wrapper_module("praisonai.api")
|
||||
return getattr(_mod, "call")
|
||||
|
||||
|
||||
def _fw_registry_module():
|
||||
from praisonai_code._wrapper_bridge import import_wrapper_module
|
||||
return import_wrapper_module("praisonai.framework_adapters.registry")
|
||||
|
||||
|
||||
def _fw_validators_module():
|
||||
from praisonai_code._wrapper_bridge import import_wrapper_module
|
||||
return import_wrapper_module("praisonai.framework_adapters.validators")
|
||||
|
||||
|
||||
def _fw_workflow_module():
|
||||
from praisonai_code._wrapper_bridge import import_wrapper_module
|
||||
return import_wrapper_module("praisonai.framework_adapters.workflow_framework")
|
||||
|
||||
|
||||
def _provider_preflight_message():
|
||||
try:
|
||||
from praisonai_code.llm.credentials import inject_credentials_into_env, is_configured
|
||||
inject_credentials_into_env()
|
||||
runtime_model = os.environ.get("MODEL_NAME") or os.environ.get("OPENAI_MODEL_NAME")
|
||||
if is_configured(model=runtime_model):
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
return (
|
||||
"No LLM provider is configured.\\n\\n"
|
||||
"PraisonAI supports OpenAI, Anthropic, Google/Gemini, Groq, "
|
||||
"Cohere, Ollama, OpenRouter and 100+ models via LiteLLM.\\n\\n"
|
||||
"Easiest setup (interactive, no shell 'export' needed):\\n"
|
||||
" praisonai setup\\n\\n"
|
||||
"Or set a provider API key, for example:\\n"
|
||||
" export OPENAI_API_KEY=... # OpenAI\\n"
|
||||
" export ANTHROPIC_API_KEY=... # Anthropic Claude\\n"
|
||||
" export GEMINI_API_KEY=... # Google Gemini\\n"
|
||||
)
|
||||
|
||||
|
||||
def stream_subprocess(command, env=None):
|
||||
"""Execute a subprocess and stream output to the terminal."""
|
||||
import subprocess
|
||||
from rich import print
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
env=env,
|
||||
)
|
||||
for line in iter(process.stdout.readline, ''):
|
||||
print(line, end='')
|
||||
sys.stdout.flush()
|
||||
process.stdout.close()
|
||||
return_code = process.wait()
|
||||
if return_code != 0:
|
||||
raise subprocess.CalledProcessError(return_code, command)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
install_warning_filters()
|
||||
praison_ai = PraisonAI()
|
||||
praison_ai.main()
|
||||
'''
|
||||
)
|
||||
THIN_MAIN.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
all_lines = read_lines()
|
||||
write_env_security(all_lines)
|
||||
write_subcommand_handlers(all_lines)
|
||||
write_workflow_commands(all_lines)
|
||||
write_direct_prompt(all_lines)
|
||||
write_argparse_builder(all_lines)
|
||||
write_interactive_legacy(all_lines)
|
||||
write_praison_ai(all_lines)
|
||||
write_thin_main()
|
||||
print("C8.4 decomposition complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,298 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Real-API smoke tests for C8.4 main.py decomposition paths."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
MODEL = os.environ.get("TEST_MODEL", os.environ.get("OPENAI_MODEL_NAME", "gpt-4o-mini"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
name: str
|
||||
ok: bool
|
||||
detail: str
|
||||
seconds: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Suite:
|
||||
results: list[Result] = field(default_factory=list)
|
||||
|
||||
def add(self, r: Result) -> None:
|
||||
self.results.append(r)
|
||||
mark = "PASS" if r.ok else "FAIL"
|
||||
print(f"[{mark}] {r.name} ({r.seconds:.1f}s) — {r.detail[:140]}")
|
||||
|
||||
def exit_code(self) -> int:
|
||||
passed = sum(1 for r in self.results if r.ok)
|
||||
failed = len(self.results) - passed
|
||||
print("\n" + "=" * 72)
|
||||
print(f"C8.4 Real-API Smoke: {passed}/{len(self.results)} passed, {failed} failed")
|
||||
for r in self.results:
|
||||
if not r.ok:
|
||||
print(f" FAIL: {r.name}: {r.detail[:200]}")
|
||||
print("=" * 72)
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
def env_wrapper() -> dict[str, str]:
|
||||
base = {k: v for k, v in os.environ.items() if not k.startswith("PYTEST_")}
|
||||
agents = str(REPO / "src" / "praisonai-agents")
|
||||
code = str(REPO / "src" / "praisonai-code")
|
||||
wrapper = str(REPO / "src" / "praisonai")
|
||||
base["PYTHONPATH"] = f"{agents}{os.pathsep}{code}{os.pathsep}{wrapper}"
|
||||
base.setdefault("LOGLEVEL", "WARNING")
|
||||
return base
|
||||
|
||||
|
||||
def env_code_only() -> dict[str, str]:
|
||||
base = {k: v for k, v in os.environ.items() if not k.startswith("PYTEST_")}
|
||||
agents = str(REPO / "src" / "praisonai-agents")
|
||||
code = str(REPO / "src" / "praisonai-code")
|
||||
base["PYTHONPATH"] = f"{agents}{os.pathsep}{code}"
|
||||
base.setdefault("LOGLEVEL", "WARNING")
|
||||
return base
|
||||
|
||||
|
||||
def run_py(script: str, *, env: dict[str, str], timeout: int = 120, expect: str | None = None) -> tuple[bool, str]:
|
||||
try:
|
||||
p = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
cwd=str(REPO),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
out = (p.stdout or "") + (p.stderr or "")
|
||||
ok = p.returncode == 0
|
||||
if expect and expect not in out:
|
||||
ok = False
|
||||
out += f"\n(missing {expect!r})"
|
||||
return ok, out.strip()[-600:]
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"timeout after {timeout}s"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def run_cmd(cmd: list[str], *, env: dict[str, str], timeout: int = 120, expect: str | None = None) -> tuple[bool, str]:
|
||||
try:
|
||||
p = subprocess.run(cmd, cwd=str(REPO), env=env, capture_output=True, text=True, timeout=timeout)
|
||||
out = (p.stdout or "") + (p.stderr or "")
|
||||
ok = p.returncode == 0
|
||||
if expect and expect not in out:
|
||||
ok = False
|
||||
out += f"\n(missing {expect!r})"
|
||||
return ok, out.strip()[-600:]
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"timeout after {timeout}s"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("ERROR: OPENAI_API_KEY not set (source ~/.zshrc or ~/.bashrc)")
|
||||
return 1
|
||||
|
||||
suite = Suite()
|
||||
wenv = env_wrapper()
|
||||
cenv = env_code_only()
|
||||
py = sys.executable
|
||||
|
||||
print("=" * 72)
|
||||
print("C8.4 Real-API Smoke — delegate paths, legacy CLI, Typer hot path")
|
||||
print(f"Model: {MODEL}")
|
||||
print("=" * 72)
|
||||
|
||||
# --- Module identity & delegates (no LLM) ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_py(
|
||||
"""
|
||||
import praisonai.cli.main as old
|
||||
import praisonai_code.cli.main as new
|
||||
from praisonai.cli.main import PraisonAI
|
||||
assert old is new
|
||||
assert hasattr(PraisonAI, 'handle_direct_prompt')
|
||||
assert hasattr(PraisonAI, '_start_interactive_mode')
|
||||
assert hasattr(PraisonAI, 'handle_memory_command')
|
||||
print('IDENTITY_OK')
|
||||
""",
|
||||
env=wenv,
|
||||
expect="IDENTITY_OK",
|
||||
)
|
||||
suite.add(Result("C5 module identity + C8.4 delegates", ok, detail, time.time() - t0))
|
||||
|
||||
# --- direct_prompt delegate (real LLM) ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_py(
|
||||
f"""
|
||||
import os
|
||||
os.environ['LOGLEVEL'] = 'ERROR'
|
||||
from praisonai.cli.main import PraisonAI
|
||||
import argparse
|
||||
p = PraisonAI()
|
||||
p.args = argparse.Namespace(
|
||||
llm={MODEL!r}, verbose=0, memory=False, planning=False, workflow=None,
|
||||
profile=False, no_tools=True, toolset=None, web=False, stream=False,
|
||||
query_rewrite=False, expand_prompt=False, output_format=None,
|
||||
)
|
||||
out = p.handle_direct_prompt('Reply with exactly: C84_DIRECT_OK')
|
||||
text = str(out)
|
||||
print(text)
|
||||
assert 'C84_DIRECT_OK' in text, text[:200]
|
||||
print('DIRECT_OK')
|
||||
""",
|
||||
env=wenv,
|
||||
timeout=90,
|
||||
expect="DIRECT_OK",
|
||||
)
|
||||
suite.add(Result("handle_direct_prompt delegate (legacy import path)", ok, detail, time.time() - t0))
|
||||
|
||||
# --- praison_ai direct (code module path) ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_py(
|
||||
f"""
|
||||
import os
|
||||
os.environ['LOGLEVEL'] = 'ERROR'
|
||||
from praisonai_code.cli.legacy.praison_ai import PraisonAI
|
||||
import argparse
|
||||
p = PraisonAI()
|
||||
p.args = argparse.Namespace(
|
||||
llm={MODEL!r}, verbose=0, memory=False, planning=False, workflow=None,
|
||||
profile=False, no_tools=True, toolset=None, web=False, stream=False,
|
||||
query_rewrite=False, expand_prompt=False, output_format=None,
|
||||
)
|
||||
out = p.handle_direct_prompt('Reply with exactly: C84_PRAISON_AI_OK')
|
||||
assert 'C84_PRAISON_AI_OK' in str(out)
|
||||
print('PRAISON_AI_OK')
|
||||
""",
|
||||
env=wenv,
|
||||
timeout=90,
|
||||
expect="PRAISON_AI_OK",
|
||||
)
|
||||
suite.add(Result("handle_direct_prompt via praison_ai.py", ok, detail, time.time() - t0))
|
||||
|
||||
# --- Typer run (code tier standalone) ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(
|
||||
[py, "-m", "praisonai_code", "run", "-m", MODEL, "Reply exactly: C84_CODE_RUN_OK"],
|
||||
env=cenv,
|
||||
timeout=90,
|
||||
expect="C84_CODE_RUN_OK",
|
||||
)
|
||||
suite.add(Result("Typer praisonai-code run (standalone)", ok, detail, time.time() - t0))
|
||||
|
||||
# --- Typer code command (single prompt) ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(
|
||||
[py, "-m", "praisonai_code", "code", "-m", MODEL, "Reply exactly: C84_CODE_CODE_OK"],
|
||||
env=cenv,
|
||||
timeout=90,
|
||||
expect="C84_CODE_CODE_OK",
|
||||
)
|
||||
suite.add(Result("Typer praisonai-code code (standalone)", ok, detail, time.time() - t0))
|
||||
|
||||
# --- Typer run (wrapper) ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(
|
||||
[py, "-m", "praisonai", "run", "-m", MODEL, "Reply exactly: C84_WRAPPER_RUN_OK"],
|
||||
env=wenv,
|
||||
timeout=90,
|
||||
expect="C84_WRAPPER_RUN_OK",
|
||||
)
|
||||
suite.add(Result("Typer praisonai run (wrapper)", ok, detail, time.time() - t0))
|
||||
|
||||
# --- Legacy CLI main direct prompt ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(
|
||||
[py, "-m", "praisonai.cli.main", "--llm", MODEL, "--no-tools", "Reply exactly: C84_LEGACY_MAIN_OK"],
|
||||
env=wenv,
|
||||
timeout=90,
|
||||
expect="C84_LEGACY_MAIN_OK",
|
||||
)
|
||||
suite.add(Result("Legacy python -m praisonai.cli.main prompt", ok, detail, time.time() - t0))
|
||||
|
||||
# --- Memory handler delegate (no LLM) ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(
|
||||
[py, "-m", "praisonai", "memory", "show"],
|
||||
env=wenv,
|
||||
timeout=60,
|
||||
)
|
||||
suite.add(Result("memory show (subcommand_handlers delegate)", ok, detail, time.time() - t0))
|
||||
|
||||
# --- Workflow list delegate ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(
|
||||
[py, "-m", "praisonai", "workflow", "list"],
|
||||
env=wenv,
|
||||
timeout=60,
|
||||
)
|
||||
suite.add(Result("workflow list (workflow_commands delegate)", ok, detail, time.time() - t0))
|
||||
|
||||
# --- Legacy YAML multi-agent (uses praison_ai main flow) ---
|
||||
t0 = time.time()
|
||||
yaml_content = textwrap.dedent(
|
||||
f"""
|
||||
framework: praisonai
|
||||
llm: {MODEL}
|
||||
agents:
|
||||
- name: analyst
|
||||
role: Analyst
|
||||
goal: Compute 3+5 and state the number only
|
||||
instructions: Reply with the numeric answer only.
|
||||
- name: writer
|
||||
role: Writer
|
||||
goal: Summarise analyst output ending with C84_YAML_OK
|
||||
instructions: One sentence summary ending with C84_YAML_OK
|
||||
tasks:
|
||||
- description: What is 3+5?
|
||||
agent: analyst
|
||||
- description: Summarise the analyst result. End with C84_YAML_OK.
|
||||
agent: writer
|
||||
"""
|
||||
).strip()
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
|
||||
f.write(yaml_content)
|
||||
ypath = f.name
|
||||
try:
|
||||
ok, detail = run_cmd(
|
||||
[py, "-m", "praisonai.cli.main", "--framework", "praisonai", ypath],
|
||||
env=wenv,
|
||||
timeout=180,
|
||||
expect="C84_YAML_OK",
|
||||
)
|
||||
if not ok and re.search(r"\b8\b", detail):
|
||||
ok = True
|
||||
detail = "YAML workflow produced numeric output"
|
||||
suite.add(Result("Legacy YAML via praison_ai.main()", ok, detail, time.time() - t0))
|
||||
finally:
|
||||
Path(ypath).unlink(missing_ok=True)
|
||||
|
||||
# --- Gates ---
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(["bash", "scripts/check_main_py_lines.sh"], env=wenv)
|
||||
suite.add(Result("check_main_py_lines.sh", ok, detail, time.time() - t0))
|
||||
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(["bash", "scripts/check_c7_imports.sh"], env=wenv)
|
||||
suite.add(Result("check_c7_imports.sh", ok, detail, time.time() - t0))
|
||||
|
||||
return suite.exit_code()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert direct ``from praisonai`` imports to bridge calls in a file."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Match: from praisonai.foo.bar import A, B
|
||||
FROM_IMPORT = re.compile(
|
||||
r"^(\s*)from praisonai(\.[\w.]+)? import (.+)$"
|
||||
)
|
||||
# Match: from praisonai import foo
|
||||
FROM_ROOT = re.compile(
|
||||
r"^(\s*)from praisonai import (.+)$"
|
||||
)
|
||||
|
||||
|
||||
def _convert_from_line(line: str) -> str | None:
|
||||
m = FROM_ROOT.match(line)
|
||||
if m:
|
||||
indent, names = m.groups()
|
||||
mod = "praisonai"
|
||||
return (
|
||||
f"{indent}from praisonai_code._wrapper_bridge import import_wrapper_module\n"
|
||||
f"{indent}_mod = import_wrapper_module({mod!r})\n"
|
||||
f"{indent}({names}) = tuple(getattr(_mod, n.strip()) for n in {names!r}.split(','))"
|
||||
)
|
||||
m = FROM_IMPORT.match(line)
|
||||
if not m:
|
||||
return None
|
||||
indent, suffix, names = m.groups()
|
||||
mod = f"praisonai{suffix or ''}"
|
||||
# Handle ``import x as y``
|
||||
parts = [p.strip() for p in names.split(",")]
|
||||
assigns = []
|
||||
for part in parts:
|
||||
if " as " in part:
|
||||
orig, alias = part.split(" as ", 1)
|
||||
orig, alias = orig.strip(), alias.strip()
|
||||
assigns.append(f"{alias} = getattr(_mod, {orig!r})")
|
||||
else:
|
||||
assigns.append(f"{part} = getattr(_mod, {part!r})")
|
||||
body = "\n".join(f"{indent}{a}" for a in assigns)
|
||||
return (
|
||||
f"{indent}from praisonai_code._wrapper_bridge import import_wrapper_module\n"
|
||||
f"{indent}_mod = import_wrapper_module({mod!r})\n"
|
||||
f"{body}"
|
||||
)
|
||||
|
||||
|
||||
def convert_file(path: Path) -> int:
|
||||
text = path.read_text()
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
changed = 0
|
||||
for line in lines:
|
||||
if line.strip().startswith("#") and "from praisonai" in line:
|
||||
out.append(line)
|
||||
continue
|
||||
if "from praisonai" in line and "praisonai_code" not in line and "praisonaiagents" not in line:
|
||||
new = _convert_from_line(line)
|
||||
if new:
|
||||
out.append(new)
|
||||
changed += 1
|
||||
continue
|
||||
out.append(line)
|
||||
if changed:
|
||||
path.write_text("\n".join(out) + ("\n" if text.endswith("\n") else ""))
|
||||
return changed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
total = 0
|
||||
for p in sys.argv[1:]:
|
||||
n = convert_file(Path(p))
|
||||
print(f"{p}: {n} lines converted")
|
||||
total += n
|
||||
print(f"total: {total}")
|
||||
@@ -1,393 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""In-depth real-API smoke tests for all three PraisonAI tiers (post C8 release)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
ENV_FILE = REPO / ".env"
|
||||
MODEL = os.environ.get("TEST_MODEL", "gpt-4o-mini")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
name: str
|
||||
tier: str
|
||||
ok: bool
|
||||
detail: str
|
||||
seconds: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Suite:
|
||||
results: list[Result] = field(default_factory=list)
|
||||
|
||||
def add(self, r: Result) -> None:
|
||||
self.results.append(r)
|
||||
mark = "PASS" if r.ok else "FAIL"
|
||||
print(f"[{mark}] [{r.tier}] {r.name} ({r.seconds:.1f}s) — {r.detail[:120]}")
|
||||
|
||||
def summary(self) -> int:
|
||||
passed = sum(1 for r in self.results if r.ok)
|
||||
failed = len(self.results) - passed
|
||||
print("\n" + "=" * 72)
|
||||
print(f"TOTAL: {passed}/{len(self.results)} passed, {failed} failed")
|
||||
for r in self.results:
|
||||
if not r.ok:
|
||||
print(f" FAIL: [{r.tier}] {r.name}: {r.detail}")
|
||||
print("=" * 72)
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
def load_env() -> None:
|
||||
if ENV_FILE.exists():
|
||||
for line in ENV_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
||||
|
||||
|
||||
def has_openai() -> bool:
|
||||
return bool(os.environ.get("OPENAI_API_KEY"))
|
||||
|
||||
|
||||
def run_cmd(
|
||||
cmd: list[str],
|
||||
*,
|
||||
env: dict[str, str],
|
||||
timeout: int = 120,
|
||||
expect_in_output: Optional[str] = None,
|
||||
) -> tuple[bool, str]:
|
||||
try:
|
||||
p = subprocess.run(
|
||||
cmd,
|
||||
cwd=str(REPO),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
out = (p.stdout or "") + (p.stderr or "")
|
||||
ok = p.returncode == 0
|
||||
if expect_in_output and expect_in_output not in out:
|
||||
ok = False
|
||||
out += f"\n(missing expected substring: {expect_in_output!r})"
|
||||
if not ok and p.returncode != 0:
|
||||
out += f"\n(exit {p.returncode})"
|
||||
return ok, out.strip()[-800:]
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"timeout after {timeout}s"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def tier_env(tier: str) -> dict[str, str]:
|
||||
base = {k: v for k, v in os.environ.items() if not k.startswith("PYTEST_")}
|
||||
agents = str(REPO / "src" / "praisonai-agents")
|
||||
code = str(REPO / "src" / "praisonai-code")
|
||||
wrapper = str(REPO / "src" / "praisonai")
|
||||
if tier == "sdk":
|
||||
base["PYTHONPATH"] = agents
|
||||
elif tier == "code":
|
||||
base["PYTHONPATH"] = f"{agents}{os.pathsep}{code}"
|
||||
elif tier == "wrapper":
|
||||
base["PYTHONPATH"] = f"{agents}{os.pathsep}{code}{os.pathsep}{wrapper}"
|
||||
else:
|
||||
raise ValueError(tier)
|
||||
return base
|
||||
|
||||
|
||||
def test_sdk_single_agent(suite: Suite) -> None:
|
||||
t0 = time.time()
|
||||
sys.path.insert(0, str(REPO / "src" / "praisonai-agents"))
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="smoke",
|
||||
instructions="Reply with exactly the token given. No extra words.",
|
||||
llm=MODEL,
|
||||
)
|
||||
out = agent.start("Reply with exactly: SDK_SINGLE_OK")
|
||||
text = str(out).strip()
|
||||
ok = "SDK_SINGLE_OK" in text
|
||||
suite.add(Result("single Agent.start()", "sdk", ok, text[:200], time.time() - t0))
|
||||
except Exception as e:
|
||||
suite.add(Result("single Agent.start()", "sdk", False, str(e), time.time() - t0))
|
||||
finally:
|
||||
if sys.path[0].endswith("praisonai-agents"):
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def test_sdk_multi_agent_team(suite: Suite) -> None:
|
||||
t0 = time.time()
|
||||
sys.path.insert(0, str(REPO / "src" / "praisonai-agents"))
|
||||
try:
|
||||
from praisonaiagents import Agent, AgentTeam, Task
|
||||
|
||||
researcher = Agent(
|
||||
name="researcher",
|
||||
instructions="You are a researcher. Answer in one short sentence with a number.",
|
||||
llm=MODEL,
|
||||
)
|
||||
writer = Agent(
|
||||
name="writer",
|
||||
instructions="You are a writer. Use the prior task output. End with exactly: TEAM_OK",
|
||||
llm=MODEL,
|
||||
)
|
||||
t1 = Task(
|
||||
name="research",
|
||||
description="What is 2+2? Reply with just the number.",
|
||||
agent=researcher,
|
||||
)
|
||||
t2 = Task(
|
||||
name="write",
|
||||
description="Say 'Result is' plus the research answer. End with TEAM_OK.",
|
||||
agent=writer,
|
||||
)
|
||||
team = AgentTeam(agents=[researcher, writer], tasks=[t1, t2])
|
||||
out = team.start()
|
||||
text = str(out).strip()
|
||||
ok = "TEAM_OK" in text or "4" in text
|
||||
suite.add(Result("AgentTeam 2-agent workflow", "sdk", ok, text[:300], time.time() - t0))
|
||||
except Exception as e:
|
||||
suite.add(Result("AgentTeam 2-agent workflow", "sdk", False, str(e), time.time() - t0))
|
||||
finally:
|
||||
if sys.path[0].endswith("praisonai-agents"):
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def test_sdk_handoff(suite: Suite) -> None:
|
||||
t0 = time.time()
|
||||
sys.path.insert(0, str(REPO / "src" / "praisonai-agents"))
|
||||
try:
|
||||
from praisonaiagents import Agent, handoff
|
||||
|
||||
billing = Agent(
|
||||
name="billing",
|
||||
instructions="Billing agent. If asked about billing, reply: BILLING_HANDOFF_OK",
|
||||
llm=MODEL,
|
||||
)
|
||||
triage = Agent(
|
||||
name="triage",
|
||||
instructions="Triage agent. For billing questions, hand off to billing agent.",
|
||||
llm=MODEL,
|
||||
handoffs=[handoff(billing)],
|
||||
)
|
||||
out = triage.start("I have a billing question about my invoice.")
|
||||
text = str(out).strip()
|
||||
ok = "BILLING_HANDOFF_OK" in text or "billing" in text.lower()
|
||||
suite.add(Result("Agent handoff billing", "sdk", ok, text[:300], time.time() - t0))
|
||||
except Exception as e:
|
||||
suite.add(Result("Agent handoff billing", "sdk", False, str(e), time.time() - t0))
|
||||
finally:
|
||||
if sys.path[0].endswith("praisonai-agents"):
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def test_sdk_parallel_team(suite: Suite) -> None:
|
||||
"""Three-agent parallel async AgentTeam."""
|
||||
t0 = time.time()
|
||||
sys.path.insert(0, str(REPO / "src" / "praisonai-agents"))
|
||||
try:
|
||||
from praisonaiagents import Agent, AgentTeam, Task
|
||||
|
||||
workers = [
|
||||
Agent(name=f"worker{i}", instructions=f"Say exactly WORKER_{i}_OK", llm=MODEL)
|
||||
for i in (1, 2, 3)
|
||||
]
|
||||
tasks = [
|
||||
Task(
|
||||
name=f"t{i}",
|
||||
description=f"Say WORKER_{i}_OK",
|
||||
agent=workers[i - 1],
|
||||
async_execution=True,
|
||||
is_start=True,
|
||||
)
|
||||
for i in (1, 2, 3)
|
||||
]
|
||||
agg = Agent(
|
||||
name="agg",
|
||||
instructions="Summarise worker outputs; include PARALLEL3_OK",
|
||||
llm=MODEL,
|
||||
)
|
||||
tasks.append(
|
||||
Task(
|
||||
name="aggregate",
|
||||
description="Summarise with PARALLEL3_OK",
|
||||
agent=agg,
|
||||
)
|
||||
)
|
||||
team = AgentTeam(agents=workers + [agg], tasks=tasks)
|
||||
text = str(team.start()).strip()
|
||||
ok = "PARALLEL3_OK" in text or "WORKER_1_OK" in text
|
||||
suite.add(Result("AgentTeam 3-agent parallel", "sdk", ok, text[:300], time.time() - t0))
|
||||
except Exception as e:
|
||||
suite.add(Result("AgentTeam 3-agent parallel", "sdk", False, str(e), time.time() - t0))
|
||||
finally:
|
||||
if sys.path[0].endswith("praisonai-agents"):
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def cli_test(suite: Suite, tier: str, name: str, cmd: list[str], expect: str) -> None:
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(cmd, env=tier_env(tier), expect_in_output=expect)
|
||||
suite.add(Result(name, tier, ok, detail, time.time() - t0))
|
||||
|
||||
|
||||
def test_wrapper_agent_team_python(suite: Suite) -> None:
|
||||
"""AgentTeam via wrapper PYTHONPATH (full stack import)."""
|
||||
t0 = time.time()
|
||||
env = tier_env("wrapper")
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
from praisonaiagents import Agent, AgentTeam, Task
|
||||
MODEL = {MODEL!r}
|
||||
a = Agent(name='math', instructions='Return only the number.', llm=MODEL)
|
||||
b = Agent(name='echo', instructions='Append WRAPPER_TEAM_OK', llm=MODEL)
|
||||
team = AgentTeam(
|
||||
agents=[a, b],
|
||||
tasks=[
|
||||
Task(name='t1', description='What is 7+3?', agent=a),
|
||||
Task(name='t2', description='Echo prior; end WRAPPER_TEAM_OK', agent=b),
|
||||
],
|
||||
)
|
||||
print(team.start())
|
||||
"""
|
||||
).strip()
|
||||
ok, detail = run_cmd([sys.executable, "-c", script], env=env, expect_in_output="WRAPPER_TEAM_OK")
|
||||
suite.add(Result("wrapper-path AgentTeam", "wrapper", ok, detail[:300], time.time() - t0))
|
||||
|
||||
|
||||
def test_code_no_wrapper_import(suite: Suite) -> None:
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(
|
||||
["bash", "scripts/check_c7_imports.sh"],
|
||||
env=tier_env("code"),
|
||||
)
|
||||
suite.add(Result("C7 import gate (0 wrapper lines)", "code", ok, detail.splitlines()[-3:], time.time() - t0))
|
||||
|
||||
|
||||
def test_wrapper_repatriated_commands(suite: Suite) -> None:
|
||||
for cmd_name in ("langfuse", "train", "replay", "docs", "mcp"):
|
||||
t0 = time.time()
|
||||
ok, detail = run_cmd(
|
||||
[sys.executable, "-m", "praisonai", cmd_name, "--help"],
|
||||
env=tier_env("wrapper"),
|
||||
)
|
||||
suite.add(Result(f"repatriated `{cmd_name} --help`", "wrapper", ok, detail[:150], time.time() - t0))
|
||||
|
||||
|
||||
def test_wrapper_multi_agent_yaml(suite: Suite) -> None:
|
||||
"""Run sequential multi-agent YAML via legacy praisonai.cli.main."""
|
||||
t0 = time.time()
|
||||
yaml_content = textwrap.dedent(
|
||||
f"""
|
||||
framework: praisonai
|
||||
llm: {MODEL}
|
||||
agents:
|
||||
- name: analyst
|
||||
role: Analyst
|
||||
goal: Compute 3+5 and state the number only
|
||||
instructions: Reply with the numeric answer only.
|
||||
- name: summariser
|
||||
role: Summariser
|
||||
goal: Summarise analyst output ending with YAML_MULTI_OK
|
||||
instructions: One sentence summary ending with YAML_MULTI_OK
|
||||
tasks:
|
||||
- description: What is 3+5?
|
||||
agent: analyst
|
||||
- description: Summarise the analyst result. End with YAML_MULTI_OK.
|
||||
agent: summariser
|
||||
"""
|
||||
).strip()
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
|
||||
f.write(yaml_content)
|
||||
path = f.name
|
||||
try:
|
||||
ok, detail = run_cmd(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"praisonai.cli.main",
|
||||
"--framework",
|
||||
"praisonai",
|
||||
"--file",
|
||||
path,
|
||||
"run workflow",
|
||||
],
|
||||
env=tier_env("wrapper"),
|
||||
timeout=180,
|
||||
expect_in_output="YAML_MULTI_OK",
|
||||
)
|
||||
if not ok and "8" in detail:
|
||||
ok = True
|
||||
detail = "Numeric workflow output (8) from legacy YAML path"
|
||||
suite.add(Result("legacy YAML multi-agent workflow", "wrapper", ok, detail[:400], time.time() - t0))
|
||||
finally:
|
||||
Path(path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
load_env()
|
||||
suite = Suite()
|
||||
|
||||
print("=" * 72)
|
||||
print("PraisonAI C8 Release — Real API In-Depth Smoke Tests")
|
||||
print(f"Model: {MODEL} | OpenAI configured: {has_openai()}")
|
||||
print(f"Repo: {REPO}")
|
||||
try:
|
||||
import praisonaiagents as pa
|
||||
|
||||
print(f"SDK path: {pa.__file__}")
|
||||
except Exception:
|
||||
pass
|
||||
print("=" * 72)
|
||||
|
||||
if not has_openai():
|
||||
print("ERROR: OPENAI_API_KEY not set (check .env)")
|
||||
return 1
|
||||
|
||||
# Tier 1 — Core SDK only
|
||||
print("\n--- Tier 1: praisonaiagents (SDK only) ---")
|
||||
test_sdk_single_agent(suite)
|
||||
test_sdk_multi_agent_team(suite)
|
||||
test_sdk_parallel_team(suite)
|
||||
test_sdk_handoff(suite)
|
||||
|
||||
# Tier 2 — praisonai-code standalone (no wrapper on PYTHONPATH)
|
||||
print("\n--- Tier 2: praisonai-code (standalone) ---")
|
||||
test_code_no_wrapper_import(suite)
|
||||
py = sys.executable
|
||||
cli_test(suite, "code", "code run LLM", [py, "-m", "praisonai_code", "run", "-m", MODEL, "Reply exactly: CODE_RUN_OK"], "CODE_RUN_OK")
|
||||
cli_test(suite, "code", "code assistant LLM", [py, "-m", "praisonai_code", "code", "-m", MODEL, "Reply exactly: CODE_CODE_OK"], "CODE_CODE_OK")
|
||||
cli_test(suite, "code", "code version", [py, "-m", "praisonai_code", "version"], "0.0.")
|
||||
cli_test(suite, "code", "code doctor env", [py, "-m", "praisonai_code", "doctor", "env"], "passed")
|
||||
|
||||
# Tier 3 — Full wrapper
|
||||
print("\n--- Tier 3: praisonai wrapper (full stack) ---")
|
||||
cli_test(suite, "wrapper", "wrapper run LLM", [py, "-m", "praisonai", "run", "-m", MODEL, "Reply exactly: WRAPPER_RUN_OK"], "WRAPPER_RUN_OK")
|
||||
cli_test(suite, "wrapper", "wrapper code LLM", [py, "-m", "praisonai", "code", "-m", MODEL, "Reply exactly: WRAPPER_CODE_OK"], "WRAPPER_CODE_OK")
|
||||
cli_test(suite, "wrapper", "wrapper version", [py, "-m", "praisonai", "version"], "4.6.")
|
||||
test_wrapper_repatriated_commands(suite)
|
||||
test_wrapper_agent_team_python(suite)
|
||||
cli_test(suite, "wrapper", "standardise bridge", [py, "-m", "praisonai", "standardise", "check", "--path", ".", "--scope", "cli"], "standardise")
|
||||
cli_test(suite, "wrapper", "serve help", [py, "-m", "praisonai", "serve", "--help"], "agents")
|
||||
test_wrapper_multi_agent_yaml(suite)
|
||||
|
||||
return suite.summary()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""C8 repatriation helper — move impl from praisonai-code to praisonai wrapper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
CODE = REPO / "src/praisonai-code/praisonai_code"
|
||||
WRAP = REPO / "src/praisonai/praisonai"
|
||||
|
||||
|
||||
def repatriate_file(code_rel: str, wrap_rel: str | None = None) -> None:
|
||||
"""Copy code file to wrapper and delete code copy."""
|
||||
wrap_rel = wrap_rel or code_rel
|
||||
src = CODE / code_rel
|
||||
dst = WRAP / wrap_rel
|
||||
if not src.exists():
|
||||
print(f"SKIP missing: {src}", file=sys.stderr)
|
||||
return
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
src.unlink()
|
||||
print(f"repatriated {code_rel} -> {wrap_rel}")
|
||||
|
||||
|
||||
def repatriate_command(name: str) -> None:
|
||||
repatriate_file(f"cli/commands/{name}.py")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("items", nargs="+", help="command names or code-relative paths")
|
||||
parser.add_argument("--feature", action="store_true", help="path under cli/features/")
|
||||
args = parser.parse_args()
|
||||
for item in args.items:
|
||||
if args.feature:
|
||||
repatriate_file(f"cli/features/{item}")
|
||||
elif "/" in item:
|
||||
repatriate_file(item)
|
||||
else:
|
||||
repatriate_command(item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +0,0 @@
|
||||
# Files allowed to reference praisonai_code directly (bridges / bootstrap only).
|
||||
_code_bridge.py
|
||||
_bootstrap.py
|
||||
_registry.py
|
||||
@@ -1,253 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mechanical migration: praisonai wrapper bot/gateway tree → praisonai_bot package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
WRAPPER = REPO / "src" / "praisonai" / "praisonai"
|
||||
BOT_PKG = REPO / "src" / "praisonai-bot" / "praisonai_bot"
|
||||
TESTS_SRC = REPO / "src" / "praisonai" / "tests"
|
||||
TESTS_DST = REPO / "src" / "praisonai-bot" / "tests"
|
||||
|
||||
# (source relative to WRAPPER, dest relative to BOT_PKG)
|
||||
DIR_COPIES = [
|
||||
"bots",
|
||||
"gateway",
|
||||
"daemon",
|
||||
"kanban",
|
||||
"claw",
|
||||
]
|
||||
|
||||
INTEGRATION_FILES = [
|
||||
"integration/gateway_host.py",
|
||||
"integration/host_app.py",
|
||||
"integration/context_files.py",
|
||||
"integration/pages/bot_health.py",
|
||||
"integration/bridges/kanban_bridge.py",
|
||||
]
|
||||
|
||||
CLI_COMMANDS = [
|
||||
"cli/commands/bot.py",
|
||||
"cli/commands/gateway.py",
|
||||
"cli/commands/pairing.py",
|
||||
"cli/commands/identity.py",
|
||||
"cli/commands/onboard.py",
|
||||
"cli/commands/kanban.py",
|
||||
"cli/commands/claw.py",
|
||||
"cli/commands/mint_link.py",
|
||||
]
|
||||
|
||||
CLI_FEATURES = [
|
||||
"cli/features/gateway.py",
|
||||
"cli/features/bots_cli.py",
|
||||
"cli/features/onboard.py",
|
||||
"cli/features/approval.py",
|
||||
]
|
||||
|
||||
CLI_LEGACY = [
|
||||
"cli/legacy/dispatch/legacy_dispatch.py",
|
||||
"cli/legacy/dispatch/argparse_builder.py",
|
||||
]
|
||||
|
||||
TOOLS_FILES = ["tools/audio.py"]
|
||||
|
||||
TEST_DIRS = [
|
||||
"unit/bots",
|
||||
"unit/gateway",
|
||||
"integration/bots",
|
||||
"integration/gateway",
|
||||
]
|
||||
|
||||
IMPORT_REPLACEMENTS = [
|
||||
(re.compile(r"\bfrom praisonai\.bots\b"), "from praisonai_bot.bots"),
|
||||
(re.compile(r"\bimport praisonai\.bots\b"), "import praisonai_bot.bots"),
|
||||
(re.compile(r"\bfrom praisonai\.gateway\b"), "from praisonai_bot.gateway"),
|
||||
(re.compile(r"\bimport praisonai\.gateway\b"), "import praisonai_bot.gateway"),
|
||||
(re.compile(r"\bfrom praisonai\.daemon\b"), "from praisonai_bot.daemon"),
|
||||
(re.compile(r"\bimport praisonai\.daemon\b"), "import praisonai_bot.daemon"),
|
||||
(re.compile(r"\bfrom praisonai\.kanban\b"), "from praisonai_bot.kanban"),
|
||||
(re.compile(r"\bimport praisonai\.kanban\b"), "import praisonai_bot.kanban"),
|
||||
(re.compile(r"\bfrom praisonai\.claw\b"), "from praisonai_bot.claw"),
|
||||
(re.compile(r"\bimport praisonai\.claw\b"), "import praisonai_bot.claw"),
|
||||
(re.compile(r"\bfrom praisonai\.integration\b"), "from praisonai_bot.integration"),
|
||||
(re.compile(r"\bimport praisonai\.integration\b"), "import praisonai_bot.integration"),
|
||||
(re.compile(r"\bfrom praisonai\.tools\.audio\b"), "from praisonai_bot.tools.audio"),
|
||||
(re.compile(r"\bfrom praisonai\.tool_resolver\b"), "from praisonai_code.tool_resolver"),
|
||||
(re.compile(r"\bfrom praisonai\.llm\.env\b"), "from praisonai_code.llm.env"),
|
||||
(re.compile(r"\bfrom praisonai\.cli\.features\.gateway\b"), "from praisonai_bot.cli.features.gateway"),
|
||||
(re.compile(r"\bfrom praisonai\.cli\.features\.bots_cli\b"), "from praisonai_bot.cli.features.bots_cli"),
|
||||
(re.compile(r"\bfrom praisonai\.cli\.features\.onboard\b"), "from praisonai_bot.cli.features.onboard"),
|
||||
(re.compile(r"\bfrom praisonai\.cli\.features\.approval\b"), "from praisonai_bot.cli.features.approval"),
|
||||
(re.compile(r"\bfrom praisonai\.cli\.commands\.bot\b"), "from praisonai_bot.cli.commands.bot"),
|
||||
(re.compile(r"\bfrom praisonai\.cli\.commands\.gateway\b"), "from praisonai_bot.cli.commands.gateway"),
|
||||
(re.compile(r"\bfrom praisonai\.cli\.legacy\.dispatch\b"), "from praisonai_bot.cli.legacy.dispatch"),
|
||||
]
|
||||
|
||||
SHIM_PACKAGES = [
|
||||
("praisonai.bots", "praisonai_bot.bots", WRAPPER / "bots"),
|
||||
("praisonai.gateway", "praisonai_bot.gateway", WRAPPER / "gateway"),
|
||||
("praisonai.daemon", "praisonai_bot.daemon", WRAPPER / "daemon"),
|
||||
("praisonai.kanban", "praisonai_bot.kanban", WRAPPER / "kanban"),
|
||||
("praisonai.claw", "praisonai_bot.claw", WRAPPER / "claw"),
|
||||
]
|
||||
|
||||
SHIM_SINGLE_FILES = [
|
||||
("praisonai.cli.commands.bot", "praisonai_bot.cli.commands.bot", WRAPPER / "cli/commands/bot.py"),
|
||||
("praisonai.cli.commands.gateway", "praisonai_bot.cli.commands.gateway", WRAPPER / "cli/commands/gateway.py"),
|
||||
("praisonai.cli.commands.pairing", "praisonai_bot.cli.commands.pairing", WRAPPER / "cli/commands/pairing.py"),
|
||||
("praisonai.cli.commands.identity", "praisonai_bot.cli.commands.identity", WRAPPER / "cli/commands/identity.py"),
|
||||
("praisonai.cli.commands.onboard", "praisonai_bot.cli.commands.onboard", WRAPPER / "cli/commands/onboard.py"),
|
||||
("praisonai.cli.commands.kanban", "praisonai_bot.cli.commands.kanban", WRAPPER / "cli/commands/kanban.py"),
|
||||
("praisonai.cli.commands.claw", "praisonai_bot.cli.commands.claw", WRAPPER / "cli/commands/claw.py"),
|
||||
("praisonai.cli.commands.mint_link", "praisonai_bot.cli.commands.mint_link", WRAPPER / "cli/commands/mint_link.py"),
|
||||
]
|
||||
|
||||
|
||||
def rewrite_imports(path: Path) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
original = text
|
||||
for pattern, repl in IMPORT_REPLACEMENTS:
|
||||
text = pattern.sub(repl, text)
|
||||
if text != original:
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def copy_tree(src: Path, dst: Path) -> None:
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
for py in dst.rglob("*.py"):
|
||||
rewrite_imports(py)
|
||||
|
||||
|
||||
def copy_file(src: Path, dst: Path) -> None:
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
rewrite_imports(dst)
|
||||
|
||||
|
||||
def write_shim_package(old_name: str, new_name: str, shim_dir: Path) -> None:
|
||||
shim_dir.mkdir(parents=True, exist_ok=True)
|
||||
init = shim_dir / "__init__.py"
|
||||
init.write_text(
|
||||
textwrap.dedent(
|
||||
f'''\
|
||||
"""Backward-compatibility shim for :mod:`{old_name}`."""
|
||||
from praisonai._bootstrap import ensure_praisonai_bot
|
||||
|
||||
ensure_praisonai_bot()
|
||||
from praisonai.cli._shim import alias_package
|
||||
|
||||
alias_package("{old_name}", "{new_name}")
|
||||
'''
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
for child in list(shim_dir.glob("*.py")):
|
||||
if child.name != "__init__.py":
|
||||
child.unlink()
|
||||
for child in list(shim_dir.iterdir()):
|
||||
if child.is_dir() and child.name != "__pycache__":
|
||||
shutil.rmtree(child)
|
||||
|
||||
|
||||
def write_shim_module(old_mod: str, new_mod: str, shim_path: Path) -> None:
|
||||
shim_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shim_path.write_text(
|
||||
textwrap.dedent(
|
||||
f'''\
|
||||
"""Backward-compatibility shim for :mod:`{old_mod}`."""
|
||||
import sys as _sys
|
||||
|
||||
from praisonai._bootstrap import ensure_praisonai_bot
|
||||
|
||||
ensure_praisonai_bot()
|
||||
import {new_mod} as _impl
|
||||
|
||||
_sys.modules[__name__] = _impl
|
||||
'''
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
BOT_PKG.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for name in DIR_COPIES:
|
||||
src = WRAPPER / name
|
||||
if src.is_dir():
|
||||
copy_tree(src, BOT_PKG / name)
|
||||
|
||||
for rel in INTEGRATION_FILES:
|
||||
copy_file(WRAPPER / rel, BOT_PKG / rel)
|
||||
|
||||
for rel in TOOLS_FILES:
|
||||
copy_file(WRAPPER / rel, BOT_PKG / rel)
|
||||
|
||||
for rel in CLI_COMMANDS:
|
||||
copy_file(WRAPPER / rel, BOT_PKG / rel)
|
||||
|
||||
for rel in CLI_FEATURES:
|
||||
copy_file(WRAPPER / rel, BOT_PKG / rel)
|
||||
|
||||
for rel in CLI_LEGACY:
|
||||
copy_file(WRAPPER / rel, BOT_PKG / rel)
|
||||
|
||||
# Fix relative imports in botos (..gateway -> praisonai_bot.gateway stays relative)
|
||||
for py in BOT_PKG.rglob("*.py"):
|
||||
rewrite_imports(py)
|
||||
|
||||
for old_name, new_name, shim_dir in SHIM_PACKAGES:
|
||||
write_shim_package(old_name, new_name, shim_dir)
|
||||
|
||||
for old_mod, new_mod, shim_path in SHIM_SINGLE_FILES:
|
||||
write_shim_module(old_mod, new_mod, shim_path)
|
||||
|
||||
# Feature shims (single files in hybrid package)
|
||||
for feat in ("gateway", "bots_cli", "onboard", "approval"):
|
||||
write_shim_module(
|
||||
f"praisonai.cli.features.{feat}",
|
||||
f"praisonai_bot.cli.features.{feat}",
|
||||
WRAPPER / "cli/features" / f"{feat}.py",
|
||||
)
|
||||
|
||||
# Tests
|
||||
for rel in TEST_DIRS:
|
||||
src = TESTS_SRC / rel
|
||||
dst = TESTS_DST / rel
|
||||
if src.is_dir():
|
||||
copy_tree(src, dst)
|
||||
|
||||
for pattern in (
|
||||
"test_bot*.py",
|
||||
"test_gateway*.py",
|
||||
"test_bots_cli.py",
|
||||
"test_botos*.py",
|
||||
):
|
||||
for src in TESTS_SRC.glob(f"unit/{pattern}"):
|
||||
dst = TESTS_DST / "unit" / src.name
|
||||
copy_file(src, dst)
|
||||
|
||||
for src in (TESTS_SRC / "unit/cli").glob("test_gateway*.py"):
|
||||
copy_file(src, TESTS_DST / "unit/cli" / src.name)
|
||||
for src in (TESTS_SRC / "unit/cli").glob("test_onboard*.py"):
|
||||
copy_file(src, TESTS_DST / "unit/cli" / src.name)
|
||||
for src in (TESTS_SRC / "unit/cli").glob("test_legacy_parse_args.py"):
|
||||
copy_file(src, TESTS_DST / "unit/cli" / src.name)
|
||||
if (TESTS_SRC / "unit/daemon/test_daemon_dispatch.py").exists():
|
||||
copy_file(
|
||||
TESTS_SRC / "unit/daemon/test_daemon_dispatch.py",
|
||||
TESTS_DST / "unit/daemon/test_daemon_dispatch.py",
|
||||
)
|
||||
|
||||
print("C9 migration copy complete:", BOT_PKG)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,3 +0,0 @@
|
||||
# C9: allowlisted praisonai wrapper imports inside praisonai_bot (lazy bridges only).
|
||||
# gateway/server.py scheduled executor — lazy import inside method
|
||||
gateway/server.py
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C9 bot→code gate: production code must not hard-import praisonai_code (use _code_bridge).
|
||||
set -euo pipefail
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ROOT="src/praisonai-bot/praisonai_bot"
|
||||
ALLOWLIST="scripts/c9_code_import_allowlist.txt"
|
||||
RE='(from praisonai_code|import praisonai_code)'
|
||||
FAIL=0
|
||||
|
||||
allowed_line() {
|
||||
local rel="$1"
|
||||
if [ ! -f "$ALLOWLIST" ]; then
|
||||
return 1
|
||||
fi
|
||||
sed -E 's/[[:space:]]*#.*$//;/^[[:space:]]*$/d' "$ALLOWLIST" | grep -Fxq "$rel"
|
||||
}
|
||||
|
||||
while IFS= read -r f; do
|
||||
rel="${f#"$ROOT/"}"
|
||||
if allowed_line "$rel"; then
|
||||
continue
|
||||
fi
|
||||
if grep -E "$RE" "$f" 2>/dev/null | grep -v '^[[:space:]]*#'; then
|
||||
echo "FAIL: direct praisonai_code import in $rel (use praisonai_bot._code_bridge)"
|
||||
FAIL=1
|
||||
fi
|
||||
done < <(find "$ROOT" -name '*.py' -type f)
|
||||
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "check_bot_code_imports ok"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C10 import-direction gate for praisonai-train.
|
||||
#
|
||||
# - praisonai_train must not import the praisonai wrapper at module level
|
||||
# (standalone `pip install praisonai-train` must work without it).
|
||||
# - praisonai_train must not import praisonai_code at module level
|
||||
# (code access goes through the lazy _code_bridge).
|
||||
# - praisonai-code hot paths must not import praisonai_train at module level.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
CODE_ROOT="${C10_CODE_ROOT:-src/praisonai-code/praisonai_code}"
|
||||
TRAIN_ROOT="${C10_TRAIN_ROOT:-src/praisonai-train/praisonai_train}"
|
||||
|
||||
ANY_WRAPPER_RE='(^[[:space:]]*from praisonai([[:space:]]|\.)|^[[:space:]]*import praisonai($|\.))'
|
||||
ANY_CODE_RE='(^from praisonai_code([[:space:]]|\.)|^import praisonai_code($|\.))'
|
||||
ANY_TRAIN_RE='(^[[:space:]]*from praisonai_train([[:space:]]|\.)|^[[:space:]]*import praisonai_train($|\.))'
|
||||
|
||||
echo "== C10 praisonai_train wrapper import gate (must be zero) =="
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
MATCHES="$(rg -n "$ANY_WRAPPER_RE" "$TRAIN_ROOT" --glob '*.py' 2>/dev/null | grep -Ev 'praisonai_train|praisonaiagents' || true)"
|
||||
else
|
||||
MATCHES="$(grep -rEn --include='*.py' "$ANY_WRAPPER_RE" "$TRAIN_ROOT" 2>/dev/null | grep -Ev 'praisonai_train|praisonaiagents' || true)"
|
||||
fi
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "$MATCHES"
|
||||
echo "FAIL: praisonai_train imports the praisonai wrapper"
|
||||
exit 1
|
||||
fi
|
||||
echo "train wrapper import gate ok"
|
||||
|
||||
echo "== C10 praisonai_train module-level praisonai_code import gate =="
|
||||
# Column-0 imports only: in-function (indented) lazy imports via _code_bridge are allowed.
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
MATCHES="$(rg -n "$ANY_CODE_RE" "$TRAIN_ROOT" --glob '*.py' 2>/dev/null || true)"
|
||||
else
|
||||
MATCHES="$(grep -rEn --include='*.py' "$ANY_CODE_RE" "$TRAIN_ROOT" 2>/dev/null || true)"
|
||||
fi
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "$MATCHES"
|
||||
echo "FAIL: module-level praisonai_code import in praisonai_train"
|
||||
exit 1
|
||||
fi
|
||||
echo "train code import gate ok"
|
||||
|
||||
echo "== C10 praisonai-code hot-path gate (no praisonai_train at module level) =="
|
||||
for f in \
|
||||
"$CODE_ROOT/cli/main.py" \
|
||||
"$CODE_ROOT/cli/app.py" \
|
||||
"$CODE_ROOT/cli/commands/run.py" \
|
||||
"$CODE_ROOT/cli/commands/chat.py" \
|
||||
"$CODE_ROOT/cli/commands/code.py"
|
||||
do
|
||||
if [ -f "$f" ] && head -n 80 "$f" | grep -E "$ANY_TRAIN_RE" 2>/dev/null; then
|
||||
echo "FAIL: module-level praisonai_train import in code hot path $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "code hot-path gate ok"
|
||||
|
||||
echo "C10 import gates passed"
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C11 import-direction gate for praisonai-browser.
|
||||
#
|
||||
# - praisonai_browser must not import the praisonai wrapper at module level
|
||||
# (standalone `pip install praisonai-browser` must work without it).
|
||||
# - praisonai_browser must not import praisonai_code at module level.
|
||||
# - praisonai-code hot paths must not import praisonai_browser at module level.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
CODE_ROOT="${C11_CODE_ROOT:-src/praisonai-code/praisonai_code}"
|
||||
BROWSER_ROOT="${C11_BROWSER_ROOT:-src/praisonai-browser/praisonai_browser}"
|
||||
|
||||
ANY_WRAPPER_RE='(^[[:space:]]*from praisonai([[:space:]]|\.)|^[[:space:]]*import praisonai($|\.))'
|
||||
ANY_CODE_RE='(^from praisonai_code([[:space:]]|\.)|^import praisonai_code($|\.))'
|
||||
ANY_BROWSER_RE='(^[[:space:]]*from praisonai_browser([[:space:]]|\.)|^[[:space:]]*import praisonai_browser($|\.))'
|
||||
|
||||
echo "== C11 praisonai_browser wrapper import gate (must be zero) =="
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
MATCHES="$(rg -n "$ANY_WRAPPER_RE" "$BROWSER_ROOT" --glob '*.py' 2>/dev/null | grep -Ev 'praisonai_browser|praisonaiagents' || true)"
|
||||
else
|
||||
MATCHES="$(grep -rEn --include='*.py' "$ANY_WRAPPER_RE" "$BROWSER_ROOT" 2>/dev/null | grep -Ev 'praisonai_browser|praisonaiagents' || true)"
|
||||
fi
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "$MATCHES"
|
||||
echo "FAIL: praisonai_browser imports the praisonai wrapper"
|
||||
exit 1
|
||||
fi
|
||||
echo "browser wrapper import gate ok"
|
||||
|
||||
echo "== C11 praisonai_browser module-level praisonai_code import gate =="
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
MATCHES="$(rg -n "$ANY_CODE_RE" "$BROWSER_ROOT" --glob '*.py' 2>/dev/null || true)"
|
||||
else
|
||||
MATCHES="$(grep -rEn --include='*.py' "$ANY_CODE_RE" "$BROWSER_ROOT" 2>/dev/null || true)"
|
||||
fi
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "$MATCHES"
|
||||
echo "FAIL: module-level praisonai_code import in praisonai_browser"
|
||||
exit 1
|
||||
fi
|
||||
echo "browser code import gate ok"
|
||||
|
||||
echo "== C11 praisonai-code hot-path gate (no praisonai_browser at module level) =="
|
||||
for f in \
|
||||
"$CODE_ROOT/cli/main.py" \
|
||||
"$CODE_ROOT/cli/app.py" \
|
||||
"$CODE_ROOT/cli/commands/run.py" \
|
||||
"$CODE_ROOT/cli/commands/chat.py" \
|
||||
"$CODE_ROOT/cli/commands/code.py"
|
||||
do
|
||||
if [ -f "$f" ] && head -n 80 "$f" | grep -E "$ANY_BROWSER_RE" 2>/dev/null; then
|
||||
echo "FAIL: module-level praisonai_browser import in code hot path $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "code hot-path gate ok"
|
||||
|
||||
echo "C11 import gates passed"
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C12 import-direction gate for praisonai-mcp.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
CODE_ROOT="${C12_CODE_ROOT:-src/praisonai-code/praisonai_code}"
|
||||
MCP_ROOT="${C12_MCP_ROOT:-src/praisonai-mcp/praisonai_mcp}"
|
||||
|
||||
ANY_WRAPPER_RE='(^[[:space:]]*from praisonai([[:space:]]|\.)|^[[:space:]]*import praisonai($|\.))'
|
||||
ANY_CODE_RE='(^from praisonai_code([[:space:]]|\.)|^import praisonai_code($|\.))'
|
||||
ANY_MCP_RE='(^[[:space:]]*from praisonai_mcp([[:space:]]|\.)|^[[:space:]]*import praisonai_mcp($|\.))'
|
||||
|
||||
echo "== C12 praisonai_mcp wrapper import gate (must be zero outside bridges) =="
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
MATCHES="$(rg -n "$ANY_WRAPPER_RE" "$MCP_ROOT" --glob '*.py' 2>/dev/null | grep -v '_wrapper_bridge.py' | grep -v '_bootstrap.py' | grep -v '_code_bridge.py' || true)"
|
||||
else
|
||||
MATCHES="$(grep -rEn --include='*.py' "$ANY_WRAPPER_RE" "$MCP_ROOT" 2>/dev/null | grep -v '_wrapper_bridge.py' | grep -v '_bootstrap.py' | grep -v '_code_bridge.py' || true)"
|
||||
fi
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "$MATCHES"
|
||||
echo "FAIL: praisonai_mcp imports the praisonai wrapper"
|
||||
exit 1
|
||||
fi
|
||||
echo "mcp wrapper import gate ok"
|
||||
|
||||
echo "== C12 praisonai_mcp module-level praisonai_code import gate =="
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
MATCHES="$(rg -n "$ANY_CODE_RE" "$MCP_ROOT" --glob '*.py' 2>/dev/null | grep -v '_code_bridge.py' | grep -v 'cli/_configuration.py' | grep -v 'cli/output/console.py' || true)"
|
||||
else
|
||||
MATCHES="$(grep -rEn --include='*.py' "$ANY_CODE_RE" "$MCP_ROOT" 2>/dev/null | grep -v '_code_bridge.py' | grep -v 'cli/_configuration.py' | grep -v 'cli/output/console.py' || true)"
|
||||
fi
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "$MATCHES"
|
||||
echo "FAIL: module-level praisonai_code import in praisonai_mcp"
|
||||
exit 1
|
||||
fi
|
||||
echo "mcp code import gate ok"
|
||||
|
||||
echo "== C12 praisonai-code hot-path gate (no praisonai_mcp at module level) =="
|
||||
for f in \
|
||||
"$CODE_ROOT/cli/main.py" \
|
||||
"$CODE_ROOT/cli/app.py" \
|
||||
"$CODE_ROOT/cli/commands/run.py" \
|
||||
"$CODE_ROOT/cli/commands/chat.py" \
|
||||
"$CODE_ROOT/cli/commands/code.py"
|
||||
do
|
||||
if [ -f "$f" ] && head -n 80 "$f" | grep -E "$ANY_MCP_RE" 2>/dev/null; then
|
||||
echo "FAIL: module-level praisonai_mcp import in code hot path $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "code hot-path gate ok"
|
||||
|
||||
echo "C12 import gates passed"
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C13 import-direction gate for praisonai-sandbox.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
CODE_ROOT="${C13_CODE_ROOT:-src/praisonai-code/praisonai_code}"
|
||||
AGENTS_ROOT="${C13_AGENTS_ROOT:-src/praisonai-agents/praisonaiagents}"
|
||||
SANDBOX_ROOT="${C13_SANDBOX_ROOT:-src/praisonai-sandbox/praisonai_sandbox}"
|
||||
|
||||
ANY_WRAPPER_RE='(^[[:space:]]*from praisonai([[:space:]]|\.)|^[[:space:]]*import praisonai($|\.))'
|
||||
ANY_CODE_RE='(^from praisonai_code([[:space:]]|\.)|^import praisonai_code($|\.))'
|
||||
ANY_SANDBOX_RE='(^[[:space:]]*from praisonai_sandbox([[:space:]]|\.)|^[[:space:]]*import praisonai_sandbox($|\.))'
|
||||
|
||||
echo "== C13 praisonai_sandbox wrapper import gate (must be zero outside bridges) =="
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
MATCHES="$(rg -n "$ANY_WRAPPER_RE" "$SANDBOX_ROOT" --glob '*.py' 2>/dev/null | grep -v '_wrapper_bridge.py' | grep -v '_bootstrap.py' | grep -v '_code_bridge.py' || true)"
|
||||
else
|
||||
MATCHES="$(grep -rEn --include='*.py' "$ANY_WRAPPER_RE" "$SANDBOX_ROOT" 2>/dev/null | grep -v '_wrapper_bridge.py' | grep -v '_bootstrap.py' | grep -v '_code_bridge.py' || true)"
|
||||
fi
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "$MATCHES"
|
||||
echo "FAIL: praisonai_sandbox imports the praisonai wrapper"
|
||||
exit 1
|
||||
fi
|
||||
echo "sandbox wrapper import gate ok"
|
||||
|
||||
echo "== C13 praisonai_sandbox module-level praisonai_code import gate =="
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
MATCHES="$(rg -n "$ANY_CODE_RE" "$SANDBOX_ROOT" --glob '*.py' 2>/dev/null | grep -v '_code_bridge.py' | grep -v '_registry.py' || true)"
|
||||
else
|
||||
MATCHES="$(grep -rEn --include='*.py' "$ANY_CODE_RE" "$SANDBOX_ROOT" 2>/dev/null | grep -v '_code_bridge.py' | grep -v '_registry.py' || true)"
|
||||
fi
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "$MATCHES"
|
||||
echo "FAIL: module-level praisonai_code import in praisonai_sandbox"
|
||||
exit 1
|
||||
fi
|
||||
echo "sandbox code import gate ok"
|
||||
|
||||
echo "== C13 praisonaiagents hot-path gate (no praisonai_sandbox at module level) =="
|
||||
for f in \
|
||||
"$AGENTS_ROOT/sandbox/manager.py" \
|
||||
"$AGENTS_ROOT/sandbox/protocols.py" \
|
||||
"$AGENTS_ROOT/sandbox/config.py"
|
||||
do
|
||||
if [ -f "$f" ] && head -n 80 "$f" | grep -E "$ANY_SANDBOX_RE" 2>/dev/null; then
|
||||
echo "FAIL: module-level praisonai_sandbox import in agents hot path $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "agents hot-path gate ok"
|
||||
|
||||
echo "== C13 praisonai-code hot-path gate (no praisonai_sandbox at module level) =="
|
||||
for f in \
|
||||
"$CODE_ROOT/cli/main.py" \
|
||||
"$CODE_ROOT/cli/app.py" \
|
||||
"$CODE_ROOT/cli/commands/run.py" \
|
||||
"$CODE_ROOT/cli/commands/chat.py" \
|
||||
"$CODE_ROOT/cli/commands/code.py"
|
||||
do
|
||||
if [ -f "$f" ] && head -n 80 "$f" | grep -E "$ANY_SANDBOX_RE" 2>/dev/null; then
|
||||
echo "FAIL: module-level praisonai_sandbox import in code hot path $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "code hot-path gate ok"
|
||||
|
||||
echo "C13 import gates passed"
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C7 import-direction gate for praisonai-code (standalone hot path).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${1:-src/praisonai-code/praisonai_code}"
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# Module-level hot path: praisonai wrapper only (not praisonaiagents / praisonai_code).
|
||||
HOT_PATH_RE='^from praisonai([[:space:]]|\.|$)|^import praisonai([[:space:]]|\.|$)'
|
||||
# Line-level wrapper imports (exclude praisonaiagents, praisonai_code, praisonai-tools).
|
||||
# Count indented bare ``import praisonai`` as well (C8.1 regex fix).
|
||||
ANY_WRAPPER_RE='(^[[:space:]]*from praisonai([[:space:]]|\.)|^[[:space:]]*import praisonai($|\.))'
|
||||
ALLOWLIST="scripts/c7_wrapper_import_allowlist.txt"
|
||||
|
||||
echo "== C7 hot-path module-level gate =="
|
||||
for f in \
|
||||
"$ROOT/cli/main.py" \
|
||||
"$ROOT/cli/app.py" \
|
||||
"$ROOT/cli/commands/run.py" \
|
||||
"$ROOT/cli/commands/chat.py" \
|
||||
"$ROOT/cli/commands/code.py"
|
||||
do
|
||||
if grep -E "$HOT_PATH_RE" "$f" 2>/dev/null; then
|
||||
echo "FAIL: module-level wrapper import in $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "hot-path module-level gate ok"
|
||||
|
||||
echo "== C7 strict gate (run/chat/code/app — no wrapper imports) =="
|
||||
for f in \
|
||||
"$ROOT/cli/app.py" \
|
||||
"$ROOT/cli/commands/run.py" \
|
||||
"$ROOT/cli/commands/chat.py" \
|
||||
"$ROOT/cli/commands/code.py"
|
||||
do
|
||||
if grep -E "$ANY_WRAPPER_RE" "$f" 2>/dev/null; then
|
||||
echo "FAIL: wrapper import in $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "strict hot-path gate ok"
|
||||
|
||||
echo "== C7 regression baseline (total wrapper import lines) =="
|
||||
BASELINE="${C8_WRAPPER_IMPORT_BASELINE:-${C7_WRAPPER_IMPORT_BASELINE:-50}}"
|
||||
# Count wrapper import lines with a portable tool. Prefer ripgrep when present
|
||||
# (fast, respects the same regex) and fall back to POSIX grep -r otherwise, so
|
||||
# the gate does not exit 127 on runners without rg. The `|| true` keeps a
|
||||
# no-match (grep/rg exit 1) from aborting the script under `set -o pipefail`.
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
COUNT="$( { rg -c "$ANY_WRAPPER_RE" "$ROOT" --glob '*.py' 2>/dev/null || true; } | awk -F: '{s+=$2} END {print s+0}')"
|
||||
else
|
||||
COUNT="$( { grep -rEc --include='*.py' "$ANY_WRAPPER_RE" "$ROOT" 2>/dev/null || true; } | awk -F: '{s+=$2} END {print s+0}')"
|
||||
fi
|
||||
echo "wrapper import lines: $COUNT (baseline $BASELINE)"
|
||||
if [ "$COUNT" -gt "$BASELINE" ]; then
|
||||
echo "FAIL: wrapper import count regressed above baseline ($COUNT > $BASELINE)"
|
||||
exit 1
|
||||
fi
|
||||
echo "regression baseline ok"
|
||||
|
||||
echo "== C7 allowlist enforcement =="
|
||||
if [ -f "$ALLOWLIST" ]; then
|
||||
# Files (relative to $ROOT) that carry wrapper imports.
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
OFFENDERS="$( { rg -l "$ANY_WRAPPER_RE" "$ROOT" --glob '*.py' 2>/dev/null || true; } )"
|
||||
else
|
||||
OFFENDERS="$( { grep -rlE --include='*.py' "$ANY_WRAPPER_RE" "$ROOT" 2>/dev/null || true; } )"
|
||||
fi
|
||||
# Strip the reasons/comments and blank/comment lines from the allowlist.
|
||||
ALLOWED="$(sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$ALLOWLIST" | sed -E 's/[[:space:]]+$//')"
|
||||
FAIL=0
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
rel="${f#"$ROOT/"}"
|
||||
if ! printf '%s\n' "$ALLOWED" | grep -Fxq "$rel"; then
|
||||
echo "FAIL: non-allowlisted wrapper import in $rel (add to $ALLOWLIST after review)"
|
||||
FAIL=1
|
||||
fi
|
||||
done <<< "$OFFENDERS"
|
||||
if [ "$FAIL" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "allowlist enforcement ok"
|
||||
else
|
||||
echo "allowlist file missing: $ALLOWLIST (skipping enforcement)"
|
||||
fi
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C9 import-direction gate for praisonai-bot and praisonai-code hot paths.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
CODE_ROOT="${C9_CODE_ROOT:-src/praisonai-code/praisonai_code}"
|
||||
BOT_ROOT="${C9_BOT_ROOT:-src/praisonai-bot/praisonai_bot}"
|
||||
|
||||
ANY_WRAPPER_RE='(^[[:space:]]*from praisonai([[:space:]]|\.)|^[[:space:]]*import praisonai($|\.))'
|
||||
ANY_BOT_RE='(^[[:space:]]*from praisonai_bot([[:space:]]|\.)|^[[:space:]]*import praisonai_bot($|\.))'
|
||||
CODE_HOT='^from praisonai([[:space:]]|\.|$)|^import praisonai([[:space:]]|\.|$)'
|
||||
BOT_ALLOWLIST="scripts/c9_wrapper_import_allowlist.txt"
|
||||
|
||||
count_wrapper_imports() {
|
||||
local root="$1"
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
{ rg -c "$ANY_WRAPPER_RE" "$root" --glob '*.py' 2>/dev/null || true; } | awk -F: '{s+=$2} END {print s+0}'
|
||||
else
|
||||
{ grep -rEc --include='*.py' "$ANY_WRAPPER_RE" "$root" 2>/dev/null || true; } | awk -F: '{s+=$2} END {print s+0}'
|
||||
fi
|
||||
}
|
||||
|
||||
echo "== C9 praisonai-code hot-path gate (no praisonai_bot at module level) =="
|
||||
for f in \
|
||||
"$CODE_ROOT/cli/main.py" \
|
||||
"$CODE_ROOT/cli/app.py" \
|
||||
"$CODE_ROOT/cli/commands/run.py" \
|
||||
"$CODE_ROOT/cli/commands/chat.py" \
|
||||
"$CODE_ROOT/cli/commands/code.py"
|
||||
do
|
||||
if [ -f "$f" ] && head -n 80 "$f" | grep -E "$CODE_HOT" 2>/dev/null; then
|
||||
echo "FAIL: module-level wrapper import in $f"
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$f" ] && head -n 80 "$f" | grep -E "$ANY_BOT_RE" 2>/dev/null; then
|
||||
echo "FAIL: module-level praisonai_bot import in code hot path $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "code hot-path gate ok"
|
||||
|
||||
echo "== C9 praisonai-bot wrapper import regression =="
|
||||
BASELINE="${C9_WRAPPER_IMPORT_BASELINE:-20}"
|
||||
COUNT="$(count_wrapper_imports "$BOT_ROOT")"
|
||||
echo "wrapper import lines in praisonai_bot: $COUNT (baseline $BASELINE)"
|
||||
if [ "$COUNT" -gt "$BASELINE" ]; then
|
||||
echo "FAIL: praisonai_bot wrapper import count $COUNT > $BASELINE"
|
||||
exit 1
|
||||
fi
|
||||
echo "bot wrapper import baseline ok"
|
||||
|
||||
echo "C9 import gates passed"
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C8.4 line-count ratchet for praisonai_code/cli/main.py
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MAIN_PY="${1:-$ROOT/src/praisonai-code/praisonai_code/cli/main.py}"
|
||||
MAX_LINES="${MAX_MAIN_PY_LINES:-300}"
|
||||
|
||||
if [[ ! -f "$MAIN_PY" ]]; then
|
||||
echo "ERROR: $MAIN_PY not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LINES=$(wc -l < "$MAIN_PY" | tr -d ' ')
|
||||
echo "main.py line count: $LINES (max: $MAX_LINES)"
|
||||
|
||||
if [[ "$LINES" -gt "$MAX_LINES" ]]; then
|
||||
echo "FAIL: main.py exceeds $MAX_LINES lines ($LINES)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: main.py within limit"
|
||||
+21
-192
@@ -26,8 +26,6 @@ Simpler than competitors • More extensible • Faster • Agent-centric
|
||||
Powerful, lightweight, highly reliable and robust.
|
||||
Easy and developer-friendly for non-developers.
|
||||
Each feature runs 3 ways: **CLI, YAML, Python**.
|
||||
|
||||
> **Lightweight & powerful — no scope creep.** The aim of this package is to stay lightweight and powerful. Do a critical review at **each stage** — issue triage, fix planning, and PR review/implementation. Reject scope creep for the sake of adding features: prefer existing Agent capabilities (`instructions`/`backstory`/`tools`/`hooks`/`memory`) over new params, modules, or exports. A change must genuinely strengthen the SDK; never add a knob with no live consumer or that duplicates existing behaviour. Even when fixing a raised issue, keep the fix minimal and lightweight rather than expanding surface area.
|
||||
---
|
||||
|
||||
## 2. Repository Structure
|
||||
@@ -44,11 +42,7 @@ Each feature runs 3 ways: **CLI, YAML, Python**.
|
||||
│ │ └── pyproject.toml # Package config
|
||||
│ │
|
||||
│ ├── praisonai/ # Wrapper (praisonai)
|
||||
│ │ ├── praisonai/ # Bot/channel CLI, integrations, heavy impls
|
||||
│ │ └── pyproject.toml
|
||||
│ │
|
||||
│ ├── praisonai-code/ # Agentic terminal CLI (praisonai_code)
|
||||
│ │ ├── praisonai_code/ # run, chat, code, runtime, cli_backends
|
||||
│ │ ├── praisonai/ # CLI, integrations, heavy impls
|
||||
│ │ └── pyproject.toml
|
||||
│ │
|
||||
│ └── praisonai-ts/ # TypeScript SDK
|
||||
@@ -86,12 +80,8 @@ Each feature runs 3 ways: **CLI, YAML, Python**.
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ praisonai (Wrapper) │
|
||||
│ Integrations • framework_adapters • serve • dashboard │
|
||||
├─────────────────────┬─────────────────────┬─────────────────────┤
|
||||
│ praisonai-code │ praisonai-bot │ praisonai-train │
|
||||
│ run • chat • code │ bots • gateway • │ LLM fine-tune • │
|
||||
│ • doctor │ BotOS • CLI │ agent training │
|
||||
├─────────────────────┴─────────────────────┴─────────────────────┤
|
||||
│ CLI • Integrations • Heavy Implementations • Optional Deps │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ praisonaiagents (Core SDK) │
|
||||
│ Protocols • Hooks • Adapters • Base Classes • Decorators │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
@@ -100,128 +90,6 @@ Each feature runs 3 ways: **CLI, YAML, Python**.
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 `praisonai-code` — Agentic Terminal CLI Split (C5/C6)
|
||||
|
||||
The **agentic terminal CLI** (`run`, `chat`, `code`, warm runtime, CLI backends, Typer app) lives in **`praisonai-code`** (`praisonai_code`). **Bots, gateway, and channel CLI** live in **`praisonai-bot`** (`praisonai_bot`) after C9. **LLM fine-tuning and agent training** live in **`praisonai-train`** (`praisonai_train`) after C10. The **`praisonai`** wrapper keeps integrations, framework adapters, serve, and dashboard. **`pip install praisonai`** still installs everything; old import paths are preserved via shims.
|
||||
|
||||
| Product | Location | Stays out of |
|
||||
|---------|----------|--------------|
|
||||
| Agentic terminal CLI | `src/praisonai-code/praisonai_code/` | Must **not** depend on `praisonai` (PyPI cycle) |
|
||||
| Bot/channel runtime | `src/praisonai-bot/praisonai_bot/` | Must **not** PyPI-depend on `praisonai` or `praisonai-code` |
|
||||
| Core SDK | `src/praisonai-agents/praisonaiagents/` | Heavy CLI / integrations |
|
||||
| Wrapper shims | `src/praisonai/praisonai/bots`, `gateway`, … | `alias_package` → `praisonai_bot` |
|
||||
|
||||
**Moved into `praisonai_code` (~304 modules):**
|
||||
|
||||
| Step | Content |
|
||||
|------|---------|
|
||||
| C1 | `runtime/`, `cli_backends/` |
|
||||
| C2 | `cli/interactive/`, `execution/`, `ui/`, `output/`, `state/` |
|
||||
| C3 | `cli/commands/*` (80 Typer command modules) |
|
||||
| C4 | `cli/features/*` (158 feature handlers; bot features excepted) |
|
||||
| C5 | `cli/main.py`, `app.py`, `configuration/`, `session/`, `utils/`, schema modules |
|
||||
|
||||
**Intentionally retained in the wrapper (not shims):**
|
||||
|
||||
- **Commands:** `gateway`, `bot`, `pairing`, `identity`, `onboard`, `kanban`, `dashboard`, `claw`
|
||||
- **Features:** `gateway`, `bots_cli`, `onboard`, `approval`, `serve`
|
||||
- **Domains:** `praisonai.gateway.*`, `praisonai.bots.*`, integrations, endpoints
|
||||
|
||||
**Backward-compat shim patterns** (old `praisonai.*` paths must keep working):
|
||||
|
||||
| Pattern | Used for | Module identity |
|
||||
|---------|----------|-----------------|
|
||||
| `sys.modules[__name__] = _impl` | `cli/main`, `cli/app`, each moved `cli/commands/*`, schema modules | Same object as `praisonai_code.*` |
|
||||
| `alias_package()` + `_AliasFinder` | `cli/output`, `interactive`, `execution`, `ui`, `state`, `session`, `configuration`, `utils` | Same submodule objects |
|
||||
| Package `__getattr__` + submodule alias | `runtime/`, `cli_backends/` | Submodules identical; package object differs |
|
||||
| `features/__init__.py` `__path__` extension | Extracted features; five bot features stay local | Handler imports via `__getattr__` |
|
||||
|
||||
**Typer lazy-load:** `praisonai_code.cli.app._WRAPPER_COMMANDS` lists bot/channel commands that resolve via **`praisonai.cli.commands.*`** (wrapper), not `praisonai_code.cli.commands.*`.
|
||||
|
||||
**Monorepo bootstrap:** `praisonai._bootstrap.ensure_praisonai_code()` adds sibling `src/praisonai-code` to `sys.path` so `PYTHONPATH=src/praisonai-agents:src/praisonai` works without an explicit code entry.
|
||||
|
||||
**Install order (CI and local dev):** `praisonai-agents` → `praisonai-code` → `praisonai` (see `.github/actions/install-monorepo-packages`).
|
||||
|
||||
**Invocation paths that must remain valid:**
|
||||
|
||||
```bash
|
||||
praisonai … # console script → praisonai.__main__
|
||||
python -m praisonai …
|
||||
python -m praisonai.cli.main …
|
||||
python -m praisonai.runtime …
|
||||
from praisonai.cli.main import PraisonAI
|
||||
from praisonai.cli.commands.run import …
|
||||
unittest.mock.patch("praisonai.cli.commands.run.X") # same module object
|
||||
```
|
||||
|
||||
**Regression gate:** `src/praisonai/tests/unit/test_c5_backward_compat.py` (also run in optimised CI smoke). Standalone `pip install praisonai-code` requires declared deps (e.g. `toml` for config resolver).
|
||||
|
||||
**Key files:**
|
||||
|
||||
| What | Where |
|
||||
|------|-------|
|
||||
| Typer app + lazy commands | `praisonai_code/cli/app.py` |
|
||||
| Shim helper | `praisonai/cli/_shim.py` |
|
||||
| Bootstrap | `praisonai/_bootstrap.py` |
|
||||
| CLI main shim | `praisonai/cli/main.py` |
|
||||
| Runtime / backends shims | `praisonai/runtime/`, `praisonai/cli_backends/` |
|
||||
|
||||
**Do not** recreate `src/praisonai/praisonai_code/` — it shadows the real package.
|
||||
|
||||
**PyPI publish order:** `praisonaiagents` → `praisonai-code` → `praisonai` (see `src/praisonai/scripts/bump_and_release.py` and `.github/workflows/pypi-release.yml`). Gateway/bots changes release via wrapper only; agentic CLI changes also bump `praisonai-code`.
|
||||
|
||||
#### C6 integration gate (#2519)
|
||||
|
||||
C6 is the **sign-off step** after C0–C5 file moves: no further moves, only regression verification and release wiring.
|
||||
|
||||
| Check | Command / artefact |
|
||||
|-------|-------------------|
|
||||
| Smoke `--help` | `praisonai`, `praisonai run/chat/code/daemon/attach/gateway/bot/doctor --help` |
|
||||
| Compat regression | `pytest src/praisonai/tests/unit/test_c5_backward_compat.py` |
|
||||
| CLI shard | `pytest src/praisonai/tests/unit/cli/` |
|
||||
| Warm runtime | `pytest src/praisonai/tests/unit/test_warm_runtime.py` |
|
||||
| Doctor shard | `pytest src/praisonai/tests/unit/doctor/` |
|
||||
| Import perf | `pytest src/praisonai/tests/unit/test_performance_benchmarks.py` (< 500ms) |
|
||||
| Real LLM smoke | `RUN_REAL_KEY_TESTS=1 PRAISONAI_ALLOW_NETWORK=1 PRAISONAI_TEST_PROVIDERS=all pytest src/praisonai/tests/integration/test_real_key_smoke.py` |
|
||||
| Verification log | `src/praisonai/tests/C6_VERIFICATION.md` |
|
||||
|
||||
**Command split (wrapper vs code):**
|
||||
|
||||
| Install target | Agentic CLI (`run`, `chat`, `code`, …) | Bot/gateway (`bot`, `gateway`, `pairing`, …) |
|
||||
|----------------|----------------------------------------|---------------------------------------------|
|
||||
| `pip install praisonai` | Yes (via `praisonai-code` dep + shims) | Yes |
|
||||
| `pip install praisonai-code` only | Yes — agentic hot path (`run`, `chat`, `code`, warm runtime) without wrapper import | No — wrapper-only commands hidden when `praisonai` absent |
|
||||
|
||||
**C7 (hot path complete):** Core agentic CLI imports no longer require the `praisonai`
|
||||
wrapper at module level. Remaining lazy wrapper imports (~211, regression-gated) are optional commands
|
||||
and features (capabilities, bots, framework adapters) — use
|
||||
`praisonai_code._wrapper_bridge` or `pip install praisonai`. See
|
||||
`src/praisonai/tests/C7_VERIFICATION.md`. CI gate: `scripts/check_c7_imports.sh`.
|
||||
|
||||
### 2.4 Package ownership (C7.1)
|
||||
|
||||
| Tier | Package | Owns |
|
||||
|------|---------|------|
|
||||
| 1 | `praisonaiagents` | Agent, tools, memory, hooks, `frameworks/` protocols |
|
||||
| 2 | `praisonai-code` | `run`/`chat`/`code`, Typer, runtime, llm, tool resolution |
|
||||
| 2b | `praisonai-bot` | Bots, gateway, channel CLI, OS daemon |
|
||||
| 2c | `praisonai-train` | LLM fine-tuning (Unsloth), agent training, `train` CLI |
|
||||
| 3 | `praisonai` | `framework_adapters/`, capabilities, serve orchestration, dashboard |
|
||||
|
||||
**Cross-tier rule:** `praisonai-code` must not declare a PyPI dependency on `praisonai`. Use
|
||||
[`praisonai_code._wrapper_bridge`](../praisonai-code/praisonai_code/_wrapper_bridge.py) for lazy wrapper access.
|
||||
|
||||
**framework_adapters:** protocols in SDK; registry + CrewAI/AutoGen/PraisonAI bodies stay in
|
||||
[`praisonai/framework_adapters/`](../praisonai/praisonai/framework_adapters/). Do not move to `praisonai-code`.
|
||||
|
||||
**Typer routing sets** (in `praisonai_code/cli/app.py`): `_BOT_RESIDENT_COMMANDS` loads from
|
||||
`praisonai_bot.cli.commands.*` (`bot`, `gateway`, `pairing`, …); `_TRAIN_RESIDENT_COMMANDS` loads
|
||||
from `praisonai_train.cli.commands.*` (`train`, C10); `_WRAPPER_RESIDENT_COMMANDS` loads from
|
||||
`praisonai.cli.commands.*` (`dashboard`, `flow`, …). Commands implemented in `praisonai_code`
|
||||
(`serve`, …) stay local; bridge internal wrapper imports.
|
||||
|
||||
Full boundary doc: [`src/praisonai/tests/C7.1_BOUNDARIES.md`](../praisonai/tests/C7.1_BOUNDARIES.md).
|
||||
|
||||
---
|
||||
|
||||
## 3. Core SDK Architecture (praisonaiagents)
|
||||
@@ -310,14 +178,15 @@ mongodb = ["pymongo>=4.6.3", "motor>=3.4.0"]
|
||||
### 4.1 Protocol-Driven Core (MUST)
|
||||
|
||||
```
|
||||
Core SDK (praisonaiagents) praisonai-code (terminal CLI) Wrapper (praisonai)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━
|
||||
✅ Protocols ✅ run/chat/code/doctor/setup ✅ Bot/gateway CLI
|
||||
✅ Hooks ✅ runtime, cli_backends ✅ Integrations
|
||||
✅ Adapters ✅ Typer app, moved commands ✅ Heavy implementations
|
||||
✅ Base classes ✅ Depends on agents only ✅ Optional deps
|
||||
✅ Decorators ❌ Must not import praisonai ✅ UI, DB adapters
|
||||
❌ Heavy implementations ❌ Bot/channel commands ❌ Core logic
|
||||
Core SDK (praisonaiagents) Wrapper (praisonai)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━
|
||||
✅ Protocols ✅ Heavy implementations
|
||||
✅ Hooks ✅ CLI commands
|
||||
✅ Adapters ✅ External integrations
|
||||
✅ Base classes ✅ Optional deps
|
||||
✅ Decorators ✅ UI components
|
||||
✅ Dataclasses ✅ Database adapters
|
||||
❌ Heavy implementations ❌ Core logic
|
||||
```
|
||||
|
||||
### 4.2 No Performance Impact (MUST)
|
||||
@@ -870,12 +739,6 @@ from praisonaiagents import Workflow, Route, Parallel, Loop
|
||||
| Policy engine | `praisonaiagents/policy/engine.py` |
|
||||
| Scheduler module | `praisonaiagents/scheduler/` |
|
||||
| Schedule tools | `praisonaiagents/tools/schedule_tools.py` |
|
||||
| Agentic CLI (Typer) | `praisonai_code/cli/app.py` |
|
||||
| CLI main / run | `praisonai_code/cli/main.py` |
|
||||
| Warm runtime | `praisonai_code/runtime/` |
|
||||
| CLI backends | `praisonai_code/cli_backends/` |
|
||||
| Back-compat shims | `praisonai/cli/_shim.py`, `praisonai/cli/main.py` |
|
||||
| C5 regression tests | `praisonai/tests/unit/test_c5_backward_compat.py` |
|
||||
|
||||
---
|
||||
|
||||
@@ -904,24 +767,22 @@ BotOS is the multi-platform bot orchestration layer for PraisonAI. It follows th
|
||||
|-------|---------|---------|
|
||||
| `BotOSProtocol` | `praisonaiagents` (Core SDK) | Interface contract — lightweight |
|
||||
| `BotOSConfig` | `praisonaiagents` (Core SDK) | Configuration dataclass |
|
||||
| `Bot`, `BotOS`, adapters | `praisonai-bot` (Tier 2b) | Platform bots and multi-bot orchestration |
|
||||
| `_registry` | `praisonai_bot.bots._registry` | Platform adapter registry (entry-point `praisonai.channels`) |
|
||||
| Shims | `praisonai.bots` / `praisonai.gateway` | Backward-compat `alias_package` re-exports |
|
||||
| `Bot` | `praisonai` (Wrapper) | Single-platform bot wrapper |
|
||||
| `BotOS` | `praisonai` (Wrapper) | Multi-platform orchestrator |
|
||||
| `_registry` | `praisonai` (Wrapper) | Platform adapter registry |
|
||||
|
||||
### 12.3 Usage Patterns
|
||||
|
||||
```python
|
||||
# === Level 1: Single bot, 3 lines ===
|
||||
from praisonai_bot.bots import Bot
|
||||
from praisonai.bots import Bot
|
||||
from praisonaiagents import Agent
|
||||
|
||||
bot = Bot("telegram", agent=Agent(name="assistant", instructions="Be helpful"))
|
||||
bot.run()
|
||||
|
||||
# Shims: ``from praisonai.bots import Bot`` still works when the wrapper is installed.
|
||||
|
||||
# === Level 2: Multi-platform, shared agent ===
|
||||
from praisonai_bot.bots import BotOS
|
||||
from praisonai.bots import BotOS
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="assistant", instructions="Be helpful")
|
||||
@@ -929,7 +790,7 @@ botos = BotOS(agent=agent, platforms=["telegram", "discord"])
|
||||
botos.run()
|
||||
|
||||
# === Level 3: AgentTeam in BotOS ===
|
||||
from praisonai_bot.bots import BotOS, Bot
|
||||
from praisonai.bots import BotOS, Bot
|
||||
from praisonaiagents import Agent, AgentTeam, Task
|
||||
|
||||
researcher = Agent(name="researcher", instructions="Research topics")
|
||||
@@ -976,9 +837,9 @@ Tokens are resolved in order: `explicit token=` > env var > empty string.
|
||||
|------|-------|
|
||||
| BotOSProtocol | `praisonaiagents/bots/protocols.py` |
|
||||
| BotOSConfig | `praisonaiagents/bots/config.py` |
|
||||
| Bot class | `praisonai_bot/bots/bot.py` (shim: `praisonai/bots/bot.py`) |
|
||||
| BotOS class | `praisonai_bot/bots/botos.py` (shim: `praisonai/bots/botos.py`) |
|
||||
| Platform registry | `praisonai_bot/bots/_registry.py` |
|
||||
| Bot class | `praisonai/bots/bot.py` |
|
||||
| BotOS class | `praisonai/bots/botos.py` |
|
||||
| Platform registry | `praisonai/bots/_registry.py` |
|
||||
| Unit tests | `tests/unit/test_botos_protocol.py` |
|
||||
| Integration tests | `tests/unit/test_botos_integration.py` |
|
||||
|
||||
@@ -1049,36 +910,4 @@ The core remains **free and open source**. Clear upgrade paths to paid value:
|
||||
|
||||
---
|
||||
|
||||
## 15. Context / Harness / Loop (CHL) Engineering
|
||||
|
||||
PraisonAI's reliability engineering rests on three pillars — **C**ontext, **H**arness, **L**oop. Full principles, cross-links, and the **measurable rubric** live in the docs: [`concepts/chl-engineering`](https://docs.praison.ai/concepts/chl-engineering) (source: `PraisonAIDocs/docs/concepts/chl-engineering.mdx`).
|
||||
|
||||
| Pillar | Meaning | Code anchors | Evaluator (rubric) |
|
||||
|--------|---------|--------------|--------------------|
|
||||
| **Context** | Budget, compaction, injection, handoff | `context/`, `compaction/`, `ContextAgent` | `ContextEvaluator` |
|
||||
| **Harness** | Turn-context parity, traces, artifacts | `runtime/turn_context.py`, interactive test harness | `HarnessEvaluator` |
|
||||
| **Loop** | Convergence, guardrails, efficiency | `eval/loop.py` (`EvaluationLoop`), `agent/autonomy.py` | `LoopEvaluator` |
|
||||
|
||||
**Rubric summary** (targets are defaults, configurable per project):
|
||||
|
||||
| Principle | Metric | Target |
|
||||
|-----------|--------|--------|
|
||||
| Context handoff | handoff score 0–10 | ≥ 8.0 |
|
||||
| Context budget | tokens ≤ budget | 100% |
|
||||
| Compaction loss | semantic retention (judge) | ≥ 7.0 |
|
||||
| Harness parity | tool schema hash match | 100% |
|
||||
| Harness artifacts | required files present | 100% |
|
||||
| Loop convergence | iterations to threshold | ≤ N |
|
||||
| Doom-loop safety | guard fires on repeat fixture | required |
|
||||
|
||||
> **Evaluator status.** The `ContextEvaluator` / `HarnessEvaluator` / `LoopEvaluator` classes named in the rubric are **planned follow-ups** (PA-CHL-001–004), not yet in the repo. Until they land, the equivalent checks are covered by existing modules:
|
||||
>
|
||||
> - **Context** — `context/tokens.py` and `eval/tokens.py` (`estimate_tokens` / `count_tokens`) for budget; `compaction/compactor.py` (`ContextCompactor`) for compaction/retention.
|
||||
> - **Harness** — `runtime/turn_context.py` for turn-context parity; `runtime/example_harness.py` and `tests/` for harness/artifact checks.
|
||||
> - **Loop** — `eval/loop.py` (`EvaluationLoop`) and `agent/autonomy.py` for convergence and doom-loop guards.
|
||||
>
|
||||
> Keep terminology consistent with the `agent/context_agent.py` (`ContextAgent`) PRP methodology.
|
||||
|
||||
---
|
||||
|
||||
*This document is the source of truth for the PraisonAI SDK architecture and design principles.*
|
||||
@@ -1,6 +1,6 @@
|
||||
# Documentation Parity Tracker (Python)
|
||||
|
||||
> **Categories:** 66 | **Documented:** 66 | **Parity:** 100.0%
|
||||
> **Categories:** 66 | **Documented:** 65 | **Parity:** 98.5%
|
||||
|
||||
This report compares **Python SDK feature categories** against **Python documentation** (docs/concepts, docs/features, etc.).
|
||||
|
||||
@@ -9,86 +9,85 @@ This report compares **Python SDK feature categories** against **Python document
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Feature Categories | 66 |
|
||||
| **Documented Categories** | **66** |
|
||||
| **Undocumented Categories** | **0** |
|
||||
| **Parity** | **100.0%** |
|
||||
| **Documented Categories** | **65** |
|
||||
| **Undocumented Categories** | **1** |
|
||||
| **Parity** | **98.5%** |
|
||||
|
||||
## Documented Categories
|
||||
|
||||
| Category | Features | Docs | Lines |
|
||||
|----------|----------|------|-------|
|
||||
| ✅ AGUI | 1 | 2 | 420 |
|
||||
| ✅ Agent | 21 | 45 | 14056 |
|
||||
| ✅ Agent-to-Agent (A2A) | 1 | 7 | 2965 |
|
||||
| ✅ Agent-to-User (A2U) | 1 | 3 | 585 |
|
||||
| ✅ Approval | 1 | 5 | 2262 |
|
||||
| ✅ Audio | 2 | 12 | 1000 |
|
||||
| ✅ Auto Generation | 5 | 10 | 2868 |
|
||||
| ✅ Autonomy | 3 | 7 | 2705 |
|
||||
| ✅ Bots | 7 | 28 | 11056 |
|
||||
| ✅ Agent | 21 | 42 | 11340 |
|
||||
| ✅ Agent-to-Agent (A2A) | 1 | 7 | 2880 |
|
||||
| ✅ Agent-to-User (A2U) | 1 | 3 | 575 |
|
||||
| ✅ Approval | 1 | 3 | 1344 |
|
||||
| ✅ Audio | 2 | 10 | 649 |
|
||||
| ✅ Auto Generation | 5 | 6 | 1637 |
|
||||
| ✅ Autonomy | 3 | 5 | 1919 |
|
||||
| ✅ Bots | 7 | 23 | 7714 |
|
||||
| ✅ Budget | 1 | 1 | 287 |
|
||||
| ✅ CLI | 5 | 116 | 31950 |
|
||||
| ✅ Chunking | 2 | 2 | 385 |
|
||||
| ✅ CLI | 5 | 108 | 26975 |
|
||||
| ✅ Chunking | 2 | 2 | 288 |
|
||||
| ✅ Citations | 2 | 1 | 202 |
|
||||
| ✅ Code Execution | 2 | 13 | 3907 |
|
||||
| ✅ Conditions | 1 | 3 | 1179 |
|
||||
| ✅ Configuration | 3 | 7 | 2765 |
|
||||
| ✅ Context Management | 16 | 34 | 13176 |
|
||||
| ✅ Database | 1 | 40 | 6124 |
|
||||
| ✅ Deep Research | 8 | 2 | 587 |
|
||||
| ✅ Display | 6 | 3 | 741 |
|
||||
| ✅ Embeddings | 6 | 23 | 2049 |
|
||||
| ✅ Evaluation | 1 | 7 | 2902 |
|
||||
| ✅ Events | 1 | 2 | 749 |
|
||||
| ✅ Execution | 3 | 4 | 1528 |
|
||||
| ✅ Failover | 2 | 1 | 400 |
|
||||
| ✅ Files | 2 | 6 | 1964 |
|
||||
| ✅ Flow | 1 | 3 | 777 |
|
||||
| ✅ Gateway | 7 | 51 | 18026 |
|
||||
| ✅ Guardrails | 4 | 4 | 1673 |
|
||||
| ✅ Handoffs | 11 | 6 | 2454 |
|
||||
| ✅ Hooks | 2 | 9 | 3928 |
|
||||
| ✅ Image | 1 | 11 | 1255 |
|
||||
| ✅ Knowledge | 4 | 13 | 4160 |
|
||||
| ✅ LLM | 3 | 14 | 4767 |
|
||||
| ✅ Loops | 4 | 4 | 1202 |
|
||||
| ✅ MCP | 1 | 58 | 13241 |
|
||||
| ✅ Memory | 6 | 17 | 6481 |
|
||||
| ✅ OCR | 2 | 1 | 237 |
|
||||
| ✅ Observability | 2 | 23 | 2779 |
|
||||
| ✅ Optimizer | 1 | 2 | 792 |
|
||||
| ✅ Output | 3 | 5 | 1158 |
|
||||
| ✅ Parallel Execution | 3 | 2 | 497 |
|
||||
| ✅ Planning | 6 | 6 | 1545 |
|
||||
| ✅ Plugins | 8 | 3 | 1879 |
|
||||
| ✅ Prompts | 2 | 8 | 1726 |
|
||||
| ✅ Providers | 1 | 55 | 7455 |
|
||||
| ✅ Query | 1 | 2 | 736 |
|
||||
| ✅ RAG | 5 | 15 | 2936 |
|
||||
| ✅ Realtime | 2 | 5 | 668 |
|
||||
| ✅ Reflection | 3 | 3 | 680 |
|
||||
| ✅ Retrieval | 2 | 5 | 1044 |
|
||||
| ✅ Routing | 1 | 2 | 430 |
|
||||
| ✅ Sandbox | 5 | 6 | 2686 |
|
||||
| ✅ Security | 1 | 3 | 2903 |
|
||||
| ✅ Sessions | 4 | 13 | 5198 |
|
||||
| ✅ Skills | 6 | 13 | 4893 |
|
||||
| ✅ Tasks | 2 | 6 | 2743 |
|
||||
| ✅ Telemetry | 1 | 2 | 604 |
|
||||
| ✅ Templates | 1 | 8 | 1657 |
|
||||
| ✅ Tools | 12 | 131 | 37117 |
|
||||
| ✅ Code Execution | 2 | 13 | 3554 |
|
||||
| ✅ Conditions | 1 | 3 | 1085 |
|
||||
| ✅ Configuration | 3 | 7 | 2160 |
|
||||
| ✅ Context Management | 16 | 33 | 10280 |
|
||||
| ✅ Database | 1 | 40 | 5373 |
|
||||
| ✅ Deep Research | 8 | 2 | 437 |
|
||||
| ✅ Display | 6 | 2 | 1175 |
|
||||
| ✅ Embeddings | 6 | 23 | 1966 |
|
||||
| ✅ Evaluation | 1 | 6 | 2539 |
|
||||
| ✅ Events | 1 | 2 | 764 |
|
||||
| ✅ Execution | 3 | 3 | 1099 |
|
||||
| ✅ Failover | 2 | 1 | 372 |
|
||||
| ✅ Files | 2 | 6 | 1632 |
|
||||
| ✅ Flow | 1 | 2 | 435 |
|
||||
| ✅ Gateway | 7 | 21 | 7477 |
|
||||
| ✅ Guardrails | 4 | 4 | 1619 |
|
||||
| ✅ Handoffs | 11 | 6 | 2499 |
|
||||
| ✅ Hooks | 2 | 8 | 3150 |
|
||||
| ✅ Image | 1 | 11 | 1096 |
|
||||
| ✅ Knowledge | 4 | 24 | 4525 |
|
||||
| ✅ LLM | 3 | 10 | 3674 |
|
||||
| ✅ Loops | 4 | 3 | 774 |
|
||||
| ✅ MCP | 1 | 51 | 10939 |
|
||||
| ✅ Memory | 6 | 16 | 5879 |
|
||||
| ✅ OCR | 2 | 1 | 226 |
|
||||
| ✅ Observability | 2 | 23 | 2754 |
|
||||
| ✅ Optimizer | 1 | 2 | 653 |
|
||||
| ✅ Output | 3 | 4 | 860 |
|
||||
| ✅ Parallel Execution | 3 | 2 | 399 |
|
||||
| ✅ Planning | 6 | 5 | 1198 |
|
||||
| ✅ Plugins | 8 | 3 | 1502 |
|
||||
| ✅ Prompts | 2 | 7 | 1569 |
|
||||
| ✅ Providers | 1 | 52 | 6214 |
|
||||
| ✅ Query | 1 | 2 | 616 |
|
||||
| ✅ RAG | 5 | 15 | 3193 |
|
||||
| ✅ Realtime | 2 | 5 | 584 |
|
||||
| ✅ Reflection | 3 | 2 | 477 |
|
||||
| ✅ Retrieval | 2 | 5 | 945 |
|
||||
| ✅ Routing | 1 | 2 | 597 |
|
||||
| ✅ Sandbox | 5 | 6 | 2512 |
|
||||
| ✅ Security | 1 | 3 | 2563 |
|
||||
| ✅ Sessions | 4 | 12 | 3793 |
|
||||
| ✅ Skills | 6 | 10 | 3663 |
|
||||
| ✅ Tasks | 2 | 6 | 3064 |
|
||||
| ✅ Telemetry | 1 | 2 | 560 |
|
||||
| ✅ Templates | 1 | 7 | 1411 |
|
||||
| ✅ Tools | 12 | 118 | 25895 |
|
||||
| ✅ Tracing | 3 | 2 | 139 |
|
||||
| ✅ Vector Store | 1 | 12 | 1193 |
|
||||
| ✅ Video | 2 | 6 | 649 |
|
||||
| ✅ Vision | 2 | 1 | 329 |
|
||||
| ✅ Web | 3 | 8 | 2055 |
|
||||
| ✅ Workflows | 5 | 16 | 6522 |
|
||||
| ✅ Video | 2 | 6 | 510 |
|
||||
| ✅ Vision | 2 | 1 | 283 |
|
||||
| ✅ Web | 3 | 7 | 1537 |
|
||||
| ✅ Workflows | 4 | 14 | 5893 |
|
||||
|
||||
## Documentation Without Features
|
||||
## Undocumented Categories (Need Documentation)
|
||||
|
||||
These docs exist but don't match any implemented feature category:
|
||||
|
||||
- ℹ️ Documents (1 docs, 787 lines)
|
||||
| Category | Features |
|
||||
|----------|----------|
|
||||
| ❌ Vector Store | 1 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ print("1. Import telemetry module...")
|
||||
import praisonaiagents.telemetry
|
||||
print(" Telemetry module imported")
|
||||
|
||||
print("\n2. Check atexit handler registration...")
|
||||
print(f" _atexit_registered: {praisonaiagents.telemetry._atexit_registered}")
|
||||
print("\n2. Check if auto_instrument_all was called...")
|
||||
print(f" _initialized: {praisonaiagents.telemetry._initialized}")
|
||||
|
||||
print("\n3. Import Agent and Agents...")
|
||||
from praisonaiagents import Agent, AgentTeam
|
||||
|
||||
@@ -141,17 +141,10 @@ _LAZY_IMPORTS = {
|
||||
'get_toolset': ('praisonaiagents.toolsets', 'get_toolset'),
|
||||
'unregister_toolset': ('praisonaiagents.toolsets', 'unregister_toolset'),
|
||||
'has_toolset': ('praisonaiagents.toolsets', 'has_toolset'),
|
||||
|
||||
# Model-family-aware harness (base prompt + edit-format selection)
|
||||
'HarnessProfile': ('praisonaiagents.model_harness', 'HarnessProfile'),
|
||||
'resolve_harness': ('praisonaiagents.model_harness', 'resolve_harness'),
|
||||
'register_profile': ('praisonaiagents.model_harness', 'register_profile'),
|
||||
|
||||
|
||||
# Run outcomes - typed validation and agent execution results
|
||||
'AgentRunOutcome': ('praisonaiagents.run_outcome', 'AgentRunOutcome'),
|
||||
'RunStatus': ('praisonaiagents.run_outcome', 'RunStatus'),
|
||||
'TerminationReason': ('praisonaiagents.run_outcome', 'TerminationReason'),
|
||||
'termination_to_run_status': ('praisonaiagents.run_outcome', 'termination_to_run_status'),
|
||||
'validate_decision_string': ('praisonaiagents.run_outcome', 'validate_decision_string'),
|
||||
|
||||
# Main display utilities (imports rich)
|
||||
@@ -190,7 +183,6 @@ _LAZY_IMPORTS = {
|
||||
'Include': ('praisonaiagents.workflows', 'Include'),
|
||||
'MAX_NESTING_DEPTH': ('praisonaiagents.workflows', 'MAX_NESTING_DEPTH'),
|
||||
'YAMLWorkflowParser': ('praisonaiagents.workflows', 'YAMLWorkflowParser'),
|
||||
'WorkflowHooksConfig': ('praisonaiagents.workflows', 'WorkflowHooksConfig'),
|
||||
|
||||
# Conditions (Protocol-driven condition evaluation)
|
||||
'ConditionProtocol': ('praisonaiagents.conditions.protocols', 'ConditionProtocol'),
|
||||
@@ -199,13 +191,6 @@ _LAZY_IMPORTS = {
|
||||
'DictCondition': ('praisonaiagents.conditions.evaluator', 'DictCondition'),
|
||||
'evaluate_condition': ('praisonaiagents.conditions.evaluator', 'evaluate_condition'),
|
||||
|
||||
# Goal Engineering (structured, measurable goals + verification)
|
||||
'GoalEngineer': ('praisonaiagents.goal', 'GoalEngineer'),
|
||||
'GoalConfig': ('praisonaiagents.goal', 'GoalConfig'),
|
||||
'Goal': ('praisonaiagents.goal', 'Goal'),
|
||||
'SuccessCriterion': ('praisonaiagents.goal', 'SuccessCriterion'),
|
||||
'GoalVerificationResult': ('praisonaiagents.goal', 'GoalVerificationResult'),
|
||||
|
||||
# Handoff
|
||||
'Handoff': ('praisonaiagents.agent.handoff', 'Handoff'),
|
||||
'handoff': ('praisonaiagents.agent.handoff', 'handoff'),
|
||||
@@ -419,7 +404,6 @@ _LAZY_IMPORTS = {
|
||||
'KnowledgeConfig': ('praisonaiagents.config.feature_configs', 'KnowledgeConfig'),
|
||||
'PlanningConfig': ('praisonaiagents.config.feature_configs', 'PlanningConfig'),
|
||||
'ReflectionConfig': ('praisonaiagents.config.feature_configs', 'ReflectionConfig'),
|
||||
'RulesConfig': ('praisonaiagents.config.feature_configs', 'RulesConfig'),
|
||||
'GuardrailConfig': ('praisonaiagents.config.feature_configs', 'GuardrailConfig'),
|
||||
'WebConfig': ('praisonaiagents.config.feature_configs', 'WebConfig'),
|
||||
'OutputConfig': ('praisonaiagents.config.feature_configs', 'OutputConfig'),
|
||||
@@ -538,9 +522,6 @@ _LAZY_IMPORTS = {
|
||||
'BotConfig': ('praisonaiagents.bots.config', 'BotConfig'),
|
||||
'BotOSProtocol': ('praisonaiagents.bots.protocols', 'BotOSProtocol'),
|
||||
'BotOSConfig': ('praisonaiagents.bots.config', 'BotOSConfig'),
|
||||
'PlatformCapabilities': ('praisonaiagents.bots.protocols', 'PlatformCapabilities'),
|
||||
'BasePlatformAdapter': ('praisonaiagents.bots.base', 'BasePlatformAdapter'),
|
||||
'SendResult': ('praisonaiagents.bots.base', 'SendResult'),
|
||||
|
||||
# Sandbox protocols and config (implementations in praisonai wrapper)
|
||||
'SandboxProtocol': ('praisonaiagents.sandbox.protocols', 'SandboxProtocol'),
|
||||
@@ -552,10 +533,6 @@ _LAZY_IMPORTS = {
|
||||
|
||||
# Managed backend protocol (implementation + config in praisonai wrapper)
|
||||
'ManagedBackendProtocol': ('praisonaiagents.agent.protocols', 'ManagedBackendProtocol'),
|
||||
|
||||
# Framework adapter protocol (implementations in praisonai wrapper / praisonai-frameworks)
|
||||
'FrameworkAdapterProtocol': ('praisonaiagents.frameworks.protocols', 'FrameworkAdapterProtocol'),
|
||||
'BaseFrameworkAdapter': ('praisonaiagents.frameworks.base', 'BaseFrameworkAdapter'),
|
||||
|
||||
# Managed agent events (provider-agnostic)
|
||||
'ManagedEvent': ('praisonaiagents.managed.events', 'ManagedEvent'),
|
||||
|
||||
@@ -22,12 +22,9 @@ Usage:
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import threading
|
||||
from typing import Any, Callable, Dict, Optional, Tuple
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Global cache lock for thread-safe access
|
||||
_cache_lock = threading.Lock()
|
||||
|
||||
@@ -204,26 +201,8 @@ def create_lazy_getattr_with_fallback(
|
||||
value = lazy_import(module_path, attr_name, _cache)
|
||||
_cache[name] = value
|
||||
return value
|
||||
except AttributeError as exc:
|
||||
# Module imported but the attribute is missing: this usually
|
||||
# indicates a stale/renamed mapping entry rather than a genuinely
|
||||
# optional dependency, so surface it louder. Preserve the graceful
|
||||
# None fallback for backward compatibility.
|
||||
_logger.warning(
|
||||
"Lazy import of %r (%s.%s) failed with a missing attribute; "
|
||||
"this may indicate a stale mapping entry: %s",
|
||||
name, module_path, attr_name, exc,
|
||||
)
|
||||
_cache[name] = None
|
||||
return None
|
||||
except ImportError as exc:
|
||||
# Optional module not available: preserve the graceful None
|
||||
# fallback, but record why so real import failures remain
|
||||
# diagnosable instead of surfacing later as NoneType errors.
|
||||
_logger.debug(
|
||||
"Lazy import of %r (%s.%s) failed: %s",
|
||||
name, module_path, attr_name, exc,
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
# Optional module not available
|
||||
_cache[name] = None
|
||||
return None
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ This module patches the warnings module to intercept specific messages.
|
||||
|
||||
import warnings
|
||||
import functools
|
||||
import sys
|
||||
|
||||
# Apply aggressive warning filters first
|
||||
warnings.filterwarnings("ignore", message=".*There is no current event loop.*", category=DeprecationWarning)
|
||||
@@ -67,7 +68,10 @@ def _patched_warn_explicit(message, category, filename, lineno, module=None, reg
|
||||
_original_warn_explicit(message, category, filename, lineno, module, registry, module_globals, source)
|
||||
|
||||
# Apply the patches
|
||||
# Note: `warnings` IS `sys.modules['warnings']`, so these assignments patch the
|
||||
# exact module object every importer sees via `import warnings`.
|
||||
warnings.warn = _patched_warn
|
||||
warnings.warn_explicit = _patched_warn_explicit
|
||||
warnings.warn_explicit = _patched_warn_explicit
|
||||
|
||||
# Also patch sys.modules warnings if it exists
|
||||
if 'warnings' in sys.modules:
|
||||
sys.modules['warnings'].warn = _patched_warn
|
||||
sys.modules['warnings'].warn_explicit = _patched_warn_explicit
|
||||
File diff suppressed because it is too large.
Load diff
@@ -141,11 +141,6 @@ class AutonomyConfig:
|
||||
snapshot_dir: Optional[str] = None # Defaults to ~/.praisonai/snapshots
|
||||
# Default tools for autonomy mode (auto-populated with code intelligence tools)
|
||||
default_tools: Optional[List[Any]] = None # None = auto (uses get_autonomy_default_tools())
|
||||
# Spend kill-switches — independent of turn/time caps. None = unlimited
|
||||
# (today's behaviour). Checked against cumulative run cost/tokens each iteration.
|
||||
max_budget_usd: Optional[float] = None # hard cumulative USD spend cap for the run
|
||||
max_tokens: Optional[int] = None # hard cumulative-token cap for the run
|
||||
budget_action: str = "pause" # "pause" (recoverable) | "stop"
|
||||
|
||||
def __post_init__(self):
|
||||
if self.level not in VALID_AUTONOMY_LEVELS:
|
||||
@@ -168,38 +163,6 @@ class AutonomyConfig:
|
||||
if self.snapshot_dir is None:
|
||||
from ..paths import get_snapshots_dir
|
||||
self.snapshot_dir = str(get_snapshots_dir())
|
||||
# Validate spend kill-switch settings
|
||||
if self.budget_action not in ("pause", "stop"):
|
||||
raise ValueError(
|
||||
f"Invalid budget_action: {self.budget_action!r}. "
|
||||
"Must be 'pause' or 'stop'."
|
||||
)
|
||||
if self.max_budget_usd is not None:
|
||||
if isinstance(self.max_budget_usd, bool) or not isinstance(
|
||||
self.max_budget_usd, (int, float)
|
||||
):
|
||||
raise ValueError(
|
||||
f"Invalid max_budget_usd: {self.max_budget_usd!r}. "
|
||||
"Must be a non-negative number or None."
|
||||
)
|
||||
if self.max_budget_usd < 0:
|
||||
raise ValueError(
|
||||
f"Invalid max_budget_usd: {self.max_budget_usd!r}. "
|
||||
"Must be non-negative."
|
||||
)
|
||||
if self.max_tokens is not None:
|
||||
if isinstance(self.max_tokens, bool) or not isinstance(
|
||||
self.max_tokens, int
|
||||
):
|
||||
raise ValueError(
|
||||
f"Invalid max_tokens: {self.max_tokens!r}. "
|
||||
"Must be a non-negative integer or None."
|
||||
)
|
||||
if self.max_tokens < 0:
|
||||
raise ValueError(
|
||||
f"Invalid max_tokens: {self.max_tokens!r}. "
|
||||
"Must be non-negative."
|
||||
)
|
||||
|
||||
@property
|
||||
def effective_track_changes(self) -> bool:
|
||||
@@ -230,9 +193,6 @@ class AutonomyConfig:
|
||||
sandbox=data.get("sandbox"),
|
||||
snapshot_dir=data.get("snapshot_dir"),
|
||||
default_tools=data.get("default_tools"),
|
||||
max_budget_usd=data.get("max_budget_usd"),
|
||||
max_tokens=data.get("max_tokens"),
|
||||
budget_action=data.get("budget_action", "pause"),
|
||||
)
|
||||
|
||||
class AutonomySignal(str, Enum):
|
||||
@@ -326,29 +286,23 @@ class AutonomyResult:
|
||||
Attributes:
|
||||
success: Whether the task completed successfully
|
||||
output: Final output/response
|
||||
completion_reason: Why execution stopped. One of the
|
||||
``TerminationReason`` string values: goal, tool_completion, promise,
|
||||
no_tool_calls, max_iterations, timeout, budget_exhausted, doom_loop,
|
||||
needs_help, interrupted, cancelled, error.
|
||||
completion_reason: Why execution stopped (goal, timeout, max_iterations, doom_loop, error, promise)
|
||||
iterations: Number of iterations executed
|
||||
stage: Final execution stage
|
||||
actions: List of actions taken
|
||||
duration_seconds: Total execution time
|
||||
error: Error message if failed
|
||||
started_at: ISO 8601 timestamp when execution started
|
||||
metadata: Optional structured metadata (e.g. spend_usd/tokens/status
|
||||
on a budget_exhausted outcome)
|
||||
"""
|
||||
success: bool
|
||||
output: str = ""
|
||||
completion_reason: str = "goal" # a TerminationReason value; see docstring
|
||||
completion_reason: str = "goal" # goal, timeout, max_iterations, doom_loop, error, promise
|
||||
iterations: int = 0
|
||||
stage: str = "direct"
|
||||
actions: List[Dict[str, Any]] = field(default_factory=list)
|
||||
duration_seconds: float = 0.0
|
||||
error: Optional[str] = None
|
||||
started_at: Optional[str] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the output text for display purposes.
|
||||
|
||||
@@ -6,15 +6,113 @@ Extracted from agent.py for better modularity and maintainability.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Any, Dict, List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatHandlerMixin:
|
||||
"""Mixin reserved for chat coordination on the Agent class.
|
||||
|
||||
This is a documented placeholder retained for the public mixin surface.
|
||||
Concrete chat logic (``chat``/``achat``, history management, and response
|
||||
processing) lives in ``ChatMixin``/``SessionManagerMixin``.
|
||||
"""Mixin providing chat handling methods for the Agent class.
|
||||
|
||||
This mixin focuses on high-level chat coordination and response processing,
|
||||
while the actual LLM interaction is handled by ChatMixin.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _format_chat_response(self, response: str, metadata: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""Format chat response with any necessary post-processing.
|
||||
|
||||
Args:
|
||||
response: Raw response from the LLM
|
||||
metadata: Optional metadata about the response
|
||||
|
||||
Returns:
|
||||
Formatted response string
|
||||
"""
|
||||
if not response:
|
||||
return ""
|
||||
|
||||
# Basic response formatting - can be extended with additional logic
|
||||
formatted = response.strip()
|
||||
|
||||
# Add any metadata-based formatting if needed
|
||||
if metadata and metadata.get('add_timestamp'):
|
||||
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
formatted = f"[{timestamp}] {formatted}"
|
||||
|
||||
return formatted
|
||||
|
||||
def _validate_chat_input(self, prompt: str) -> bool:
|
||||
"""Validate chat input before processing.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt to validate
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
if not prompt or not isinstance(prompt, str):
|
||||
logger.warning("Invalid chat input: prompt must be a non-empty string")
|
||||
return False
|
||||
|
||||
if len(prompt.strip()) == 0:
|
||||
logger.warning("Invalid chat input: prompt cannot be empty or whitespace only")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _prepare_chat_context(self, prompt: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Prepare context for chat processing.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
**kwargs: Additional context parameters
|
||||
|
||||
Returns:
|
||||
Context dictionary for chat processing
|
||||
"""
|
||||
context = {
|
||||
'prompt': prompt,
|
||||
'timestamp': time.time(),
|
||||
'agent_name': getattr(self, 'name', None) or 'Agent',
|
||||
}
|
||||
|
||||
# Add any additional context from kwargs
|
||||
context.update(kwargs)
|
||||
|
||||
return context
|
||||
|
||||
def _handle_chat_error(self, error: Exception, prompt: str) -> str:
|
||||
"""Handle chat processing errors gracefully.
|
||||
|
||||
Args:
|
||||
error: The exception that occurred
|
||||
prompt: The original prompt that caused the error
|
||||
|
||||
Returns:
|
||||
Error response message
|
||||
"""
|
||||
logger.error(f"Chat error occurred: {error}", exc_info=True)
|
||||
|
||||
# Return a user-friendly error message without exposing internal details.
|
||||
# Include a timestamp reference so users can correlate with server logs.
|
||||
error_ref = time.strftime('%Y%m%d%H%M%S')
|
||||
return f"I encountered an error while processing your request (ref: {error_ref}). Please try again."
|
||||
|
||||
def _log_chat_interaction(self, prompt: str, response: str, duration: Optional[float] = None):
|
||||
"""Log chat interaction for debugging and analytics.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
response: Generated response
|
||||
duration: Processing duration in seconds
|
||||
"""
|
||||
agent_name = getattr(self, 'name', None) or 'Agent'
|
||||
duration_str = f" (took {duration:.2f}s)" if duration else ""
|
||||
|
||||
logger.debug(f"[{agent_name}] Chat interaction{duration_str}")
|
||||
logger.debug(f"[{agent_name}] Prompt: {prompt[:100]}{'...' if len(prompt) > 100 else ''}")
|
||||
logger.debug(f"[{agent_name}] Response: {response[:100]}{'...' if len(response) > 100 else ''}")
|
||||
File diff suppressed because it is too large.
Load diff
@@ -742,7 +742,7 @@ Write the complete compiled report:"""
|
||||
|
||||
# Show animated status during LLM call if verbose
|
||||
if self.verbose and is_tty:
|
||||
from ..main import PRAISON_COLORS, sync_display_callbacks, _callbacks_lock
|
||||
from ..main import PRAISON_COLORS, sync_display_callbacks
|
||||
import threading
|
||||
import time as time_module
|
||||
|
||||
@@ -763,11 +763,9 @@ Write the complete compiled report:"""
|
||||
tools_called.append(tool_name)
|
||||
current_status[0] = f"Calling tool: {tool_name}..."
|
||||
|
||||
# Store original callback and register ours (use the module's
|
||||
# own lock so this doesn't race concurrent verbose agents)
|
||||
with _callbacks_lock:
|
||||
original_tool_callback = sync_display_callbacks.get('tool_call')
|
||||
sync_display_callbacks['tool_call'] = status_tool_callback
|
||||
# Store original callback and register ours
|
||||
original_tool_callback = sync_display_callbacks.get('tool_call')
|
||||
sync_display_callbacks['tool_call'] = status_tool_callback
|
||||
|
||||
# Animation state
|
||||
result_holder = [None]
|
||||
@@ -805,16 +803,11 @@ Write the complete compiled report:"""
|
||||
error_holder[0] = e
|
||||
finally:
|
||||
self.verbose = original_verbose_chat
|
||||
# Restore original callback under the lock. The identity
|
||||
# check guards the entire restore, so we only mutate the
|
||||
# entry while it's still ours and never clobber a callback
|
||||
# a concurrent verbose agent has since installed.
|
||||
with _callbacks_lock:
|
||||
if sync_display_callbacks.get('tool_call') is status_tool_callback:
|
||||
if original_tool_callback:
|
||||
sync_display_callbacks['tool_call'] = original_tool_callback
|
||||
else:
|
||||
del sync_display_callbacks['tool_call']
|
||||
# Restore original callback
|
||||
if original_tool_callback:
|
||||
sync_display_callbacks['tool_call'] = original_tool_callback
|
||||
elif 'tool_call' in sync_display_callbacks:
|
||||
del sync_display_callbacks['tool_call']
|
||||
|
||||
# Start chat in background thread
|
||||
chat_thread = threading.Thread(target=run_chat)
|
||||
@@ -992,83 +985,6 @@ Write the complete compiled report:"""
|
||||
"""Backward-compatible async alias for :meth:`alearn_skill`."""
|
||||
return await self.alearn_skill(request, **kwargs)
|
||||
|
||||
def optimize_instructions(
|
||||
self,
|
||||
evalset: List[Any],
|
||||
*,
|
||||
metric: Optional[Any] = None,
|
||||
scorer: Optional[Any] = None,
|
||||
criteria: str = "",
|
||||
n_candidates: int = 6,
|
||||
apply: bool = True,
|
||||
) -> Any:
|
||||
"""Optimise this agent's own ``instructions`` against an eval set.
|
||||
|
||||
Opt-in, off by default. Generates ``n_candidates`` instruction variants,
|
||||
scores each over ``evalset`` (LLM ``Judge`` by default, or a numeric
|
||||
``metric`` you supply), keeps the highest-scoring one, and — when
|
||||
``apply=True`` — writes it back to ``self.instructions``.
|
||||
|
||||
Args:
|
||||
evalset: List of ``(prompt, expected)`` cases to score candidates on.
|
||||
metric: Optional numeric metric ``(output, expected) -> float``.
|
||||
When set, empirical scoring replaces the LLM Judge.
|
||||
scorer: Optional custom ``Judge`` instance (ignored if ``metric`` set).
|
||||
criteria: Optional criteria for the default Judge.
|
||||
n_candidates: Number of instruction variants to try (default: 6).
|
||||
apply: Write the winning instructions back to the agent (default: True).
|
||||
|
||||
Returns:
|
||||
``OptimizeResult`` with ``best_instructions``, ``best_score``,
|
||||
``base_score`` and the full ``trials`` list.
|
||||
|
||||
Example::
|
||||
|
||||
result = agent.optimize_instructions(
|
||||
evalset=[("summarise X", gold_x)], metric=rouge_l,
|
||||
)
|
||||
print(result.best_score, result.best_instructions)
|
||||
"""
|
||||
from praisonaiagents.eval.prompt_optimizer import PromptOptimizer
|
||||
|
||||
return PromptOptimizer(
|
||||
self,
|
||||
evalset,
|
||||
scorer=scorer,
|
||||
metric=metric,
|
||||
criteria=criteria,
|
||||
n_candidates=n_candidates,
|
||||
apply=apply,
|
||||
).optimize()
|
||||
|
||||
async def aoptimize_instructions(
|
||||
self,
|
||||
evalset: List[Any],
|
||||
*,
|
||||
metric: Optional[Any] = None,
|
||||
scorer: Optional[Any] = None,
|
||||
criteria: str = "",
|
||||
n_candidates: int = 6,
|
||||
apply: bool = True,
|
||||
) -> Any:
|
||||
"""Async twin of :meth:`optimize_instructions`.
|
||||
|
||||
The optimiser runs synchronous agent calls internally; this variant
|
||||
offloads the run to a worker thread so async callers never block the
|
||||
event loop.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
return await asyncio.to_thread(
|
||||
self.optimize_instructions,
|
||||
evalset,
|
||||
metric=metric,
|
||||
scorer=scorer,
|
||||
criteria=criteria,
|
||||
n_candidates=n_candidates,
|
||||
apply=apply,
|
||||
)
|
||||
|
||||
def _ensure_skill_management_tools(self) -> None:
|
||||
"""Ensure the agent has the ``skill_manage`` tool for authoring skills.
|
||||
|
||||
@@ -1130,145 +1046,8 @@ Write the complete compiled report:"""
|
||||
return await self.achat(prompt, task_name=task_name, task_description=task_description, task_id=task_id)
|
||||
|
||||
async def execute_tool_async(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None, tools_override: Optional[List] = None) -> Any:
|
||||
"""Async version of execute_tool with retry policy support.
|
||||
|
||||
Routes through the user-supplied tool middleware (``Agent(hooks=[...])``)
|
||||
when present so ``before_tool``/``after_tool``/``wrap_tool_call`` gate
|
||||
async tool calls exactly as they do sync ones (security parity). The
|
||||
fast path (no tool hooks) calls straight into the retry loop with zero
|
||||
overhead.
|
||||
"""
|
||||
|
||||
# Record the tool name for this turn so the self-improve review policy
|
||||
# sees async tool usage too (issue #3037). Mirrors the sync path in
|
||||
# _execute_tool_with_context; skipped during a guarded review turn so
|
||||
# the review's own calls are not tracked and cannot recurse.
|
||||
if not getattr(self, "_in_skill_review", False):
|
||||
turn_tools = getattr(self, "_turn_tools_used", None)
|
||||
if turn_tools is None:
|
||||
self._turn_tools_used = []
|
||||
turn_tools = self._turn_tools_used
|
||||
turn_tools.append(function_name)
|
||||
|
||||
# Enforce BEFORE_TOOL/AFTER_TOOL security hooks for every async caller,
|
||||
# mirroring the sync execute_tool path in tool_execution.py. Without
|
||||
# this the primary async path (_execute_unified_achat_completion) would
|
||||
# silently skip HookEvent.BEFORE_TOOL gating registered via
|
||||
# Agent(hooks=HookRegistry(...)). Zero overhead when no hooks apply.
|
||||
hook_runner = getattr(self, '_hook_runner', None)
|
||||
if hook_runner is not None:
|
||||
from ..hooks import HookEvent
|
||||
if hook_runner.registry.has_hooks(HookEvent.BEFORE_TOOL):
|
||||
from ..hooks import BeforeToolInput
|
||||
before_tool_input = BeforeToolInput(
|
||||
session_id=getattr(self, '_session_id', 'default'),
|
||||
cwd=os.getcwd(),
|
||||
event_name=HookEvent.BEFORE_TOOL,
|
||||
timestamp=str(time.time()),
|
||||
agent_name=self.name,
|
||||
tool_name=function_name,
|
||||
tool_input=arguments,
|
||||
)
|
||||
before_results = await hook_runner.execute(
|
||||
HookEvent.BEFORE_TOOL, before_tool_input, target=function_name
|
||||
)
|
||||
if hook_runner.is_blocked(before_results):
|
||||
logging.warning(f"Tool {function_name} execution blocked by BEFORE_TOOL hook")
|
||||
return f"Execution of {function_name} was blocked by security policy."
|
||||
for res in before_results:
|
||||
if res.output and res.output.modified_input:
|
||||
arguments.update(res.output.modified_input)
|
||||
|
||||
result = await self._execute_tool_async_dispatch(
|
||||
function_name, arguments, tool_call_id, tools_override
|
||||
)
|
||||
|
||||
if hook_runner.registry.has_hooks(HookEvent.AFTER_TOOL):
|
||||
from ..hooks import AfterToolInput
|
||||
after_tool_input = AfterToolInput(
|
||||
session_id=getattr(self, '_session_id', 'default'),
|
||||
cwd=os.getcwd(),
|
||||
event_name=HookEvent.AFTER_TOOL,
|
||||
timestamp=str(time.time()),
|
||||
agent_name=self.name,
|
||||
tool_name=function_name,
|
||||
tool_input=arguments,
|
||||
tool_output=result,
|
||||
)
|
||||
after_results = await hook_runner.execute(
|
||||
HookEvent.AFTER_TOOL, after_tool_input, target=function_name
|
||||
)
|
||||
extra_context = hook_runner.aggregate_context(after_results)
|
||||
if extra_context:
|
||||
if isinstance(result, str):
|
||||
result = f"{result}\n\n{extra_context}"
|
||||
elif isinstance(result, dict):
|
||||
result.setdefault("_additional_context", extra_context)
|
||||
return result
|
||||
|
||||
return await self._execute_tool_async_dispatch(
|
||||
function_name, arguments, tool_call_id, tools_override
|
||||
)
|
||||
|
||||
async def _execute_tool_async_dispatch(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None, tools_override: Optional[List] = None) -> Any:
|
||||
"""Route an async tool call through the middleware chain (if any) then retry loop."""
|
||||
# Route async tool calls through the same tool middleware chain as the
|
||||
# sync path. The chain (before_tool/after_tool/wrap_tool_call) is
|
||||
# synchronous, so we run it in a worker thread and bridge its final
|
||||
# handler back to this event loop via run_coroutine_threadsafe.
|
||||
manager = self._get_tool_middleware_manager()
|
||||
if manager is not None:
|
||||
return await self._execute_tool_async_via_middleware(
|
||||
manager, function_name, arguments, tool_call_id, tools_override
|
||||
)
|
||||
return await self._execute_tool_async_with_retry(
|
||||
function_name, arguments, tool_call_id, tools_override
|
||||
)
|
||||
|
||||
async def _execute_tool_async_via_middleware(
|
||||
self, manager, function_name, arguments, tool_call_id, tools_override
|
||||
):
|
||||
"""Drive the (sync) tool middleware chain around an async tool call.
|
||||
|
||||
The middleware manager and its ``wrap_tool_call`` chain are synchronous
|
||||
by contract, so they run in a thread-pool worker; the innermost handler
|
||||
schedules the actual async execution back on the current running loop
|
||||
and blocks the worker on the result. This preserves short-circuit,
|
||||
audit, and argument-mutation semantics for async tools.
|
||||
"""
|
||||
from ..hooks import ToolRequest, ToolResponse, InvocationContext
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
request = ToolRequest(
|
||||
tool_name=function_name,
|
||||
arguments=arguments,
|
||||
context=InvocationContext(
|
||||
agent_id=self.name,
|
||||
run_id=getattr(self, '_current_run_id', 'unknown'),
|
||||
session_id=getattr(self, '_session_id', None) or 'default',
|
||||
tool_name=function_name,
|
||||
),
|
||||
)
|
||||
|
||||
def _final_handler(req):
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self._execute_tool_async_with_retry(
|
||||
req.tool_name, req.arguments, tool_call_id, tools_override
|
||||
),
|
||||
loop,
|
||||
)
|
||||
result = future.result()
|
||||
return ToolResponse(tool_name=req.tool_name, result=result)
|
||||
|
||||
def _run_chain():
|
||||
return manager.execute_tool_call(request, _final_handler)
|
||||
|
||||
response = await loop.run_in_executor(None, _run_chain)
|
||||
return response.result if isinstance(response, ToolResponse) else response
|
||||
|
||||
async def _execute_tool_async_with_retry(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None, tools_override: Optional[List] = None) -> Any:
|
||||
"""Async tool execution with retry policy (middleware-agnostic core)."""
|
||||
|
||||
"""Async version of execute_tool with retry policy support"""
|
||||
|
||||
# Get retry policy (tool-level > agent-level > default)
|
||||
retry_policy = self._get_tool_retry_policy(function_name)
|
||||
|
||||
@@ -1354,17 +1133,7 @@ Write the complete compiled report:"""
|
||||
error_msg = f"Error during approval process: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return {"error": error_msg, "approval_error": True}
|
||||
|
||||
# Policy/guardrail gate (protocol-driven). Mirrors the sync path in
|
||||
# _execute_tool_impl so async callers cannot bypass a PolicyEngine
|
||||
# deny or a tool-call guardrail. The check is pure/sync (no awaits).
|
||||
check = getattr(self, "_check_tool_policy_and_guardrails", None)
|
||||
if check is not None:
|
||||
policy_result = check(function_name, arguments)
|
||||
if isinstance(policy_result, dict):
|
||||
return policy_result # Error dict
|
||||
_, arguments = policy_result
|
||||
|
||||
|
||||
# Try to find the function in the override tools list first, then agent's tools list
|
||||
func = None
|
||||
tools_to_search = tools_override if tools_override is not None else self.tools
|
||||
@@ -1384,10 +1153,7 @@ Write the complete compiled report:"""
|
||||
else:
|
||||
logging.debug(f"Executing sync function in executor: {function_name}")
|
||||
loop = asyncio.get_running_loop()
|
||||
from ..trace.context_events import copy_context_to_callable
|
||||
result = await loop.run_in_executor(
|
||||
None, copy_context_to_callable(lambda: func(**arguments))
|
||||
)
|
||||
result = await loop.run_in_executor(None, lambda: func(**arguments))
|
||||
|
||||
# Ensure result is JSON serializable
|
||||
logging.debug(f"Raw result from tool: {result}")
|
||||
|
||||
@@ -7,7 +7,6 @@ for maintainability.
|
||||
"""
|
||||
|
||||
import os
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
@@ -299,14 +298,8 @@ class MemoryMixin:
|
||||
from ..session import get_default_session_store
|
||||
self._session_store = get_default_session_store()
|
||||
|
||||
# Restore chat history from previous session. Prefer the compacted
|
||||
# working history (summary + retained tail) when a compaction
|
||||
# checkpoint exists so resume is cheap and won't overflow context
|
||||
# (Issue #2741). Falls back to raw chat history for backward compat.
|
||||
if hasattr(self._session_store, "get_working_history"):
|
||||
history = self._session_store.get_working_history(self._session_id)
|
||||
else:
|
||||
history = self._session_store.get_chat_history(self._session_id)
|
||||
# Restore chat history from previous session
|
||||
history = self._session_store.get_chat_history(self._session_id)
|
||||
if history:
|
||||
# Only restore if chat_history is empty (avoid duplicates)
|
||||
if not self.chat_history:
|
||||
@@ -368,25 +361,8 @@ class MemoryMixin:
|
||||
|
||||
self._current_run_id = None
|
||||
|
||||
def _persist_message(
|
||||
self,
|
||||
role: str,
|
||||
content: str,
|
||||
tool_calls=None,
|
||||
tool_call_id: Optional[str] = None,
|
||||
):
|
||||
"""Persist a message to the DB or session store.
|
||||
|
||||
Args:
|
||||
role: Message role ("user", "assistant", "system", "tool").
|
||||
content: Message text.
|
||||
tool_calls: Optional structured tool calls on an assistant turn
|
||||
(Issue #3089). Persisted faithfully to the default JSON store
|
||||
so a resumed tool-using session reconstructs the same message
|
||||
list the model saw before.
|
||||
tool_call_id: Optional id linking a ``role="tool"`` result turn to
|
||||
the assistant tool call it answers (Issue #3089).
|
||||
"""
|
||||
def _persist_message(self, role: str, content: str):
|
||||
"""Persist a message to the DB or session store."""
|
||||
# Try DB adapter first
|
||||
if self._db is not None:
|
||||
try:
|
||||
@@ -404,23 +380,7 @@ class MemoryMixin:
|
||||
if role == "user":
|
||||
self._session_store.add_user_message(self._session_id, content)
|
||||
elif role == "assistant":
|
||||
# Faithful transcript: carry any tool calls the assistant
|
||||
# requested so resume replays them (Issue #3089). Falls back
|
||||
# to a plain text turn for stores predating the tool fields.
|
||||
if tool_calls:
|
||||
self._add_message_with_tool_fields(
|
||||
"assistant", content, tool_calls=tool_calls
|
||||
)
|
||||
else:
|
||||
self._session_store.add_assistant_message(
|
||||
self._session_id, content
|
||||
)
|
||||
elif role == "tool":
|
||||
# Persist the tool-result turn linked to its call id so the
|
||||
# resumed message list interleaves results in order (#3089).
|
||||
self._add_message_with_tool_fields(
|
||||
"tool", content, tool_call_id=tool_call_id
|
||||
)
|
||||
self._session_store.add_assistant_message(self._session_id, content)
|
||||
# Keep auto_save index in sync when per-turn persist shares session_id
|
||||
if self.auto_save and self.auto_save == self._session_id:
|
||||
with self._history_lock:
|
||||
@@ -431,45 +391,6 @@ class MemoryMixin:
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to persist message to session store: {e}")
|
||||
|
||||
def _add_message_with_tool_fields(self, role, content, **tool_fields):
|
||||
"""Add a message carrying tool fields, tolerating stores without them.
|
||||
|
||||
The default JSON store and the SQLite store accept ``tool_calls`` /
|
||||
``tool_call_id``, but custom stores implementing the older
|
||||
``SessionStoreProtocol`` (and the built-in ``HierarchicalSessionStore``)
|
||||
only accept ``(session_id, role, content, metadata)``. Passing the new
|
||||
keywords to those would raise ``TypeError`` and drop the turn entirely
|
||||
(Issue #3089). We feature-detect once via the call signature and fall
|
||||
back to a plain-text turn so no exchange is silently lost.
|
||||
"""
|
||||
add_message = self._session_store.add_message
|
||||
if self._store_supports_tool_fields(add_message):
|
||||
add_message(self._session_id, role, content, **tool_fields)
|
||||
return
|
||||
# Legacy store: preserve the turn as plain text so resume still sees it.
|
||||
add_message(self._session_id, role, content)
|
||||
|
||||
def _store_supports_tool_fields(self, add_message) -> bool:
|
||||
"""Return True if ``add_message`` accepts the tool keyword fields."""
|
||||
cached = getattr(self, "_session_store_tool_fields", None)
|
||||
if cached is not None:
|
||||
return cached
|
||||
supported = True
|
||||
try:
|
||||
params = inspect.signature(add_message).parameters
|
||||
has_var_kw = any(
|
||||
p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values()
|
||||
)
|
||||
supported = has_var_kw or (
|
||||
"tool_calls" in params and "tool_call_id" in params
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
# Builtins / C-callables without introspectable signatures: assume
|
||||
# the conservative legacy shape.
|
||||
supported = False
|
||||
self._session_store_tool_fields = supported
|
||||
return supported
|
||||
|
||||
def _persist_session_stats(self):
|
||||
"""Flush agent cost/token stats into session JSON metadata."""
|
||||
store = getattr(self, "_session_store", None)
|
||||
|
||||
@@ -239,22 +239,6 @@ class SteeringMixin:
|
||||
"""Whether message steering is enabled for this agent."""
|
||||
return self._message_steering is not None and self._message_steering.enabled
|
||||
|
||||
@staticmethod
|
||||
def _format_steering_note(content: str, priority: str) -> str:
|
||||
"""
|
||||
Format a steering message according to its priority.
|
||||
|
||||
Shared by ``_check_steering_messages`` (pre-turn) and
|
||||
``_drain_steering_messages`` (mid-run) so INTERRUPT/URGENT priority is
|
||||
preserved consistently on every injection path.
|
||||
"""
|
||||
if priority == "INTERRUPT":
|
||||
return f"\n[INTERRUPT USER GUIDANCE]: {content}\nPlease stop current work and follow this guidance immediately."
|
||||
elif priority in ("HIGH", "URGENT"):
|
||||
return f"\n[URGENT USER GUIDANCE]: {content}\nPlease acknowledge and adjust your approach accordingly."
|
||||
else:
|
||||
return f"\n[USER GUIDANCE]: {content}\nPlease consider this feedback as you continue."
|
||||
|
||||
def _check_steering_messages(self) -> Optional[str]:
|
||||
"""
|
||||
Check for and process steering messages.
|
||||
@@ -275,43 +259,15 @@ class SteeringMixin:
|
||||
if steering_messages:
|
||||
# Get the most recent message
|
||||
latest = steering_messages[-1]
|
||||
return self._format_steering_note(latest["content"], latest["priority"])
|
||||
content = latest["content"]
|
||||
priority = latest["priority"]
|
||||
|
||||
# Format as system message
|
||||
if priority == "INTERRUPT":
|
||||
return f"\n[INTERRUPT USER GUIDANCE]: {content}\nPlease stop current work and follow this guidance immediately."
|
||||
elif priority in ("HIGH", "URGENT"):
|
||||
return f"\n[URGENT USER GUIDANCE]: {content}\nPlease acknowledge and adjust your approach accordingly."
|
||||
else:
|
||||
return f"\n[USER GUIDANCE]: {content}\nPlease consider this feedback as you continue."
|
||||
|
||||
return None
|
||||
|
||||
def _drain_steering_messages(self) -> List[str]:
|
||||
"""
|
||||
Drain all pending steering messages for mid-run injection.
|
||||
|
||||
Unlike ``_check_steering_messages`` (which is rate-limited and returns a
|
||||
single formatted string for pre-turn use), this returns every pending
|
||||
steering message formatted with priority-aware guidance so it can be
|
||||
injected as ``user`` messages between tool iterations of an in-flight
|
||||
run. INTERRUPT/URGENT priority is preserved so a mid-run interrupt is
|
||||
not silently downgraded to normal guidance.
|
||||
|
||||
Returns:
|
||||
List of formatted steering notes (may be empty). Never raises.
|
||||
"""
|
||||
steering = getattr(self, "_message_steering", None)
|
||||
if steering is None:
|
||||
return []
|
||||
notes: List[str] = []
|
||||
try:
|
||||
while steering.has_pending_messages():
|
||||
msg = steering._message_queue.dequeue(timeout=0)
|
||||
if msg is None:
|
||||
break
|
||||
content = getattr(msg, "content", None)
|
||||
if content is None:
|
||||
# Non-SteeringMessage payload - inject raw content as-is.
|
||||
if msg:
|
||||
notes.append(str(msg))
|
||||
continue
|
||||
if not content:
|
||||
continue
|
||||
priority = getattr(getattr(msg, "priority", None), "name", "NORMAL")
|
||||
notes.append(self._format_steering_note(str(content), priority))
|
||||
except Exception as e: # pragma: no cover - defensive
|
||||
logger.warning(f"Draining steering messages failed: {e}")
|
||||
return notes
|
||||
return None
|
||||
@@ -16,8 +16,6 @@ This logic lives in its own mixin so ``agent.py`` stays lean.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,22 +23,6 @@ logger = logging.getLogger(__name__)
|
||||
class SkillReviewMixin:
|
||||
"""Mixin providing the guarded skill-review pass for ``Agent``."""
|
||||
|
||||
def _skill_review_lock(self):
|
||||
"""Return the per-agent lock serialising the guarded review turn.
|
||||
|
||||
The review mutates shared per-agent state (``chat_history`` and the
|
||||
``_in_skill_review`` guard). In ``"background"`` mode it runs off the
|
||||
hot path on a worker thread, so the caller may start the next turn on
|
||||
the *same* agent before it finishes. Holding this lock for the whole
|
||||
review keeps its history snapshot/restore atomic and prevents it from
|
||||
interleaving with another turn (or a second review) on the same agent.
|
||||
"""
|
||||
lock = getattr(self, "_skill_review_lock_obj", None)
|
||||
if lock is None:
|
||||
lock = threading.RLock()
|
||||
self._skill_review_lock_obj = lock
|
||||
return lock
|
||||
|
||||
def _skill_review_policy(self):
|
||||
"""Return the active review policy, building the default lazily."""
|
||||
policy = getattr(self, "_self_improve_policy", None)
|
||||
@@ -134,83 +116,47 @@ class SkillReviewMixin:
|
||||
response: The final response produced for the task.
|
||||
tools_used: Names of tools used during the task (if known).
|
||||
"""
|
||||
with self._skill_review_lock():
|
||||
prepared = self._prepare_skill_review(prompt, response, tools_used)
|
||||
if prepared is None:
|
||||
return
|
||||
review_prompt, review_tools = prepared
|
||||
prepared = self._prepare_skill_review(prompt, response, tools_used)
|
||||
if prepared is None:
|
||||
return
|
||||
review_prompt, review_tools = prepared
|
||||
|
||||
self._in_skill_review = True
|
||||
history_len = self._snapshot_chat_history_len()
|
||||
try:
|
||||
if getattr(self, "verbose", False):
|
||||
logger.info("Agent %s running guarded skill-review pass", self.name)
|
||||
# Restricted turn: only skill_manage is exposed via the tools= arg.
|
||||
self.chat(review_prompt, tools=review_tools)
|
||||
except Exception as e:
|
||||
name, session_id = self._skill_review_log_context()
|
||||
logger.warning(
|
||||
"Skill review pass failed for agent=%s session_id=%s: %s",
|
||||
name, session_id, e, exc_info=True,
|
||||
)
|
||||
finally:
|
||||
self._restore_chat_history(history_len)
|
||||
self._in_skill_review = False
|
||||
self._in_skill_review = True
|
||||
history_len = self._snapshot_chat_history_len()
|
||||
try:
|
||||
if getattr(self, "verbose", False):
|
||||
logger.info("Agent %s running guarded skill-review pass", self.name)
|
||||
# Restricted turn: only skill_manage is exposed via the tools= arg.
|
||||
self.chat(review_prompt, tools=review_tools)
|
||||
except Exception as e:
|
||||
name, session_id = self._skill_review_log_context()
|
||||
logger.warning(
|
||||
"Skill review pass failed for agent=%s session_id=%s: %s",
|
||||
name, session_id, e, exc_info=True,
|
||||
)
|
||||
finally:
|
||||
self._restore_chat_history(history_len)
|
||||
self._in_skill_review = False
|
||||
|
||||
async def _arun_skill_review(self, prompt, response, tools_used=None):
|
||||
"""Async variant of :meth:`_run_skill_review`."""
|
||||
with self._skill_review_lock():
|
||||
prepared = self._prepare_skill_review(prompt, response, tools_used)
|
||||
if prepared is None:
|
||||
return
|
||||
review_prompt, review_tools = prepared
|
||||
prepared = self._prepare_skill_review(prompt, response, tools_used)
|
||||
if prepared is None:
|
||||
return
|
||||
review_prompt, review_tools = prepared
|
||||
|
||||
self._in_skill_review = True
|
||||
history_len = self._snapshot_chat_history_len()
|
||||
try:
|
||||
if getattr(self, "verbose", False):
|
||||
logger.info("Agent %s running guarded skill-review pass", self.name)
|
||||
await self.achat(review_prompt, tools=review_tools)
|
||||
except Exception as e:
|
||||
name, session_id = self._skill_review_log_context()
|
||||
logger.warning(
|
||||
"Skill review pass failed for agent=%s session_id=%s: %s",
|
||||
name, session_id, e, exc_info=True,
|
||||
)
|
||||
finally:
|
||||
self._restore_chat_history(history_len)
|
||||
self._in_skill_review = False
|
||||
|
||||
def _schedule_self_improvement(self, run_review):
|
||||
"""Enqueue the guarded skill-review pass on the core background runner.
|
||||
|
||||
The reply has already been returned to the caller by the time this is
|
||||
called; the review turn (a full extra LLM round-trip) runs off the hot
|
||||
path on the shared :class:`BackgroundJobManager` so a long-lived
|
||||
gateway/bot agent never pays its latency on the user-visible path
|
||||
(issue #2985). Best-effort: if the runner cannot be reached the review
|
||||
falls back to running inline so behaviour is never silently dropped.
|
||||
|
||||
Args:
|
||||
run_review: A zero-arg callable that performs the guarded review
|
||||
(already bound to the captured trajectory).
|
||||
"""
|
||||
self._in_skill_review = True
|
||||
history_len = self._snapshot_chat_history_len()
|
||||
try:
|
||||
from ..background.job_manager import get_job_manager
|
||||
if getattr(self, "verbose", False):
|
||||
logger.info("Agent %s running guarded skill-review pass", self.name)
|
||||
await self.achat(review_prompt, tools=review_tools)
|
||||
except Exception as e:
|
||||
name, session_id = self._skill_review_log_context()
|
||||
scope = session_id or name or "default"
|
||||
# Unique per invocation: the job manager keys its job/future maps by
|
||||
# job_id, so a stable id would let a later review replace (and drop)
|
||||
# an earlier one still queued for the same agent/session. The short
|
||||
# uuid suffix keeps ids human-scannable while guaranteeing each
|
||||
# scheduled review is tracked independently.
|
||||
get_job_manager().start_job(
|
||||
run_review,
|
||||
job_id=f"self-improve:{scope}:{uuid.uuid4().hex[:8]}",
|
||||
logger.warning(
|
||||
"Skill review pass failed for agent=%s session_id=%s: %s",
|
||||
name, session_id, e, exc_info=True,
|
||||
)
|
||||
except Exception as e: # pragma: no cover - defensive fallback
|
||||
logger.debug(
|
||||
"Backgrounding skill review failed (%s); running inline", e,
|
||||
exc_info=True,
|
||||
)
|
||||
run_review()
|
||||
finally:
|
||||
self._restore_chat_history(history_len)
|
||||
self._in_skill_review = False
|
||||
+1
-1
@@ -10,7 +10,7 @@ import time
|
||||
from unittest.mock import Mock, patch, MagicMock, AsyncMock
|
||||
import asyncio
|
||||
|
||||
from praisonaiagents.agent.handoff import Handoff, HandoffConfig, ContextPolicy, HandoffResult
|
||||
from .handoff import Handoff, HandoffConfig, ContextPolicy, HandoffResult
|
||||
|
||||
|
||||
class MockAgent:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,16 +1,6 @@
|
||||
"""
|
||||
Unified Execution Mixin - implements Gap 1 from Issue #1392.
|
||||
|
||||
.. deprecated::
|
||||
UnifiedExecutionMixin is unused and scheduled for removal (Issue #2644).
|
||||
None of its methods are invoked by production code: the live execution paths
|
||||
use ``_chat_impl`` / ``_execute_unified_chat_completion`` (in ``chat_mixin.py``)
|
||||
and ``execute_tool`` (in ``tool_execution.py``), which are distinct from this
|
||||
mixin's methods despite the similar names. This module remains importable and
|
||||
emits a ``DeprecationWarning`` from its public methods during the deprecation
|
||||
window; it will be removed from the ``Agent`` MRO after the cycle completes.
|
||||
Use ``Agent.chat()`` / ``Agent.achat()`` / ``Agent.execute_tool()`` instead.
|
||||
|
||||
This module consolidates sync/async execution paths into a single async-first
|
||||
implementation with a thin sync bridge, eliminating code duplication between
|
||||
chat()/achat() and execute_tool()/execute_tool_async().
|
||||
@@ -31,33 +21,16 @@ Design principles:
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
import warnings
|
||||
from typing import List, Optional, Any, Dict, Union
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEPRECATION_MESSAGE = (
|
||||
"UnifiedExecutionMixin is unused and scheduled for removal (Issue #2644); "
|
||||
"no production path calls its methods. Use Agent.chat() / Agent.achat() / "
|
||||
"Agent.execute_tool() instead."
|
||||
)
|
||||
|
||||
|
||||
def _warn_deprecated() -> None:
|
||||
"""Emit a DeprecationWarning for the unused UnifiedExecutionMixin public API."""
|
||||
warnings.warn(_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=3)
|
||||
|
||||
|
||||
class UnifiedExecutionMixin:
|
||||
"""
|
||||
Mixin providing unified sync/async execution for Agent class.
|
||||
|
||||
.. deprecated::
|
||||
Unused and scheduled for removal (Issue #2644). Its public methods emit a
|
||||
``DeprecationWarning``. Use ``Agent.chat()`` / ``Agent.achat()`` /
|
||||
``Agent.execute_tool()`` instead.
|
||||
|
||||
|
||||
This replaces the duplicated logic between chat/achat and execute_tool/execute_tool_async
|
||||
with a single async-first implementation plus sync bridge.
|
||||
"""
|
||||
@@ -211,8 +184,8 @@ class UnifiedExecutionMixin:
|
||||
|
||||
# Update prompt if modified by hooks
|
||||
for res in hook_results:
|
||||
if res.output and res.output.modified_input and "prompt" in res.output.modified_input:
|
||||
prompt = res.output.modified_input["prompt"]
|
||||
if res.output and res.output.modified_data and "prompt" in res.output.modified_data:
|
||||
prompt = res.output.modified_data["prompt"]
|
||||
llm_prompt = self._build_multimodal_prompt(prompt, attachments) if attachments else prompt
|
||||
|
||||
# Reset the final display flag for each new conversation
|
||||
@@ -389,11 +362,7 @@ class UnifiedExecutionMixin:
|
||||
|
||||
This replaces the duplicated _chat_impl logic by using the unified
|
||||
async core with proper event loop bridging.
|
||||
|
||||
.. deprecated::
|
||||
Unused and scheduled for removal (Issue #2644). Use ``Agent.chat()``.
|
||||
"""
|
||||
_warn_deprecated()
|
||||
return self._run_async_in_sync_context(
|
||||
self._unified_chat_impl(*args, **kwargs)
|
||||
)
|
||||
@@ -404,11 +373,7 @@ class UnifiedExecutionMixin:
|
||||
|
||||
This replaces the duplicated _achat_impl logic by using the unified
|
||||
async core directly.
|
||||
|
||||
.. deprecated::
|
||||
Unused and scheduled for removal (Issue #2644). Use ``Agent.achat()``.
|
||||
"""
|
||||
_warn_deprecated()
|
||||
return await self._unified_chat_impl(*args, **kwargs)
|
||||
|
||||
async def _unified_tool_execution(
|
||||
@@ -434,11 +399,7 @@ class UnifiedExecutionMixin:
|
||||
def unified_execute_tool(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None) -> Any:
|
||||
"""
|
||||
Sync entry point for unified tool execution.
|
||||
|
||||
.. deprecated::
|
||||
Unused and scheduled for removal (Issue #2644). Use ``Agent.execute_tool()``.
|
||||
"""
|
||||
_warn_deprecated()
|
||||
return self._run_async_in_sync_context(
|
||||
self._unified_tool_execution(function_name, arguments, tool_call_id)
|
||||
)
|
||||
@@ -446,9 +407,5 @@ class UnifiedExecutionMixin:
|
||||
async def unified_execute_tool_async(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None) -> Any:
|
||||
"""
|
||||
Async entry point for unified tool execution.
|
||||
|
||||
.. deprecated::
|
||||
Unused and scheduled for removal (Issue #2644). Use ``Agent.execute_tool()``.
|
||||
"""
|
||||
_warn_deprecated()
|
||||
return await self._unified_tool_execution(function_name, arguments, tool_call_id)
|
||||
return await self._unified_tool_execution(function_name, arguments, tool_call_id)
|
||||
@@ -111,6 +111,16 @@ class _AgentServerRegistry:
|
||||
with self._lock:
|
||||
return list(self._endpoints.get(port, {}).keys())
|
||||
|
||||
def has_route(self, port: int, path: str) -> bool:
|
||||
"""Thread-safe check whether a path is registered on a port."""
|
||||
with self._lock:
|
||||
return path in self._endpoints.get(port, {})
|
||||
|
||||
def is_server_started(self, port: int) -> bool:
|
||||
"""Check if server is started for this port."""
|
||||
with self._lock:
|
||||
return self._started.get(port, False)
|
||||
|
||||
def start_server_if_needed(self, port: int, host: str = "0.0.0.0", **kwargs) -> bool: # noqa: S104
|
||||
"""Start server with proper readiness signaling. Returns True if server was started."""
|
||||
with self._lock:
|
||||
@@ -357,12 +367,8 @@ def _build_execution_context(agents_instance, task_id):
|
||||
"Set task.agent or provide task.agent_config before execution."
|
||||
)
|
||||
|
||||
# Set current agent for token tracking.
|
||||
# Prefer the real LLM instance (executor_agent.llm is always a model-name
|
||||
# string, so it never exposes set_current_agent/last_token_metrics).
|
||||
llm = getattr(executor_agent, 'llm_instance', None)
|
||||
if llm is None:
|
||||
llm = getattr(executor_agent, 'llm', None)
|
||||
# Set current agent for token tracking
|
||||
llm = getattr(executor_agent, 'llm', None) or getattr(executor_agent, 'llm_instance', None)
|
||||
if llm and hasattr(llm, 'set_current_agent'):
|
||||
llm.set_current_agent(executor_agent.display_name)
|
||||
|
||||
@@ -457,14 +463,8 @@ def _process_task_result(agents_instance, context, agent_output):
|
||||
if token_metrics:
|
||||
task_output.token_metrics = token_metrics
|
||||
|
||||
cleaned = agent_output
|
||||
if task.output_json or task.output_pydantic:
|
||||
try:
|
||||
cleaned = agents_instance.clean_json_output(agent_output)
|
||||
except Exception as e:
|
||||
logger.warning(f"Warning: Could not clean output of task {task_id}: {e}")
|
||||
|
||||
if task.output_json:
|
||||
cleaned = agents_instance.clean_json_output(agent_output)
|
||||
try:
|
||||
parsed = json.loads(cleaned)
|
||||
task_output.json_dict = parsed
|
||||
@@ -474,6 +474,7 @@ def _process_task_result(agents_instance, context, agent_output):
|
||||
logger.debug(f"Output that failed JSON parsing: {agent_output}")
|
||||
|
||||
if task.output_pydantic:
|
||||
cleaned = agents_instance.clean_json_output(agent_output)
|
||||
try:
|
||||
parsed = json.loads(cleaned)
|
||||
pyd_obj = task.output_pydantic(**parsed)
|
||||
@@ -609,9 +610,7 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
Args:
|
||||
agents: List of Agent instances
|
||||
tasks: Optional list of Task instances (auto-generated from agents if None)
|
||||
process: Execution process type ("sequential", "workflow", "hierarchical").
|
||||
For parallel fan-out, set async_execution=True on individual Task
|
||||
objects within a "workflow" or "sequential" process.
|
||||
process: Execution process type ("sequential", "parallel", "hierarchical")
|
||||
manager_llm: LLM model for manager agent
|
||||
llm: Default LLM model for all agents
|
||||
name: Name for this agent collection
|
||||
@@ -818,14 +817,6 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
self._state_lock = threading.Lock() # Thread-safe state mutations
|
||||
self.verbose = _verbose
|
||||
self.max_retries = _max_retries
|
||||
_VALID_PROCESSES = {"workflow", "sequential", "hierarchical"}
|
||||
if process not in _VALID_PROCESSES:
|
||||
raise ValueError(
|
||||
f"Unknown process type {process!r}. Valid values are: "
|
||||
f"{sorted(_VALID_PROCESSES)}. Note: parallel fan-out is achieved "
|
||||
f"by setting async_execution=True on individual Task objects within "
|
||||
f"a 'workflow' or 'sequential' process, not via process=\"parallel\"."
|
||||
)
|
||||
self.process = process
|
||||
self.stream = _stream
|
||||
self.name = name
|
||||
@@ -969,8 +960,6 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
return task_id
|
||||
|
||||
def clean_json_output(self, output: str) -> str:
|
||||
# NOTE: This method is duplicated in chat_mixin.ChatMixin.clean_json_output.
|
||||
# Keep both implementations in sync when modifying either.
|
||||
cleaned = output.strip()
|
||||
if cleaned.startswith("```json"):
|
||||
cleaned = cleaned[len("```json"):].strip()
|
||||
@@ -1125,63 +1114,6 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
logger.warning(f"Task {task_id}: Guardrail processing error (retry {task.retry_count}/{task.max_retries}): {e}")
|
||||
return task_output, True # Signal retry needed
|
||||
|
||||
def _run_task_start_hook(self, task, task_id):
|
||||
"""Run the on_task_start hook and propagate global variables to the task.
|
||||
|
||||
Shared by run_task (sync) and arun_task (async) so the lifecycle hooks
|
||||
and variable propagation stay consistent across both paths.
|
||||
"""
|
||||
if self.on_task_start:
|
||||
try:
|
||||
self.on_task_start(task, task_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_task_start callback: {e}")
|
||||
|
||||
# Apply global variables to task if not already set. Use a shallow copy so a
|
||||
# task mutating its variables doesn't leak back into the shared AgentTeam state.
|
||||
if self.variables and not getattr(task, 'variables', None):
|
||||
task.variables = self.variables.copy()
|
||||
|
||||
def _run_task_complete_hook(self, task, task_output):
|
||||
"""Run the on_task_complete hook. Shared by run_task and arun_task."""
|
||||
if self.on_task_complete:
|
||||
try:
|
||||
self.on_task_complete(task, task_output)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_task_complete callback: {e}")
|
||||
|
||||
async def _arun_task_start_hook(self, task, task_id):
|
||||
"""Async-aware on_task_start hook for arun_task.
|
||||
|
||||
Awaits coroutine callbacks and offloads synchronous ones to the executor so
|
||||
a blocking hook can't stall the event loop. Falls back to the sync helper's
|
||||
variable propagation.
|
||||
"""
|
||||
if self.on_task_start:
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(self.on_task_start):
|
||||
await self.on_task_start(task, task_id)
|
||||
else:
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, self.on_task_start, task, task_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_task_start callback: {e}")
|
||||
|
||||
if self.variables and not getattr(task, 'variables', None):
|
||||
task.variables = self.variables.copy()
|
||||
|
||||
async def _arun_task_complete_hook(self, task, task_output):
|
||||
"""Async-aware on_task_complete hook for arun_task (see _arun_task_start_hook)."""
|
||||
if self.on_task_complete:
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(self.on_task_complete):
|
||||
await self.on_task_complete(task, task_output)
|
||||
else:
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, self.on_task_complete, task, task_output)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_task_complete callback: {e}")
|
||||
|
||||
|
||||
async def arun_task(self, task_id):
|
||||
"""Async version of run_task method"""
|
||||
@@ -1193,9 +1125,6 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
logger.info(f"Task with ID {task_id} is already completed")
|
||||
return
|
||||
|
||||
# Call on_task_start callback and propagate variables (async-aware, mirrors run_task)
|
||||
await self._arun_task_start_hook(task, task_id)
|
||||
|
||||
# Use per-task max_retries if available
|
||||
task_max = getattr(task, "max_retries", self.max_retries)
|
||||
retries = 0
|
||||
@@ -1224,10 +1153,6 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
raise
|
||||
|
||||
self.save_output_to_file(task, task_output)
|
||||
|
||||
# Call on_task_complete callback (async-aware, mirrors run_task)
|
||||
await self._arun_task_complete_hook(task, task_output)
|
||||
|
||||
if self.verbose >= 1:
|
||||
logger.info(f"Task {task_id} completed successfully.")
|
||||
else:
|
||||
@@ -1365,9 +1290,7 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
if task.output_file:
|
||||
try:
|
||||
if task.create_directory:
|
||||
dir_name = os.path.dirname(task.output_file)
|
||||
if dir_name:
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(task.output_file), exist_ok=True)
|
||||
with open(task.output_file, "w") as f:
|
||||
f.write(str(task_output))
|
||||
if self.verbose >= 1:
|
||||
@@ -1423,9 +1346,17 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
logger.info(f"Task with ID {task_id} is already completed")
|
||||
return
|
||||
|
||||
# Call on_task_start callback and propagate variables (shared with arun_task)
|
||||
self._run_task_start_hook(task, task_id)
|
||||
|
||||
# Call on_task_start callback if provided
|
||||
if self.on_task_start:
|
||||
try:
|
||||
self.on_task_start(task, task_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_task_start callback: {e}")
|
||||
|
||||
# Apply global variables to task if not already set
|
||||
if self.variables and not getattr(task, 'variables', None):
|
||||
task.variables = self.variables
|
||||
|
||||
# Use per-task max_retries if available
|
||||
task_max = getattr(task, "max_retries", self.max_retries)
|
||||
retries = 0
|
||||
@@ -1450,10 +1381,14 @@ class AgentTeam(SpawnAnnounceProtocol):
|
||||
logger.exception(e)
|
||||
|
||||
self.save_output_to_file(task, task_output)
|
||||
|
||||
# Call on_task_complete callback (shared with arun_task)
|
||||
self._run_task_complete_hook(task, task_output)
|
||||
|
||||
|
||||
# Call on_task_complete callback if provided
|
||||
if self.on_task_complete:
|
||||
try:
|
||||
self.on_task_complete(task, task_output)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_task_complete callback: {e}")
|
||||
|
||||
if self.verbose >= 1:
|
||||
logger.info(f"Task {task_id} completed successfully.")
|
||||
else:
|
||||
|
||||
@@ -14,25 +14,8 @@ from praisonaiagents._logging import get_logger
|
||||
import os
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from ..main import display_instruction, display_tool_call, display_interaction
|
||||
from ..llm import get_openai_client, LLM, OpenAIClient
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazily expose selected llm symbols as module attributes.
|
||||
|
||||
Keeps ``praisonaiagents.agents`` import light (the heavy ``llm.llm`` and
|
||||
``llm.openai_client`` module bodies are only loaded on first use) while
|
||||
still allowing these names to be resolved and patched as module-level
|
||||
attributes (e.g. ``patch('...autoagents.LLM')``).
|
||||
"""
|
||||
if name in ("get_openai_client", "LLM"):
|
||||
from .. import llm as _llm
|
||||
value = getattr(_llm, name)
|
||||
setattr(sys.modules[__name__], name, value)
|
||||
return value
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
# Define Pydantic models for structured output
|
||||
class TaskConfig(BaseModel):
|
||||
@@ -370,14 +353,11 @@ DO NOT use strings for tasks. Each task MUST be a complete object with all four
|
||||
else:
|
||||
prompt = base_prompt
|
||||
|
||||
# Resolve get_openai_client / LLM via module attributes so they stay
|
||||
# lazily loaded yet remain patchable in tests (see module __getattr__).
|
||||
_module = sys.modules[__name__]
|
||||
try:
|
||||
# Check if we have OpenAI API and the model supports structured output
|
||||
from ..llm import supports_structured_outputs
|
||||
if self.llm and supports_structured_outputs(self.llm):
|
||||
client = _module.get_openai_client()
|
||||
client = get_openai_client()
|
||||
use_openai_structured = True
|
||||
except Exception as e:
|
||||
# If OpenAI client is not available, we'll use the LLM class
|
||||
@@ -398,7 +378,7 @@ DO NOT use strings for tasks. Each task MUST be a complete object with all four
|
||||
last_response = json.dumps(config.model_dump(), indent=2)
|
||||
else:
|
||||
# Use LLM class for all other providers (Gemini, Anthropic, etc.)
|
||||
llm_instance = _module.LLM(
|
||||
llm_instance = LLM(
|
||||
model=self.llm,
|
||||
base_url=self.base_url,
|
||||
api_key=self.api_key
|
||||
|
||||
@@ -111,9 +111,6 @@ def is_env_auto_approve() -> bool:
|
||||
def set_yaml_approved_tools(tools: List[str]) -> contextvars.Token:
|
||||
return get_approval_registry().set_yaml_approved_tools(tools)
|
||||
|
||||
def add_yaml_approved_tools(tools: List[str]) -> contextvars.Token:
|
||||
return get_approval_registry().add_yaml_approved_tools(tools)
|
||||
|
||||
def reset_yaml_approved_tools(token: contextvars.Token) -> None:
|
||||
get_approval_registry().reset_yaml_approved_tools(token)
|
||||
|
||||
|
||||
@@ -45,30 +45,6 @@ def _get_rich_confirm():
|
||||
_rich_confirm = Confirm
|
||||
return _rich_confirm
|
||||
|
||||
_rich_prompt = None
|
||||
|
||||
def _get_rich_prompt():
|
||||
global _rich_prompt
|
||||
if _rich_prompt is None:
|
||||
from rich.prompt import Prompt
|
||||
_rich_prompt = Prompt
|
||||
return _rich_prompt
|
||||
|
||||
def _suggest_scope_pattern(request: ApprovalRequest) -> str:
|
||||
"""Return a reusable "always" pattern for *request* (best-effort).
|
||||
|
||||
Bridges the tool call to a :class:`PermissionManager`-style target and
|
||||
generalises shell commands into a reusable prefix glob. Never raises —
|
||||
falls back to the raw target so the prompt always has something to show.
|
||||
"""
|
||||
from .utils import build_permission_target
|
||||
target = build_permission_target(request.tool_name, request.arguments)
|
||||
try:
|
||||
from ..permissions import PermissionManager
|
||||
return PermissionManager(agent_name=request.agent_name).suggest_scope_pattern(target)
|
||||
except Exception: # noqa: BLE001 — suggestion is advisory only
|
||||
return target
|
||||
|
||||
class AutoApproveBackend:
|
||||
"""Always approves. Use for bots or trusted unattended environments."""
|
||||
|
||||
@@ -81,17 +57,11 @@ class AutoApproveBackend:
|
||||
class ConsoleBackend:
|
||||
"""Interactive Rich terminal prompt. Default for CLI usage."""
|
||||
|
||||
def _prompt_user(self, request: ApprovalRequest):
|
||||
"""Show Rich panel and ask once/session/always/no.
|
||||
|
||||
Returns a ``(approved, scope, scope_pattern)`` tuple where ``scope`` is
|
||||
one of ``"once"`` / ``"session"`` / ``"always"`` and ``scope_pattern``
|
||||
is the reusable target to persist for ``always`` (``None`` otherwise).
|
||||
A denial returns ``(False, "once", None)``.
|
||||
"""
|
||||
def _prompt_user(self, request: ApprovalRequest) -> bool:
|
||||
"""Show Rich panel and ask yes/no. Returns True if approved."""
|
||||
Console = _get_rich_console()
|
||||
Panel = _get_rich_panel()
|
||||
Prompt = _get_rich_prompt()
|
||||
Confirm = _get_rich_confirm()
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -121,42 +91,20 @@ class ConsoleBackend:
|
||||
title_align="left",
|
||||
))
|
||||
|
||||
suggested = _suggest_scope_pattern(request)
|
||||
always_label = f"always ({suggested})" if suggested else "always"
|
||||
|
||||
try:
|
||||
console.print(
|
||||
f"[{risk_color}]Allow {request.tool_name}?[/{risk_color}] "
|
||||
f"[o] once [s] this session [a] {always_label} [n] no"
|
||||
)
|
||||
choice = Prompt.ask(
|
||||
"Choice",
|
||||
choices=["o", "s", "a", "n"],
|
||||
default="n",
|
||||
return Confirm.ask(
|
||||
f"[{risk_color}]Do you want to execute this {request.risk_level} risk tool?[/{risk_color}]",
|
||||
default=False,
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return (False, "once", None)
|
||||
|
||||
if choice == "n":
|
||||
return (False, "once", None)
|
||||
if choice == "s":
|
||||
return (True, "session", None)
|
||||
if choice == "a":
|
||||
return (True, "always", suggested or None)
|
||||
return (True, "once", None)
|
||||
return False
|
||||
|
||||
def request_approval_sync(self, request: ApprovalRequest) -> ApprovalDecision:
|
||||
"""Synchronous approval via Rich console prompt."""
|
||||
try:
|
||||
approved, scope, scope_pattern = self._prompt_user(request)
|
||||
approved = self._prompt_user(request)
|
||||
if approved:
|
||||
return ApprovalDecision(
|
||||
approved=True,
|
||||
reason=f"User approved ({scope})",
|
||||
approver="console",
|
||||
scope=scope,
|
||||
scope_pattern=scope_pattern,
|
||||
)
|
||||
return ApprovalDecision(approved=True, reason="User approved", approver="console")
|
||||
return ApprovalDecision(approved=False, reason="User denied", approver="console")
|
||||
except Exception as e:
|
||||
logger.error("Console approval error: %s", e)
|
||||
|
||||
@@ -53,18 +53,6 @@ class ApprovalDecision:
|
||||
modified_args: Optional replacement arguments (empty = use originals).
|
||||
approver: Who/what approved (user, system, webhook, …).
|
||||
metadata: Backend-specific metadata (timestamps, IPs, …).
|
||||
scope: How long the decision should be remembered:
|
||||
``"once"`` (default — this call only, backward
|
||||
compatible), ``"session"`` (auto-approve matching calls
|
||||
for the rest of this run) or ``"always"`` (persist an
|
||||
allow-rule to disk so future runs don't re-ask). The
|
||||
registry bridges ``session``/``always`` into the durable
|
||||
:class:`PermissionManager` store; ``once`` keeps the
|
||||
existing in-memory fast-path.
|
||||
scope_pattern: Optional reusable target/pattern to persist for
|
||||
``session``/``always`` scopes (e.g. ``"bash:git status *"``
|
||||
or ``"edit:src/app.py"``). When ``None`` the registry
|
||||
derives one from the tool call.
|
||||
"""
|
||||
|
||||
approved: bool
|
||||
@@ -72,8 +60,6 @@ class ApprovalDecision:
|
||||
modified_args: Dict[str, Any] = field(default_factory=dict)
|
||||
approver: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
scope: str = "once"
|
||||
scope_pattern: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -101,18 +87,12 @@ class ApprovalConfig:
|
||||
permissions: Declarative permission rules mapping patterns to actions
|
||||
(e.g., {"read:*": "allow", "bash:rm *": "deny"}).
|
||||
Used for CI-safe, non-interactive permission policies.
|
||||
permission_mode: Optional Claude-Code-parity mode controlling the
|
||||
overall permission behaviour (``PermissionMode`` enum or its
|
||||
string value). ``bypass_permissions`` skips all checks,
|
||||
``plan`` denies write operations, ``dont_ask`` auto-denies
|
||||
prompts. ``None``/``default`` uses the normal rule flow.
|
||||
"""
|
||||
|
||||
backend: Any = None
|
||||
all_tools: bool = False
|
||||
timeout: Optional[float] = 0
|
||||
permissions: Optional[Dict[str, Any]] = None
|
||||
permission_mode: Optional[Any] = None
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
|
||||
@@ -36,8 +36,6 @@ DEFAULT_DANGEROUS_TOOLS: Dict[str, str] = {
|
||||
"execute_code": "critical",
|
||||
"acp_execute_command": "critical",
|
||||
"write_file": "high",
|
||||
"edit_file": "high",
|
||||
"apply_patch": "high",
|
||||
"delete_file": "high",
|
||||
"move_file": "high",
|
||||
"copy_file": "high",
|
||||
@@ -98,12 +96,6 @@ class ApprovalRegistry:
|
||||
# Per-agent, per-tool auto-approval (G-A fix)
|
||||
self._agent_tool_auto_approve: Dict[tuple[str, str], bool] = {}
|
||||
|
||||
# In-memory "this session" scoped approvals: (agent_name, target) grants
|
||||
# that live only for the current process (never written to disk), keyed
|
||||
# by the reusable permission target so they cover matching calls for the
|
||||
# rest of the run. Cleared by ``clear_approved``.
|
||||
self._session_scoped_targets: Set[tuple[Optional[str], str]] = set()
|
||||
|
||||
# Context variables (per-coroutine / per-thread)
|
||||
self._approved_context: contextvars.ContextVar[Set[str]] = contextvars.ContextVar(
|
||||
"approved_context", default=set()
|
||||
@@ -199,125 +191,12 @@ class ApprovalRegistry:
|
||||
return False
|
||||
return self._approval_cache_key(tool_name, arguments or {}) in self._approved_context.get(set())
|
||||
|
||||
def _is_session_scoped(
|
||||
self, agent_name: Optional[str], tool_name: str, arguments: Optional[Dict]
|
||||
) -> bool:
|
||||
"""Return True if a "this session" grant covers this call for the run.
|
||||
|
||||
Checks the in-memory session store by the reusable permission target so
|
||||
a single ``session`` approval covers matching calls (e.g. the same
|
||||
``bash:git status *`` prefix) without persisting anything to disk.
|
||||
"""
|
||||
if not self._session_scoped_targets:
|
||||
return False
|
||||
try:
|
||||
from .utils import build_permission_target
|
||||
|
||||
target = build_permission_target(tool_name, arguments)
|
||||
except Exception: # noqa: BLE001 — never block on target derivation
|
||||
return False
|
||||
return (agent_name, target) in self._session_scoped_targets
|
||||
|
||||
def _persist_scoped_decision(
|
||||
self,
|
||||
agent_name: Optional[str],
|
||||
tool_name: str,
|
||||
arguments: Optional[Dict],
|
||||
decision: ApprovalDecision,
|
||||
) -> None:
|
||||
"""Record a ``session``/``always`` decision for reuse this run (or beyond).
|
||||
|
||||
* ``always`` decisions are routed into :class:`PermissionManager` (which
|
||||
writes to ``approvals.json``) so future runs short-circuit too.
|
||||
* ``session`` decisions are recorded **only** in the in-memory
|
||||
``_session_scoped_targets`` store — never on disk — so they cover
|
||||
matching calls for the rest of *this* run and then vanish. Persisting
|
||||
them via ``PermissionManager`` would reload them next run and violate
|
||||
the "this session only" contract shown in the prompt.
|
||||
|
||||
A missing ``agent_name`` is skipped for the durable ``always`` path: an
|
||||
approval stored without an agent boundary matches *any* later agent
|
||||
making the same target call, so a nameless grant is not persisted where
|
||||
it could cross agent boundaries (it still gets the in-memory fast-path).
|
||||
|
||||
Any failure is swallowed — the in-memory fast-path still applies, so a
|
||||
persistence hiccup never blocks execution.
|
||||
"""
|
||||
scope = getattr(decision, "scope", "once")
|
||||
if scope not in ("session", "always"):
|
||||
return
|
||||
if not decision.approved:
|
||||
return
|
||||
|
||||
if scope == "session":
|
||||
try:
|
||||
from .utils import build_permission_target
|
||||
|
||||
target = build_permission_target(tool_name, arguments)
|
||||
self._session_scoped_targets.add((agent_name, target))
|
||||
except Exception as e: # noqa: BLE001 — best-effort, in-memory only
|
||||
logger.debug(
|
||||
"Could not record session approval for tool '%s': %s",
|
||||
tool_name, e,
|
||||
)
|
||||
return
|
||||
|
||||
# scope == "always" — persist to the durable store.
|
||||
if not agent_name:
|
||||
logger.debug(
|
||||
"Skipping persistent 'always' approval for tool '%s': no agent "
|
||||
"name (would match any agent). Kept in-memory for this run.",
|
||||
tool_name,
|
||||
)
|
||||
# Fall back to session semantics so the grant still helps this run.
|
||||
try:
|
||||
from .utils import build_permission_target
|
||||
|
||||
self._session_scoped_targets.add(
|
||||
(agent_name, build_permission_target(tool_name, arguments))
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return
|
||||
try:
|
||||
from ..permissions import PermissionManager
|
||||
from .utils import build_permission_target
|
||||
|
||||
target = build_permission_target(tool_name, arguments)
|
||||
manager = PermissionManager(agent_name=agent_name)
|
||||
manager.approve(
|
||||
target,
|
||||
decision.approved,
|
||||
scope=scope,
|
||||
agent_name=agent_name,
|
||||
reusable_scope=True,
|
||||
pattern=getattr(decision, "scope_pattern", None),
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 — persistence is best-effort
|
||||
logger.warning(
|
||||
"Could not persist %s approval for tool '%s': %s", scope, tool_name, e
|
||||
)
|
||||
|
||||
def clear_approved(self) -> None:
|
||||
self._approved_context.set(set())
|
||||
self._session_scoped_targets.clear()
|
||||
|
||||
def set_yaml_approved_tools(self, tools: List[str]) -> contextvars.Token:
|
||||
return self._yaml_approved_tools.set(set(tools))
|
||||
|
||||
def add_yaml_approved_tools(self, tools: List[str]) -> contextvars.Token:
|
||||
"""Merge ``tools`` into the YAML-approved set without clobbering it.
|
||||
|
||||
Unlike :meth:`set_yaml_approved_tools`, this preserves any tools already
|
||||
approved in the current context. Returns a token that can be passed to
|
||||
:meth:`reset_yaml_approved_tools` to restore the prior approval set.
|
||||
"""
|
||||
try:
|
||||
current = set(self._yaml_approved_tools.get())
|
||||
except LookupError:
|
||||
current = set()
|
||||
return self._yaml_approved_tools.set(current | set(tools))
|
||||
|
||||
def reset_yaml_approved_tools(self, token: contextvars.Token) -> None:
|
||||
self._yaml_approved_tools.reset(token)
|
||||
|
||||
@@ -354,11 +233,6 @@ class ApprovalRegistry:
|
||||
if self.is_already_approved(tool_name, arguments):
|
||||
return ApprovalDecision(approved=True, reason="Already approved in context")
|
||||
|
||||
# "This session" scoped grant covers matching calls for the run
|
||||
if self._is_session_scoped(agent_name, tool_name, arguments):
|
||||
self.mark_approved(tool_name, arguments)
|
||||
return ApprovalDecision(approved=True, reason="Approved (session)", approver="session")
|
||||
|
||||
# Check per-tool auto-approval (G-A fix)
|
||||
if self.is_auto_approved(tool_name, agent_name):
|
||||
self.mark_approved(tool_name, arguments)
|
||||
@@ -396,7 +270,6 @@ class ApprovalRegistry:
|
||||
|
||||
if decision.approved:
|
||||
self.mark_approved(tool_name, arguments)
|
||||
self._persist_scoped_decision(agent_name, tool_name, arguments, decision)
|
||||
return decision
|
||||
|
||||
async def approve_async(
|
||||
@@ -413,11 +286,6 @@ class ApprovalRegistry:
|
||||
if self.is_already_approved(tool_name, arguments):
|
||||
return ApprovalDecision(approved=True, reason="Already approved in context")
|
||||
|
||||
# "This session" scoped grant covers matching calls for the run
|
||||
if self._is_session_scoped(agent_name, tool_name, arguments):
|
||||
self.mark_approved(tool_name, arguments)
|
||||
return ApprovalDecision(approved=True, reason="Approved (session)", approver="session")
|
||||
|
||||
# Check per-tool auto-approval (G-A fix)
|
||||
if self.is_auto_approved(tool_name, agent_name):
|
||||
self.mark_approved(tool_name, arguments)
|
||||
@@ -449,5 +317,4 @@ class ApprovalRegistry:
|
||||
|
||||
if decision.approved:
|
||||
self.mark_approved(tool_name, arguments)
|
||||
self._persist_scoped_decision(agent_name, tool_name, arguments, decision)
|
||||
return decision
|
||||
@@ -7,85 +7,11 @@ across the approval system.
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar
|
||||
from typing import Any, Awaitable, Callable, Optional, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
# Tool names that map to a shell-command permission target (``bash:<command>``)
|
||||
# so the reusable command-prefix machinery in ``PermissionManager`` applies.
|
||||
_SHELL_TOOLS = frozenset({
|
||||
"execute_command",
|
||||
"acp_execute_command",
|
||||
})
|
||||
|
||||
# File-mutating tool names -> the permission-target prefix used for their path,
|
||||
# so an "always" grant reads naturally (e.g. ``edit:src/app.py``).
|
||||
# NOTE: ``apply_patch`` is deliberately absent. It takes ``patch`` (multi-file
|
||||
# patch text), not a single path, so there is no stable path to pin a scoped
|
||||
# grant to — it falls through to ``tool:apply_patch`` rather than a misleading
|
||||
# ``edit:<...>`` target that could silently cover unrelated files on reuse.
|
||||
_FILE_TOOL_PREFIXES: Dict[str, str] = {
|
||||
"edit_file": "edit",
|
||||
"acp_edit_file": "edit",
|
||||
"write_file": "write",
|
||||
"acp_create_file": "write",
|
||||
"delete_file": "delete",
|
||||
"acp_delete_file": "delete",
|
||||
"move_file": "move",
|
||||
"copy_file": "copy",
|
||||
}
|
||||
|
||||
# Argument keys commonly holding the shell command / file path, in priority order.
|
||||
_COMMAND_KEYS = ("command", "cmd", "code", "query")
|
||||
# ``src`` covers ``move_file``/``copy_file`` (which take ``src``/``dst``) so a
|
||||
# scoped grant is pinned to the concrete source path rather than falling back to
|
||||
# a tool-wide ``tool:move_file`` allow-rule.
|
||||
_PATH_KEYS = ("file_path", "path", "filename", "file", "target", "filepath", "src")
|
||||
|
||||
|
||||
def build_permission_target(
|
||||
tool_name: str, arguments: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
"""Build a :class:`PermissionManager`-compatible target for a tool call.
|
||||
|
||||
Maps a tool name + arguments to a target string the permission store can
|
||||
match against (and generalise via ``suggest_scope_pattern``):
|
||||
|
||||
* shell tools -> ``bash:<command>``
|
||||
* file tools -> ``<edit|write|delete|…>:<path>``
|
||||
* everything else -> ``tool:<tool_name>``
|
||||
|
||||
Falls back to ``tool:<tool_name>`` whenever the expected argument is missing
|
||||
so a target is always produced.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool requesting approval.
|
||||
arguments: The arguments the tool will be called with.
|
||||
|
||||
Returns:
|
||||
A target string such as ``"bash:git status -s"`` or ``"edit:src/app.py"``.
|
||||
"""
|
||||
args = arguments or {}
|
||||
|
||||
if tool_name in _SHELL_TOOLS:
|
||||
for key in _COMMAND_KEYS:
|
||||
value = args.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return f"bash:{value.strip()}"
|
||||
return f"tool:{tool_name}"
|
||||
|
||||
prefix = _FILE_TOOL_PREFIXES.get(tool_name)
|
||||
if prefix is not None:
|
||||
for key in _PATH_KEYS:
|
||||
value = args.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return f"{prefix}:{value.strip()}"
|
||||
return f"tool:{tool_name}"
|
||||
|
||||
return f"tool:{tool_name}"
|
||||
|
||||
|
||||
def run_coroutine_safely(
|
||||
coro: Awaitable[T],
|
||||
timeout: Optional[float] = None
|
||||
|
||||
@@ -9,14 +9,10 @@ import threading
|
||||
import time
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from concurrent.futures import ThreadPoolExecutor, Future
|
||||
|
||||
from praisonaiagents._logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class JobStatus(str, Enum):
|
||||
"""Status of a background job."""
|
||||
@@ -25,11 +21,6 @@ class JobStatus(str, Enum):
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
# Terminal state assigned during startup reconciliation to a job that was
|
||||
# ``RUNNING`` when its process died. The work was abandoned by the crash;
|
||||
# ``LOST`` records that fact so the job is queryable rather than silently
|
||||
# vanishing (see :meth:`BackgroundJobManager.reconcile_on_start`).
|
||||
LOST = "lost"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -42,19 +33,7 @@ class JobInfo:
|
||||
completed_at: Optional[float] = None
|
||||
result: Any = None
|
||||
error: Optional[str] = None
|
||||
# Optional origin/delivery context captured at spawn time so a gateway
|
||||
# can route the result back to the originating conversation on completion
|
||||
# (mirrors the scheduler's ``deliver="origin"`` grammar). Empty by default,
|
||||
# preserving the pull-only behaviour when no delivery target is set.
|
||||
origin: Dict[str, Any] = field(default_factory=dict)
|
||||
# Whether the terminal result has been delivered back to ``origin`` via the
|
||||
# ``on_complete`` callback. Used by restart reconciliation to distinguish a
|
||||
# job that COMPLETED-and-was-delivered from one that COMPLETED-but-whose
|
||||
# deliver-back never fired (e.g. the process died between recording the
|
||||
# result and running the callback). Only meaningful when a store persists
|
||||
# ``JobInfo`` across restarts; ``False`` preserves prior behaviour.
|
||||
delivered: bool = False
|
||||
|
||||
|
||||
@property
|
||||
def duration(self) -> Optional[float]:
|
||||
"""Get job duration in seconds."""
|
||||
@@ -64,40 +43,6 @@ class JobInfo:
|
||||
return end_time - self.started_at
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class BackgroundJobStore(Protocol):
|
||||
"""Persistence contract for durable background jobs.
|
||||
|
||||
Mirrors the store-protocol pattern used elsewhere in the SDK (e.g. the
|
||||
session/scheduler stores): the core runner stays heavy-import-free and a
|
||||
concrete SQLite implementation lives in the wrapper/bot layer alongside the
|
||||
other durable stores (``OutboundQueue``, DLQ, approvals). Injecting a store
|
||||
is entirely opt-in — when none is supplied the runner behaves exactly as the
|
||||
original pure in-memory implementation.
|
||||
|
||||
Implementations must be safe to call from the runner's worker threads.
|
||||
"""
|
||||
|
||||
def upsert(self, job: JobInfo) -> None:
|
||||
"""Insert or update the persisted record for ``job`` (keyed by id)."""
|
||||
...
|
||||
|
||||
def get(self, job_id: str) -> Optional[JobInfo]:
|
||||
"""Return the persisted :class:`JobInfo`, or ``None`` if unknown."""
|
||||
...
|
||||
|
||||
def list_unreconciled(self) -> List[JobInfo]:
|
||||
"""Return jobs needing reconciliation after a restart.
|
||||
|
||||
Specifically: jobs left ``PENDING`` or ``RUNNING`` at the last write
|
||||
(their process died before or during the run — both are orphaned and
|
||||
reconciled to ``LOST``) and jobs ``COMPLETED`` with an ``origin`` that
|
||||
were never marked ``delivered``. Returned in a stable order for
|
||||
deterministic replay.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BackgroundJobManager:
|
||||
"""
|
||||
Manages background jobs for agent tasks.
|
||||
@@ -113,8 +58,6 @@ class BackgroundJobManager:
|
||||
self,
|
||||
max_workers: int = 4,
|
||||
auto_background_threshold: float = 5.0,
|
||||
*,
|
||||
store: Optional[BackgroundJobStore] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the background job manager.
|
||||
@@ -123,36 +66,13 @@ class BackgroundJobManager:
|
||||
max_workers: Maximum number of concurrent background jobs
|
||||
auto_background_threshold: Time in seconds after which a job
|
||||
should be automatically backgrounded
|
||||
store: Optional durable :class:`BackgroundJobStore`. When supplied,
|
||||
every job state transition is persisted so jobs survive a
|
||||
process restart and can be reconciled via
|
||||
:meth:`reconcile_on_start`. When ``None`` (the default) the
|
||||
runner is pure in-memory exactly as before — zero overhead, no
|
||||
behaviour change.
|
||||
"""
|
||||
self.max_workers = max_workers
|
||||
self.auto_background_threshold = auto_background_threshold
|
||||
self._store = store
|
||||
self._executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||
self._jobs: Dict[str, JobInfo] = {}
|
||||
self._futures: Dict[str, Future] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _persist(self, job: JobInfo) -> None:
|
||||
"""Best-effort persist of a job's current state to the store.
|
||||
|
||||
A persistence failure must never crash a worker thread or lose the
|
||||
in-memory job, so exceptions are swallowed and logged. No-op when no
|
||||
store is configured (the default), keeping the hot path free.
|
||||
"""
|
||||
if self._store is None:
|
||||
return
|
||||
try:
|
||||
self._store.upsert(job)
|
||||
except Exception as e: # noqa: BLE001 — persistence must never crash worker
|
||||
logger.debug(
|
||||
"Persisting job %s failed (non-fatal): %s", job.job_id, e,
|
||||
)
|
||||
|
||||
@property
|
||||
def active_jobs(self) -> int:
|
||||
@@ -167,8 +87,6 @@ class BackgroundJobManager:
|
||||
self,
|
||||
func: Callable[[], Any],
|
||||
job_id: Optional[str] = None,
|
||||
on_complete: Optional[Callable[["JobInfo"], None]] = None,
|
||||
origin: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Start a new background job.
|
||||
@@ -176,18 +94,6 @@ class BackgroundJobManager:
|
||||
Args:
|
||||
func: The function to execute in the background
|
||||
job_id: Optional custom job ID (auto-generated if not provided)
|
||||
on_complete: Optional callback invoked with the terminal
|
||||
:class:`JobInfo` once the job reaches a terminal state
|
||||
(completed or failed). This is the observable completion
|
||||
signal a gateway subscribes to in order to proactively
|
||||
deliver a background result back to the originating chat
|
||||
without an active turn. Best-effort — any exception it
|
||||
raises is swallowed so a delivery failure never crashes the
|
||||
worker thread. Defaults to ``None`` (pull-only behaviour).
|
||||
origin: Optional origin/delivery context (e.g. ``platform``,
|
||||
``chat_id``, ``thread_id``, ``session_id``, ``deliver``)
|
||||
stored on the :class:`JobInfo` so ``on_complete`` can route
|
||||
the result. Empty/omitted preserves today's behaviour.
|
||||
|
||||
Returns:
|
||||
The job ID
|
||||
@@ -197,60 +103,28 @@ class BackgroundJobManager:
|
||||
job_info = JobInfo(
|
||||
job_id=job_id,
|
||||
status=JobStatus.PENDING,
|
||||
origin=dict(origin) if origin else {},
|
||||
)
|
||||
|
||||
with self._lock:
|
||||
self._jobs[job_id] = job_info
|
||||
# Persist the PENDING record before it starts so a crash between spawn
|
||||
# and first run still leaves a recoverable trace (reconciled as LOST).
|
||||
self._persist(job_info)
|
||||
|
||||
def _fire_complete() -> None:
|
||||
if on_complete is None:
|
||||
return
|
||||
with self._lock:
|
||||
info = self._jobs.get(job_id)
|
||||
if info is None:
|
||||
return
|
||||
try:
|
||||
on_complete(info)
|
||||
# Record successful deliver-back so restart reconciliation does
|
||||
# not re-deliver an already-delivered result.
|
||||
with self._lock:
|
||||
info.delivered = True
|
||||
self._persist(info)
|
||||
except Exception as e: # noqa: BLE001 — delivery must never crash worker
|
||||
logger.debug(
|
||||
"on_complete callback for job %s raised (non-fatal): %s",
|
||||
job_id, e,
|
||||
)
|
||||
|
||||
def _run_job():
|
||||
with self._lock:
|
||||
job = self._jobs[job_id]
|
||||
job.status = JobStatus.RUNNING
|
||||
job.started_at = time.time()
|
||||
self._persist(job)
|
||||
self._jobs[job_id].status = JobStatus.RUNNING
|
||||
self._jobs[job_id].started_at = time.time()
|
||||
|
||||
try:
|
||||
result = func()
|
||||
with self._lock:
|
||||
job = self._jobs[job_id]
|
||||
job.status = JobStatus.COMPLETED
|
||||
job.completed_at = time.time()
|
||||
job.result = result
|
||||
self._persist(job)
|
||||
_fire_complete()
|
||||
self._jobs[job_id].status = JobStatus.COMPLETED
|
||||
self._jobs[job_id].completed_at = time.time()
|
||||
self._jobs[job_id].result = result
|
||||
return result
|
||||
except Exception as e:
|
||||
with self._lock:
|
||||
job = self._jobs[job_id]
|
||||
job.status = JobStatus.FAILED
|
||||
job.completed_at = time.time()
|
||||
job.error = str(e)
|
||||
self._persist(job)
|
||||
_fire_complete()
|
||||
self._jobs[job_id].status = JobStatus.FAILED
|
||||
self._jobs[job_id].completed_at = time.time()
|
||||
self._jobs[job_id].error = str(e)
|
||||
raise
|
||||
|
||||
future = self._executor.submit(_run_job)
|
||||
@@ -273,12 +147,9 @@ class BackgroundJobManager:
|
||||
KeyError: If job_id is not found
|
||||
"""
|
||||
with self._lock:
|
||||
if job_id in self._jobs:
|
||||
return self._jobs[job_id].status
|
||||
job = self._store.get(job_id) if self._store else None
|
||||
if job is None:
|
||||
raise KeyError(f"Job {job_id} not found")
|
||||
return job.status
|
||||
if job_id not in self._jobs:
|
||||
raise KeyError(f"Job {job_id} not found")
|
||||
return self._jobs[job_id].status
|
||||
|
||||
def get_job_info(self, job_id: str) -> JobInfo:
|
||||
"""
|
||||
@@ -294,12 +165,9 @@ class BackgroundJobManager:
|
||||
KeyError: If job_id is not found
|
||||
"""
|
||||
with self._lock:
|
||||
if job_id in self._jobs:
|
||||
return self._jobs[job_id]
|
||||
job = self._store.get(job_id) if self._store else None
|
||||
if job is None:
|
||||
raise KeyError(f"Job {job_id} not found")
|
||||
return job
|
||||
if job_id not in self._jobs:
|
||||
raise KeyError(f"Job {job_id} not found")
|
||||
return self._jobs[job_id]
|
||||
|
||||
def get_result(self, job_id: str, timeout: Optional[float] = None) -> Any:
|
||||
"""
|
||||
@@ -345,108 +213,8 @@ class BackgroundJobManager:
|
||||
if cancelled:
|
||||
self._jobs[job_id].status = JobStatus.CANCELLED
|
||||
self._jobs[job_id].completed_at = time.time()
|
||||
job = self._jobs[job_id]
|
||||
else:
|
||||
job = None
|
||||
if job is not None:
|
||||
self._persist(job)
|
||||
return cancelled
|
||||
|
||||
def reconcile_on_start(
|
||||
self,
|
||||
redeliver: Optional[Callable[[JobInfo], Any]] = None,
|
||||
) -> Dict[str, int]:
|
||||
"""Reconcile persisted jobs after a process restart.
|
||||
|
||||
Closes the durability gap where a restart mid-job silently drops work
|
||||
and its deferred deliver-back. For every job the store reports as
|
||||
unreconciled:
|
||||
|
||||
- A job left ``RUNNING`` when its process died is marked
|
||||
:attr:`JobStatus.LOST` (a terminal, queryable state) and persisted.
|
||||
The abandoned work is not automatically re-run — that decision is
|
||||
left to the caller, who can inspect ``LOST`` jobs and re-submit.
|
||||
- A job that ``COMPLETED`` with an ``origin`` but was never
|
||||
``delivered`` has ``redeliver`` invoked with its :class:`JobInfo`,
|
||||
re-firing the deliver-back-to-origin the crash interrupted. On a
|
||||
successful redeliver the job is marked ``delivered`` and persisted so
|
||||
it is not delivered twice on the next restart.
|
||||
|
||||
Every persisted job is also re-hydrated into this manager's in-memory
|
||||
map so it remains queryable by id after the restart.
|
||||
|
||||
No-op (returns zero counts) when no store is configured. Safe to call
|
||||
once at startup, mirroring ``OutboundQueue.drain`` wiring.
|
||||
|
||||
Args:
|
||||
redeliver: Callable invoked with a COMPLETED-but-undelivered
|
||||
:class:`JobInfo` to replay its deliver-back. Best-effort — an
|
||||
exception it raises is logged and the job is left undelivered
|
||||
for a future retry, never crashing startup. When ``None``,
|
||||
undelivered jobs are re-hydrated but not delivered.
|
||||
|
||||
Returns:
|
||||
Counts dict: ``{"lost": n, "redelivered": n, "rehydrated": n}``.
|
||||
"""
|
||||
counts = {"lost": 0, "redelivered": 0, "rehydrated": 0}
|
||||
if self._store is None:
|
||||
return counts
|
||||
|
||||
try:
|
||||
unreconciled = list(self._store.list_unreconciled())
|
||||
except Exception as e: # noqa: BLE001 — reconciliation must never crash startup
|
||||
logger.warning("Listing unreconciled jobs failed: %s", e)
|
||||
return counts
|
||||
|
||||
for job in unreconciled:
|
||||
# A job left RUNNING (or still PENDING) when its process died was
|
||||
# orphaned by the crash. Transition it to the terminal LOST state
|
||||
# *before* it becomes visible in the in-memory map, so a concurrent
|
||||
# get_status never observes the transient RUNNING/PENDING status the
|
||||
# caller is specifically reconciling away. Stamp completed_at so the
|
||||
# terminal record is age-evictable by cleanup_completed().
|
||||
orphaned = job.status in (JobStatus.RUNNING, JobStatus.PENDING)
|
||||
if orphaned:
|
||||
prior_status = job.status.value
|
||||
job.status = JobStatus.LOST
|
||||
if job.completed_at is None:
|
||||
job.completed_at = time.time()
|
||||
|
||||
with self._lock:
|
||||
self._jobs[job.job_id] = job
|
||||
counts["rehydrated"] += 1
|
||||
|
||||
if orphaned:
|
||||
self._persist(job)
|
||||
counts["lost"] += 1
|
||||
logger.info(
|
||||
"Reconciled orphaned %s job %s as LOST",
|
||||
prior_status, job.job_id,
|
||||
)
|
||||
elif (
|
||||
job.status == JobStatus.COMPLETED
|
||||
and job.origin
|
||||
and not job.delivered
|
||||
):
|
||||
if redeliver is None:
|
||||
continue
|
||||
try:
|
||||
redeliver(job)
|
||||
job.delivered = True
|
||||
self._persist(job)
|
||||
counts["redelivered"] += 1
|
||||
logger.info(
|
||||
"Re-delivered undelivered completed job %s to origin",
|
||||
job.job_id,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 — redeliver is best-effort
|
||||
logger.warning(
|
||||
"Re-delivery for job %s failed (will retry next "
|
||||
"restart): %s", job.job_id, e,
|
||||
)
|
||||
|
||||
return counts
|
||||
|
||||
return cancelled
|
||||
|
||||
def list_jobs(self, status: Optional[JobStatus] = None) -> Dict[str, JobInfo]:
|
||||
"""
|
||||
List all jobs, optionally filtered by status.
|
||||
@@ -466,23 +234,12 @@ class BackgroundJobManager:
|
||||
if info.status == status
|
||||
}
|
||||
|
||||
# Terminal states whose in-memory records are age-evictable. LOST is
|
||||
# included so reconciled orphans (see reconcile_on_start) do not accumulate
|
||||
# unbounded across restarts — without it every restart would leak a fresh
|
||||
# batch of LOST entries for the process lifetime.
|
||||
_EVICTABLE_STATES = (
|
||||
JobStatus.COMPLETED,
|
||||
JobStatus.FAILED,
|
||||
JobStatus.CANCELLED,
|
||||
JobStatus.LOST,
|
||||
)
|
||||
|
||||
def cleanup_completed(self, max_age: float = 3600.0) -> int:
|
||||
"""
|
||||
Remove terminal jobs older than max_age seconds.
|
||||
Remove completed jobs older than max_age seconds.
|
||||
|
||||
Args:
|
||||
max_age: Maximum age in seconds for terminal jobs
|
||||
max_age: Maximum age in seconds for completed jobs
|
||||
|
||||
Returns:
|
||||
Number of jobs removed
|
||||
@@ -493,7 +250,7 @@ class BackgroundJobManager:
|
||||
with self._lock:
|
||||
to_remove = []
|
||||
for job_id, info in self._jobs.items():
|
||||
if info.status in self._EVICTABLE_STATES:
|
||||
if info.status in (JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED):
|
||||
if info.completed_at and (now - info.completed_at) > max_age:
|
||||
to_remove.append(job_id)
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
Bot Protocols and Configuration for PraisonAI Agents.
|
||||
|
||||
Defines the interfaces and configuration for messaging bot implementations.
|
||||
All implementations live in the ``praisonai-bot`` package (``praisonai_bot.bots``).
|
||||
Backward-compatible imports: ``from praisonai.bots import Bot`` via wrapper shims.
|
||||
All implementations live in the praisonai wrapper package.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -28,14 +27,8 @@ from .protocols import (
|
||||
EmailInbox,
|
||||
SupportsPresentation,
|
||||
PlatformCapabilities,
|
||||
ChannelField,
|
||||
ChannelDescriptor,
|
||||
WebhookVerifierProtocol,
|
||||
)
|
||||
from .base import (
|
||||
BasePlatformAdapter,
|
||||
SendResult,
|
||||
)
|
||||
from .presentation import (
|
||||
MessagePresentation,
|
||||
PresentationBlock,
|
||||
@@ -66,19 +59,7 @@ from .agent_reply import (
|
||||
AgentReply,
|
||||
extract_presentation,
|
||||
)
|
||||
from .config import BotConfig, BotOSConfig, DisplayPolicy, resolve_display_policy
|
||||
from .silence import (
|
||||
SILENT_REPLY_TOKEN,
|
||||
is_intentional_silence_response,
|
||||
BotLoopPolicy,
|
||||
BotLoopGuard,
|
||||
)
|
||||
from .run_status import (
|
||||
RunPhase,
|
||||
RunStatusController,
|
||||
StallState,
|
||||
StallWatchdog,
|
||||
)
|
||||
from .config import BotConfig, BotOSConfig
|
||||
|
||||
__all__ = [
|
||||
"BotProtocol",
|
||||
@@ -89,8 +70,6 @@ __all__ = [
|
||||
"BotUser",
|
||||
"BotChannel",
|
||||
"BotConfig",
|
||||
"DisplayPolicy",
|
||||
"resolve_display_policy",
|
||||
"MessageType",
|
||||
"ChatCommandInfo",
|
||||
"ChatCommandProtocol",
|
||||
@@ -112,11 +91,7 @@ __all__ = [
|
||||
"BlockType",
|
||||
"adapt_presentation",
|
||||
"PlatformCapabilities",
|
||||
"ChannelField",
|
||||
"ChannelDescriptor",
|
||||
"WebhookVerifierProtocol",
|
||||
"BasePlatformAdapter",
|
||||
"SendResult",
|
||||
"InteractiveContext",
|
||||
"InteractiveRegistry",
|
||||
"InteractiveHandler",
|
||||
@@ -131,12 +106,4 @@ __all__ = [
|
||||
"REPLY_NAMESPACE",
|
||||
"AgentReply",
|
||||
"extract_presentation",
|
||||
"SILENT_REPLY_TOKEN",
|
||||
"is_intentional_silence_response",
|
||||
"BotLoopPolicy",
|
||||
"BotLoopGuard",
|
||||
"RunPhase",
|
||||
"RunStatusController",
|
||||
"StallState",
|
||||
"StallWatchdog",
|
||||
]
|
||||
Loaded 100 of 1617 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user