mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8536325cc1 | ||
|
|
8b4db44159 | ||
|
|
3d3fdd045e | ||
|
|
bfa9d0ca48 | ||
|
|
3f14324036 |
No files matched your search
@@ -17,7 +17,6 @@ GROQ_API_KEY=gsk_... # Groq (fast inference)
|
||||
COHERE_API_KEY=... # Cohere models
|
||||
AZURE_OPENAI_API_KEY=... # Azure OpenAI
|
||||
AZURE_OPENAI_ENDPOINT=... # Azure OpenAI endpoint
|
||||
EVOLINK_API_KEY=... # EvoLink.AI OpenAI-compatible models. Get yours at https://evolink.ai/dashboard/keys
|
||||
|
||||
# === Optional: Memory backends ===
|
||||
MEM0_API_KEY=... # Mem0 memory service
|
||||
|
||||
@@ -6,11 +6,6 @@ labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## SDK / package
|
||||
- [ ] Python (`pip install praisonai` / `praisonaiagents`)
|
||||
- [ ] TypeScript / JavaScript (`npm install praisonai`) — fixes in `src/praisonai-ts/`
|
||||
- [ ] Cross-language / parity (file here on PraisonAI)
|
||||
|
||||
## Environment
|
||||
- Provider (select one):
|
||||
- [ ] Anthropic
|
||||
@@ -19,7 +14,6 @@ assignees: ''
|
||||
- [ ] AWS Bedrock
|
||||
- [ ] Other: <!-- specify -->
|
||||
- PraisonAI version: <!-- if known -->
|
||||
- Node.js version (TS/JS only): <!-- e.g. 20.x -->
|
||||
- Operating System: <!-- e.g. macOS 14.3, Windows 11, Ubuntu 22.04 -->
|
||||
|
||||
## Full Code
|
||||
@@ -37,4 +31,4 @@ assignees: ''
|
||||
<!-- What actually happened -->
|
||||
|
||||
## Additional Context
|
||||
<!-- Add any other context about the problem here, such as screenshots, logs, etc. -->
|
||||
<!-- Add any other context about the problem here, such as screenshots, logs, etc. -->
|
||||
@@ -222,18 +222,14 @@ 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).
|
||||
6. TypeScript SDK → src/praisonai-ts/ in this monorepo (read src/praisonai-ts/AGENTS.md for TS rules).
|
||||
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.
|
||||
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
|
||||
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead. PRs with >100 lines added to agent.py require manual review (merge gate will not auto-merge).
|
||||
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead.
|
||||
- Is there a name collision with existing exports in __init__.py?
|
||||
If ANY of these conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
|
||||
|
||||
@@ -264,71 +260,37 @@ runs:
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
INPUT_MODEL: ${{ inputs.model }}
|
||||
INPUT_MAX_TURNS: ${{ inputs.max_turns }}
|
||||
INPUT_ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
||||
INPUT_DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
|
||||
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
|
||||
CLAUDE_CODE_SUBAGENT_MODEL: inherit
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4-6
|
||||
run: |
|
||||
echo "🚀 Running Claude Code with GitHub context..."
|
||||
|
||||
# Build command arguments
|
||||
CMD_ARGS=("-p" "--verbose" "--output-format" "stream-json")
|
||||
|
||||
if [[ -n "$INPUT_MODEL" ]]; then
|
||||
CMD_ARGS+=("--model" "$INPUT_MODEL")
|
||||
export ANTHROPIC_MODEL="$INPUT_MODEL"
|
||||
fi
|
||||
|
||||
# Add max turns if specified
|
||||
if [[ -n "$INPUT_MAX_TURNS" ]]; then
|
||||
CMD_ARGS+=("--max-turns" "$INPUT_MAX_TURNS")
|
||||
fi
|
||||
|
||||
# Add allowed tools (include GitHub tools by default)
|
||||
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__add_issue_comment,mcp__github__get_pull_request,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,Read,Write,Edit,Bash"
|
||||
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_comment,Read,Write,Edit,Bash"
|
||||
if [[ -n "$INPUT_ALLOWED_TOOLS" ]]; then
|
||||
TOOLS="$TOOLS,$INPUT_ALLOWED_TOOLS"
|
||||
fi
|
||||
|
||||
# Remove any explicitly disallowed tools from the effective allow-list so
|
||||
# the read-only contract is enforced deterministically (no reliance on CLI
|
||||
# precedence between --allowedTools and --disallowedTools).
|
||||
if [[ -n "$INPUT_DISALLOWED_TOOLS" ]]; then
|
||||
DISALLOWED_CSV=$(echo "$INPUT_DISALLOWED_TOOLS" | tr ',\n' ' ' | xargs)
|
||||
FILTERED=""
|
||||
IFS=',' read -ra TOOL_ARR <<< "$TOOLS"
|
||||
for tool in "${TOOL_ARR[@]}"; do
|
||||
tool=$(echo "$tool" | xargs)
|
||||
[[ -z "$tool" ]] && continue
|
||||
skip="false"
|
||||
for bad in $DISALLOWED_CSV; do
|
||||
if [[ "$tool" == "$bad" ]]; then
|
||||
skip="true"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$skip" == "false" ]]; then
|
||||
FILTERED="${FILTERED:+$FILTERED,}$tool"
|
||||
fi
|
||||
done
|
||||
TOOLS="$FILTERED"
|
||||
fi
|
||||
CMD_ARGS+=("--allowedTools" "$TOOLS")
|
||||
|
||||
# Add disallowed tools
|
||||
if [[ -n "$INPUT_DISALLOWED_TOOLS" ]]; then
|
||||
DISALLOWED_FLAG=$(echo "$INPUT_DISALLOWED_TOOLS" | tr '\n' ',' | sed 's/,*$//')
|
||||
CMD_ARGS+=("--disallowedTools" "$DISALLOWED_FLAG")
|
||||
CMD_ARGS+=("--disallowedTools" "$INPUT_DISALLOWED_TOOLS")
|
||||
fi
|
||||
|
||||
echo "📝 Executing Claude Code with prompt from file..."
|
||||
|
||||
# Execute Claude Code with timeout, using stdin for the prompt
|
||||
TIMEOUT_SECONDS=$(($INPUT_TIMEOUT_MINUTES * 60))
|
||||
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "$PROMPT_FILE" \
|
||||
| tee "$GITHUB_WORKSPACE/.claude-assess-output.jsonl"
|
||||
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "$PROMPT_FILE"
|
||||
|
||||
echo "✅ Claude Code execution completed"
|
||||
@@ -1,96 +0,0 @@
|
||||
name: Install PraisonAI monorepo packages
|
||||
description: Install praisonai-agents, praisonai-code, praisonai-bot, praisonai-train, praisonai-browser, praisonai-mcp, praisonai-sandbox, praisonai-deploy, and praisonai wrapper from local paths (mirrors PyPI nine-package install order).
|
||||
|
||||
inputs:
|
||||
repo-root:
|
||||
description: Path prefix to monorepo root (e.g. PraisonAI/ for cross-repo checkout)
|
||||
required: false
|
||||
default: ""
|
||||
use-uv:
|
||||
description: Use uv pip instead of pip
|
||||
required: false
|
||||
default: "false"
|
||||
agents-extras:
|
||||
description: Optional extras for praisonaiagents (e.g. .[all] or [all])
|
||||
required: false
|
||||
default: ""
|
||||
wrapper-extras:
|
||||
description: Optional extras for praisonai wrapper (e.g. .[chat,code] or [chat,code])
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install monorepo packages
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
WORKSPACE="${GITHUB_WORKSPACE:-$(pwd)}"
|
||||
ROOT="${{ inputs.repo-root }}"
|
||||
if [ -n "$ROOT" ]; then
|
||||
BASE="${WORKSPACE%/}/${ROOT%/}/"
|
||||
else
|
||||
BASE="${WORKSPACE%/}/"
|
||||
fi
|
||||
AGENTS_PKG="${BASE}src/praisonai-agents"
|
||||
CODE_PKG="${BASE}src/praisonai-code"
|
||||
BOT_PKG="${BASE}src/praisonai-bot"
|
||||
TRAIN_PKG="${BASE}src/praisonai-train"
|
||||
BROWSER_PKG="${BASE}src/praisonai-browser"
|
||||
MCP_PKG="${BASE}src/praisonai-mcp"
|
||||
SANDBOX_PKG="${BASE}src/praisonai-sandbox"
|
||||
DEPLOY_PKG="${BASE}src/praisonai-deploy"
|
||||
WRAPPER_PKG="${BASE}src/praisonai"
|
||||
|
||||
normalise_extras() {
|
||||
local value="${1#.}"
|
||||
if [ -n "$value" ]; then
|
||||
printf '.%s' "$value"
|
||||
else
|
||||
printf '.'
|
||||
fi
|
||||
}
|
||||
|
||||
AGENTS_SUFFIX="$(normalise_extras "${{ inputs.agents-extras }}")"
|
||||
WRAPPER_SUFFIX="$(normalise_extras "${{ inputs.wrapper-extras }}")"
|
||||
|
||||
if [ "${{ inputs.use-uv }}" = "true" ]; then
|
||||
cd "$AGENTS_PKG"
|
||||
uv pip install --system -e "${AGENTS_SUFFIX}"
|
||||
cd "$CODE_PKG"
|
||||
uv pip install --system -e .
|
||||
cd "$BOT_PKG"
|
||||
uv pip install --system -e .
|
||||
cd "$TRAIN_PKG"
|
||||
uv pip install --system -e .
|
||||
cd "$BROWSER_PKG"
|
||||
uv pip install --system -e .
|
||||
cd "$MCP_PKG"
|
||||
uv pip install --system -e .
|
||||
cd "$SANDBOX_PKG"
|
||||
uv pip install --system -e .
|
||||
cd "$DEPLOY_PKG"
|
||||
uv pip install --system -e .
|
||||
cd "$WRAPPER_PKG"
|
||||
uv pip install --system "${WRAPPER_SUFFIX}"
|
||||
else
|
||||
cd "$AGENTS_PKG"
|
||||
pip install -e "${AGENTS_SUFFIX}"
|
||||
cd "$CODE_PKG"
|
||||
pip install -e .
|
||||
cd "$BOT_PKG"
|
||||
pip install -e .
|
||||
cd "$TRAIN_PKG"
|
||||
pip install -e .
|
||||
cd "$BROWSER_PKG"
|
||||
pip install -e .
|
||||
cd "$MCP_PKG"
|
||||
pip install -e .
|
||||
cd "$SANDBOX_PKG"
|
||||
pip install -e .
|
||||
cd "$DEPLOY_PKG"
|
||||
pip install -e .
|
||||
cd "$WRAPPER_PKG"
|
||||
pip install -e "${WRAPPER_SUFFIX}"
|
||||
fi
|
||||
@@ -33,20 +33,12 @@ steps:
|
||||
Second, read the issue: execute_command 'cat .github/triage-context.md'. Do NOT pass cwd.
|
||||
|
||||
Third, read architecture guidelines: execute_command 'cat src/praisonai-agents/AGENTS.md'.
|
||||
If the issue is TypeScript/JavaScript SDK, also execute_command 'cat src/praisonai-ts/AGENTS.md'.
|
||||
|
||||
Fourth, decide routing before planning:
|
||||
- TypeScript/JavaScript SDK → src/praisonai-ts/ in this monorepo (read src/praisonai-ts/AGENTS.md).
|
||||
- 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 for Python protocols and CLI; external repos for Tools/Plugins/Docs only.
|
||||
|
||||
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 / TS-monorepo / 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,34 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
const chain = require('./bot-pr-review-chain.js');
|
||||
|
||||
let failed = 0;
|
||||
function assert(name, cond) {
|
||||
if (!cond) {
|
||||
console.error('FAIL:', name);
|
||||
failed++;
|
||||
} else {
|
||||
console.log('ok:', name);
|
||||
}
|
||||
}
|
||||
|
||||
assert('chainKickPosted requires both markers', chain.chainKickPosted([
|
||||
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
|
||||
{ user: { login: 'MervinPraison' }, body: '/review' },
|
||||
]));
|
||||
assert('chainKickPosted false with only coderabbit', !chain.chainKickPosted([
|
||||
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
|
||||
]));
|
||||
assert('coderabbitKickPosted detects coderabbit', chain.coderabbitKickPosted([
|
||||
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
|
||||
]));
|
||||
assert('qodoKickPosted detects qodo', chain.qodoKickPosted([
|
||||
{ user: { login: 'github-actions[bot]' }, body: '/review' },
|
||||
]));
|
||||
assert('ignores coderabbit bot comment', !chain.coderabbitKickPosted([
|
||||
{ user: { login: 'coderabbitai[bot]' }, body: '@coderabbitai review' },
|
||||
]));
|
||||
assert('bot PR author match', chain.isBotOpenedPr({ user: { login: 'praisonai-triage-agent[bot]' } }));
|
||||
assert('generic bot type match', chain.isBotOpenedPr({ user: { login: 'my-bot', type: 'Bot' } }));
|
||||
assert('human author excluded', !chain.isBotOpenedPr({ user: { login: 'MervinPraison', type: 'User' } }));
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -1,153 +0,0 @@
|
||||
/**
|
||||
* Idempotent CodeRabbit/Qodo kick for bot-opened PRs.
|
||||
* @see .github/workflows/auto-pr-comment.yml, claude.yml, bot-pr-recovery.yml
|
||||
*/
|
||||
|
||||
const KICK_AUTHORS = new Set(['MervinPraison', 'github-actions[bot]', 'praisonai-triage-agent[bot]']);
|
||||
const BOT_PR_AUTHORS = new Set(['praisonai-triage-agent[bot]', 'github-actions[bot]']);
|
||||
|
||||
async function listAllComments(github, owner, repo, issueNumber) {
|
||||
if (typeof github.paginate === 'function') {
|
||||
return github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
const { data } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
function kickAuthored(comment, marker) {
|
||||
return (
|
||||
KICK_AUTHORS.has(comment.user?.login) &&
|
||||
(comment.body || '').includes(marker)
|
||||
);
|
||||
}
|
||||
|
||||
function coderabbitKickPosted(comments) {
|
||||
return comments.some((c) => kickAuthored(c, '@coderabbitai review'));
|
||||
}
|
||||
|
||||
function qodoKickPosted(comments) {
|
||||
return comments.some((c) => kickAuthored(c, '/review'));
|
||||
}
|
||||
|
||||
function chainKickPosted(comments) {
|
||||
return coderabbitKickPosted(comments) && qodoKickPosted(comments);
|
||||
}
|
||||
|
||||
function isBotOpenedPr(pr) {
|
||||
if (BOT_PR_AUTHORS.has(pr.user?.login)) return true;
|
||||
return pr.user?.type === 'Bot';
|
||||
}
|
||||
|
||||
async function kickReviewChain(github, owner, repo, prNumber, core, preFetchedComments = null) {
|
||||
const comments = preFetchedComments || await listAllComments(github, owner, repo, prNumber);
|
||||
const needCoderabbit = !coderabbitKickPosted(comments);
|
||||
const needQodo = !qodoKickPosted(comments);
|
||||
|
||||
if (!needCoderabbit && !needQodo) {
|
||||
core?.info?.(`Review chain already kicked on PR #${prNumber}`);
|
||||
return { kicked: false, reason: 'already_kicked' };
|
||||
}
|
||||
|
||||
if (needCoderabbit) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: '@coderabbitai review',
|
||||
});
|
||||
}
|
||||
if (needQodo) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: '/review',
|
||||
});
|
||||
}
|
||||
core?.info?.(`Kicked review chain for PR #${prNumber} (coderabbit=${needCoderabbit}, qodo=${needQodo})`);
|
||||
return { kicked: true, coderabbit: needCoderabbit, qodo: needQodo };
|
||||
}
|
||||
|
||||
async function findOpenPrForIssue(github, owner, repo, issueNumber) {
|
||||
const prefix = `claude/issue-${issueNumber}-`;
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'desc',
|
||||
per_page: 30,
|
||||
});
|
||||
return (
|
||||
prs.find(
|
||||
(p) => (p.head?.ref || '').startsWith(prefix) && isBotOpenedPr(p)
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
async function kickReviewChainForIssue(github, owner, repo, issueNumber, core) {
|
||||
const pr = await findOpenPrForIssue(github, owner, repo, issueNumber);
|
||||
if (!pr) {
|
||||
core?.info?.(`No open PR for issue #${issueNumber}, skipping review kick`);
|
||||
return { kicked: false, reason: 'no_pr' };
|
||||
}
|
||||
const result = await kickReviewChain(github, owner, repo, pr.number, core);
|
||||
return { ...result, prNumber: pr.number };
|
||||
}
|
||||
|
||||
async function recoverStalledBotPrs(github, owner, repo, options, core) {
|
||||
const { prNumber = null, minAgeMs = 10 * 60 * 1000, maxRecover = 10 } = options || {};
|
||||
let prs;
|
||||
if (prNumber) {
|
||||
const { data } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
prs = [data];
|
||||
} else if (typeof github.paginate === 'function') {
|
||||
prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
} else {
|
||||
const { data } = await github.rest.pulls.list({ owner, repo, state: 'open', per_page: 100 });
|
||||
prs = data;
|
||||
}
|
||||
|
||||
const cutoff = Date.now() - minAgeMs;
|
||||
let recovered = 0;
|
||||
for (const pr of prs) {
|
||||
if (recovered >= maxRecover) break;
|
||||
if (!isBotOpenedPr(pr)) continue;
|
||||
if (!prNumber && new Date(pr.created_at).getTime() > cutoff) continue;
|
||||
const comments = await listAllComments(github, owner, repo, pr.number);
|
||||
if (chainKickPosted(comments)) continue;
|
||||
await kickReviewChain(github, owner, repo, pr.number, core, comments);
|
||||
recovered += 1;
|
||||
}
|
||||
core?.info?.(`Recovery complete (${recovered} PR(s) kicked)`);
|
||||
return recovered;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
KICK_AUTHORS,
|
||||
BOT_PR_AUTHORS,
|
||||
listAllComments,
|
||||
chainKickPosted,
|
||||
coderabbitKickPosted,
|
||||
qodoKickPosted,
|
||||
isBotOpenedPr,
|
||||
kickReviewChain,
|
||||
findOpenPrForIssue,
|
||||
kickReviewChainForIssue,
|
||||
recoverStalledBotPrs,
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Run: node .github/scripts/ci-failure-claude-selftest.js
|
||||
*/
|
||||
const ciFix = require('./ci-failure-claude.js');
|
||||
const mergeGate = require('./merge-gate.js');
|
||||
|
||||
let failed = 0;
|
||||
function assert(name, cond) {
|
||||
if (!cond) {
|
||||
console.error('FAIL:', name);
|
||||
failed += 1;
|
||||
} else {
|
||||
console.log('ok:', name);
|
||||
}
|
||||
}
|
||||
|
||||
const LOG_2767 = `
|
||||
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8096670Z FAILED (0.0100s) tests/unit/cli/test_typer_cli.py::TestConfigCommand::test_config_list - ValueError: I/O operation on closed file.
|
||||
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8097323Z ============= 1 failed, 1401 passed, 1 skipped in 88.86s (0:01:28) =============
|
||||
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8693341Z ##[error]Process completed with exit code 1.
|
||||
`;
|
||||
|
||||
const parsed = ciFix.parsePytestFailures(LOG_2767);
|
||||
assert('parses pytest failure from log', parsed.length === 1);
|
||||
assert(
|
||||
'extracts test path',
|
||||
parsed[0]?.testId === 'tests/unit/cli/test_typer_cli.py::TestConfigCommand::test_config_list'
|
||||
);
|
||||
assert(
|
||||
'extracts error message',
|
||||
parsed[0]?.error.includes('ValueError: I/O operation on closed file')
|
||||
);
|
||||
|
||||
const comment = ciFix.buildCiFixComment({
|
||||
headSha: 'dfb81bb0ae7f36bf8d66d8f45053464c18c005a3',
|
||||
failedChecks: [
|
||||
{
|
||||
name: 'test-core (cli)',
|
||||
workflow: 'Core Tests',
|
||||
html_url: 'https://github.com/MervinPraison/PraisonAI/actions/runs/28885180173/job/85683605433',
|
||||
},
|
||||
],
|
||||
failureSummaries: [{ jobName: 'test-core (cli)', failures: parsed }],
|
||||
});
|
||||
assert('comment includes @claude', comment.includes('@claude'));
|
||||
assert('comment includes short sha', comment.includes('dfb81bb0'));
|
||||
assert('comment includes test path', comment.includes('test_typer_cli.py::TestConfigCommand::test_config_list'));
|
||||
assert('comment includes job url', comment.includes('85683605433'));
|
||||
assert('comment requires critical review verdict', comment.includes('Critical review first'));
|
||||
assert('comment forbids weakening tests', comment.includes('do not weaken, skip, or delete tests'));
|
||||
assert('comment mentions SDK guardrails', comment.includes('SDK guardrails'));
|
||||
assert('comment asks for legitimacy verdict', comment.includes('legitimate fix'));
|
||||
|
||||
const headSha = 'dfb81bb0ae7f36bf8d66d8f45053464c18c005a3';
|
||||
const commentsWithFix = [
|
||||
{
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude CI failed on HEAD `dfb81bb0`. Please fix...',
|
||||
created_at: '2026-07-07T18:00:00Z',
|
||||
},
|
||||
];
|
||||
assert('sha dedup detects existing comment', ciFix.hasCiFixCommentForSha(commentsWithFix, headSha));
|
||||
|
||||
const skipNoFinal = ciFix.shouldSkipCiFix({
|
||||
comments: [],
|
||||
headSha,
|
||||
labels: [],
|
||||
hasFinal: false,
|
||||
claudeInProgress: false,
|
||||
failedChecks: [{ name: 'test-core' }],
|
||||
});
|
||||
assert('skips when final not posted', skipNoFinal.skip && skipNoFinal.reason.includes('final'));
|
||||
|
||||
const skipPending = ciFix.shouldSkipCiFix({
|
||||
comments: [],
|
||||
headSha,
|
||||
labels: [ciFix.CI_FIX_LABEL],
|
||||
hasFinal: true,
|
||||
claudeInProgress: false,
|
||||
failedChecks: [{ name: 'test-core' }],
|
||||
});
|
||||
assert('skips when ci fix pending label', skipPending.skip && skipPending.reason.includes('pending'));
|
||||
|
||||
const failedRuns = mergeGate.listFailedChecksOnSha([
|
||||
{ status: 'completed', conclusion: 'success', name: 'smoke' },
|
||||
{ status: 'completed', conclusion: 'failure', name: 'test-core (cli)' },
|
||||
{ status: 'in_progress', conclusion: null, name: 'pending-job' },
|
||||
]);
|
||||
assert('listFailedChecksOnSha finds failures only', failedRuns.length === 1);
|
||||
assert('failed check name', failedRuns[0].name === 'test-core (cli)');
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -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,
|
||||
};
|
||||
@@ -1,382 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Local self-test for merge-gate.js heuristics (no GitHub API).
|
||||
* Run: node .github/scripts/merge-gate-selftest.js
|
||||
*/
|
||||
const mg = require('./merge-gate.js');
|
||||
|
||||
let failed = 0;
|
||||
function assert(name, cond) {
|
||||
if (!cond) {
|
||||
console.error('FAIL:', name);
|
||||
failed++;
|
||||
} else {
|
||||
console.log('ok:', name);
|
||||
}
|
||||
}
|
||||
|
||||
// Stale FINAL: push after FINAL, no @claude since head
|
||||
const finals = [
|
||||
{ user: { login: 'github-actions[bot]' }, body: '@claude FINAL architecture reviewer', created_at: '2026-06-12T08:00:00Z' },
|
||||
];
|
||||
assert('stale when head after final', mg.isStaleFinalAfterPush(finals, '2026-06-12T09:00:00Z'));
|
||||
|
||||
const withRecovery = [
|
||||
...finals,
|
||||
{ user: { login: 'github-actions[bot]' }, body: '@claude FINAL architecture reviewer', created_at: '2026-06-12T09:30:00Z' },
|
||||
];
|
||||
assert('not stale when @claude after head', !mg.isStaleFinalAfterPush(withRecovery, '2026-06-12T09:00:00Z'));
|
||||
|
||||
const withClaudeReply = [
|
||||
...finals,
|
||||
{
|
||||
user: { login: 'praisonai-triage-agent[bot]' },
|
||||
body: "**Claude finished @MervinPraison's task** —— [View job](https://github.com/)",
|
||||
created_at: '2026-06-12T08:30:00Z',
|
||||
},
|
||||
];
|
||||
assert('not stale when Claude replied after FINAL', !mg.isStaleFinalAfterPush(withClaudeReply, '2026-06-12T09:00:00Z'));
|
||||
assert('claude final reply detected', mg.isClaudeFinalReplyComment(withClaudeReply[1]));
|
||||
|
||||
assert('cancelled detect-and-trigger does not block', mg.OPTIONAL_CANCELLED_CHECKS.has('detect-and-trigger'));
|
||||
|
||||
const coreGreenRuns = [
|
||||
{ name: 'test-core', status: 'completed', conclusion: 'success' },
|
||||
{ name: 'test-core (cli)', status: 'completed', conclusion: 'success' },
|
||||
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
|
||||
{ name: 'test-windows', status: 'completed', conclusion: 'cancelled' },
|
||||
];
|
||||
assert('core green allows cancelled smoke', mg.isAcceptableCheckConclusion(
|
||||
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
|
||||
coreGreenRuns
|
||||
));
|
||||
assert('core green allows cancelled test-windows', mg.isAcceptableCheckConclusion(
|
||||
{ name: 'test-windows', status: 'completed', conclusion: 'cancelled' },
|
||||
coreGreenRuns
|
||||
));
|
||||
assert('cancelled smoke blocks when core missing', !mg.isAcceptableCheckConclusion(
|
||||
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
|
||||
[{ name: 'smoke', status: 'completed', conclusion: 'cancelled' }]
|
||||
));
|
||||
assert('cancelled smoke blocks when core failed', !mg.isAcceptableCheckConclusion(
|
||||
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
|
||||
[
|
||||
{ name: 'test-core', status: 'completed', conclusion: 'failure' },
|
||||
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
|
||||
]
|
||||
));
|
||||
assert('bestRunsByName prefers success over cancelled', (() => {
|
||||
const best = mg.bestRunsByName([
|
||||
{ name: 'smoke', conclusion: 'cancelled' },
|
||||
{ name: 'smoke', conclusion: 'success' },
|
||||
]);
|
||||
return best.length === 1 && best[0].conclusion === 'success';
|
||||
})());
|
||||
assert('bestRunsByName prefers pending over completed cancelled', (() => {
|
||||
const best = mg.bestRunsByName([
|
||||
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
|
||||
{ name: 'smoke', status: 'in_progress', conclusion: null },
|
||||
]);
|
||||
return best.length === 1 && best[0].status === 'in_progress';
|
||||
})());
|
||||
assert('bestRunsByName keeps pending regardless of order', (() => {
|
||||
const best = mg.bestRunsByName([
|
||||
{ name: 'smoke', status: 'in_progress', conclusion: null },
|
||||
{ name: 'smoke', status: 'completed', conclusion: 'success' },
|
||||
]);
|
||||
return best.length === 1 && best[0].status === 'in_progress';
|
||||
})());
|
||||
assert('allChecksGreenOnSha style: pending re-run blocks despite core green', (() => {
|
||||
const runs = mg.bestRunsByName([
|
||||
{ name: 'test-core', status: 'completed', conclusion: 'success' },
|
||||
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
|
||||
{ name: 'smoke', status: 'queued', conclusion: null },
|
||||
]);
|
||||
const smoke = runs.find((r) => r.name === 'smoke');
|
||||
return smoke.status !== 'completed';
|
||||
})());
|
||||
|
||||
// Stale-FINAL recovery guards (PR #2560 push loop)
|
||||
const nowMs = Date.now();
|
||||
const iso = (ms) => new Date(ms).toISOString();
|
||||
const firstFinal = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude You are the FINAL architecture reviewer.',
|
||||
created_at: iso(nowMs - 20 * 60 * 1000),
|
||||
};
|
||||
const pushSoonAfter = mg.shouldSkipStaleFinalRecovery(
|
||||
[firstFinal],
|
||||
iso(nowMs - 16 * 60 * 1000)
|
||||
);
|
||||
assert('debounce push soon after FINAL', pushSoonAfter.skip && pushSoonAfter.reason.includes('soon after'));
|
||||
|
||||
const secondFinal = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude You are the FINAL architecture reviewer.',
|
||||
created_at: iso(nowMs - 20 * 60 * 1000),
|
||||
};
|
||||
const capped = mg.shouldSkipStaleFinalRecovery(
|
||||
[firstFinal, secondFinal],
|
||||
iso(nowMs - 2 * 60 * 1000)
|
||||
);
|
||||
assert('hourly cap blocks third FINAL in window', capped.skip && capped.reason.includes('capped'));
|
||||
|
||||
const botPush = mg.shouldSkipStaleFinalRecovery(
|
||||
[firstFinal],
|
||||
iso(nowMs - 16 * 60 * 1000),
|
||||
'praisonai-triage-agent[bot]'
|
||||
);
|
||||
assert('skip when automation pushed head', botPush.skip && botPush.reason.includes('automation'));
|
||||
|
||||
assert('claude automation login', mg.isClaudeAutomationLogin('praisonai-triage-agent[bot]'));
|
||||
|
||||
// Bot CHANGES_REQUESTED then APPROVE
|
||||
const reviews = [
|
||||
{ user: { login: 'coderabbit[bot]', type: 'Bot' }, state: 'CHANGES_REQUESTED', submitted_at: '2026-06-12T08:00:00Z' },
|
||||
{ user: { login: 'coderabbit[bot]', type: 'Bot' }, state: 'APPROVED', submitted_at: '2026-06-12T09:00:00Z' },
|
||||
];
|
||||
assert('bot approve clears CR', !mg.hasAnyChangesRequested(reviews));
|
||||
assert('human CR blocks', mg.hasAnyChangesRequested([
|
||||
{ user: { login: 'MervinPraison', type: 'User' }, state: 'CHANGES_REQUESTED', submitted_at: '2026-06-12T09:00:00Z' },
|
||||
]));
|
||||
|
||||
// Verdict after HEAD
|
||||
const verdictComments = [
|
||||
{ body: 'MERGE_GATE_VERDICT: APPROVE', created_at: '2026-06-12T08:00:00Z' },
|
||||
];
|
||||
assert('verdict before head rejected', mg.findMergeGateVerdict(verdictComments, null, '2026-06-12T09:00:00Z') === null);
|
||||
assert('verdict after head accepted', mg.findMergeGateVerdict(
|
||||
[{ body: 'MERGE_GATE_VERDICT: APPROVE', created_at: '2026-06-12T10:00:00Z' }],
|
||||
null,
|
||||
'2026-06-12T09:00:00Z'
|
||||
) === 'APPROVE');
|
||||
|
||||
const fallbackApprove = [
|
||||
{
|
||||
body: 'MERGE_GATE_VERDICT: APPROVE\n\nAutomated fallback — Claude assess did not post a verdict comment.',
|
||||
created_at: '2026-06-12T10:00:00Z',
|
||||
},
|
||||
];
|
||||
assert(
|
||||
'automated fallback APPROVE ignored when Opus required',
|
||||
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z', { excludeAutomatedFallback: true }) === null
|
||||
);
|
||||
assert(
|
||||
'automated fallback APPROVE still visible without Opus-only filter',
|
||||
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z') === 'APPROVE'
|
||||
);
|
||||
|
||||
const fallbackBlockNewFormat = [
|
||||
{
|
||||
body: 'MERGE_GATE_VERDICT: BLOCK\n\nAutomated fallback — Opus merge gate assessment did not complete.',
|
||||
created_at: '2026-06-12T10:00:00Z',
|
||||
},
|
||||
];
|
||||
assert(
|
||||
'new-format fallback BLOCK ignored when Opus required',
|
||||
mg.findMergeGateVerdict(fallbackBlockNewFormat, null, '2026-06-12T09:00:00Z', { excludeAutomatedFallback: true }) === null
|
||||
);
|
||||
assert(
|
||||
'new-format fallback BLOCK detected by marker helper',
|
||||
mg.isAutomatedFallbackVerdict(fallbackBlockNewFormat[0].body)
|
||||
);
|
||||
assert(
|
||||
'findMergeGateVerdict null-safe with explicit null options',
|
||||
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z', null) === 'APPROVE'
|
||||
);
|
||||
|
||||
const noise = [{ user: { login: 'MervinPraison' }, body: '**Merge gate scan** — wait for `@claude`', created_at: new Date().toISOString() }];
|
||||
assert('diagnostic comment not a trigger', !mg.hasRecentClaudeTrigger(noise, 35));
|
||||
|
||||
// Cooldown skip requires an actual verdict on HEAD, not just a FINAL trigger comment
|
||||
const recentFinalOnHead = [
|
||||
{
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude You are the FINAL architecture reviewer.',
|
||||
created_at: '2026-06-27T10:00:00Z',
|
||||
},
|
||||
];
|
||||
assert(
|
||||
'final trigger alone does not count as verdict on head',
|
||||
mg.findMergeGateVerdict(recentFinalOnHead, null, '2026-06-27T09:55:00Z') === null
|
||||
);
|
||||
const recentVerdictOnHead = [
|
||||
...recentFinalOnHead,
|
||||
{
|
||||
user: { login: 'github-actions[bot]' },
|
||||
body: 'MERGE_GATE_VERDICT: APPROVE',
|
||||
created_at: '2026-06-27T10:05:00Z',
|
||||
},
|
||||
];
|
||||
assert(
|
||||
'verdict on head skips cooldown gate',
|
||||
mg.findMergeGateVerdict(recentVerdictOnHead, null, '2026-06-27T09:55:00Z') === 'APPROVE'
|
||||
);
|
||||
|
||||
assert(
|
||||
'scan kickoff text is not a verdict',
|
||||
mg.findMergeGateVerdict([
|
||||
{
|
||||
body: '**Merge gate scan** — eligible for assessment. Claude merge gate will assess and may auto-merge if `MERGE_GATE_VERDICT: APPROVE`.',
|
||||
created_at: '2026-06-27T10:05:00Z',
|
||||
},
|
||||
], null, '2026-06-27T09:55:00Z') === null
|
||||
);
|
||||
|
||||
const finalWithFinished = [
|
||||
{ user: { login: 'MervinPraison' }, body: '@claude You are the FINAL architecture reviewer.', created_at: '2026-06-27T10:00:00Z' },
|
||||
{ user: { login: 'praisonai-triage-agent[bot]' }, body: 'Claude finished', created_at: '2026-06-27T10:05:00Z' },
|
||||
];
|
||||
assert('final complete requires Claude finished reply', mg.finalClaudeCompletedOnSha(finalWithFinished, '2026-06-27T09:55:00Z'));
|
||||
assert('final trigger alone is incomplete', !mg.finalClaudeCompletedOnSha(
|
||||
[{ user: { login: 'MervinPraison' }, body: '@claude You are the FINAL architecture reviewer.', created_at: '2026-06-27T10:00:00Z' }],
|
||||
'2026-06-27T09:55:00Z'
|
||||
));
|
||||
|
||||
assert('recent scan comment detected', mg.hasRecentMergeGateScanComment([
|
||||
{ body: '**Merge gate scan** — eligible', created_at: new Date().toISOString() },
|
||||
]));
|
||||
assert('old scan comment not recent', !mg.hasRecentMergeGateScanComment([
|
||||
{ body: '**Merge gate scan** — eligible', created_at: '2020-01-01T00:00:00Z' },
|
||||
]));
|
||||
|
||||
// Sensitive + secrets
|
||||
assert('workflow path sensitive', mg.sensitivePathReasons([{ filename: '.github/workflows/foo.yml' }]).length === 1);
|
||||
assert('ci-only label exempts workflows', mg.sensitivePathReasons(
|
||||
[{ filename: '.github/workflows/foo.yml' }],
|
||||
[mg.WORKFLOW_ONLY_LABEL]
|
||||
).length === 0);
|
||||
assert('ci-only label not exempt mixed changes', mg.sensitivePathReasons(
|
||||
[{ filename: '.github/workflows/foo.yml' }, { filename: 'src/foo.py' }],
|
||||
[mg.WORKFLOW_ONLY_LABEL]
|
||||
).length === 1);
|
||||
assert('secret in patch', mg.secretScanReasons([{ filename: 'x.py', patch: '+key = "ghp_abcdefghijklmnopqrstuvwxyz1234567890"' }]).length === 1);
|
||||
|
||||
// Claude run scoping — other PR branches must not block
|
||||
assert('other branch claude does not block', !mg.hasBlockingClaudeRunForPr(
|
||||
[{ event: 'issue_comment', head_branch: 'other-branch' }],
|
||||
'my-branch'
|
||||
));
|
||||
assert('same branch claude blocks', mg.hasBlockingClaudeRunForPr(
|
||||
[{ event: 'issue_comment', head_branch: 'my-branch' }],
|
||||
'my-branch'
|
||||
));
|
||||
assert('issues event never blocks', !mg.hasBlockingClaudeRunForPr(
|
||||
[{ event: 'issues', head_branch: 'my-branch' }],
|
||||
'my-branch'
|
||||
));
|
||||
|
||||
// Conflict rebase clears after bot completion + FINAL on HEAD (within 12h cooldown)
|
||||
const conflictNowMs = Date.now();
|
||||
const conflictIso = (offsetMs) => new Date(conflictNowMs + offsetMs).toISOString();
|
||||
const conflictTrigger = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude this PR has merge conflicts with main. Please rebase',
|
||||
created_at: conflictIso(-30 * 60 * 1000),
|
||||
};
|
||||
const rebaseDone = {
|
||||
user: { login: 'praisonai-triage-agent[bot]' },
|
||||
body: 'Rebase complete — PR #2308 onto latest main',
|
||||
created_at: conflictIso(-29 * 60 * 1000),
|
||||
};
|
||||
const finalAfterRebase = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude You are the FINAL architecture reviewer.',
|
||||
created_at: conflictIso(-20 * 60 * 1000),
|
||||
};
|
||||
const claudeFinishedAfterRebase = {
|
||||
user: { login: 'praisonai-triage-agent[bot]' },
|
||||
body: 'Claude finished',
|
||||
created_at: conflictIso(-19 * 60 * 1000),
|
||||
};
|
||||
const headAfterRebase = conflictIso(-21 * 60 * 1000);
|
||||
const rebaseComments = [conflictTrigger, rebaseDone, finalAfterRebase, claudeFinishedAfterRebase];
|
||||
assert('conflict blocks before rebase done', mg.hasRecentConflictComment([conflictTrigger], headAfterRebase));
|
||||
assert('conflict clears after rebase + FINAL on HEAD', !mg.hasRecentConflictComment(rebaseComments, headAfterRebase));
|
||||
assert('conflict still blocks without FINAL on HEAD', mg.hasRecentConflictComment(
|
||||
[conflictTrigger, rebaseDone],
|
||||
headAfterRebase
|
||||
));
|
||||
|
||||
// Tests heuristic
|
||||
assert('sdk without tests', mg.missingTestsReason([{ filename: 'src/praisonai-agents/a/b.py', additions: 3 }]) !== null);
|
||||
assert('sdk with tests ok', mg.missingTestsReason([
|
||||
{ filename: 'src/praisonai-agents/a/b.py', additions: 3 },
|
||||
{ filename: 'src/praisonai-agents/tests/test_x.py', additions: 10 },
|
||||
]) === null);
|
||||
assert('ts sdk without tests', mg.missingTestsReason([
|
||||
{ filename: 'src/praisonai-ts/src/agent/simple.ts', additions: 5 },
|
||||
]) !== null);
|
||||
assert('ts sdk with tests ok', mg.missingTestsReason([
|
||||
{ filename: 'src/praisonai-ts/src/agent/simple.ts', additions: 5 },
|
||||
{ filename: 'src/praisonai-ts/tests/unit/agent/history-restore.test.ts', additions: 10 },
|
||||
]) === null);
|
||||
assert('touches ts sdk', mg.touchesSdk([{ filename: 'src/praisonai-ts/src/agent/simple.ts' }]));
|
||||
assert('final body requires sdk value table', mg.FINAL_CLAUDE_REVIEW_BODY.includes('| **SDK value** |'));
|
||||
assert('final body requires ts agents md', mg.FINAL_CLAUDE_REVIEW_BODY.includes('src/praisonai-ts/AGENTS.md'));
|
||||
|
||||
// PR size
|
||||
assert('large PR blocked', mg.prSizeReasons([{ additions: 900 }]).length > 0);
|
||||
|
||||
assert('internal PR link accepted', mg.isInternalPullRequestLink(
|
||||
{ base: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
|
||||
'MervinPraison',
|
||||
'PraisonAI'
|
||||
));
|
||||
assert('fork sync PR link rejected', !mg.isInternalPullRequestLink(
|
||||
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAI' } } },
|
||||
'MervinPraison',
|
||||
'PraisonAI'
|
||||
));
|
||||
|
||||
assert('dispatch blocked when merge gate active', mg.mergeGateDispatchBlockedReason({
|
||||
labels: ['claude-merge-gate-active'],
|
||||
pendingRuns: 0,
|
||||
}) === 'merge gate already active on PR');
|
||||
assert('dispatch blocked when queue saturated', mg.mergeGateDispatchBlockedReason({
|
||||
labels: [],
|
||||
pendingRuns: 3,
|
||||
})?.includes('already queued or in progress'));
|
||||
assert('dispatch allowed when queue below cap', mg.mergeGateDispatchBlockedReason({
|
||||
labels: [],
|
||||
pendingRuns: 2,
|
||||
}) == null);
|
||||
|
||||
// Pending count excludes workflow_run auto-dispatch probes (PR #4534)
|
||||
assert('workflow_run probes excluded from pending count', mg.countSubstantiveMergeGateRuns([
|
||||
{ event: 'workflow_dispatch' },
|
||||
{ event: 'workflow_run' },
|
||||
{ event: 'workflow_run' },
|
||||
]) === 1);
|
||||
assert('all-probe runs count as zero pending', mg.countSubstantiveMergeGateRuns([
|
||||
{ event: 'workflow_run' },
|
||||
{ event: 'workflow_run' },
|
||||
]) === 0);
|
||||
assert('substantive dispatches still counted', mg.countSubstantiveMergeGateRuns([
|
||||
{ event: 'workflow_dispatch' },
|
||||
{ event: 'schedule' },
|
||||
]) === 2);
|
||||
assert('countSubstantiveMergeGateRuns null-safe', mg.countSubstantiveMergeGateRuns(null) === 0);
|
||||
assert('rate limit threshold is 200', mg.MIN_CORE_RATE_LIMIT_REMAINING === 200);
|
||||
|
||||
(async () => {
|
||||
const skipLow = await mg.shouldSkipMergeGateDispatch(
|
||||
{
|
||||
rest: {
|
||||
rateLimit: {
|
||||
get: async () => ({ data: { resources: { core: { remaining: 50 } } } }),
|
||||
},
|
||||
},
|
||||
},
|
||||
'owner',
|
||||
'repo',
|
||||
1,
|
||||
{ info: () => {} }
|
||||
);
|
||||
assert('rate limit low skips dispatch', skipLow === true);
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
})().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Run: node .github/scripts/pipeline-status-selftest.js
|
||||
*/
|
||||
const ps = require('./pipeline-status.js');
|
||||
const mg = require('./merge-gate.js');
|
||||
|
||||
let failed = 0;
|
||||
function assert(name, cond) {
|
||||
if (!cond) {
|
||||
console.error('FAIL:', name);
|
||||
failed++;
|
||||
} else {
|
||||
console.log('ok:', name);
|
||||
}
|
||||
}
|
||||
|
||||
const kickComments = [
|
||||
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
|
||||
{ user: { login: 'MervinPraison' }, body: '/review' },
|
||||
];
|
||||
|
||||
const finalComment = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude You are the FINAL architecture reviewer.',
|
||||
created_at: '2026-06-26T10:00:00Z',
|
||||
};
|
||||
|
||||
assert(
|
||||
'reviews pending when not kicked',
|
||||
ps.deriveStage([], { ready: false, reasons: ['no FINAL Claude review trigger'] })
|
||||
=== 'pipeline/reviews-pending'
|
||||
);
|
||||
assert(
|
||||
'final pending after kick',
|
||||
ps.deriveStage(kickComments, { ready: false, reasons: ['no FINAL Claude review trigger'] })
|
||||
=== 'pipeline/final-claude-pending'
|
||||
);
|
||||
assert(
|
||||
'awaiting merge gate after final',
|
||||
ps.deriveStage(
|
||||
[...kickComments, finalComment],
|
||||
{ ready: false, reasons: ['recent @claude within 35min'] }
|
||||
) === 'pipeline/awaiting-merge-gate'
|
||||
);
|
||||
assert(
|
||||
'merge ready',
|
||||
ps.deriveStage([...kickComments, finalComment], { ready: true, reasons: [] })
|
||||
=== 'pipeline/merge-ready'
|
||||
);
|
||||
|
||||
const blockers = ps.deriveBlockerLabels({
|
||||
ready: false,
|
||||
reasons: ['CI not green on HEAD', 'SDK code added without test file changes — requires manual review'],
|
||||
});
|
||||
assert('maps ci blocker', blockers.includes('pipeline/blocked:ci'));
|
||||
assert('maps manual blocker', blockers.includes('pipeline/blocked:manual-review'));
|
||||
|
||||
assert(
|
||||
'internal link matches upstream base',
|
||||
mg.isInternalPullRequestLink(
|
||||
{ base: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
|
||||
'MervinPraison',
|
||||
'PraisonAI'
|
||||
)
|
||||
);
|
||||
assert(
|
||||
'fork sync link rejected',
|
||||
!mg.isInternalPullRequestLink(
|
||||
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAI' } } },
|
||||
'MervinPraison',
|
||||
'PraisonAI'
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -1,258 +0,0 @@
|
||||
/**
|
||||
* Sync pipeline stage/blocker labels on open PRs.
|
||||
* @see .github/workflows/pipeline-status-sync.yml
|
||||
*/
|
||||
|
||||
const mergeGate = require('./merge-gate.js');
|
||||
const chain = require('./bot-pr-review-chain.js');
|
||||
const ciFix = require('./ci-failure-claude.js');
|
||||
|
||||
const PIPELINE_PREFIX = 'pipeline/';
|
||||
const STAGE_LABELS = [
|
||||
'pipeline/reviews-pending',
|
||||
'pipeline/final-claude-pending',
|
||||
'pipeline/awaiting-merge-gate',
|
||||
'pipeline/merge-ready',
|
||||
'pipeline/merged',
|
||||
];
|
||||
const BLOCKER_LABELS = [
|
||||
'pipeline/blocked:ci',
|
||||
'pipeline/blocked:conflict',
|
||||
'pipeline/blocked:manual-review',
|
||||
'pipeline/blocked:cooldown',
|
||||
'pipeline/blocked:stale-final',
|
||||
'pipeline/blocked:no-final',
|
||||
];
|
||||
const ALL_PIPELINE_LABELS = [...STAGE_LABELS, ...BLOCKER_LABELS];
|
||||
|
||||
const LABEL_SPECS = [
|
||||
{ name: 'pipeline/reviews-pending', color: 'fbca04', description: 'Waiting for CodeRabbit/Qodo/Copilot reviews' },
|
||||
{ name: 'pipeline/final-claude-pending', color: 'd4c5f9', description: 'Reviews done; waiting for FINAL @claude' },
|
||||
{ name: 'pipeline/awaiting-merge-gate', color: 'c5def5', description: 'FINAL done; waiting for merge gate / CI' },
|
||||
{ name: 'pipeline/merge-ready', color: '0e8a16', description: 'Eligible for merge gate auto-merge' },
|
||||
{ name: 'pipeline/merged', color: '6f42c1', description: 'Merged via pipeline' },
|
||||
{ name: 'pipeline/blocked:ci', color: 'd93f0b', description: 'Blocked: CI not green on HEAD' },
|
||||
{ name: 'pipeline/blocked:conflict', color: 'b60205', description: 'Blocked: merge conflict or rebase pending' },
|
||||
{ name: 'pipeline/blocked:manual-review', color: 'e99695', description: 'Blocked: requires manual review' },
|
||||
{ name: 'pipeline/blocked:cooldown', color: 'fef2c0', description: 'Blocked: post-push or @claude cooldown' },
|
||||
{ name: 'pipeline/blocked:stale-final', color: 'f9d0c4', description: 'Blocked: FINAL stale after new commits' },
|
||||
{ name: 'pipeline/blocked:no-final', color: 'ededed', description: 'Blocked: no FINAL @claude trigger yet' },
|
||||
];
|
||||
|
||||
function deriveStage(comments, evalResult) {
|
||||
if (evalResult.ready) return 'pipeline/merge-ready';
|
||||
if (!chain.chainKickPosted(comments)) return 'pipeline/reviews-pending';
|
||||
if (!mergeGate.hasFinalClaudeReviewTrigger(comments)) return 'pipeline/final-claude-pending';
|
||||
return 'pipeline/awaiting-merge-gate';
|
||||
}
|
||||
|
||||
function reasonToBlockerLabel(reason) {
|
||||
const r = (reason || '').toLowerCase();
|
||||
if (r.includes('ci not green') || r.includes('sdk code changed but no ci')) return 'pipeline/blocked:ci';
|
||||
if (
|
||||
r.includes('conflict') ||
|
||||
r.includes('mergestate=dirty') ||
|
||||
r.includes('not mergeable')
|
||||
) return 'pipeline/blocked:conflict';
|
||||
if (
|
||||
r.includes('manual') ||
|
||||
r.includes('requires manual') ||
|
||||
r.includes('sensitive path') ||
|
||||
r.includes('no-auto-merge') ||
|
||||
r.includes('manual-only label') ||
|
||||
r.includes('without test') ||
|
||||
r.includes('files changed') ||
|
||||
r.includes('possible secret') ||
|
||||
r.includes('agent.py')
|
||||
) return 'pipeline/blocked:manual-review';
|
||||
if (r.includes('recent @claude') || r.includes('post-push buffer')) return 'pipeline/blocked:cooldown';
|
||||
if (r.includes('stale final')) return 'pipeline/blocked:stale-final';
|
||||
if (r.includes('no final claude')) return 'pipeline/blocked:no-final';
|
||||
if (r.includes('final claude not complete')) return 'pipeline/blocked:stale-final';
|
||||
if (r.includes('claude.yml in progress') || r.includes('claude-merge-gate-active')) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function deriveBlockerLabels(evalResult) {
|
||||
if (evalResult.ready) return [];
|
||||
const labels = new Set();
|
||||
for (const reason of evalResult.reasons || []) {
|
||||
const mapped = reasonToBlockerLabel(reason);
|
||||
if (mapped && mapped.startsWith('pipeline/blocked:')) labels.add(mapped);
|
||||
}
|
||||
return [...labels];
|
||||
}
|
||||
|
||||
function computePipelineLabels(comments, evalResult) {
|
||||
const stage = deriveStage(comments, evalResult);
|
||||
const blockers = stage === 'pipeline/merge-ready' ? [] : deriveBlockerLabels(evalResult);
|
||||
return { stage, blockers, all: [stage, ...blockers] };
|
||||
}
|
||||
|
||||
async function ensurePipelineLabels(github, owner, repo, core) {
|
||||
let existing;
|
||||
if (typeof github.paginate === 'function') {
|
||||
existing = await github.paginate(github.rest.issues.listLabelsForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
} else {
|
||||
({ data: existing } = await github.rest.issues.listLabelsForRepo({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
}));
|
||||
}
|
||||
const names = new Set(existing.map((l) => l.name));
|
||||
for (const spec of LABEL_SPECS) {
|
||||
if (names.has(spec.name)) continue;
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: spec.name,
|
||||
color: spec.color,
|
||||
description: spec.description,
|
||||
});
|
||||
core?.info?.(`Created label ${spec.name}`);
|
||||
} catch (err) {
|
||||
if (err.status !== 422) throw err;
|
||||
core?.info?.(`Label ${spec.name} already exists`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function syncPipelineLabels(github, owner, repo, prNumber, core) {
|
||||
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
|
||||
if (ctx.pr.state !== 'open') return { synced: false, reason: 'not_open' };
|
||||
|
||||
const evalResult = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core
|
||||
);
|
||||
const { stage, blockers, all } = computePipelineLabels(ctx.comments, evalResult);
|
||||
|
||||
const current = ctx.labels.filter((l) => l.startsWith(PIPELINE_PREFIX));
|
||||
const desired = new Set(all);
|
||||
const toRemove = current.filter((l) => !desired.has(l) && l !== 'pipeline/merged');
|
||||
const toAdd = all.filter((l) => !current.includes(l));
|
||||
|
||||
for (const name of toRemove) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name });
|
||||
} catch (err) {
|
||||
if (err.status !== 404) throw err;
|
||||
}
|
||||
}
|
||||
if (toAdd.length) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels: toAdd,
|
||||
});
|
||||
}
|
||||
|
||||
core?.info?.(`PR #${prNumber}: stage=${stage} blockers=[${blockers.join(', ')}]`);
|
||||
|
||||
const ciNotGreen = (evalResult.reasons || []).some((r) =>
|
||||
r.toLowerCase().includes('ci not green')
|
||||
);
|
||||
if (ciNotGreen || blockers.includes('pipeline/blocked:ci')) {
|
||||
await ciFix.maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
|
||||
} else {
|
||||
await ciFix.maybeClearCiFixLabel(
|
||||
github, owner, repo, prNumber, ctx.labels, ctx.headSha, core
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
synced: true,
|
||||
stage,
|
||||
blockers,
|
||||
ready: evalResult.ready,
|
||||
reasons: evalResult.reasons,
|
||||
labels: ctx.labels,
|
||||
createdAt: new Date(ctx.pr.created_at).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
async function dispatchMergeGateForOldestReady(github, owner, repo, readyCandidates, core) {
|
||||
if (!readyCandidates.length) return 0;
|
||||
const mergeGate = require('./merge-gate.js');
|
||||
readyCandidates.sort((a, b) => a.createdAt - b.createdAt);
|
||||
for (const cand of readyCandidates) {
|
||||
if (await mergeGate.shouldSkipMergeGateDispatch(github, owner, repo, cand.prNumber, core)) {
|
||||
continue;
|
||||
}
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner,
|
||||
repo,
|
||||
event_type: 'claude-merge-gate',
|
||||
client_payload: { pr_number: cand.prNumber },
|
||||
});
|
||||
core?.info?.(`Dispatched merge gate for ready PR #${cand.prNumber}`);
|
||||
return cand.prNumber;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function syncOpenPullRequests(github, owner, repo, options, core) {
|
||||
const { maxPrs = 20, dispatchMergeGate = true } = options || {};
|
||||
await ensurePipelineLabels(github, owner, repo, core);
|
||||
let prs;
|
||||
if (maxPrs <= 100) {
|
||||
({ data: prs } = await github.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
}));
|
||||
} else {
|
||||
prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
let synced = 0;
|
||||
const readyCandidates = [];
|
||||
for (const pr of prs) {
|
||||
if (synced >= maxPrs) break;
|
||||
if (pr.draft) continue;
|
||||
if (pr.head?.repo?.full_name && pr.head.repo.full_name !== `${owner}/${repo}`) continue;
|
||||
const result = await syncPipelineLabels(github, owner, repo, pr.number, core);
|
||||
if (result.ready) {
|
||||
readyCandidates.push({
|
||||
prNumber: pr.number,
|
||||
createdAt: result.createdAt || new Date(pr.created_at).getTime(),
|
||||
labels: result.labels || [],
|
||||
});
|
||||
}
|
||||
synced += 1;
|
||||
}
|
||||
let dispatched = 0;
|
||||
if (dispatchMergeGate && readyCandidates.length) {
|
||||
dispatched = await dispatchMergeGateForOldestReady(
|
||||
github, owner, repo, readyCandidates, core
|
||||
);
|
||||
}
|
||||
core?.info?.(`Pipeline label sync complete (${synced} PR(s), dispatched=${dispatched || 'none'})`);
|
||||
return synced;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
STAGE_LABELS,
|
||||
BLOCKER_LABELS,
|
||||
ALL_PIPELINE_LABELS,
|
||||
deriveStage,
|
||||
deriveBlockerLabels,
|
||||
computePipelineLabels,
|
||||
ensurePipelineLabels,
|
||||
syncPipelineLabels,
|
||||
syncOpenPullRequests,
|
||||
dispatchMergeGateForOldestReady,
|
||||
};
|
||||
@@ -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,122 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const rg = require('./release-gate.js');
|
||||
|
||||
assert.strictEqual(rg.bumpPatch('1.6.80'), '1.6.81');
|
||||
assert.strictEqual(rg.bumpPatch('4.6.80'), '4.6.81');
|
||||
assert.strictEqual(rg.bumpPatch('0.0.2'), '0.0.3');
|
||||
|
||||
const versions = rg.readVersionsFromTree();
|
||||
assert.ok(versions.currentAgents);
|
||||
assert.ok(versions.currentCode);
|
||||
assert.ok(versions.currentWrapper);
|
||||
assert.ok(versions.targetCode);
|
||||
assert.ok(rg.PACKAGE_PATHS.includes('src/praisonai-code'));
|
||||
// Every released package must make the gate's path-change check eligible.
|
||||
for (const p of [
|
||||
'src/praisonai', 'src/praisonai-agents', 'src/praisonai-code',
|
||||
'src/praisonai-bot', 'src/praisonai-train', 'src/praisonai-browser',
|
||||
'src/praisonai-mcp', 'src/praisonai-sandbox', 'src/praisonai-deploy',
|
||||
]) {
|
||||
assert.ok(rg.PACKAGE_PATHS.includes(p), `PACKAGE_PATHS missing ${p}`);
|
||||
}
|
||||
|
||||
const noonUtc = new Date('2026-07-08T12:00:00Z');
|
||||
const dayStart = rg.utcDayStart(noonUtc);
|
||||
assert.strictEqual(dayStart.toISOString(), '2026-07-08T00:00:00.000Z');
|
||||
|
||||
function mockGithub({ runs = [], releases = [] } = {}) {
|
||||
return {
|
||||
rest: {
|
||||
actions: {
|
||||
listWorkflowRuns: async () => ({ data: { workflow_runs: runs } }),
|
||||
},
|
||||
repos: {
|
||||
listReleases: async () => ({ data: releases }),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
assert.strictEqual(await rg.pypiVersionExists('praisonaiagents', '0.0.0'), false);
|
||||
|
||||
assert.strictEqual(rg.PATCH_RELEASE_INTERVAL_DAYS, 3);
|
||||
|
||||
// Dedupe is release-based: a v* release inside the window blocks…
|
||||
const recentRelease = mockGithub({
|
||||
releases: [{ tag_name: 'v4.6.159', published_at: '2026-07-08T09:00:00Z' }],
|
||||
});
|
||||
assert.strictEqual(
|
||||
await rg.hasSuccessfulReleaseWithinDays(recentRelease, 'o', 'r', noonUtc),
|
||||
true
|
||||
);
|
||||
|
||||
const result = await rg.evaluateReleasePreflight(
|
||||
recentRelease, 'o', 'r',
|
||||
{ headSha: 'abc', isCiTrigger: false, bump: 'patch', now: noonUtc },
|
||||
null
|
||||
);
|
||||
assert.ok(result.reasons.some((r) => r.includes('every 3 days')));
|
||||
|
||||
// …an old release does not…
|
||||
const oldRelease = mockGithub({
|
||||
releases: [{ tag_name: 'v4.6.150', published_at: '2026-07-04T09:00:00Z' }],
|
||||
});
|
||||
assert.strictEqual(
|
||||
await rg.hasSuccessfulReleaseWithinDays(oldRelease, 'o', 'r', noonUtc),
|
||||
false
|
||||
);
|
||||
|
||||
// …and a dry-run "success" (workflow run, no release) does not consume the
|
||||
// window: only real releases count.
|
||||
const dryRunOnly = mockGithub({
|
||||
runs: [{ conclusion: 'success', created_at: '2026-07-08T09:00:00Z', status: 'completed' }],
|
||||
releases: [],
|
||||
});
|
||||
assert.strictEqual(
|
||||
await rg.hasSuccessfulReleaseWithinDays(dryRunOnly, 'o', 'r', noonUtc),
|
||||
false
|
||||
);
|
||||
|
||||
// Active-run blocking: fresh waiting run blocks…
|
||||
const freshWaiting = mockGithub({
|
||||
runs: [{ status: 'waiting', conclusion: null, created_at: '2026-07-08T11:00:00Z', html_url: 'x' }],
|
||||
});
|
||||
assert.strictEqual(
|
||||
await rg.hasActiveReleaseRun(freshWaiting, 'o', 'r', noonUtc, null),
|
||||
true
|
||||
);
|
||||
|
||||
// …a waiting run stuck longer than STALE_WAITING_HOURS no longer blocks…
|
||||
const staleWaiting = mockGithub({
|
||||
runs: [{ status: 'waiting', conclusion: null, created_at: '2026-07-08T01:00:00Z', html_url: 'x' }],
|
||||
});
|
||||
const warnings = [];
|
||||
const fakeCore = { warning: (m) => warnings.push(m), info: () => {} };
|
||||
assert.strictEqual(rg.STALE_WAITING_HOURS, 6);
|
||||
assert.strictEqual(
|
||||
await rg.hasActiveReleaseRun(staleWaiting, 'o', 'r', noonUtc, fakeCore),
|
||||
false
|
||||
);
|
||||
assert.ok(warnings.length === 1 && warnings[0].includes('approve or cancel'));
|
||||
|
||||
// …but an in_progress run always blocks regardless of age.
|
||||
const oldInProgress = mockGithub({
|
||||
runs: [{ status: 'in_progress', conclusion: null, created_at: '2026-07-07T01:00:00Z', html_url: 'x' }],
|
||||
});
|
||||
assert.strictEqual(
|
||||
await rg.hasActiveReleaseRun(oldInProgress, 'o', 'r', noonUtc, null),
|
||||
true
|
||||
);
|
||||
|
||||
console.log('ok: bumpPatch');
|
||||
console.log('ok: readVersionsFromTree');
|
||||
console.log('ok: PACKAGE_PATHS covers all 9 released packages');
|
||||
console.log('ok: pypiVersionExists missing version');
|
||||
console.log('ok: utcDayStart');
|
||||
console.log('ok: release-based dedupe (recent blocks, old does not)');
|
||||
console.log('ok: dry-run success does not consume the release window');
|
||||
console.log('ok: 3-day dedupe blocks second release');
|
||||
console.log('ok: fresh waiting run blocks; stale waiting run ignored with warning');
|
||||
console.log('ok: in_progress always blocks');
|
||||
})();
|
||||
@@ -1,315 +0,0 @@
|
||||
/**
|
||||
* Release gate preflight — path changes, CI SHA, dedupe, PyPI version checks.
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
/** Every directory whose changes should make a release eligible — all nine
|
||||
* published packages (mirrors the skip_* inputs in pypi-release.yml). */
|
||||
const PACKAGE_PATHS = [
|
||||
'src/praisonai',
|
||||
'src/praisonai-agents',
|
||||
'src/praisonai-code',
|
||||
'src/praisonai-bot',
|
||||
'src/praisonai-train',
|
||||
'src/praisonai-browser',
|
||||
'src/praisonai-mcp',
|
||||
'src/praisonai-sandbox',
|
||||
'src/praisonai-deploy',
|
||||
];
|
||||
/** Minimum days between successful patch auto-releases. */
|
||||
const PATCH_RELEASE_INTERVAL_DAYS = 3;
|
||||
const ACTIVE_RELEASE_STATUSES = new Set([
|
||||
'queued', 'in_progress', 'waiting', 'pending', 'requested',
|
||||
]);
|
||||
/** A run stuck in `waiting` (environment approval) longer than this no longer
|
||||
* blocks the gate. GitHub only auto-fails unapproved deployments after 30
|
||||
* days, so without a cutoff one forgotten manual dispatch stalls all
|
||||
* auto-releases for up to a month. The run is warned about, never cancelled —
|
||||
* if later approved, the pypi-release concurrency group serializes it and its
|
||||
* pypi_exists checks no-op anything already published. */
|
||||
const STALE_WAITING_HOURS = 6;
|
||||
|
||||
function bumpPatch(version) {
|
||||
const parts = version.split('.');
|
||||
if (parts.length !== 3) throw new Error(`Invalid version: ${version}`);
|
||||
return `${parts[0]}.${parts[1]}.${Number(parts[2]) + 1}`;
|
||||
}
|
||||
|
||||
function readVersionsFromTree(root = '.') {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const agentsToml = fs.readFileSync(
|
||||
path.join(root, 'src/praisonai-agents/pyproject.toml'), 'utf8'
|
||||
);
|
||||
const agentsMatch = agentsToml.match(/^version\s*=\s*"([^"]+)"/m);
|
||||
if (!agentsMatch) throw new Error('Could not read agents version');
|
||||
|
||||
const codeToml = fs.readFileSync(
|
||||
path.join(root, 'src/praisonai-code/pyproject.toml'), 'utf8'
|
||||
);
|
||||
const codeMatch = codeToml.match(/^version\s*=\s*"([^"]+)"/m);
|
||||
if (!codeMatch) throw new Error('Could not read praisonai-code version');
|
||||
|
||||
const wrapperPy = fs.readFileSync(
|
||||
path.join(root, 'src/praisonai/praisonai/version.py'), 'utf8'
|
||||
);
|
||||
const wrapperMatch = wrapperPy.match(/__version__ = "([^"]+)"/);
|
||||
if (!wrapperMatch) throw new Error('Could not read wrapper version');
|
||||
return {
|
||||
currentAgents: agentsMatch[1],
|
||||
currentCode: codeMatch[1],
|
||||
currentWrapper: wrapperMatch[1],
|
||||
targetAgents: bumpPatch(agentsMatch[1]),
|
||||
targetCode: bumpPatch(codeMatch[1]),
|
||||
targetWrapper: bumpPatch(wrapperMatch[1]),
|
||||
};
|
||||
}
|
||||
|
||||
function pypiVersionExists(packageName, version) {
|
||||
return new Promise((resolve) => {
|
||||
const req = https.get(
|
||||
`https://pypi.org/pypi/${packageName}/${version}/json`,
|
||||
{ timeout: 15000 },
|
||||
(res) => {
|
||||
res.resume();
|
||||
resolve(res.statusCode === 200);
|
||||
}
|
||||
);
|
||||
req.on('error', () => resolve(false));
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function hasActiveReleaseRun(github, owner, repo, now = new Date(), core = null) {
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'pypi-release.yml',
|
||||
per_page: 20,
|
||||
});
|
||||
const staleCutoff = now.getTime() - STALE_WAITING_HOURS * 60 * 60 * 1000;
|
||||
return runs.data.workflow_runs.some((r) => {
|
||||
if (!ACTIVE_RELEASE_STATUSES.has(r.status) || r.conclusion) return false;
|
||||
if (r.status === 'waiting' && new Date(r.created_at).getTime() < staleCutoff) {
|
||||
if (core) {
|
||||
core.warning(
|
||||
`Ignoring release run waiting >${STALE_WAITING_HOURS}h for environment approval: `
|
||||
+ `${r.html_url} — approve or cancel it. It no longer blocks auto-releases.`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function utcDayStart(now = new Date()) {
|
||||
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||
}
|
||||
|
||||
function releaseIntervalStart(now = new Date(), intervalDays = PATCH_RELEASE_INTERVAL_DAYS) {
|
||||
return new Date(now.getTime() - intervalDays * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async function hasSuccessfulReleaseWithinDays(
|
||||
github,
|
||||
owner,
|
||||
repo,
|
||||
now = new Date(),
|
||||
intervalDays = PATCH_RELEASE_INTERVAL_DAYS,
|
||||
) {
|
||||
const windowStart = releaseIntervalStart(now, intervalDays);
|
||||
// Dedupe on GitHub releases (v* tags) rather than run conclusions: a
|
||||
// dry_run=true dispatch concludes 'success' without publishing anything and
|
||||
// must not consume the release window. Only a real full release creates a
|
||||
// v* GitHub release (bump_and_release.py's `gh release create`).
|
||||
const releases = await github.rest.repos.listReleases({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 10,
|
||||
});
|
||||
return releases.data.some(
|
||||
(r) => r.tag_name && r.tag_name.startsWith('v')
|
||||
&& new Date(r.published_at || r.created_at) >= windowStart
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated Use hasSuccessfulReleaseWithinDays — kept for selftests. */
|
||||
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
|
||||
return hasSuccessfulReleaseWithinDays(github, owner, repo, now, 1);
|
||||
}
|
||||
|
||||
async function lastGreenCoreTestsSha(github, owner, repo) {
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'test-core.yml',
|
||||
branch: 'main',
|
||||
status: 'completed',
|
||||
per_page: 30,
|
||||
});
|
||||
const hit = runs.data.workflow_runs.find((r) => r.conclusion === 'success');
|
||||
return hit ? hit.head_sha : '';
|
||||
}
|
||||
|
||||
async function evaluateReleasePreflight(github, owner, repo, options, core) {
|
||||
const {
|
||||
headSha,
|
||||
isCiTrigger = false,
|
||||
bump = 'patch',
|
||||
} = options;
|
||||
|
||||
const reasons = [];
|
||||
const out = {
|
||||
ready: false,
|
||||
reasons,
|
||||
headSha: headSha || '',
|
||||
lastTag: '',
|
||||
targetAgents: '',
|
||||
targetCode: '',
|
||||
targetWrapper: '',
|
||||
};
|
||||
|
||||
if (bump !== 'patch') {
|
||||
reasons.push('only patch auto-release supported');
|
||||
return out;
|
||||
}
|
||||
|
||||
const referenceTime = options.now instanceof Date ? options.now : new Date();
|
||||
|
||||
if (await hasActiveReleaseRun(github, owner, repo, referenceTime, core)) {
|
||||
reasons.push('PyPI Release already in progress or awaiting approval');
|
||||
return out;
|
||||
}
|
||||
|
||||
if (await hasSuccessfulReleaseWithinDays(github, owner, repo, referenceTime)) {
|
||||
reasons.push(
|
||||
`already released within last ${PATCH_RELEASE_INTERVAL_DAYS} days; `
|
||||
+ `max one patch release every ${PATCH_RELEASE_INTERVAL_DAYS} days`
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
let versions;
|
||||
try {
|
||||
versions = readVersionsFromTree();
|
||||
} catch (err) {
|
||||
reasons.push(err.message);
|
||||
return out;
|
||||
}
|
||||
out.targetAgents = versions.targetAgents;
|
||||
out.targetCode = versions.targetCode;
|
||||
out.targetWrapper = versions.targetWrapper;
|
||||
|
||||
const agentsOnPypi = await pypiVersionExists('praisonaiagents', versions.targetAgents);
|
||||
const codeOnPypi = await pypiVersionExists('praisonai-code', versions.targetCode);
|
||||
const wrapperOnPypi = await pypiVersionExists('praisonai', versions.targetWrapper);
|
||||
if (agentsOnPypi && codeOnPypi && wrapperOnPypi) {
|
||||
reasons.push(
|
||||
`already published: praisonaiagents==${versions.targetAgents}, `
|
||||
+ `praisonai-code==${versions.targetCode}, praisonai==${versions.targetWrapper}`
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
let lastTag = '';
|
||||
try {
|
||||
lastTag = execSync('git describe --tags --match "v*" --abbrev=0', { encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
reasons.push('no v* tag found');
|
||||
return out;
|
||||
}
|
||||
out.lastTag = lastTag;
|
||||
|
||||
let changed = '';
|
||||
try {
|
||||
changed = execSync(
|
||||
`git diff --name-only ${lastTag} HEAD -- ${PACKAGE_PATHS.join(' ')}`,
|
||||
{ encoding: 'utf8' }
|
||||
).trim();
|
||||
} catch {
|
||||
reasons.push('git diff failed');
|
||||
return out;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
reasons.push(`no changes in ${PACKAGE_PATHS.join(' or ')} since ${lastTag}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
if (isCiTrigger) {
|
||||
let mainSha = '';
|
||||
try {
|
||||
mainSha = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
reasons.push('could not resolve origin/main');
|
||||
return out;
|
||||
}
|
||||
if (headSha !== mainSha) {
|
||||
reasons.push(`superseded: green SHA ${headSha.slice(0, 7)} != main ${mainSha.slice(0, 7)}`);
|
||||
return out;
|
||||
}
|
||||
out.headSha = headSha;
|
||||
} else {
|
||||
const evalSha = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
|
||||
out.headSha = evalSha;
|
||||
const greenSha = await lastGreenCoreTestsSha(github, owner, repo);
|
||||
if (greenSha !== evalSha) {
|
||||
// Core Tests has path filters and honors [skip ci] (the release
|
||||
// safety-net commit uses it), so the tip of main may legitimately have
|
||||
// no Core Tests run of its own — e.g. a docs-only commit, or the
|
||||
// "chore(release): … [skip ci]" commit right after a release. Accept a
|
||||
// green ancestor when nothing under the released package paths changed
|
||||
// after it; otherwise the cron path stalls until the next src/** push.
|
||||
let greenAncestorOk = false;
|
||||
if (greenSha && /^[0-9a-f]{40}$/.test(greenSha)) {
|
||||
try {
|
||||
execSync(`git merge-base --is-ancestor ${greenSha} ${evalSha}`);
|
||||
const delta = execSync(
|
||||
`git diff --name-only ${greenSha} ${evalSha} -- ${PACKAGE_PATHS.join(' ')}`,
|
||||
{ encoding: 'utf8' }
|
||||
).trim();
|
||||
greenAncestorOk = delta === '';
|
||||
} catch {
|
||||
greenAncestorOk = false;
|
||||
}
|
||||
}
|
||||
if (!greenAncestorOk) {
|
||||
reasons.push(`CI not green on HEAD (last green: ${greenSha ? greenSha.slice(0, 7) : 'none'})`);
|
||||
return out;
|
||||
}
|
||||
if (core) {
|
||||
core.info(
|
||||
`HEAD ${evalSha.slice(0, 7)} has no Core Tests run; accepting green ancestor `
|
||||
+ `${greenSha.slice(0, 7)} (no package-path changes since).`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.ready = true;
|
||||
out.reasons = ['ready'];
|
||||
if (core) core.info(`Release preflight passed for ${out.headSha || headSha}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bumpPatch,
|
||||
readVersionsFromTree,
|
||||
pypiVersionExists,
|
||||
PATCH_RELEASE_INTERVAL_DAYS,
|
||||
STALE_WAITING_HOURS,
|
||||
releaseIntervalStart,
|
||||
hasActiveReleaseRun,
|
||||
hasSuccessfulReleaseWithinDays,
|
||||
hasSuccessfulReleaseToday,
|
||||
utcDayStart,
|
||||
evaluateReleasePreflight,
|
||||
ACTIVE_RELEASE_STATUSES,
|
||||
PACKAGE_PATHS,
|
||||
};
|
||||
@@ -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: TypeScript → src/praisonai-ts/; 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.
|
||||
@@ -12,247 +15,466 @@ on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
schedule:
|
||||
- cron: '30 3,9,15,21 * * *'
|
||||
workflow_dispatch:
|
||||
types: [opened]
|
||||
|
||||
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:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
pull-requests: write
|
||||
outputs:
|
||||
pr_number: ${{ steps.trigger.outputs.pr_number }}
|
||||
triggered: ${{ steps.trigger.outputs.triggered }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Post Claude FINAL after prior reviewers
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
id: trigger
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const prNumber =
|
||||
context.payload.issue?.number ||
|
||||
context.payload.pull_request?.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner, repo, pull_number: prNumber,
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const result = await chain.maybeTriggerClaudeFinal(
|
||||
github, owner, repo, prNumber,
|
||||
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core,
|
||||
{ skipCopilot: true, optionalWaitMs: 0, prCreatedAt: pr.created_at }
|
||||
// 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 (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);
|
||||
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 FINAL after PR open if reviewers are slow
|
||||
# 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_target' &&
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Wait for prior reviewers (30 min)
|
||||
run: sleep 1800
|
||||
|
||||
- name: Post Claude FINAL (skip Copilot)
|
||||
- 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 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 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 =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
|
||||
# ================================================================
|
||||
# Recovery: post FINAL @claude after push or when chain missed
|
||||
# ================================================================
|
||||
claude-review-recovery:
|
||||
if: |
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.action == 'synchronize'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Re-post FINAL Claude review when stale or missing
|
||||
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 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 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).\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:
|
||||
- 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 pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
const comments = await mergeGate.listAllComments(github, owner, repo, pr);
|
||||
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr))
|
||||
|| context.payload.pull_request.updated_at;
|
||||
const headPusher = context.payload.sender?.login || null;
|
||||
// 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);
|
||||
|
||||
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
|
||||
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
|
||||
|
||||
if (mergeGate.shouldSkipFinalRecovery(comments, headPushedAt)) {
|
||||
core.info('Recent @claude within 35min and FINAL not stale — skip recovery');
|
||||
return;
|
||||
}
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner,
|
||||
repo,
|
||||
per_page: 50,
|
||||
sort: 'created',
|
||||
direction: 'desc'
|
||||
});
|
||||
|
||||
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr)) {
|
||||
core.info('Claude workflow in progress — skip recovery');
|
||||
return;
|
||||
}
|
||||
// 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'
|
||||
);
|
||||
|
||||
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}`);
|
||||
if (copilotResponse) {
|
||||
console.log(`Copilot responded at ${copilotResponse.created_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
const result = await chain.maybeTriggerClaudeFinal(
|
||||
github, owner, repo, pr, finalBody, core, opts
|
||||
|
||||
// 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 (result.posted) core.info(`Posted stale-FINAL recovery @claude on PR #${pr}`);
|
||||
return;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFinal) {
|
||||
core.info('FINAL Claude review current on HEAD — skip recovery');
|
||||
return;
|
||||
}
|
||||
console.log('Copilot did not respond within timeout, triggering Claude anyway');
|
||||
core.setOutput('copilot_responded', 'timeout');
|
||||
|
||||
const result = await chain.maybeTriggerClaudeFinal(
|
||||
github, owner, repo, pr, finalBody, core, opts
|
||||
);
|
||||
if (result.posted) core.info(`Posted missing FINAL Claude review on PR #${pr}`);
|
||||
|
||||
- name: Sync pipeline status labels
|
||||
- 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 path = require('path');
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const pr = context.payload.pull_request.number;
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
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 =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
# Scheduled + manual: post missing or stale FINAL @claude on all open PRs (incl. forks)
|
||||
post-missing-claude-final:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
- 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\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
console.log(`Claude triggered on PR #${pr}`);
|
||||
|
||||
# ================================================================
|
||||
# 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:
|
||||
- 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 =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
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\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
console.log(`Claude triggered on PR #${pr}`);
|
||||
|
||||
# ================================================================
|
||||
# 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: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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 =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Post missing/stale FINAL @claude on open PRs
|
||||
- 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: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner, repo, state: 'open', per_page: 100,
|
||||
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\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
let posted = 0;
|
||||
for (const pr of prs) {
|
||||
if (pr.draft) continue;
|
||||
const comments = await mergeGate.listAllComments(github, owner, repo, pr.number);
|
||||
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr.number)) || pr.updated_at;
|
||||
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
|
||||
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
|
||||
|
||||
if (hasFinal && !isStale) continue;
|
||||
if (mergeGate.hasRecentClaudeTrigger(comments, 10)) {
|
||||
core.info(`PR #${pr.number}: @claude within 10min — skip`);
|
||||
continue;
|
||||
}
|
||||
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr.number)) {
|
||||
core.info(`PR #${pr.number}: Claude in progress — skip`);
|
||||
continue;
|
||||
}
|
||||
if (hasFinal && isStale) {
|
||||
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt);
|
||||
if (gate.skip) {
|
||||
core.info(`PR #${pr.number}: skip stale — ${gate.reason}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await chain.maybeTriggerClaudeFinal(
|
||||
github, owner, repo, pr.number,
|
||||
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core,
|
||||
{ skipCopilot: true, optionalWaitMs: 0 }
|
||||
);
|
||||
if (result.posted) {
|
||||
posted++;
|
||||
core.info(`Posted Claude FINAL on PR #${pr.number}`);
|
||||
}
|
||||
if (posted >= 10) break;
|
||||
}
|
||||
core.info(`Missing/stale Claude FINAL sweep complete (${posted} posted)`);
|
||||
|
||||
# ================================================================
|
||||
# BOT PRs: kick CodeRabbit + Qodo (Greptile may follow on its own)
|
||||
# BOT 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: |
|
||||
@@ -266,29 +488,38 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Trigger CodeRabbit and Qodo reviews
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
|
||||
const pr = context.payload.pull_request.number;
|
||||
await chain.kickReviewChain(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
- name: Sync pipeline status labels
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const pr = context.payload.pull_request.number;
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
// 1. Trigger CodeRabbit review
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr, owner, repo,
|
||||
body: '@coderabbitai review'
|
||||
});
|
||||
console.log(`Triggered CodeRabbit for bot PR #${pr}`);
|
||||
|
||||
// 2. Trigger Qodo review
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr, owner, repo,
|
||||
body: '/review'
|
||||
});
|
||||
console.log(`Triggered Qodo for bot PR #${pr}`);
|
||||
|
||||
// 3. Trigger Gemini review (commented out)
|
||||
// await github.rest.issues.createComment({
|
||||
// issue_number: pr, owner, repo,
|
||||
// body: '@gemini review this PR'
|
||||
// });
|
||||
// console.log(`Triggered Gemini for bot PR #${pr}`);
|
||||
|
||||
// NOTE: @copilot is NOT triggered here.
|
||||
// It will be triggered by copilot-after-coderabbit or copilot-after-qodo
|
||||
// once those bots finish, ensuring Copilot always reviews LAST.
|
||||
console.log('Copilot will be triggered after CodeRabbit/Qodo complete.');
|
||||
@@ -1,43 +0,0 @@
|
||||
name: Bot PR Recovery
|
||||
|
||||
# Belt-and-braces: kick CodeRabbit/Qodo when pull_request:opened never fired for bot PRs.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/15 * * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Specific PR number to kick (optional)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
recover:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Find and kick stalled bot PRs
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER_INPUT: ${{ github.event.inputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const manual = (process.env.PR_NUMBER_INPUT || '').trim();
|
||||
const prNumber = manual ? parseInt(manual, 10) : null;
|
||||
if (manual && Number.isNaN(prNumber)) {
|
||||
throw new Error(`Invalid pr_number: ${manual}`);
|
||||
}
|
||||
await chain.recoverStalledBotPrs(github, owner, repo, { prNumber }, core);
|
||||
@@ -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();
|
||||
@@ -1,627 +0,0 @@
|
||||
name: Claude PR Merge Gate
|
||||
|
||||
# Read-only Claude assesses merge readiness; GH_TOKEN merges (no @claude, no gh pr review).
|
||||
# Does not overlap with merge-conflict-claude.yml or auto-pr-comment @claude chains.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Scan for merge-ready PRs every 10 minutes (assess runs only when candidates exist).
|
||||
- cron: '*/10 * * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Optional PR number (scan all open internal PRs if empty)'
|
||||
required: false
|
||||
type: string
|
||||
merge_method:
|
||||
description: 'Merge method'
|
||||
required: false
|
||||
type: choice
|
||||
default: merge
|
||||
options:
|
||||
- merge
|
||||
- squash
|
||||
- rebase
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Claude Assistant
|
||||
- Core Tests
|
||||
types:
|
||||
- completed
|
||||
repository_dispatch:
|
||||
types:
|
||||
- claude-merge-gate
|
||||
|
||||
concurrency:
|
||||
# Serialize workflow_run auto-dispatches (Core Tests / Claude Assistant fan-out).
|
||||
group: claude-merge-gate-${{ github.repository }}-${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || (github.event_name == 'workflow_run' && 'auto-dispatch') || 'scan' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
MAX_CANDIDATES: 3
|
||||
DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
auto-dispatch:
|
||||
if: github.event_name == 'workflow_run'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
actions: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Dispatch merge gate when PR pipeline is ready
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const wr = context.payload.workflow_run;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
if (wr.conclusion !== 'success') {
|
||||
core.info(`Skip dispatch: ${wr.name} conclusion=${wr.conclusion}`);
|
||||
return;
|
||||
}
|
||||
const allowed = ['Claude Assistant', 'Core Tests'];
|
||||
if (!allowed.includes(wr.name)) {
|
||||
core.info(`Skip dispatch: workflow ${wr.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const prNumber = await mergeGate.resolvePrNumberFromWorkflowRun(github, owner, repo, wr);
|
||||
if (!prNumber) {
|
||||
core.info('Skip dispatch: could not resolve PR from workflow run');
|
||||
return;
|
||||
}
|
||||
|
||||
const fromClaude = wr.name === 'Claude Assistant';
|
||||
const check = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core, {
|
||||
skipGlobalClaudeRunCheck: fromClaude,
|
||||
skipRecentClaudeCooldown: fromClaude,
|
||||
}
|
||||
);
|
||||
if (!check.ready) {
|
||||
core.info(`PR #${prNumber} not ready: ${check.reasons.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await mergeGate.shouldSkipMergeGateDispatch(github, owner, repo, prNumber, core)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner,
|
||||
repo,
|
||||
event_type: 'claude-merge-gate',
|
||||
client_payload: { pr_number: prNumber },
|
||||
});
|
||||
core.info(`Dispatched merge gate for PR #${prNumber} after ${wr.name}`);
|
||||
|
||||
scan-candidates:
|
||||
if: github.event_name != 'workflow_run'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: write
|
||||
actions: read
|
||||
id-token: write
|
||||
outputs:
|
||||
matrix: ${{ steps.scan.outputs.matrix }}
|
||||
has_candidates: ${{ steps.scan.outputs.has_candidates }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Scan open PRs for merge gate eligibility
|
||||
id: scan
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const maxCandidates = parseInt(process.env.MAX_CANDIDATES || '5', 10);
|
||||
const inputPr = String(process.env.DISPATCH_PR_NUMBER || '').trim();
|
||||
|
||||
const candidates = [];
|
||||
const skipped = [];
|
||||
|
||||
if (inputPr) {
|
||||
const n = parseInt(inputPr, 10);
|
||||
if (Number.isNaN(n)) {
|
||||
throw new Error(`Invalid pr_number input: ${inputPr}`);
|
||||
}
|
||||
const result = await mergeGate.evaluatePipelineQuiescent(github, owner, repo, n, core);
|
||||
if (result.ready) {
|
||||
candidates.push({ pr_number: n, head_sha: result.headSha });
|
||||
} else {
|
||||
skipped.push({ pr: n, reasons: result.reasons });
|
||||
}
|
||||
} else {
|
||||
const prNumbers = await mergeGate.listPrNumbersForMergeGateScan(
|
||||
github, owner, repo, core
|
||||
);
|
||||
const selected = await mergeGate.selectMergeGateCandidates(
|
||||
github, owner, repo, prNumbers, maxCandidates, core
|
||||
);
|
||||
candidates.push(...selected.candidates);
|
||||
skipped.push(...selected.skipped);
|
||||
}
|
||||
|
||||
core.info('Skipped: ' + JSON.stringify(skipped));
|
||||
core.setOutput('matrix', JSON.stringify({ include: candidates }));
|
||||
core.setOutput('has_candidates', candidates.length > 0 ? 'true' : 'false');
|
||||
|
||||
core.summary.addRaw('## Merge gate scan\n');
|
||||
core.summary.addRaw(`Candidates: ${candidates.length}\n`);
|
||||
core.summary.addRaw('```json\n' + JSON.stringify({ candidates, skipped }, null, 2) + '\n```');
|
||||
await core.summary.write();
|
||||
|
||||
if (inputPr && skipped.length === 1 && skipped[0].pr === parseInt(inputPr, 10)) {
|
||||
const reasons = skipped[0].reasons;
|
||||
const body = [
|
||||
'**Merge gate scan** — not eligible for auto-merge.',
|
||||
'',
|
||||
...reasons.map((r) => `- \`${r}\``),
|
||||
'',
|
||||
'**Actions:** wait for CI and the Claude review chain, or add label `needs-manual-review` and merge manually.',
|
||||
'**Opt out:** label `no-auto-merge`.',
|
||||
].join('\n');
|
||||
const ctx = await mergeGate.loadPrContext(github, owner, repo, skipped[0].pr);
|
||||
if (!mergeGate.hasRecentMergeGateScanComment(ctx.comments)) {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: skipped[0].pr, body,
|
||||
});
|
||||
}
|
||||
} else if (inputPr && candidates.length === 1) {
|
||||
const prNum = candidates[0].pr_number;
|
||||
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNum);
|
||||
if (!mergeGate.hasRecentMergeGateScanComment(ctx.comments)) {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: prNum,
|
||||
body: '**Merge gate scan** — eligible for assessment. Claude merge gate will assess and may auto-merge on an APPROVE verdict.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await ps.syncOpenPullRequests(
|
||||
github, owner, repo, { maxPrs: 10, dispatchMergeGate: false }, core
|
||||
);
|
||||
|
||||
claude-assess:
|
||||
needs: scan-candidates
|
||||
if: needs.scan-candidates.outputs.has_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.scan-candidates.outputs.matrix) }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: write
|
||||
actions: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Mark merge gate active
|
||||
id: mark_active
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
core.setOutput('started_at', new Date().toISOString());
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: ${{ matrix.pr_number }},
|
||||
labels: ['claude-merge-gate-active'],
|
||||
});
|
||||
|
||||
- name: Stash merge gate helpers
|
||||
run: cp .github/scripts/merge-gate.js /tmp/merge-gate.js
|
||||
|
||||
- name: Fetch PR branch
|
||||
env:
|
||||
PR_NUMBER: ${{ matrix.pr_number }}
|
||||
run: |
|
||||
git fetch origin "pull/${PR_NUMBER}/head:pr-${PR_NUMBER}"
|
||||
git checkout "pr-${PR_NUMBER}"
|
||||
|
||||
- name: Claude merge gate assessment (read-only)
|
||||
id: assess
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/claude-code-action
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CLAUDE_CODE_SUBAGENT_MODEL: inherit
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4-6
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
model: claude-opus-4-8
|
||||
trigger_phrase: '@claude-merge-gate-internal'
|
||||
direct_prompt: |
|
||||
You are the MERGE GATE reviewer for PraisonAI PR #${{ matrix.pr_number }}.
|
||||
READ-ONLY: do not modify code, commit, push, rebase, or create branches.
|
||||
|
||||
CRITICAL OUTPUT RULE:
|
||||
Post exactly one PR comment via GitHub MCP containing either:
|
||||
MERGE_GATE_VERDICT: APPROVE
|
||||
or
|
||||
MERGE_GATE_VERDICT: BLOCK
|
||||
followed by a short rationale on the next lines.
|
||||
|
||||
NEVER include @claude in any comment. NEVER mention merge conflict resolution.
|
||||
|
||||
Review checklist:
|
||||
1. Read AGENTS.md and the PR diff on the current branch.
|
||||
2. Read all PR comments and reviews (FINAL architecture reviewer / Lead Engineer must have run).
|
||||
3. Confirm SDK value gate: change strengthens the relevant SDK (Python: praisonaiagents/praisonai; TypeScript: src/praisonai-ts/) — 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. FINAL architecture review must include the Phase 1 table with an explicit **SDK value** row.
|
||||
4. Repo routing: BLOCK if agent-callable tools (→ PraisonAI-Tools), lifecycle plugins (→ PraisonAI-Plugins), or optional sandbox backends (→ PraisonAI-Plugins via `praisonai.sandbox`) were added to praisonaiagents/ instead of the external repo.
|
||||
5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
|
||||
6. If agent.py adds >100 lines or new Agent __init__ params, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
|
||||
7. BLOCK if labels security/breaking-change/needs-manual-review/release, sensitive paths (.github/workflows, auth, pyproject.toml), secrets in diff, PR >800 lines or >30 files, or SDK changes without tests. Label `merge-gate-ci-only` exempts CI-only changes under `.github/workflows/`, `.github/actions/`, or merge-gate scripts.
|
||||
8. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
|
||||
9. If ready to merge: MERGE_GATE_VERDICT: APPROVE. Otherwise: MERGE_GATE_VERDICT: BLOCK with reasons.
|
||||
|
||||
Do not approve or merge via GitHub UI/API — only post the verdict comment.
|
||||
disallowed_tools: |
|
||||
Bash
|
||||
Edit
|
||||
Write
|
||||
Replace
|
||||
allowed_tools: |
|
||||
mcp__github__add_issue_comment
|
||||
mcp__github__get_pull_request
|
||||
mcp__github__get_pull_request_files
|
||||
mcp__github__get_pull_request_reviews
|
||||
mcp__github__get_pull_request_status
|
||||
mcp__github__get_issue
|
||||
mcp__github__get_issue_comments
|
||||
Glob
|
||||
Grep
|
||||
Read
|
||||
timeout_minutes: 15
|
||||
|
||||
- name: Post Opus verdict from assess output if MCP missed
|
||||
id: relay_verdict
|
||||
if: always() && steps.assess.conclusion != 'skipped'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const mergeGate = require('/tmp/merge-gate.js');
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = ${{ matrix.pr_number }};
|
||||
|
||||
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
|
||||
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
|
||||
const existing = mergeGate.findMergeGateVerdict(
|
||||
ctx.comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
|
||||
);
|
||||
if (existing) {
|
||||
core.info(`Opus verdict already on PR: ${existing}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const outPath = `${process.env.GITHUB_WORKSPACE}/.claude-assess-output.jsonl`;
|
||||
if (!fs.existsSync(outPath)) {
|
||||
core.info('No assess output file to relay');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = fs.readFileSync(outPath, 'utf8').split('\n').filter(Boolean);
|
||||
let verdict = null;
|
||||
let rationale = '';
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
if (line.includes('MERGE_GATE_VERDICT: APPROVE')) {
|
||||
verdict = 'APPROVE';
|
||||
rationale = line;
|
||||
break;
|
||||
}
|
||||
if (line.includes('MERGE_GATE_VERDICT: BLOCK')) {
|
||||
verdict = 'BLOCK';
|
||||
rationale = line;
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const evt = JSON.parse(line);
|
||||
const text = JSON.stringify(evt);
|
||||
if (text.includes('MERGE_GATE_VERDICT: APPROVE')) {
|
||||
verdict = 'APPROVE';
|
||||
rationale = text;
|
||||
break;
|
||||
}
|
||||
if (text.includes('MERGE_GATE_VERDICT: BLOCK')) {
|
||||
verdict = 'BLOCK';
|
||||
rationale = text;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Not JSON — keep scanning.
|
||||
}
|
||||
}
|
||||
if (!verdict) {
|
||||
core.info('Assess output had no MERGE_GATE_VERDICT to relay');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = [
|
||||
`MERGE_GATE_VERDICT: ${verdict}`,
|
||||
'',
|
||||
'Relayed from Opus merge gate assess output (GitHub MCP comment post was unavailable).',
|
||||
].join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: prNumber, body,
|
||||
});
|
||||
core.info(`Relayed Opus verdict to PR comment: ${verdict}`);
|
||||
|
||||
- name: Post fallback verdict if Opus did not comment
|
||||
id: fallback_verdict
|
||||
if: always() && steps.assess.conclusion != 'skipped'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const mergeGate = require('/tmp/merge-gate.js');
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = ${{ matrix.pr_number }};
|
||||
|
||||
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
|
||||
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
|
||||
const existing = mergeGate.findMergeGateVerdict(
|
||||
ctx.comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
|
||||
);
|
||||
if (existing) {
|
||||
core.info(`Verdict already posted: ${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: ${verdict}`,
|
||||
'',
|
||||
'Automated fallback — Opus merge gate did not post a PR comment.',
|
||||
];
|
||||
if (check.ready) {
|
||||
lines.push('All deterministic merge gates passed.');
|
||||
} else {
|
||||
lines.push('Blockers:', ...check.reasons.map((r) => `- ${r}`));
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: prNumber,
|
||||
body: lines.join('\n'),
|
||||
});
|
||||
core.info(`Posted fallback verdict: ${verdict}`);
|
||||
|
||||
- name: Capture Opus MERGE_GATE_VERDICT
|
||||
id: capture_verdict
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const mergeGate = require('/tmp/merge-gate.js');
|
||||
const comments = await mergeGate.listAllComments(
|
||||
github, context.repo.owner, context.repo.repo, ${{ matrix.pr_number }}
|
||||
);
|
||||
const ctx = await mergeGate.loadPrContext(
|
||||
github, context.repo.owner, context.repo.repo, ${{ matrix.pr_number }}
|
||||
);
|
||||
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
|
||||
const verdict = mergeGate.findMergeGateVerdict(
|
||||
comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
|
||||
);
|
||||
core.setOutput('verdict', verdict || 'NONE');
|
||||
if (!verdict) {
|
||||
core.warning(
|
||||
'Opus MERGE_GATE_VERDICT not posted on PR (GitHub MCP tools may be blocked). Merge waits for APPROVE.'
|
||||
);
|
||||
} else {
|
||||
core.info(`Opus merge gate verdict: ${verdict}`);
|
||||
}
|
||||
|
||||
- name: Remove merge gate active label
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const prNumber = ${{ matrix.pr_number }};
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
name: 'claude-merge-gate-active',
|
||||
});
|
||||
core.info(`Removed claude-merge-gate-active from PR #${prNumber}`);
|
||||
} catch (e) {
|
||||
core.info(`Label claude-merge-gate-active already removed or missing on PR #${prNumber}`);
|
||||
}
|
||||
|
||||
merge-only:
|
||||
needs: [scan-candidates, claude-assess]
|
||||
if: |
|
||||
always() &&
|
||||
needs.scan-candidates.outputs.has_candidates == 'true' &&
|
||||
needs.claude-assess.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.scan-candidates.outputs.matrix) }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
actions: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Re-check gates and merge
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
MERGE_METHOD: ${{ github.event.inputs.merge_method || 'merge' }}
|
||||
SCAN_HEAD_SHA: ${{ matrix.head_sha }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = ${{ matrix.pr_number }};
|
||||
const scanHeadSha = process.env.SCAN_HEAD_SHA;
|
||||
|
||||
const removeLabels = async () => {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number: prNumber, name: 'claude-merge-gate-active',
|
||||
});
|
||||
} catch (e) {
|
||||
core.info('Label claude-merge-gate-active already removed or missing');
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
|
||||
const comments = ctx.comments;
|
||||
const verdictMin = new Date(Date.now() - 25 * 60 * 1000).toISOString();
|
||||
const verdict = mergeGate.findMergeGateVerdict(
|
||||
comments, verdictMin, ctx.headPushedAt, { excludeAutomatedFallback: true }
|
||||
);
|
||||
if (verdict !== 'APPROVE') {
|
||||
core.info(`Skip merge: verdict=${verdict || 'NONE'}`);
|
||||
await removeLabels();
|
||||
return;
|
||||
}
|
||||
|
||||
const check = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core, { forMergeStep: true }
|
||||
);
|
||||
if (!check.ready) {
|
||||
core.info(`Skip merge: ${check.reasons.join(', ')}`);
|
||||
await removeLabels();
|
||||
return;
|
||||
}
|
||||
|
||||
if (check.headSha !== scanHeadSha) {
|
||||
core.info(`Skip merge: HEAD moved ${scanHeadSha.slice(0, 7)} → ${check.headSha.slice(0, 7)}`);
|
||||
await removeLabels();
|
||||
return;
|
||||
}
|
||||
|
||||
const method = process.env.MERGE_METHOD || 'merge';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
let merged = false;
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
await github.rest.pulls.merge({
|
||||
owner, repo, pull_number: prNumber, merge_method: method,
|
||||
});
|
||||
merged = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
const msg = (e.message || '').toLowerCase();
|
||||
if (attempt < 3 && msg.includes('base branch was modified')) {
|
||||
core.info(`Merge attempt ${attempt} failed (base moved), retrying in 8s...`);
|
||||
await sleep(8000);
|
||||
const recheck = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core, { forMergeStep: true }
|
||||
);
|
||||
if (!recheck.ready) {
|
||||
core.info(`Skip merge retry: ${recheck.reasons.join(', ')}`);
|
||||
await removeLabels();
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!merged) {
|
||||
core.info('Skip merge: exhausted retries');
|
||||
await removeLabels();
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: prNumber, labels: ['auto-merged-by-gate'],
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: prNumber,
|
||||
body: [
|
||||
'Merged by **Claude PR merge gate** (`claude-merge-gate.yml`).',
|
||||
`Verdict: MERGE_GATE_VERDICT: APPROVE`,
|
||||
`SHA: \`${check.headSha.slice(0, 7)}\``,
|
||||
`Method: ${method}`,
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
core.info(`Merged PR #${prNumber}`);
|
||||
} finally {
|
||||
await removeLabels();
|
||||
}
|
||||
+56
-192
@@ -49,14 +49,12 @@ jobs:
|
||||
// --- Label triage ---
|
||||
const labels = [];
|
||||
|
||||
// Kind labels (bug before "how to" — bug reports often include "## How to fix")
|
||||
// Kind labels
|
||||
if (content.includes('feature request') || content.includes('[feature')) {
|
||||
labels.push('enhancement');
|
||||
} else if (!title.includes('[question]') && (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback'))) {
|
||||
} else if (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback')) {
|
||||
labels.push('bug');
|
||||
} else if (title.includes('[question]') || content.includes('how do i') || (content.includes('how to') && !content.includes('how to fix'))) {
|
||||
labels.push('question');
|
||||
} else if (content.includes('question')) {
|
||||
} else if (content.includes('question') || content.includes('how do i') || content.includes('how to')) {
|
||||
labels.push('question');
|
||||
}
|
||||
|
||||
@@ -98,22 +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). TypeScript/JavaScript SDK fixes land in `src/praisonai-ts/` in this repo.'
|
||||
'_A maintainer can trigger deeper analysis by commenting `@claude` on this issue._'
|
||||
].join('\n')
|
||||
});
|
||||
}
|
||||
|
||||
// --- Auto-trigger Claude for bug/enhancement reports ---
|
||||
const titleLower = (issue.title || '').toLowerCase();
|
||||
const bodyRaw = issue.body || '';
|
||||
const hasBugMarker = /#?\s*\[bug\]/i.test(issue.title || '') || /#?\s*\[bug\]/i.test(bodyRaw);
|
||||
const hasActionableTitle = /^(\[[^\]]+\]|bug:|fix[:(]|feat:|ux:|enhancement:)/i.test(titleLower);
|
||||
const isBugReport = labels.includes('bug') || hasBugMarker;
|
||||
const isEnhancement = labels.includes('enhancement') && !labels.includes('question');
|
||||
const autoClaude = isOwner || isBugReport || (hasActionableTitle && isEnhancement);
|
||||
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,
|
||||
@@ -123,43 +112,25 @@ 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 != 'pull_request_review' || contains(github.event.review.body, '@claude')) &&
|
||||
(github.event_name != 'pull_request_review_comment' || contains(github.event.comment.body, '@claude')) &&
|
||||
(github.event_name != 'issue_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
|
||||
@@ -180,67 +151,26 @@ jobs:
|
||||
id: check_fork
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const isPR = context.eventName === 'pull_request'
|
||||
|| context.eventName === 'pull_request_review'
|
||||
|| context.eventName === 'pull_request_review_comment'
|
||||
|| (context.payload.issue && context.payload.issue.pull_request);
|
||||
|
||||
if (!isPR) {
|
||||
core.setOutput('is_pr', 'false');
|
||||
core.setOutput('is_fork', 'false');
|
||||
core.setOutput('can_push_fork', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const prNumber = context.payload.pull_request
|
||||
? context.payload.pull_request.number
|
||||
: context.payload.issue.number;
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
let mergeState = '';
|
||||
try {
|
||||
const gql = await github.graphql(
|
||||
`query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) { mergeStateStatus }
|
||||
}
|
||||
}`,
|
||||
{ owner: context.repo.owner, repo: context.repo.repo, number: prNumber }
|
||||
);
|
||||
mergeState = (gql.repository.pullRequest.mergeStateStatus || '').toUpperCase();
|
||||
} catch (e) {
|
||||
core.warning(`GraphQL mergeStateStatus failed: ${e.message}`);
|
||||
}
|
||||
if (!mergeState) {
|
||||
mergeState = (pr.data.mergeable_state || pr.data.merge_state_status || '').toUpperCase();
|
||||
}
|
||||
|
||||
core.setOutput('is_pr', 'true');
|
||||
core.setOutput('pr_number', String(prNumber));
|
||||
core.setOutput('pr_branch', pr.data.head.ref);
|
||||
core.setOutput('head_repo', pr.data.head.repo.full_name);
|
||||
core.setOutput('merge_state', mergeState);
|
||||
|
||||
const baseRepo = `${context.repo.owner}/${context.repo.repo}`;
|
||||
const maintainerCanModify = pr.data.maintainer_can_modify === true;
|
||||
core.setOutput('maintainer_can_modify', String(maintainerCanModify));
|
||||
|
||||
if (pr.data.head.repo && pr.data.head.repo.full_name !== baseRepo) {
|
||||
core.setOutput('is_fork', 'true');
|
||||
core.setOutput('can_push_fork', String(maintainerCanModify));
|
||||
core.setOutput('fork_repo', pr.data.head.repo.full_name);
|
||||
core.setOutput('clone_url', pr.data.head.repo.clone_url);
|
||||
core.setOutput('branch', pr.data.head.ref);
|
||||
return;
|
||||
const isPR = context.eventName === 'pull_request' || (context.payload.issue && context.payload.issue.pull_request);
|
||||
if (isPR) {
|
||||
const prNumber = context.payload.pull_request ? context.payload.pull_request.number : context.payload.issue.number;
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
core.setOutput('pr_branch', pr.data.head.ref);
|
||||
|
||||
if (pr.data.head.repo && pr.data.head.repo.full_name !== context.repo.full_name) {
|
||||
core.setOutput('is_fork', 'true');
|
||||
core.setOutput('clone_url', pr.data.head.repo.clone_url);
|
||||
core.setOutput('branch', pr.data.head.ref);
|
||||
return;
|
||||
}
|
||||
}
|
||||
core.setOutput('is_fork', 'false');
|
||||
core.setOutput('can_push_fork', 'false');
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -250,40 +180,31 @@ jobs:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Fetch PR branch and Setup Remote
|
||||
if: steps.check_fork.outputs.is_pr == 'true'
|
||||
if: github.event.issue.pull_request
|
||||
env:
|
||||
PR_BRANCH: ${{ steps.check_fork.outputs.pr_branch }}
|
||||
PR_NUMBER: ${{ steps.check_fork.outputs.pr_number }}
|
||||
IS_FORK: ${{ steps.check_fork.outputs.is_fork }}
|
||||
CAN_PUSH_FORK: ${{ steps.check_fork.outputs.can_push_fork }}
|
||||
FORK_REPO: ${{ steps.check_fork.outputs.fork_repo }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
git fetch origin "pull/${PR_NUMBER}/head:${PR_BRANCH}"
|
||||
git checkout "${PR_BRANCH}"
|
||||
git fetch origin main
|
||||
|
||||
if [ "${IS_FORK}" = "true" ] && [ "${CAN_PUSH_FORK}" = "true" ]; then
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${FORK_REPO}.git"
|
||||
elif [ "${IS_FORK}" = "true" ]; then
|
||||
# Block push: trick claude-code-action's `git fetch origin <branch>`
|
||||
git remote set-url origin "file://$(pwd)"
|
||||
# Fetch PR head from base repo and put it into local branch
|
||||
git fetch origin pull/${{ github.event.issue.number }}/head:"${PR_BRANCH}"
|
||||
|
||||
# If it's a fork, make origin point to local to trick claude-code-action's `git fetch origin <branch>`
|
||||
if [ "${IS_FORK}" = "true" ]; then
|
||||
git remote set-url origin file://$(pwd)
|
||||
fi
|
||||
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.check_fork.outputs.can_push_fork == 'true' && secrets.GH_TOKEN || steps.app-token.outputs.token }}
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
with:
|
||||
allowed_bots: 'praisonai-triage-agent[bot]'
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
model: claude-opus-4-8
|
||||
trigger_phrase: "@claude"
|
||||
label_trigger: "claude"
|
||||
|
||||
direct_prompt: |
|
||||
${{ steps.check_fork.outputs.is_fork == 'true' && steps.check_fork.outputs.can_push_fork != 'true' && 'CRITICAL: THIS IS A PULL REQUEST FROM A FORK WITHOUT MAINTAINER EDITS. YOU DO NOT HAVE PUSH PERMISSIONS. ONLY READ FILES, VALIDATE CODE, AND PROVIDE COMMENTS/FEEDBACK ON THIS PR. DO NOT ATTEMPT TO PUSH OR CREATE PULL REQUESTS.\n\n' || (steps.check_fork.outputs.can_push_fork == 'true' && format('NOTE: Fork PR with maintainer edits allowed. Push directly to fork {0} branch {1}.\n\n', steps.check_fork.outputs.fork_repo, steps.check_fork.outputs.pr_branch) || (steps.check_fork.outputs.is_pr == 'true' && steps.check_fork.outputs.is_fork != 'true' && 'NOTE: The branch is under MervinPraison/PraisonAI (not a fork). You are able to make modifications and push directly to this branch.\n\n' || (steps.check_fork.outputs.is_pr != 'true' && 'NOTE: This is an issue (not a PR). Create a new fix branch per STEP 3 below.\n\n' || ''))) }}
|
||||
${{ steps.check_fork.outputs.is_pr == 'true' && format('CONTEXT: PR #{0} on branch {1}. head.repo={2}, is_fork={3}, can_push_fork={4}, merge_state={5}. NEVER treat cursor/*, claude/* branch names or bot authors as forks — only is_fork=true means fork.\n\n', steps.check_fork.outputs.pr_number, steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.head_repo, steps.check_fork.outputs.is_fork, steps.check_fork.outputs.can_push_fork, steps.check_fork.outputs.merge_state) || '' }}
|
||||
${{ steps.check_fork.outputs.is_fork == 'true' && 'CRITICAL: THIS IS A PULL REQUEST FROM A FORK. YOU DO NOT HAVE PUSH PERMISSIONS TO THIS REPOSITORY. ONLY READ FILES, VALIDATE CODE, AND PROVIDE YOUR COMMENTS/FEEDBACK DIRECTLY IN THIS PR VIA COMMENTS. DO NOT ATTEMPT TO PUSH OR CREATE PULL REQUESTS.\n\n' || 'NOTE: The branch is under MervinPraison/PraisonAI (not a fork). You are able to make modifications and push directly to this branch.\n\n' }}
|
||||
You are working on the PraisonAI SDK. Follow AGENTS.md strictly.
|
||||
|
||||
STEP 0 — SETUP GIT IDENTITY & AUTH (GLOBAL — required for all repos):
|
||||
@@ -294,31 +215,33 @@ jobs:
|
||||
|
||||
STEP 1 — READ GUIDELINES:
|
||||
Read AGENTS.md to understand the architecture rules.
|
||||
SCOPE: Python SDK (praisonaiagents, praisonai) and TypeScript SDK (src/praisonai-ts/) live in this monorepo. Do NOT modify src/praisonai-rust. For TS issues read src/praisonai-ts/AGENTS.md.
|
||||
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.
|
||||
SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify or touch praisonai-rust or praisonai-ts packages.
|
||||
|
||||
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.
|
||||
7. TypeScript/JavaScript SDK → src/praisonai-ts/ in this monorepo (not praisonaiagents/).
|
||||
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.
|
||||
- Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
|
||||
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory).
|
||||
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
|
||||
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
|
||||
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead. PRs with >100 lines added to agent.py require manual review (merge gate will not auto-merge).
|
||||
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead.
|
||||
- Is there a name collision with existing exports in __init__.py?
|
||||
If ANY of these conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
|
||||
|
||||
${{ steps.check_fork.outputs.is_pr == 'true' && (steps.check_fork.outputs.is_fork != 'true' || steps.check_fork.outputs.can_push_fork == 'true') && steps.check_fork.outputs.merge_state == 'DIRTY' && format('STEP 3 — RESOLVE MERGE CONFLICTS ON PR (branch: {0}, PR #{1}{2}):\n- DO NOT create a new branch or PR\n- git fetch origin main && git rebase origin/main\n- Resolve conflicts: keep this PR intent, merge in main newer logic\n- git add -A && git rebase --continue (repeat until done)\n- git push --force-with-lease origin {0}\n- Run targeted pytest on changed paths\n- Comment files resolved and what you skipped\n', steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.pr_number, steps.check_fork.outputs.can_push_fork == 'true' && format(', fork: {0}', steps.check_fork.outputs.fork_repo) || '') || '' }}
|
||||
${{ steps.check_fork.outputs.is_pr == 'true' && (steps.check_fork.outputs.is_fork != 'true' || steps.check_fork.outputs.can_push_fork == 'true') && steps.check_fork.outputs.merge_state != 'DIRTY' && format('STEP 3 — IMPLEMENT ON EXISTING PR (branch: {0}, PR #{1}{2}):\n- DO NOT create a new branch or PR\n- git checkout {0}\n- Implement minimal, focused fixes from reviewer feedback\n- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper\n- Keep changes small and backward-compatible\n- Commit: git add -A && git commit -m "fix: <description>"\n- Push: git push origin {0}\n- Comment a summary of exact files modified and what you skipped\n', steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.pr_number, steps.check_fork.outputs.can_push_fork == 'true' && format(', fork: {0}', steps.check_fork.outputs.fork_repo) || '') || '' }}
|
||||
${{ steps.check_fork.outputs.is_pr != 'true' && 'STEP 3 — IMPLEMENT (for changes in THIS repo — PraisonAI):\n- Create a fix branch: git checkout -b claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\n- Implement a minimal, focused fix\n- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper\n- Keep changes small and backward-compatible\n- Commit: git add -A && git commit -m "fix: <description> (fixes #$ISSUE_NUMBER)"\n- Push: git push origin claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\nCRITICAL: After pushing, you MUST immediately run: gh pr create --title "fix: <title>" --body "Fixes #$ISSUE_NUMBER" --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\nDo NOT stop after pushing. Do NOT say "manually create a PR". Always create the PR automatically.\n' || '' }}
|
||||
STEP 3 — IMPLEMENT (for changes in THIS repo — PraisonAI):
|
||||
- Create a fix branch: git checkout -b claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
- Implement a minimal, focused fix
|
||||
- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper
|
||||
- Keep changes small and backward-compatible
|
||||
- Commit: git add -A && git commit -m "fix: <description> (fixes #$ISSUE_NUMBER)"
|
||||
- Push: git push origin claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
CRITICAL: After pushing, you MUST immediately run: gh pr create --title "fix: <title>" --body "Fixes #$ISSUE_NUMBER" --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
Do NOT stop after pushing. Do NOT say "manually create a PR". Always create the PR automatically.
|
||||
|
||||
STEP 3-ALT — IMPLEMENT IN EXTERNAL REPO (for PraisonAI-Tools, PraisonAIDocs, PraisonAI-Plugins):
|
||||
When work must happen in a different repository, follow these steps EXACTLY. Do NOT attempt to use `cd`, as directory state is not preserved. Use `git -C` for all commands.
|
||||
@@ -338,15 +261,16 @@ jobs:
|
||||
gh pr create -R MervinPraison/<REPO_NAME> --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d) --title "feat: <title>" --body "Fixes MervinPraison/PraisonAI#$ISSUE_NUMBER
|
||||
|
||||
<body>"
|
||||
h) Comment on THIS PraisonAI issue with the external PR URL.
|
||||
|
||||
STEP 4 — TEST:
|
||||
- For Python SDK changes: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
|
||||
- For TypeScript SDK changes: cd src/praisonai-ts && npm install && npm run build && npm test
|
||||
- For SDK changes: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
|
||||
- For docs changes: verify files exist and are valid markdown/MDX
|
||||
- Ensure no regressions
|
||||
|
||||
${{ steps.check_fork.outputs.is_pr == 'true' && 'STEP 5 — SKIP: Already on an open PR. Do NOT run gh pr create.\n' || 'STEP 5 — CREATE PR:\n- Commit with descriptive message, push, and create PR using `gh pr create`\n- For external repos, use `gh pr create -R MervinPraison/<REPO_NAME>`\nCRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link or say "manual push required".\n' }}
|
||||
STEP 5 — CREATE PR:
|
||||
- Commit with descriptive message, push, and create PR using `gh pr create`
|
||||
- For external repos, use `gh pr create -R MervinPraison/<REPO_NAME>`
|
||||
CRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link or say "manual push required".
|
||||
NOTE: If you worked on an external repo, the wrapper action will report "No commits" for the main repo at the end. This is expected, do not worry about it.
|
||||
allowed_tools: |
|
||||
Bash(git:*)
|
||||
@@ -355,8 +279,6 @@ jobs:
|
||||
Bash(conda:*)
|
||||
Bash(pytest:*)
|
||||
Bash(gh:*)
|
||||
Bash(npm:*)
|
||||
Bash(node:*)
|
||||
Bash(python -m pytest:*)
|
||||
Bash(python -m pip:*)
|
||||
Bash(poetry:*)
|
||||
@@ -370,61 +292,3 @@ jobs:
|
||||
mcp__github__get_issue_comments
|
||||
mcp__github__update_issue
|
||||
timeout_minutes: 30
|
||||
|
||||
- name: Kick review chain for bot-created PR
|
||||
if: |
|
||||
steps.check_fork.outputs.is_pr != 'true' &&
|
||||
(github.event_name == 'issues' || github.event_name == 'issue_comment') &&
|
||||
github.event.issue != null
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
|
||||
const issueNumber = context.payload.issue.number;
|
||||
await chain.kickReviewChainForIssue(
|
||||
github, context.repo.owner, context.repo.repo, issueNumber, core
|
||||
);
|
||||
|
||||
- name: Sync pipeline status labels for PR
|
||||
if: steps.check_fork.outputs.is_pr == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const prNumber = parseInt('${{ steps.check_fork.outputs.pr_number }}', 10);
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, prNumber, core);
|
||||
|
||||
- name: Dispatch merge gate when PR pipeline ready
|
||||
if: steps.check_fork.outputs.is_pr == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = parseInt('${{ steps.check_fork.outputs.pr_number }}', 10);
|
||||
const check = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core, {
|
||||
skipGlobalClaudeRunCheck: true,
|
||||
skipRecentClaudeCooldown: true,
|
||||
}
|
||||
);
|
||||
if (!check.ready) {
|
||||
core.info(`PR #${prNumber} not ready for merge gate: ${check.reasons.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner,
|
||||
repo,
|
||||
event_type: 'claude-merge-gate',
|
||||
client_payload: { pr_number: prNumber },
|
||||
});
|
||||
core.info(`Dispatched merge gate for PR #${prNumber}`);
|
||||
@@ -1,77 +0,0 @@
|
||||
name: Deploy install scripts to praison.ai
|
||||
|
||||
# Production praison.ai runs on Hestia (migration-target), not Azure AKS.
|
||||
# Uses scoped deploy user (praison-deploy) — not root.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'src/praisonai/scripts/install.sh'
|
||||
- 'src/praisonai/scripts/install.ps1'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-install-scripts
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-to-hestia:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production-hestia
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sync install scripts to Hestia
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.MIGRATION_TARGET_SSH_KEY }}
|
||||
HESTIA_HOST: ${{ secrets.MIGRATION_TARGET_HOST }}
|
||||
HESTIA_USER: ${{ secrets.MIGRATION_TARGET_USER }}
|
||||
HESTIA_KNOWN_HOSTS: ${{ secrets.MIGRATION_TARGET_KNOWN_HOSTS }}
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for var in SSH_PRIVATE_KEY HESTIA_HOST HESTIA_USER; do
|
||||
if [ -z "${!var:-}" ]; then
|
||||
echo "Missing secret for deploy (need MIGRATION_TARGET_SSH_KEY, MIGRATION_TARGET_HOST, MIGRATION_TARGET_USER)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
REMOTE_WEB="/home/hestiaadmin/web/praison.ai/pa/web"
|
||||
SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=yes"
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
trap 'rm -f ~/.ssh/id_ed25519' EXIT
|
||||
|
||||
# Pin host identity via an out-of-band secret (no TOFU/ssh-keyscan).
|
||||
if [ -z "${HESTIA_KNOWN_HOSTS:-}" ]; then
|
||||
echo "Missing secret MIGRATION_TARGET_KNOWN_HOSTS (pinned host key). Refusing to deploy without a verified host identity."
|
||||
exit 1
|
||||
fi
|
||||
echo "$HESTIA_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
chmod 644 ~/.ssh/known_hosts
|
||||
|
||||
# Upload to temp files, then atomically move both into place in a
|
||||
# single remote step to avoid serving truncated or mixed-commit files.
|
||||
for f in install.sh install.ps1; do
|
||||
echo "Syncing $f..."
|
||||
scp $SSH_OPTS "src/praisonai/scripts/$f" "${HESTIA_USER}@${HESTIA_HOST}:${REMOTE_WEB}/.${f}.tmp"
|
||||
done
|
||||
|
||||
ssh $SSH_OPTS "${HESTIA_USER}@${HESTIA_HOST}" \
|
||||
"mv -f ${REMOTE_WEB}/.install.sh.tmp ${REMOTE_WEB}/install.sh && \
|
||||
mv -f ${REMOTE_WEB}/.install.ps1.tmp ${REMOTE_WEB}/install.ps1 && \
|
||||
sudo chown hestiaadmin:hestiaadmin ${REMOTE_WEB}/install.sh ${REMOTE_WEB}/install.ps1 && \
|
||||
sudo chmod 644 ${REMOTE_WEB}/install.sh ${REMOTE_WEB}/install.ps1"
|
||||
|
||||
echo "Deployed install scripts for commit ${SOURCE_SHA}"
|
||||
echo "Verify: curl -fsSL https://praison.ai/install.ps1 | head -5"
|
||||
@@ -1,267 +0,0 @@
|
||||
# Build the desktop app for macOS, Windows and Linux, and attach it to the
|
||||
# release.
|
||||
#
|
||||
# Releases carried no assets at all -- v4.7.1 shipped zero -- so there was
|
||||
# nothing for anyone to download. The macOS half of this shipped first; this
|
||||
# adds the other two, which were not merely missing from CI but impossible:
|
||||
# the bundle targets were `["app", "dmg"]`, both macOS-only, so a Windows or
|
||||
# Linux build produced zero artifacts and still exited zero.
|
||||
#
|
||||
# Unsigned, deliberately, on every platform. macOS signing needs a Developer ID
|
||||
# and Windows signing now needs a hardware-backed certificate or a hosted
|
||||
# signing service -- neither is in secrets. Shipping builds people can open
|
||||
# past the warning is better than shipping none, and the release notes say so
|
||||
# rather than letting Gatekeeper and SmartScreen say it for us.
|
||||
name: Desktop release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Existing release tag to attach the build to'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
# Queue rather than cancel: a cancelled release build looks identical to a
|
||||
# release that simply never produced assets, which is the bug being fixed.
|
||||
group: desktop-release-${{ github.event.release.tag_name || inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# macos-15, not macos-14: that image entered deprecation on
|
||||
# 2026-07-06 and is fully unsupported from 2026-11-02, with brownout
|
||||
# periods failing jobs before then. This is the only Apple-silicon
|
||||
# leg, so losing it means no arm64 DMG at all.
|
||||
- runner: macos-15 # Apple silicon
|
||||
target: aarch64-apple-darwin
|
||||
bundles: dmg
|
||||
ext: dmg
|
||||
asset: macos-apple-silicon
|
||||
# macos-15-intel, not macos-13: that image was retired on
|
||||
# 2025-12-04, so the leg would never schedule and the verify job
|
||||
# below would correctly report a missing asset on every release,
|
||||
# forever. macos-15-intel is GitHub's named x86_64 replacement.
|
||||
- runner: macos-15-intel # Intel
|
||||
target: x86_64-apple-darwin
|
||||
bundles: dmg
|
||||
ext: dmg
|
||||
asset: macos-intel
|
||||
- runner: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
bundles: nsis
|
||||
ext: exe
|
||||
asset: windows-x64-setup
|
||||
# ubuntu-22.04 rather than -latest on purpose: glibc is forward
|
||||
# compatible only, so the build host sets the *oldest* distribution
|
||||
# the .deb can run on. Building on 24.04 would produce a package that
|
||||
# refuses to start on 22.04 and Debian 12 with a "GLIBC_2.38 not
|
||||
# found" that no runtime flag can fix.
|
||||
#
|
||||
# This image begins deprecation on 2026-09-17 and is retired on
|
||||
# 2027-04-17. When it goes, move to `ubuntu-latest` with
|
||||
# `container: debian:12` rather than bumping the runner -- that keeps
|
||||
# the glibc floor decided by us instead of by GitHub's image
|
||||
# lifecycle.
|
||||
- runner: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
bundles: deb
|
||||
ext: deb
|
||||
asset: linux-x64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install the Linux build dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Retried: a degraded mirror otherwise burns the whole job and
|
||||
# reports "cancelled", which reads as our failure rather than apt's.
|
||||
for attempt in 1 2 3; do
|
||||
sudo apt-get update && break || sleep 15
|
||||
done
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
patchelf \
|
||||
xdg-utils
|
||||
# libappindicator3-dev, not the ayatana package: the .deb's generated
|
||||
# Depends: follows whichever one the binary linked against, so the
|
||||
# two must not be mixed between builds.
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src/praisonai-desktop/src-tauri -> target
|
||||
# Saving the Windows target directory costs more minutes than the
|
||||
# rebuild it saves.
|
||||
save-if: ${{ runner.os != 'Windows' }}
|
||||
|
||||
- name: Install the Tauri CLI
|
||||
shell: bash
|
||||
# `|| true` swallowed a genuine install failure and the next step then
|
||||
# died with an unrelated "no such command: tauri". It also behaved
|
||||
# differently per leg, since Windows defaults to pwsh where `true` is
|
||||
# not a command. Skip the install if it is already there; otherwise
|
||||
# let a failure be a failure.
|
||||
run: command -v cargo-tauri >/dev/null || cargo install tauri-cli --version '^2' --locked
|
||||
|
||||
- name: Set the desktop version from the release tag
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
||||
run: node src/praisonai-desktop/tools/set-release-version.mjs "$RELEASE_TAG"
|
||||
|
||||
- name: Build the bundle
|
||||
working-directory: src/praisonai-desktop/src-tauri
|
||||
run: cargo tauri build --target ${{ matrix.target }} --bundles ${{ matrix.bundles }}
|
||||
|
||||
- name: The Windows binary must carry the release version
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
working-directory: src/praisonai-desktop/src-tauri
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
||||
run: |
|
||||
$expected = $env:RELEASE_TAG -replace '^v', ''
|
||||
$core = ($expected -split '[-+]')[0]
|
||||
$exe = "target/${{ matrix.target }}/release/praisonai-desktop.exe"
|
||||
$version = (Get-Item $exe).VersionInfo
|
||||
Write-Host "ProductVersion=$($version.ProductVersion) FileVersion=$($version.FileVersion)"
|
||||
|
||||
if ($version.ProductVersion -ne $expected) {
|
||||
throw "ProductVersion $($version.ProductVersion) does not match $expected"
|
||||
}
|
||||
|
||||
# Compare the three components that carry meaning, not the string.
|
||||
#
|
||||
# This gate blocked the v4.7.4 Windows build with
|
||||
# FileVersion 4.7.4 does not match 4.7.4.0
|
||||
# It appended ".0" and compared literally, but Windows reports what
|
||||
# was stamped -- three parts here. The stamping was correct and the
|
||||
# check was not, so the release shipped with no Windows installer at
|
||||
# all: the one platform it was cut to fix.
|
||||
#
|
||||
# [version] normalises a missing fourth component to -1, so both
|
||||
# "4.7.4" and "4.7.4.0" compare equal on major.minor.build.
|
||||
$got = [version]$version.FileVersion
|
||||
$want = [version]$core
|
||||
if ($got.Major -ne $want.Major -or
|
||||
$got.Minor -ne $want.Minor -or
|
||||
$got.Build -ne $want.Build) {
|
||||
throw "FileVersion $($version.FileVersion) does not match $core"
|
||||
}
|
||||
|
||||
- name: The macOS bundle must carry a valid signature
|
||||
if: runner.os == 'macOS'
|
||||
working-directory: src/praisonai-desktop/src-tauri
|
||||
run: |
|
||||
set -euo pipefail
|
||||
dmg="$(find "target/${{ matrix.target }}/release/bundle" -name '*.dmg' -print -quit)"
|
||||
[ -n "$dmg" ] || { echo "no .dmg was produced"; exit 1; }
|
||||
|
||||
# Verify inside the DMG, not the .app beside it.
|
||||
#
|
||||
# Tauri deletes the app bundle once the DMG is built -- the build log
|
||||
# says so plainly ("Cleaning .../bundle/macos/PraisonAI.app") -- so a
|
||||
# gate that looked for the .app found nothing and failed every macOS
|
||||
# leg. Reading the DMG is also the better check: it is the artifact
|
||||
# people download, and the one that was broken.
|
||||
#
|
||||
# v4.7.2 shipped a DMG that macOS called "damaged and can't be
|
||||
# opened" on every Mac. The cause was not the missing Developer ID:
|
||||
# the only signature was the one rustc's linker puts on the bare
|
||||
# executable, whose CodeDirectory declares a resource seal that an
|
||||
# unsigned bundle does not have --
|
||||
#
|
||||
# Sealed Resources=none
|
||||
# code has no resources but signature indicates they must be present
|
||||
#
|
||||
# That is an *invalid* signature, not an unsigned one, and macOS
|
||||
# reports invalid as "damaged". Right-click -> Open cannot bypass it,
|
||||
# so the instructions shipped alongside it could not work either.
|
||||
mnt="$(mktemp -d)"
|
||||
hdiutil attach -nobrowse -readonly -quiet -mountpoint "$mnt" "$dmg"
|
||||
trap 'hdiutil detach "$mnt" -quiet || true' EXIT
|
||||
|
||||
app="$(find "$mnt" -maxdepth 1 -name '*.app' -print -quit)"
|
||||
[ -n "$app" ] || { echo "the DMG contains no .app"; ls -la "$mnt"; exit 1; }
|
||||
|
||||
echo "verifying $app"
|
||||
codesign --verify --deep --strict --verbose=2 "$app"
|
||||
# Printed, not asserted: --verify already fails on the exact bundle
|
||||
# that shipped, so a second check for the same thing would only be a
|
||||
# second way to say it. This is here to make the log readable.
|
||||
codesign -dv --verbose=2 "$app" 2>&1 | grep -E 'Sealed Resources|flags=' || true
|
||||
|
||||
- name: Name the artifact for the platform it runs on
|
||||
id: artifact
|
||||
shell: bash
|
||||
working-directory: src/praisonai-desktop/src-tauri
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${{ github.event.release.tag_name || inputs.tag }}"
|
||||
root="target/${{ matrix.target }}/release/bundle"
|
||||
found="$(find "$root" -name "*.${{ matrix.ext }}" -print -quit || true)"
|
||||
# Fail loudly rather than uploading nothing: an empty release asset
|
||||
# list is exactly the state this workflow exists to fix, and a bundler
|
||||
# given a target it does not support exits zero having written none.
|
||||
if [ -z "$found" ]; then
|
||||
echo "no .${{ matrix.ext }} was produced under $root"
|
||||
find "$root" -type f -print || echo "(the bundle directory does not exist)"
|
||||
exit 1
|
||||
fi
|
||||
out="PraisonAI-${tag}-${{ matrix.asset }}.${{ matrix.ext }}"
|
||||
mv "$found" "../$out"
|
||||
echo "path=src/praisonai-desktop/$out" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Attach it to the release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release upload \
|
||||
"${{ github.event.release.tag_name || inputs.tag }}" \
|
||||
"${{ steps.artifact.outputs.path }}" --clobber
|
||||
|
||||
verify:
|
||||
# A release with a missing platform is worse than an obviously failed
|
||||
# build, because nobody notices. This says which one is absent.
|
||||
needs: build
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check every platform produced an asset
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
assets="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--json assets -q '.assets[].name')"
|
||||
echo "$assets"
|
||||
missing=""
|
||||
for want in macos-apple-silicon macos-intel windows-x64-setup linux-x64; do
|
||||
echo "$assets" | grep -q -- "$want" || missing="$missing $want"
|
||||
done
|
||||
if [ -n "$missing" ]; then
|
||||
echo "::error::the release is missing:$missing"
|
||||
exit 1
|
||||
fi
|
||||
echo "all four platform builds are attached"
|
||||
@@ -1,115 +0,0 @@
|
||||
# The desktop app's tests.
|
||||
#
|
||||
# These existed and ran nowhere: no workflow referenced praisonai-desktop, and
|
||||
# the frontend suite could not even be installed -- jsdom was undeclared, so it
|
||||
# ran only on machines that happened to have one further up the tree, at
|
||||
# whatever version that happened to be.
|
||||
name: desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['src/praisonai-desktop/**', '.github/workflows/desktop.yml',
|
||||
'.github/workflows/desktop-release.yml']
|
||||
pull_request:
|
||||
paths: ['src/praisonai-desktop/**', '.github/workflows/desktop.yml',
|
||||
'.github/workflows/desktop-release.yml']
|
||||
|
||||
concurrency:
|
||||
group: desktop-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-desktop
|
||||
|
||||
jobs:
|
||||
engine:
|
||||
name: engine (${{ matrix.os }})
|
||||
# The engine is the part that has to behave the same on all three, which is
|
||||
# the entire point of the portability suite -- so it is run on all three.
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, macos-15, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Run the engine tests
|
||||
shell: bash
|
||||
env:
|
||||
# Never touch a real keychain on a shared runner.
|
||||
PRAISONAI_KEYCHAIN_SERVICE: ai.praison.desktop.ci
|
||||
run: |
|
||||
set -euo pipefail
|
||||
failed=0
|
||||
for suite in engine/test_*.py; do
|
||||
echo "::group::$suite"
|
||||
python "$suite" -v || failed=1
|
||||
echo "::endgroup::"
|
||||
done
|
||||
exit $failed
|
||||
|
||||
frontend:
|
||||
name: frontend
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- run: npm ci
|
||||
# `test:ci` is every suite except lifecycle, which drives real
|
||||
# conversations against a live engine and a real model -- neither of
|
||||
# which exists on a runner. It is excluded by name rather than left to
|
||||
# skip quietly, so that a skipped test here is a genuine problem.
|
||||
- name: Run the UI tests
|
||||
# A suite that skips everything still exits 0 -- that is how thirteen
|
||||
# tests came to look like a pass while proving nothing. So the skip
|
||||
# count is asserted, not just the exit status.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
set -o pipefail
|
||||
npm run test:ci 2>&1 | tee "$RUNNER_TEMP/ui.log"
|
||||
skipped=$(grep -E '^. skipped ' "$RUNNER_TEMP/ui.log" | grep -oE '[0-9]+$' | head -1)
|
||||
echo "skipped: ${skipped:-unknown}"
|
||||
test "${skipped:-1}" -eq 0
|
||||
|
||||
shell:
|
||||
name: shell (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, macos-15, windows-latest]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-desktop/src-tauri
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src/praisonai-desktop/src-tauri
|
||||
# The Windows cache is large and slow enough to cost more than it saves.
|
||||
save-if: ${{ runner.os != 'Windows' }}
|
||||
- name: Install the Linux build dependencies
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: .
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev \
|
||||
libxdo-dev libssl-dev patchelf
|
||||
- name: Test
|
||||
# Debug info is the bulk of a Tauri target directory and nothing here
|
||||
# reads a backtrace, so building without it keeps the runner's disk
|
||||
# from filling on the Windows leg.
|
||||
run: cargo test --config 'profile.test.debug=false' --config 'profile.dev.debug=false'
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
@@ -22,6 +22,8 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -36,6 +36,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Run Gemini Issue Triage
|
||||
|
||||
@@ -37,6 +37,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Set up Git
|
||||
|
||||
@@ -48,6 +48,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
ref: ${{ steps.checkout_ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -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: |
|
||||
@@ -66,16 +60,16 @@ jobs:
|
||||
- name: Run PraisonAI integration tests
|
||||
run: |
|
||||
cd PraisonAI/src/praisonai
|
||||
python -m pytest tests/integration/test_aiui_* -v
|
||||
python -m pytest tests/integration/test_aiui_* -v --timeout=300
|
||||
|
||||
- name: Run PraisonAIUI integration tests
|
||||
run: |
|
||||
cd PraisonAIUI
|
||||
if [ -f tests/integration/test_agentic_roundtrip.py ]; then
|
||||
python -m pytest tests/integration/test_agentic_roundtrip.py -v
|
||||
python -m pytest tests/integration/test_agentic_roundtrip.py -v --timeout=300
|
||||
fi
|
||||
if [ -f tests/test_feature_sdk_backends.py ]; then
|
||||
python -m pytest tests/test_feature_sdk_backends.py -v
|
||||
python -m pytest tests/test_feature_sdk_backends.py -v --timeout=300
|
||||
fi
|
||||
|
||||
- name: Test Pattern C CLI
|
||||
@@ -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
|
||||
@@ -159,7 +152,7 @@ jobs:
|
||||
run: |
|
||||
cd PraisonAI/src/praisonai
|
||||
if [ -n "$OPENAI_API_KEY" ]; then
|
||||
python -m pytest tests/integration/test_aiui_host_agentic.py -v
|
||||
python -m pytest tests/integration/test_aiui_host_agentic.py -v --timeout=600
|
||||
else
|
||||
echo "OPENAI_API_KEY not available, skipping agentic tests"
|
||||
fi
|
||||
@@ -1,185 +0,0 @@
|
||||
name: Auto Merge Conflict → Claude
|
||||
|
||||
# Detects internal PRs with mergeStateStatus=DIRTY and posts @claude to trigger claude.yml.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *'
|
||||
|
||||
concurrency:
|
||||
group: merge-conflict-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# PAT posts as MervinPraison so issue_comment triggers claude.yml (GITHUB_TOKEN cannot chain workflows)
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
detect-and-trigger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Detect conflicting PRs and trigger Claude
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ env.GH_TOKEN }}
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const baseRepo = `${owner}/${repo}`;
|
||||
const COOLDOWN_MS = 12 * 60 * 60 * 1000;
|
||||
const AUTO_ACTORS = ['github-actions[bot]', 'MervinPraison'];
|
||||
const LABEL = 'claude-conflict-pending';
|
||||
const COMMENT_BODY = [
|
||||
'@claude this PR has merge conflicts with `main`.',
|
||||
'Please rebase onto latest `main`, resolve conflicts (keep this PR\'s intent, merge in newer main logic),',
|
||||
'run targeted tests, and force-push with `--force-with-lease`.',
|
||||
'Comment which files you resolved.',
|
||||
'Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.',
|
||||
'Keep the resolution minimal — the aim is a lightweight and powerful package; do not introduce scope creep while resolving conflicts.',
|
||||
].join(' ');
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function getMergeState(prNumber) {
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
mergeStateStatus
|
||||
maintainerCanModify
|
||||
isDraft
|
||||
headRef { repository { nameWithOwner } }
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const result = await github.graphql(query, { owner, repo, number: prNumber });
|
||||
const prGql = result.repository.pullRequest;
|
||||
const status = (prGql?.mergeStateStatus || '').toUpperCase();
|
||||
if (status && status !== 'UNKNOWN') {
|
||||
return {
|
||||
status,
|
||||
isDraft: prGql.isDraft,
|
||||
headRepo: prGql.headRef?.repository?.nameWithOwner,
|
||||
maintainerCanModify: prGql.maintainerCanModify === true,
|
||||
};
|
||||
}
|
||||
if (attempt < 2) await sleep(10000);
|
||||
}
|
||||
const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
return {
|
||||
status: (pr.data.mergeable_state || '').toUpperCase(),
|
||||
isDraft: pr.data.draft,
|
||||
headRepo: pr.data.head.repo?.full_name,
|
||||
maintainerCanModify: pr.data.maintainer_can_modify === true,
|
||||
};
|
||||
}
|
||||
|
||||
function hasFinalClaudeReviewTrigger(comments) {
|
||||
return comments.some((c) => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!AUTO_ACTORS.includes(c.user.login)) return false;
|
||||
if (!body.includes('@claude')) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
}
|
||||
|
||||
function hasRecentAutoComment(comments) {
|
||||
const cutoff = Date.now() - COOLDOWN_MS;
|
||||
return comments.some((c) => {
|
||||
if (!AUTO_ACTORS.includes(c.user.login)) return false;
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !body.includes('merge conflict')) return false;
|
||||
return new Date(c.created_at).getTime() > cutoff;
|
||||
});
|
||||
}
|
||||
|
||||
async function processPR(prNumber) {
|
||||
const { status, isDraft, headRepo, maintainerCanModify } = await getMergeState(prNumber);
|
||||
core.info(`PR #${prNumber}: mergeState=${status}, head=${headRepo}, draft=${isDraft}, maintainerCanModify=${maintainerCanModify}`);
|
||||
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const labels = issue.labels.map((l) => l.name);
|
||||
|
||||
if (status !== 'DIRTY' && labels.includes(LABEL)) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
name: LABEL,
|
||||
});
|
||||
core.info(`Removed ${LABEL} from PR #${prNumber}`);
|
||||
}
|
||||
|
||||
if (status !== 'DIRTY') return { skipped: true, reason: 'not dirty' };
|
||||
if (isDraft) return { skipped: true, reason: 'draft' };
|
||||
if (headRepo && headRepo !== baseRepo && !maintainerCanModify) {
|
||||
return { skipped: true, reason: 'fork without maintainer edits' };
|
||||
}
|
||||
if (labels.includes(LABEL)) {
|
||||
return { skipped: true, reason: 'conflict resolution pending' };
|
||||
}
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
if (hasRecentAutoComment(comments)) {
|
||||
return { skipped: true, reason: 'recent auto comment' };
|
||||
}
|
||||
|
||||
if (!hasFinalClaudeReviewTrigger(comments)) {
|
||||
return { skipped: true, reason: 'awaiting final claude review trigger' };
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels: [LABEL],
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: COMMENT_BODY,
|
||||
});
|
||||
return { triggered: true };
|
||||
}
|
||||
|
||||
let prNumbers = [];
|
||||
if (context.eventName === 'pull_request') {
|
||||
prNumbers = [context.payload.pull_request.number];
|
||||
} else {
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
prNumbers = prs.map((p) => p.number);
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const num of prNumbers) {
|
||||
results.push({ pr: num, ...(await processPR(num)) });
|
||||
}
|
||||
core.summary.addRaw('```json\n' + JSON.stringify(results, null, 2) + '\n```');
|
||||
await core.summary.write();
|
||||
@@ -1,351 +0,0 @@
|
||||
# The mobile package's PLATFORM builds: a real iOS app and a real Android app.
|
||||
#
|
||||
# mobile.yml proves the crate type-checks for a phone (`cargo check` against
|
||||
# the two mobile targets). This proves the two generated platform projects,
|
||||
# src-tauri/gen/apple and src-tauri/gen/android, still turn that crate into an
|
||||
# installable app -- which is a different thing, and the thing that had never
|
||||
# happened: until #4673 neither project existed.
|
||||
#
|
||||
# What each job produces, and why that shape:
|
||||
#
|
||||
# ios-simulator -- an UNSIGNED arm64 simulator .app. A device build needs
|
||||
# an Apple Developer Team and a signing identity, which are
|
||||
# the maintainer's and not in this repo (bundle.iOS.
|
||||
# developmentTeam is unset in tauri.conf.json). A simulator
|
||||
# build needs neither, so it is the build that can run on
|
||||
# every PR. It is not decoration: it links the Rust
|
||||
# staticlib into the Xcode target, packages the webview
|
||||
# into the bundle and exercises Info.plist, i.e. every step
|
||||
# short of the certificate.
|
||||
# android-apk -- a debug APK, arm64-v8a. Debug because a release APK
|
||||
# needs a signing keystore, which is likewise the
|
||||
# maintainer's; the debug keystore is generated on the
|
||||
# runner. It installs on any arm64 Android 8.0+ device with
|
||||
# `adb install`.
|
||||
#
|
||||
# Both jobs fail if the thing they exist to produce is missing. The bundler
|
||||
# exits zero on a target it silently skipped (desktop-release.yml learned this
|
||||
# the hard way), so "the step passed" is not evidence; "the file exists and
|
||||
# contains the Rust library" is.
|
||||
#
|
||||
# The Xcode project is generated on the macOS runner the first time (this
|
||||
# laptop has no Xcode, only the Command Line Tools) and then committed, so
|
||||
# the init step is a no-op once gen/apple is in the tree. The Android project
|
||||
# was generated locally and is committed; the same guard keeps that job
|
||||
# honest if someone deletes it.
|
||||
name: mobile platforms
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths: &watched
|
||||
- 'src/praisonai-mobile/**'
|
||||
- 'src/praisonai-ts/src/**'
|
||||
- '.github/workflows/mobile-platforms.yml'
|
||||
# Without this, no platform build had ever run on main -- so a break
|
||||
# introduced by two PRs that were each green apart could sit on main
|
||||
# unnoticed. That is exactly what happened: #4737 branched from before
|
||||
# #4727 added the `file:` dependency, so the two were never installed
|
||||
# together until they had both landed.
|
||||
push:
|
||||
branches: [main]
|
||||
paths: *watched
|
||||
|
||||
concurrency:
|
||||
# Per-ref on a PR, per-SHA on main. This is the crux of the fix, not
|
||||
# cancel-in-progress alone: a concurrency group holds at most ONE
|
||||
# running run and ONE pending run, and a newer arrival always replaces
|
||||
# the pending one -- cancel-in-progress: false spares only the RUNNING
|
||||
# run, never the queued one. So with a single mobile-platforms-main
|
||||
# group, three merges in one ~3-minute window would still drop the
|
||||
# middle commit's run (it is only ever pending), which is precisely the
|
||||
# blindness the push trigger was added to remove. Giving each main SHA
|
||||
# its own group makes every commit a candidate release with its own
|
||||
# answer, with nothing to contend for and so nothing to cancel.
|
||||
group: mobile-platforms-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}
|
||||
# On a PR, superseding an in-flight build is right: only the tip
|
||||
# matters and these jobs cost ~3 minutes on two runners. On main each
|
||||
# group has a single run, so this is a no-op there -- left true because
|
||||
# the condition that mattered is now expressed in the group name above,
|
||||
# and this keeps the PR behaviour explicit.
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-mobile
|
||||
|
||||
jobs:
|
||||
ios-simulator:
|
||||
name: ios simulator (.app, unsigned)
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.18'
|
||||
cache: npm
|
||||
cache-dependency-path: src/praisonai-mobile/package-lock.json
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
# Both: `tauri ios init` installs whatever it thinks is missing via
|
||||
# rustup, and having them already present keeps that step quiet and
|
||||
# deterministic. aarch64-apple-ios-sim is the one the build below
|
||||
# actually links against.
|
||||
targets: aarch64-apple-ios,aarch64-apple-ios-sim
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src/praisonai-mobile/src-tauri
|
||||
key: ios-sim
|
||||
- name: "Build praisonai-ts (the file: dependency)"
|
||||
# praisonai-mobile depends on praisonai by `file:`, so `npm ci`
|
||||
# here runs praisonai-ts's own `prepare` -> `rimraf dist`. On a
|
||||
# fresh runner praisonai-ts has no node_modules, so rimraf is not
|
||||
# installed and `npm ci` dies with code 127 before a single
|
||||
# platform step runs. mobile.yml gives all four of its jobs this
|
||||
# step; these two were written against the tree from before the
|
||||
# dependency existed and never got it.
|
||||
working-directory: src/praisonai-ts
|
||||
run: |
|
||||
npm install --no-audit --no-fund --legacy-peer-deps
|
||||
npm run build
|
||||
- run: npm ci
|
||||
|
||||
- name: Generate the Xcode project (only if gen/apple is not committed)
|
||||
# `tauri ios init` renders the template and then runs `xcodegen`, a
|
||||
# brew package the CLI would otherwise try to install itself, mid-run,
|
||||
# with a prompt. Installing it up front is the same outcome without
|
||||
# the prompt. cocoapods is only invoked when the config declares pods;
|
||||
# it declares none.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -d src-tauri/gen/apple ]; then
|
||||
echo "src-tauri/gen/apple is committed; not regenerating it"
|
||||
exit 0
|
||||
fi
|
||||
brew list xcodegen >/dev/null 2>&1 || brew install xcodegen
|
||||
npx tauri ios init --ci --skip-targets-install
|
||||
|
||||
- name: Put the real AppIcon set into the Xcode project
|
||||
# `tauri ios init` renders the CLI's OWN placeholder AppIcon PNGs into
|
||||
# gen/apple/Assets.xcassets, and nothing in the CLI ever replaces them:
|
||||
# `tauri icon` writes to src-tauri/icons/ios/ and stops there (it wires
|
||||
# up gen/android, but there is no iOS equivalent). So the copy is done
|
||||
# here, and every filename matches one-for-one -- both sides are named
|
||||
# by the same Contents.json.
|
||||
#
|
||||
# Idempotent, and it runs whether or not init just ran, so a
|
||||
# regenerated project cannot silently ship Tauri's icon as ours.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
dest=src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset
|
||||
test -d "$dest" || { echo "::error::$dest does not exist"; find src-tauri/gen/apple -maxdepth 3 -print; exit 1; }
|
||||
cp src-tauri/icons/ios/*.png "$dest/"
|
||||
ls "$dest" | wc -l
|
||||
|
||||
- name: Build the simulator app, unsigned
|
||||
# --target aarch64-sim: the iphonesimulator SDK, arm64 (the runner is
|
||||
# Apple silicon, and so is every Mac that will run the simulator).
|
||||
# --no-sign: xcodebuild with CODE_SIGNING_ALLOWED=NO. For the
|
||||
# simulator SDK the CLI then lifts PraisonAI.app straight out of the
|
||||
# xcarchive instead of exporting an IPA (crates/tauri-cli/src/mobile/
|
||||
# ios/build.rs, the `target.sdk == "iphonesimulator"` branch), which
|
||||
# is exactly the artifact a simulator installs.
|
||||
# --debug: the dev profile links in seconds rather than minutes, and a
|
||||
# simulator build is never the one that ships.
|
||||
run: npx tauri ios build --debug --target aarch64-sim --no-sign --ci
|
||||
|
||||
- name: Is there an app, and is the Rust inside it?
|
||||
id: app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
root=src-tauri/gen/apple/build
|
||||
# Printed before anything is asserted, so a failure here is
|
||||
# diagnosable from the log alone rather than costing another run.
|
||||
echo "--- what the build left behind"
|
||||
find "$root" -maxdepth 3 -print 2>/dev/null | head -40 || echo "(no build directory)"
|
||||
|
||||
# Prefer the simulator export dir (build/arm64-sim, the `arch` of the
|
||||
# aarch64-sim target); fall back to any .app so the error message can
|
||||
# say what WAS built. The name is not hardcoded: it comes from
|
||||
# productName via the CLI, and guessing it wrong is what failed here
|
||||
# the first time.
|
||||
app="$(find "$root" -type d -name '*.app' -path '*arm64-sim*' -print -quit 2>/dev/null || true)"
|
||||
[ -n "$app" ] || app="$(find "$root" -type d -name '*.app' -print -quit 2>/dev/null || true)"
|
||||
if [ -z "$app" ]; then
|
||||
echo "::error::no .app was produced under $root"
|
||||
exit 1
|
||||
fi
|
||||
echo "app: $app"
|
||||
|
||||
# The executable's name comes from the bundle itself for the same
|
||||
# reason.
|
||||
exe="$app/$(plutil -extract CFBundleExecutable raw "$app/Info.plist")"
|
||||
test -f "$exe" || { echo "::error::$app has no executable"; ls -la "$app"; exit 1; }
|
||||
file "$exe"
|
||||
file "$exe" | grep -q 'arm64' || { echo "::error::the executable is not arm64"; exit 1; }
|
||||
|
||||
# iOS links the Rust in as a staticlib, so unlike Android there is no
|
||||
# .so to look for -- the crate is INSIDE this binary, and its symbols
|
||||
# are the only real evidence it got there. An .app that built without
|
||||
# it would be the Swift template alone.
|
||||
#
|
||||
# Dumped to files rather than piped into `grep -q`: grep exits on the
|
||||
# FIRST match, the dumper then dies of SIGPIPE, and `set -o pipefail`
|
||||
# reports that as a failed pipeline -- so the check failed precisely
|
||||
# BECAUSE it found what it was looking for. It cost a CI run.
|
||||
nm -a "$exe" > "$RUNNER_TEMP/symbols.txt" 2>/dev/null || true
|
||||
strings -a "$exe" > "$RUNNER_TEMP/strings.txt" 2>/dev/null || true
|
||||
if grep -qi 'praisonai_mobile\|start_app' "$RUNNER_TEMP/symbols.txt" \
|
||||
|| grep -qi 'praisonai_mobile' "$RUNNER_TEMP/strings.txt"; then
|
||||
echo "the Rust crate is linked into the executable"
|
||||
else
|
||||
echo "::error::the Rust library is not linked into $exe"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The frontend is embedded in that same binary by generate_context!,
|
||||
# not copied into the bundle as a resource, so it is checked the same
|
||||
# way rather than by looking for an index.html that is never there.
|
||||
plutil -p "$app/Info.plist" | grep -E 'CFBundleIdentifier|CFBundleShortVersionString|MinimumOSVersion|UIDeviceFamily'
|
||||
test -f "$app/Assets.car" || echo "note: no compiled asset catalog in the bundle"
|
||||
du -sh "$app"
|
||||
|
||||
# ditto, not zip: it keeps the bundle's symlinks and modes, which is
|
||||
# the difference between an .app the simulator installs and one it
|
||||
# rejects as an invalid bundle.
|
||||
ditto -c -k --keepParent "$app" "$RUNNER_TEMP/PraisonAI-ios-simulator.app.zip"
|
||||
ls -la "$RUNNER_TEMP/PraisonAI-ios-simulator.app.zip"
|
||||
|
||||
- name: Upload the simulator app
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PraisonAI-ios-simulator-app
|
||||
path: ${{ runner.temp }}/PraisonAI-ios-simulator.app.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
- name: Package gen/apple for committing
|
||||
# always(): the project is generated by `init`, so it is worth having
|
||||
# even on a run whose BUILD failed -- that is exactly the run where
|
||||
# someone needs to look at it.
|
||||
if: always()
|
||||
# Only what `git add src-tauri/gen/apple` would take: the template's
|
||||
# own .gitignore excludes build/, xcuserdata/ and Externals/, and this
|
||||
# respects it instead of re-listing them here. Nothing to upload when
|
||||
# the project was already committed -- the guard above skipped init.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
files="$(git ls-files --others --exclude-standard src-tauri/gen/apple || true)"
|
||||
if [ -z "$files" ]; then
|
||||
echo "gen/apple is tracked already; nothing to package"
|
||||
exit 0
|
||||
fi
|
||||
echo "$files" | tar -czf "$RUNNER_TEMP/gen-apple.tgz" -T -
|
||||
echo "$files" | wc -l
|
||||
ls -la "$RUNNER_TEMP/gen-apple.tgz"
|
||||
|
||||
- name: Upload gen/apple (only on the run that generated it)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gen-apple
|
||||
path: ${{ runner.temp }}/gen-apple.tgz
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
|
||||
android-apk:
|
||||
name: android apk (debug, arm64)
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.18'
|
||||
cache: npm
|
||||
cache-dependency-path: src/praisonai-mobile/package-lock.json
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
# 17, not the runner's default 21 or a laptop's 25: Gradle 8.14.3's
|
||||
# Groovy front end rejects class files newer than it knows
|
||||
# ("Unsupported class file major version 69" on JDK 25), and 17 is
|
||||
# what AGP 8.11 documents.
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
- uses: android-actions/setup-android@v3
|
||||
with:
|
||||
# What gen/android/app/build.gradle.kts declares (compileSdk 36),
|
||||
# plus the build-tools AGP pairs with it. Anything else AGP needs it
|
||||
# downloads itself; the licences are accepted by this action.
|
||||
packages: 'platform-tools platforms;android-36 build-tools;36.0.0'
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: ndk
|
||||
with:
|
||||
# r28c = 28.2.13676358, the NDK gen/android was generated and first
|
||||
# built with.
|
||||
ndk-version: r28c
|
||||
add-to-path: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-linux-android
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src/praisonai-mobile/src-tauri
|
||||
key: android-apk
|
||||
- name: "Build praisonai-ts (the file: dependency)"
|
||||
# praisonai-mobile depends on praisonai by `file:`, so `npm ci`
|
||||
# here runs praisonai-ts's own `prepare` -> `rimraf dist`. On a
|
||||
# fresh runner praisonai-ts has no node_modules, so rimraf is not
|
||||
# installed and `npm ci` dies with code 127 before a single
|
||||
# platform step runs. mobile.yml gives all four of its jobs this
|
||||
# step; these two were written against the tree from before the
|
||||
# dependency existed and never got it.
|
||||
working-directory: src/praisonai-ts
|
||||
run: |
|
||||
npm install --no-audit --no-fund --legacy-peer-deps
|
||||
npm run build
|
||||
- run: npm ci
|
||||
|
||||
- name: Generate the Android project (only if gen/android is not committed)
|
||||
env:
|
||||
NDK_HOME: ${{ steps.ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -d src-tauri/gen/android ]; then
|
||||
echo "src-tauri/gen/android is committed; not regenerating it"
|
||||
exit 0
|
||||
fi
|
||||
npx tauri android init --ci --skip-targets-install
|
||||
|
||||
- name: Build the debug APK
|
||||
env:
|
||||
NDK_HOME: ${{ steps.ndk.outputs.ndk-path }}
|
||||
# --target aarch64 only: every phone sold in the last several years is
|
||||
# arm64-v8a, and one target keeps the Rust build to one link. The
|
||||
# `universal` in the output path is Gradle's name for "the APK that is
|
||||
# not split per ABI", not a claim about what is inside it.
|
||||
run: npx tauri android build --debug --apk --target aarch64 --ci
|
||||
|
||||
- name: Is there an APK, and is the Rust inside it?
|
||||
run: |
|
||||
set -euo pipefail
|
||||
root=src-tauri/gen/android/app/build/outputs/apk
|
||||
apk="$(find "$root" -name '*.apk' -print -quit || true)"
|
||||
if [ -z "$apk" ]; then
|
||||
echo "::error::no .apk was produced under $root"
|
||||
find "$root" -type f -print 2>/dev/null || echo "(the outputs directory does not exist)"
|
||||
exit 1
|
||||
fi
|
||||
echo "apk: $apk"
|
||||
ls -la "$apk"
|
||||
unzip -l "$apk" | grep -q 'lib/arm64-v8a/libpraisonai_mobile_lib.so' \
|
||||
|| { echo "::error::$apk does not contain lib/arm64-v8a/libpraisonai_mobile_lib.so"; unzip -l "$apk" | head -40; exit 1; }
|
||||
unzip -l "$apk" | grep -E 'lib/|AndroidManifest|classes.dex' | head
|
||||
cp "$apk" "$RUNNER_TEMP/PraisonAI-android-debug-arm64.apk"
|
||||
|
||||
- name: Upload the APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PraisonAI-android-debug-arm64-apk
|
||||
path: ${{ runner.temp }}/PraisonAI-android-debug-arm64.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -1,164 +0,0 @@
|
||||
# The mobile package's WEB build: built, booted in a real browser, and
|
||||
# deployed to GitHub Pages.
|
||||
#
|
||||
# mobile.yml proves the package's tests pass. This workflow proves the thing
|
||||
# that ships to a browser actually ships: dist/ is built, served under the
|
||||
# repository subpath and driven through first paint, install, and an offline
|
||||
# reload by tools/web-boot.test.mjs, then published. A PR gets the build and
|
||||
# the proof; only main deploys.
|
||||
#
|
||||
# One setting is outside this file's reach: the repository's Pages source must
|
||||
# be "GitHub Actions" (Settings -> Pages -> Build and deployment -> Source).
|
||||
# Until a human turns that on, the deploy job SKIPS with a notice rather than
|
||||
# failing -- a red main every push because of a setting no workflow can set
|
||||
# would train people to ignore the mobile checks, which is how the last silent
|
||||
# breakage in this package survived.
|
||||
name: mobile web
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: &watched
|
||||
- 'src/praisonai-mobile/**'
|
||||
- '.github/workflows/mobile-web.yml'
|
||||
# `praisonai` is a `file:` link to ../praisonai-ts, and the web bundle is
|
||||
# built FROM it -- so drift on that side changes what ships to a browser
|
||||
# and must trigger this gate. mobile.yml watches these same paths for the
|
||||
# same reason; a web gate that watched only praisonai-mobile would go
|
||||
# quiet exactly when the dependency it bundles moved.
|
||||
- 'src/praisonai-ts/src/**'
|
||||
- 'src/praisonai-ts/scripts/**'
|
||||
- 'src/praisonai-ts/package.json'
|
||||
pull_request:
|
||||
paths: *watched
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-mobile
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build and boot
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency:
|
||||
group: mobile-web-build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.18'
|
||||
cache: npm
|
||||
cache-dependency-path: src/praisonai-mobile/package-lock.json
|
||||
# `praisonai` is a `file:` link to ../praisonai-ts, so its dist/ must
|
||||
# exist before this package can even INSTALL: npm runs the linked
|
||||
# package's prepare/prebuild, which fails without its own node_modules.
|
||||
# `npm install`, not `npm ci`: praisonai-ts gitignores its lockfile.
|
||||
# `--legacy-peer-deps` for the bedrock-agentcore -> @strands-agents/sdk
|
||||
# peer conflict, exactly as mobile.yml and praisonai-ts-webview.yml do.
|
||||
- name: "Build praisonai-ts (the file: dependency)"
|
||||
working-directory: src/praisonai-ts
|
||||
run: |
|
||||
npm install --no-audit --no-fund --legacy-peer-deps
|
||||
npm run build
|
||||
- run: npm ci
|
||||
- name: Cache the Playwright browser
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('src/praisonai-mobile/package-lock.json') }}
|
||||
- name: Install Chromium for the boot proof
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Boot proof
|
||||
# Serves dist/ under /PraisonAI/ in headless Chromium: composer and
|
||||
# Send render, manifest served as application/manifest+json, service
|
||||
# worker activates with the subpath as scope, an offline reload still
|
||||
# renders, and an injected inline script is blocked by the CSP.
|
||||
run: npm run test:web
|
||||
- name: dist contents
|
||||
run: ls -lR dist
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mobile-web-dist
|
||||
path: src/praisonai-mobile/dist
|
||||
if-no-files-found: error
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: src/praisonai-mobile/dist
|
||||
|
||||
# The readiness check is its OWN job, and deliberately declares no
|
||||
# `environment:`. A job that names `environment: github-pages` cannot START
|
||||
# until that environment exists, so when Pages is off the runner fails the
|
||||
# job outright -- zero steps, about one second -- and every carefully
|
||||
# written skip inside it is unreachable. Measured: run 33730322658 on main,
|
||||
# "deploy to GitHub Pages" failed with steps: 0.
|
||||
#
|
||||
# Splitting the decision out means the check runs with no preconditions and
|
||||
# the environment-bearing job is never scheduled unless it can succeed.
|
||||
pages-ready:
|
||||
name: is GitHub Pages enabled?
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
# `gh api repos/<owner>/<repo>/pages` needs Pages read. Without it the
|
||||
# request is rejected on a private repo, `2>/dev/null || echo '{}'`
|
||||
# swallows the error to `{}`, build_type falls to "none", and the deploy
|
||||
# SKIPS even though Pages is on -- the exact silent skip this file exists
|
||||
# to prevent. `contents: read` alone leaves this permission at `none`.
|
||||
pages: read
|
||||
outputs:
|
||||
ready: ${{ steps.check.outputs.ready }}
|
||||
steps:
|
||||
# Pages cannot be enabled from a workflow, so this reports the setting
|
||||
# rather than demanding it. `ready` false SKIPS the deploy with a
|
||||
# notice; it does not fail the run. deploy-pages would otherwise fail
|
||||
# with a bare 404 that says nothing about which setting is missing.
|
||||
- name: Is the Pages source "GitHub Actions"?
|
||||
id: check
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
working-directory: .
|
||||
run: |
|
||||
site=$(gh api "repos/${{ github.repository }}/pages" 2>/dev/null || echo '{}')
|
||||
build_type=$(echo "$site" | jq -r '.build_type // "none"')
|
||||
cname=$(echo "$site" | jq -r '.cname // ""')
|
||||
echo "build_type=$build_type cname=${cname:-<none>}"
|
||||
if [ "$build_type" != "workflow" ]; then
|
||||
echo "::notice::Skipping the Pages deploy: Pages is not set to build from GitHub Actions (build_type=$build_type). A repository admin must set it at https://github.com/${{ github.repository }}/settings/pages -> Build and deployment -> Source: GitHub Actions. The built site is attached to this run as the mobile-web-dist artifact."
|
||||
echo "ready=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$cname" ]; then
|
||||
echo "::notice::Skipping the Pages deploy: a custom domain ($cname) is set, so the app would be served there rather than at the github.io URL."
|
||||
echo "ready=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "ready=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
deploy:
|
||||
name: deploy to GitHub Pages
|
||||
needs: pages-ready
|
||||
if: needs.pages-ready.outputs.ready == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
concurrency:
|
||||
# Never cancel a deployment in flight; queue behind it instead.
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/configure-pages@v5
|
||||
- uses: actions/deploy-pages@v4
|
||||
id: deployment
|
||||
@@ -1,263 +0,0 @@
|
||||
# The mobile package's tests.
|
||||
#
|
||||
# Written at the same time as the package, deliberately. praisonai-desktop's
|
||||
# suite existed and ran nowhere for months because no workflow referenced it,
|
||||
# and a package whose tests never run in CI is a package whose tests are
|
||||
# decoration -- the 523 here would go stale silently.
|
||||
#
|
||||
# Four gates, and they catch different things:
|
||||
#
|
||||
# typecheck -- the boundary between erasable and non-erasable TypeScript.
|
||||
# node:test strips types rather than compiling, so an enum or
|
||||
# a constructor parameter property is a runtime parse error at
|
||||
# import; `erasableSyntaxOnly` turns that into a typecheck
|
||||
# failure at the file that caused it.
|
||||
# boundaries -- the two seams. Only engines/src/praisonai-ts may import
|
||||
# 'praisonai'; only adapters/src/tauri may import @tauri-apps.
|
||||
# These are the whole reason the package is swappable, and a
|
||||
# rule enforced only by review stops being enforced.
|
||||
# test -- the suite itself.
|
||||
# bundle gate -- webview compatibility. This is the one a normal test run
|
||||
# CANNOT do: node:test runs in Node, where a `crypto` import
|
||||
# and a top-level `process.env` both work fine, and both kill
|
||||
# a webview bundle at import time.
|
||||
name: mobile
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: &watched
|
||||
- 'src/praisonai-mobile/**'
|
||||
- '.github/workflows/mobile.yml'
|
||||
# The `upstream parity` job below typechecks the REAL praisonai-ts Agent
|
||||
# against the structural contract in engines/src/praisonai-ts/agent-api.ts.
|
||||
# Watching only praisonai-mobile made that gate fire when the CONSUMER
|
||||
# changed and stay silent when the thing it watches did -- so upstream
|
||||
# drift landed on main unchecked. Verified on 71197fa80, which edited
|
||||
# src/praisonai-ts/src/agent/simple.ts: no parity check ran on it.
|
||||
- 'src/praisonai-ts/src/**'
|
||||
# And the pieces of praisonai-ts that decide what `praisonai/mobile`
|
||||
# RESOLVES to through the file: link -- the esm shim shapes dist/esm.
|
||||
- 'src/praisonai-ts/scripts/**'
|
||||
- 'src/praisonai-ts/package.json'
|
||||
pull_request:
|
||||
paths: *watched
|
||||
|
||||
concurrency:
|
||||
group: mobile-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-mobile
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: check (node ${{ matrix.node }})
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# 22.18 is the floor declared in package.json -- tested because a
|
||||
# declared floor nobody runs is a guess. 24 is the forward check:
|
||||
# native type-stripping is the feature this package depends on most,
|
||||
# and it is exactly the kind of thing a major bump changes.
|
||||
node: ['22.18', '24']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
cache-dependency-path: src/praisonai-mobile/package-lock.json
|
||||
# `praisonai` is a `file:` link to ../praisonai-ts (see praisonai-mobile's
|
||||
# package.json for why), so its dist/ must exist before this package can
|
||||
# resolve `praisonai/mobile`. `npm install`, not `npm ci`: praisonai-ts
|
||||
# gitignores its lockfile. `--legacy-peer-deps` for the
|
||||
# bedrock-agentcore -> @strands-agents/sdk peer conflict, exactly as
|
||||
# praisonai-ts-webview.yml does.
|
||||
- name: "Build praisonai-ts (the file: dependency)"
|
||||
working-directory: src/praisonai-ts
|
||||
run: |
|
||||
npm install --no-audit --no-fund --legacy-peer-deps
|
||||
npm run build
|
||||
- run: npm ci
|
||||
- name: Cache the Playwright browser
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('src/praisonai-mobile/package-lock.json') }}
|
||||
- name: Install Chromium for the boot proof
|
||||
# `npm test` includes tools/web-boot.test.mjs, which loads the built
|
||||
# dist/ in a real browser. It fails rather than skips without one.
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
- name: Layer boundaries
|
||||
run: npm run boundaries
|
||||
- name: Tests
|
||||
run: npm test
|
||||
- name: Bundle gate fixtures
|
||||
# Runs the gate's own suite, which proves each check can FAIL -- a
|
||||
# build check that has only ever seen clean input is a check nobody
|
||||
# knows the state of.
|
||||
run: npm run test:bundle
|
||||
|
||||
upstream-parity:
|
||||
name: upstream parity
|
||||
# Its own job so drift from the real Agent is visible in the checks list
|
||||
# rather than buried in another job's log.
|
||||
#
|
||||
# It needs only THIS package's install. praisonai-ts's own dependencies are
|
||||
# not needed here (the `check` job installs them, with --legacy-peer-deps,
|
||||
# to build the file: dependency): every member the check looks at is
|
||||
# declared locally in simple.ts. That is verified rather than assumed:
|
||||
# with praisonai-ts's node_modules moved aside, injected drift is still
|
||||
# caught as TS2741.
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.18'
|
||||
cache: npm
|
||||
cache-dependency-path: src/praisonai-mobile/package-lock.json
|
||||
- name: "Build praisonai-ts (the file: dependency)"
|
||||
working-directory: src/praisonai-ts
|
||||
run: |
|
||||
npm install --no-audit --no-fund --legacy-peer-deps
|
||||
npm run build
|
||||
- run: npm ci
|
||||
- name: Does the real Agent still satisfy PraisonAgent?
|
||||
# Skips loudly if the praisonai-ts checkout is missing, rather than
|
||||
# passing quietly -- a check that silently stops covering anything is
|
||||
# worse than no check.
|
||||
run: npm run check:upstream
|
||||
|
||||
shell:
|
||||
name: rust shell (${{ matrix.os }})
|
||||
# The Rust crate had 14 tests and no job running them -- the same failure
|
||||
# this workflow's header says it was created to prevent, one language over.
|
||||
#
|
||||
# Both platforms, because the crate is cfg-heavy: the back-gesture fallback
|
||||
# is #[cfg(target_os = "android")] / ios / not(mobile), and a cfg mistake
|
||||
# compiles perfectly on whichever host you happened to try.
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, macos-15]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-mobile/src-tauri
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.18'
|
||||
cache: npm
|
||||
cache-dependency-path: src/praisonai-mobile/package-lock.json
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src/praisonai-mobile/src-tauri
|
||||
- name: Install the Linux build dependencies
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: .
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libwebkit2gtk-4.1-dev librsvg2-dev libxdo-dev libssl-dev patchelf
|
||||
# generate_context! expands frontendDist (../dist) at COMPILE time, and
|
||||
# dist/ is gitignored -- absent on a fresh checkout. Without this the
|
||||
# crate cannot compile and neither test nor clippy below ever runs.
|
||||
# `praisonai` is a `file:` link to ../praisonai-ts (see praisonai-mobile's
|
||||
# package.json for why), so its dist/ must exist before this package can
|
||||
# resolve `praisonai/mobile`. `npm install`, not `npm ci`: praisonai-ts
|
||||
# gitignores its lockfile. `--legacy-peer-deps` for the
|
||||
# bedrock-agentcore -> @strands-agents/sdk peer conflict, exactly as
|
||||
# praisonai-ts-webview.yml does.
|
||||
- name: "Build praisonai-ts (the file: dependency)"
|
||||
working-directory: src/praisonai-ts
|
||||
run: |
|
||||
npm install --no-audit --no-fund --legacy-peer-deps
|
||||
npm run build
|
||||
- name: Build the webview (creates ../dist for generate_context!)
|
||||
working-directory: src/praisonai-mobile
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
# The pure-logic tests: back-gesture arbitration, the lifecycle mapping,
|
||||
# and the event-name contract. None of them needs a phone.
|
||||
- name: Test
|
||||
run: cargo test
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# The mobile targets, which nothing compiled.
|
||||
#
|
||||
# `src/lib.rs` is `#[cfg_attr(mobile, tauri::mobile_entry_point)]`, and
|
||||
# `tauri_build::build()` emits `cargo:rustc-cfg=mobile` only for android and
|
||||
# ios targets. Both legs of `rust shell` above are HOST targets, so that
|
||||
# attribute -- the one that generates the Android JNI symbol and the iOS
|
||||
# start_app symbol -- expanded exactly zero times in CI, and neither did the
|
||||
# `#[cfg(target_os = "android")]` / `ios` arms of `commands.rs`. The job
|
||||
# above says it covers cfg mistakes; it covers the ones in `shell/`, not
|
||||
# these.
|
||||
#
|
||||
# `cargo check`, not `build`: it type-checks and macro-expands under the
|
||||
# mobile cfg, which is the thing that was never done, and needs no NDK and no
|
||||
# Xcode project. Verified at 18s for the android target.
|
||||
# ------------------------------------------------------------------
|
||||
cross-check:
|
||||
name: cross-check (${{ matrix.target }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-linux-android
|
||||
os: ubuntu-22.04
|
||||
- target: aarch64-apple-ios
|
||||
os: macos-15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-mobile/src-tauri
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.18'
|
||||
cache: npm
|
||||
cache-dependency-path: src/praisonai-mobile/package-lock.json
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src/praisonai-mobile/src-tauri
|
||||
key: ${{ matrix.target }}
|
||||
# generate_context! expands frontendDist at COMPILE time and dist/ is
|
||||
# gitignored, so the crate cannot be checked without this -- the same
|
||||
# reason the job above needs it.
|
||||
# `praisonai` is a `file:` link to ../praisonai-ts (see praisonai-mobile's
|
||||
# package.json for why), so its dist/ must exist before this package can
|
||||
# resolve `praisonai/mobile`. `npm install`, not `npm ci`: praisonai-ts
|
||||
# gitignores its lockfile. `--legacy-peer-deps` for the
|
||||
# bedrock-agentcore -> @strands-agents/sdk peer conflict, exactly as
|
||||
# praisonai-ts-webview.yml does.
|
||||
- name: "Build praisonai-ts (the file: dependency)"
|
||||
working-directory: src/praisonai-ts
|
||||
run: |
|
||||
npm install --no-audit --no-fund --legacy-peer-deps
|
||||
npm run build
|
||||
- name: Build the webview (creates ../dist for generate_context!)
|
||||
working-directory: src/praisonai-mobile
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
- name: Does the crate still expand and type-check for a phone?
|
||||
run: cargo check --lib --target ${{ matrix.target }}
|
||||
@@ -1,144 +0,0 @@
|
||||
name: Nightly Release Gate
|
||||
|
||||
# Preflight before PyPI Release (patch bump).
|
||||
# Triggers: Core Tests success on main (workflow_run) or cron every 3 days at 00:00 UTC.
|
||||
# Dedupe: max one successful patch release every 3 days (see release-gate.js).
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 */3 * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Run preflight only; do not dispatch pypi-release.yml'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Core Tests
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
concurrency:
|
||||
group: nightly-release-gate
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'schedule' ||
|
||||
(
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_branch == 'main' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
!startsWith(github.event.workflow_run.head_commit.message, 'Release v') &&
|
||||
!startsWith(github.event.workflow_run.head_commit.message, 'chore(release):')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
steps:
|
||||
- name: Resolve trigger context
|
||||
id: ctx
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_run" ]; then
|
||||
echo "checkout_sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT"
|
||||
echo "source=ci" >> "$GITHUB_OUTPUT"
|
||||
echo "is_ci=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "checkout_sha=main" >> "$GITHUB_OUTPUT"
|
||||
echo "source=nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "is_ci=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ steps.ctx.outputs.checkout_sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch main and tags
|
||||
run: git fetch --tags origin main
|
||||
|
||||
# Guards the gate logic itself — no workflow ran these selftests before.
|
||||
- name: Selftest release-gate script
|
||||
run: node .github/scripts/release-gate-selftest.js
|
||||
|
||||
- name: Preflight — path changes, CI SHA, dedupe
|
||||
id: preflight
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
HEAD_SHA: ${{ steps.ctx.outputs.checkout_sha }}
|
||||
IS_CI: ${{ steps.ctx.outputs.is_ci }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const rg = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/release-gate.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const headSha = process.env.HEAD_SHA;
|
||||
const isCi = process.env.IS_CI === 'true';
|
||||
|
||||
const result = await rg.evaluateReleasePreflight(
|
||||
github, owner, repo,
|
||||
{ headSha, isCiTrigger: isCi, bump: 'patch' },
|
||||
core
|
||||
);
|
||||
|
||||
core.setOutput('should_release', result.ready ? 'true' : 'false');
|
||||
core.setOutput('reason', result.reasons.join('; '));
|
||||
core.setOutput('head_sha', result.headSha || headSha);
|
||||
core.setOutput('last_tag', result.lastTag || '');
|
||||
core.setOutput('target_agents', result.targetAgents || '');
|
||||
core.setOutput('target_code', result.targetCode || '');
|
||||
core.setOutput('target_wrapper', result.targetWrapper || '');
|
||||
|
||||
core.summary.addRaw('## Release gate preflight\n');
|
||||
core.summary.addRaw(`Ready: ${result.ready}\n`);
|
||||
core.summary.addRaw(`Reason: ${result.reasons.join('; ')}\n`);
|
||||
if (result.targetAgents) {
|
||||
core.summary.addRaw(
|
||||
`Targets: praisonaiagents==${result.targetAgents}, `
|
||||
+ `praisonai-code==${result.targetCode}, praisonai==${result.targetWrapper}\n`
|
||||
);
|
||||
}
|
||||
await core.summary.write();
|
||||
|
||||
if (!result.ready) {
|
||||
core.info(`Skip release: ${result.reasons.join('; ')}`);
|
||||
}
|
||||
|
||||
- name: Dry run summary
|
||||
if: inputs.dry_run == true
|
||||
run: |
|
||||
echo "Dry run — preflight only, no pypi-release dispatch."
|
||||
echo "should_release=${{ steps.preflight.outputs.should_release }}"
|
||||
echo "reason=${{ steps.preflight.outputs.reason }}"
|
||||
|
||||
- name: Dispatch PyPI Release (patch)
|
||||
if: steps.preflight.outputs.should_release == 'true' && inputs.dry_run != true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh workflow run pypi-release.yml \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--ref main \
|
||||
-f bump=patch \
|
||||
-f source=${{ steps.ctx.outputs.source }} \
|
||||
-f trigger_sha=${{ steps.preflight.outputs.head_sha }} \
|
||||
-f dry_run=false
|
||||
echo "Dispatched pypi-release.yml (patch, source=${{ steps.ctx.outputs.source }})"
|
||||
if [ "${{ steps.ctx.outputs.source }}" = "ci" ]; then
|
||||
echo "Uses pypi-auto environment (no manual approval)."
|
||||
else
|
||||
echo "Uses pypi-auto environment for nightly patch releases."
|
||||
fi
|
||||
@@ -1,106 +0,0 @@
|
||||
name: Parity Gate
|
||||
|
||||
# Validates Python <-> TypeScript SDK parity on pull requests and FAILS on drift.
|
||||
#
|
||||
# Two named checks, so each kind of drift is visible in the checks list:
|
||||
# names - the exported-name tracker (PARITY.md / FEATURE_PARITY_TRACKER.json)
|
||||
# must be regenerated in the PR when the public surface changes.
|
||||
# behaviour - options accepted for parity but not yet acted on; a ratchet,
|
||||
# so the count may fall and never rise.
|
||||
# signatures - constructor/method parameters of the curated surfaces in
|
||||
# src/praisonai/praisonai/_dev/parity/signatures/surface.yaml must
|
||||
# match (after camelCase/alias/flattening rules) or be waived with a
|
||||
# reason in waivers.yaml. Stale or expired waivers also fail.
|
||||
#
|
||||
# The regenerate-and-commit job stays in update-parity-tracker.yml (push to main).
|
||||
# This workflow never commits; it only reports.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/praisonai-agents/praisonaiagents/**/*.py'
|
||||
- 'src/praisonai-ts/src/**/*.ts'
|
||||
- 'src/praisonai-rust/praisonai/src/**/*.rs'
|
||||
- 'src/praisonai/praisonai/_dev/parity/**'
|
||||
- 'src/praisonai/praisonai/cli/features/**'
|
||||
# The generator the `names` job runs. Editing it changed what the gate
|
||||
# measured without the gate running.
|
||||
- 'src/praisonai/scripts/**'
|
||||
- 'src/praisonai-ts/PARITY.md'
|
||||
- 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json'
|
||||
- 'src/praisonai-ts/SIGNATURE_PARITY.md'
|
||||
- 'src/praisonai-ts/signature-parity.json'
|
||||
# The behaviour baseline itself. These two carry the ratchet floor, so a
|
||||
# pull request that edited only them raised the floor with no job running.
|
||||
- 'src/praisonai-ts/BEHAVIOUR_PARITY.md'
|
||||
- 'src/praisonai-ts/behaviour-parity.json'
|
||||
- 'src/praisonai-rust/PARITY.md'
|
||||
- 'src/praisonai-rust/FEATURE_PARITY_TRACKER.json'
|
||||
- '.github/workflows/parity-gate.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
names:
|
||||
name: names (exported-symbol tracker up to date)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install pyyaml
|
||||
- name: Tracker files match the sources
|
||||
run: |
|
||||
if ! python3 src/praisonai/scripts/generate_parity_tracker.py --check; then
|
||||
echo "::error::Parity tracker is out of date. Run: python3 src/praisonai/scripts/generate_parity_tracker.py and commit src/praisonai-ts/PARITY.md, FEATURE_PARITY_TRACKER.json and the Rust pair."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
signatures:
|
||||
name: signatures (constructor and method parameters)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- run: pip install pyyaml
|
||||
- name: Install the TypeScript compiler only
|
||||
# praisonai-ts's full dependency tree is not needed (and does not
|
||||
# install cleanly); the extractor needs only the `typescript` package.
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/parity-ts"
|
||||
npm install --prefix "$RUNNER_TEMP/parity-ts" --no-audit --no-fund [email protected]
|
||||
echo "PARITY_TS_NODE_MODULES=$RUNNER_TEMP/parity-ts/node_modules" >> "$GITHUB_ENV"
|
||||
- name: Signatures match, or are waived with a reason
|
||||
env:
|
||||
PYTHONPATH: src/praisonai
|
||||
run: |
|
||||
if ! python3 -m praisonai._dev.parity.signatures --check; then
|
||||
echo "::error::Signature parity failed. See the output above for the surface and parameter. Fix the TypeScript side, or add a waiver with a reason to src/praisonai/praisonai/_dev/parity/signatures/waivers.yaml, then run: PYTHONPATH=src/praisonai python3 -m praisonai._dev.parity.signatures --write"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
behaviour:
|
||||
name: behaviour (options accepted but not acted on)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install pyyaml
|
||||
- name: The count of ignored options has not grown
|
||||
env:
|
||||
PYTHONPATH: src/praisonai
|
||||
run: |
|
||||
if ! python3 -m praisonai._dev.parity.behaviour --check; then
|
||||
echo "::error::An option accepted for Python parity is not acted on, and the total grew. The name and signature gates cannot see this: they check that a parameter EXISTS. Implement the behaviour and delete its entry from src/praisonai-ts/src/utils/parity-notice.ts, then run: PYTHONPATH=src/praisonai python3 -m praisonai._dev.parity.behaviour --write"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,42 +0,0 @@
|
||||
name: Pipeline Status Sync
|
||||
|
||||
# Sync pipeline/* stage and blocker labels on open internal PRs.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/10 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
issues: write
|
||||
actions: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Sync pipeline labels on open PRs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
await ps.syncOpenPullRequests(
|
||||
github, context.repo.owner, context.repo.repo,
|
||||
{ maxPrs: 30, dispatchMergeGate: true }, core
|
||||
);
|
||||
@@ -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,70 +0,0 @@
|
||||
# Does praisonai-ts still pass its own tests?
|
||||
#
|
||||
# It has a suite -- 2,000+ tests -- and until now nothing ran it. The only
|
||||
# workflow with `working-directory: src/praisonai-ts` builds the package and
|
||||
# checks the webview bundle; the two other mentions of `npm test` in this
|
||||
# directory are a comment and a line of prose telling a reviewer to run it by
|
||||
# hand. So the suite was green the way an unread report is green.
|
||||
#
|
||||
# That mattered the moment the suite grew: PR #4755 added ~880 tests for the
|
||||
# Python-parity port and not one of them would have run here. A test nobody
|
||||
# executes is worse than no test, because it is counted as cover.
|
||||
#
|
||||
# Deliberately narrow, like its webview sibling: it runs only when
|
||||
# praisonai-ts changes, installs only what it needs, and asserts two things --
|
||||
# the package typechecks, and its tests pass.
|
||||
name: praisonai-ts tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'src/praisonai-ts/src/**'
|
||||
- 'src/praisonai-ts/tests/**'
|
||||
- 'src/praisonai-ts/package.json'
|
||||
- 'src/praisonai-ts/jest.config.ts'
|
||||
- 'src/praisonai-ts/tsconfig*.json'
|
||||
- '.github/workflows/praisonai-ts-tests.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/praisonai-ts/src/**'
|
||||
- 'src/praisonai-ts/tests/**'
|
||||
- 'src/praisonai-ts/package.json'
|
||||
- 'src/praisonai-ts/jest.config.ts'
|
||||
- 'src/praisonai-ts/tsconfig*.json'
|
||||
- '.github/workflows/praisonai-ts-tests.yml'
|
||||
|
||||
concurrency:
|
||||
group: praisonai-ts-tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: unit tests
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.18'
|
||||
# `npm install --legacy-peer-deps`, matching praisonai-ts-webview.yml:
|
||||
# the lockfile is gitignored so `npm ci` has nothing to read, and the
|
||||
# dependency tree does not resolve without the flag (bedrock-agentcore
|
||||
# wants a peer of @strands-agents/sdk that is not installed).
|
||||
- name: Install
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps
|
||||
- name: Typecheck
|
||||
run: npx tsc --noEmit -p .
|
||||
# PRAISONAI_PARITY_SILENT quiets the one-line notices that options
|
||||
# accepted for Python parity print when they are not yet honoured; they
|
||||
# are deliberate output, not failures, and they drown the test log.
|
||||
- name: Tests
|
||||
run: npm test
|
||||
env:
|
||||
PRAISONAI_PARITY_SILENT: '1'
|
||||
@@ -1,62 +0,0 @@
|
||||
# Can praisonai-ts still be loaded in a webview?
|
||||
#
|
||||
# Issue #4437 was this failing: `crypto` and `events` were static imports on
|
||||
# the Agent graph, so a webview bundle died at IMPORT time with no error
|
||||
# boundary and a blank screen. PR #4438 fixed it, and nothing stopped it
|
||||
# coming back.
|
||||
#
|
||||
# `npm test` cannot catch a regression here, structurally: it runs in Node,
|
||||
# where every one of these imports works perfectly. One `import { x } from
|
||||
# "fs"` added to a file the Agent happens to reach would silently un-ship the
|
||||
# mobile app, and the first signal would be a blank screen on a device.
|
||||
#
|
||||
# Deliberately narrow. It runs only when praisonai-ts changes, installs only
|
||||
# what it needs, and asserts one thing.
|
||||
name: praisonai-ts webview
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'src/praisonai-ts/src/**'
|
||||
- 'src/praisonai-ts/package.json'
|
||||
- 'src/praisonai-ts/scripts/webview-gate.mjs'
|
||||
- '.github/workflows/praisonai-ts-webview.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/praisonai-ts/src/**'
|
||||
- 'src/praisonai-ts/package.json'
|
||||
- 'src/praisonai-ts/scripts/webview-gate.mjs'
|
||||
- '.github/workflows/praisonai-ts-webview.yml'
|
||||
|
||||
concurrency:
|
||||
group: praisonai-ts-webview-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
webview:
|
||||
name: loadable in a webview
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/praisonai-ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.18'
|
||||
# `npm install`, not `npm ci`: praisonai-ts gitignores its lockfile
|
||||
# (src/praisonai-ts/.gitignore), so `npm ci` has nothing to install from.
|
||||
# `--legacy-peer-deps` because the dependency tree does not otherwise
|
||||
# resolve -- bedrock-agentcore wants a peer of @strands-agents/sdk that
|
||||
# is not installed. That is pre-existing and out of scope here; without
|
||||
# the flag this job fails on something unrelated to what it checks.
|
||||
- name: Install
|
||||
run: npm install --no-audit --no-fund --legacy-peer-deps
|
||||
# Build first: the gate must see what actually SHIPS. The esm-shim adds
|
||||
# a CJS banner that does not exist in the TypeScript sources, and that
|
||||
# banner is what broke the published entry while the sources passed.
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Check
|
||||
run: npm run check:webview
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,8 +15,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -8,18 +8,15 @@
|
||||
|
||||
name: Upload Python Package
|
||||
|
||||
# Superseded by pypi-release.yml (manual workflow_dispatch with uv publish).
|
||||
# Kept as workflow_dispatch-only fallback; release trigger removed to avoid
|
||||
# duplicate PyPI publish when bump_and_release creates a GitHub release.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -26,8 +26,11 @@ jobs:
|
||||
# First checkout the upstream repo at the specified tag
|
||||
- name: Checkout upstream repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: MervinPraison/PraisonAI
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
fetch-depth: 0
|
||||
@@ -45,8 +48,11 @@ jobs:
|
||||
# Now checkout the fork at main branch
|
||||
- name: Checkout fork
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.inputs.fork_owner }}/PraisonAI
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
@@ -443,8 +449,11 @@ jobs:
|
||||
# First checkout the fork's main branch
|
||||
- name: Checkout fork repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
repository: ${{ github.event.inputs.fork_owner }}/PraisonAI
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Sync Claude Secrets
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_repo:
|
||||
description: 'Destination repo (owner/name)'
|
||||
required: true
|
||||
default: 'MervinPraison/PraisonAIUI'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync Claude secrets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
|
||||
TARGET: ${{ inputs.target_repo }}
|
||||
CLAUDE_APP_ID: ${{ secrets.CLAUDE_APP_ID }}
|
||||
CLAUDE_APP_PRIVATE_KEY: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
GH_TOKEN_VALUE: ${{ secrets.GH_TOKEN }}
|
||||
DOCS_GITHUB_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${GH_TOKEN:-}" ]; then
|
||||
echo "SECRETS_ADMIN_PAT is not configured in this repository"
|
||||
exit 1
|
||||
fi
|
||||
echo -n "$CLAUDE_APP_ID" | gh secret set CLAUDE_APP_ID --repo "$TARGET"
|
||||
echo -n "$CLAUDE_APP_PRIVATE_KEY" | gh secret set CLAUDE_APP_PRIVATE_KEY --repo "$TARGET"
|
||||
echo -n "$CLAUDE_CODE_OAUTH_TOKEN" | gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$TARGET"
|
||||
echo -n "$GH_TOKEN_VALUE" | gh secret set GH_TOKEN --repo "$TARGET"
|
||||
if [ -n "${DOCS_GITHUB_TOKEN:-}" ]; then
|
||||
echo -n "$DOCS_GITHUB_TOKEN" | gh secret set DOCS_GITHUB_TOKEN --repo "$TARGET"
|
||||
fi
|
||||
echo "Synced CLAUDE_APP_ID, CLAUDE_APP_PRIVATE_KEY, CLAUDE_CODE_OAUTH_TOKEN, GH_TOKEN → $TARGET"
|
||||
@@ -1,106 +0,0 @@
|
||||
name: Sync to praisonai-js
|
||||
|
||||
# Canonical TS source: src/praisonai-ts/ in this monorepo.
|
||||
# This workflow pushes the mirror to MervinPraison/praisonai-js (npm repo).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_branch:
|
||||
description: 'Target branch on MervinPraison/praisonai-js'
|
||||
required: false
|
||||
default: main
|
||||
type: string
|
||||
mode:
|
||||
description: 'pr = open pull request (default); commit = push directly to target branch'
|
||||
required: false
|
||||
default: pr
|
||||
type: choice
|
||||
options:
|
||||
- pr
|
||||
- commit
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
TARGET_REPO: MervinPraison/praisonai-js
|
||||
SOURCE_PATH: src/praisonai-ts
|
||||
TARGET_BRANCH: ${{ inputs.target_branch }}
|
||||
steps:
|
||||
- name: Validate token
|
||||
run: |
|
||||
if [ -z "${GH_TOKEN:-}" ]; then
|
||||
echo "GH_TOKEN secret is required (PAT with repo write access to praisonai-js)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout PraisonAI monorepo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout praisonai-js
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.TARGET_REPO }}
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: praisonai-js-target
|
||||
ref: ${{ env.TARGET_BRANCH }}
|
||||
|
||||
- name: Sync monorepo path into praisonai-js
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rsync -a --delete \
|
||||
--exclude '.git/' \
|
||||
--exclude 'node_modules/' \
|
||||
--exclude 'dist/' \
|
||||
--exclude 'coverage/' \
|
||||
--exclude '.DS_Store' \
|
||||
--exclude '*.tsbuildinfo' \
|
||||
"${SOURCE_PATH}/" praisonai-js-target/
|
||||
|
||||
- name: Push changes
|
||||
working-directory: praisonai-js-target
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Preserve praisonai-js automation (rsync source has no .github/)
|
||||
if git diff --quiet; then
|
||||
echo "No changes — praisonai-js already matches ${SOURCE_PATH} on monorepo main."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git add -A
|
||||
msg="sync: update from monorepo ${SOURCE_PATH}"
|
||||
|
||||
if [ "${{ inputs.mode }}" = "commit" ]; then
|
||||
git commit -m "$msg"
|
||||
git push origin "HEAD:${TARGET_BRANCH}"
|
||||
echo "Committed and pushed directly to ${TARGET_BRANCH} on praisonai-js."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
branch="sync/monorepo-praisonai-ts-$(date -u +%Y%m%d-%H%M%S)"
|
||||
git checkout -b "$branch"
|
||||
git commit -m "$msg"
|
||||
git push origin "$branch"
|
||||
|
||||
gh pr create \
|
||||
--repo "$TARGET_REPO" \
|
||||
--base "$TARGET_BRANCH" \
|
||||
--head "$branch" \
|
||||
--title "$msg" \
|
||||
--body "$(cat <<EOF
|
||||
Automated sync from [\`MervinPraison/PraisonAI\`](https://github.com/MervinPraison/PraisonAI) \`${SOURCE_PATH}/\` into the npm mirror repo.
|
||||
|
||||
Canonical development is in the monorepo; this PR updates the public \`praisonai-js\` checkout.
|
||||
|
||||
Excludes from rsync source: \`node_modules/\`, \`dist/\`, \`coverage/\`. \`.github/\` on praisonai-js is unchanged.
|
||||
EOF
|
||||
)"
|
||||
echo "Opened PR on ${TARGET_REPO} (${branch} → ${TARGET_BRANCH})."
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Terminal-Bench Smoke
|
||||
|
||||
# Manual (and optional weekly) smoke run of the `praisonai code` assistant on a
|
||||
# small verified Terminal-Bench 2.1 subset via the Harbor harness.
|
||||
# Never runs on PRs — it costs money (LLM calls) and is inherently flaky.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 6 * * 1" # Mondays 06:00 UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install Harbor + PraisonAI
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install harbor
|
||||
pip install -e src/praisonai-agents -e src/praisonai
|
||||
|
||||
- name: Run Terminal-Bench 2.1 smoke subset
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
PYTHONPATH: .
|
||||
run: |
|
||||
harbor run -c examples/terminal_bench/job_code_smoke.yaml || true
|
||||
|
||||
- name: Upload Harbor results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: harbor-results
|
||||
path: |
|
||||
runs/
|
||||
**/results.json
|
||||
if-no-files-found: warn
|
||||
@@ -37,8 +37,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -53,13 +55,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
-303
@@ -5,321 +5,70 @@ on:
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'src/praisonai-deploy/infra/**'
|
||||
- 'src/praisonai-bot/infra/**'
|
||||
- 'scripts/check_helm_charts.sh'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-core.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'src/praisonai-deploy/infra/**'
|
||||
- 'src/praisonai-bot/infra/**'
|
||||
- 'scripts/check_helm_charts.sh'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-core.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}/src/praisonai-agents
|
||||
PRAISONAI_ALLOW_NETWORK: '0'
|
||||
PRAISONAI_LIVE_TESTS: '0'
|
||||
PRAISONAI_TEST_TIER: main
|
||||
|
||||
jobs:
|
||||
test-core-collect:
|
||||
name: test-core-collect
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-timeout pytest-xdist
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
with:
|
||||
use-uv: "true"
|
||||
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
- name: Verify unit test collection
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src/praisonai
|
||||
python -m pytest tests/unit/ \
|
||||
-m "not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
|
||||
--ignore=tests/fixtures \
|
||||
--collect-only -q --disable-warnings
|
||||
|
||||
test-core-unit:
|
||||
name: test-core (${{ matrix.shard }})
|
||||
needs: test-core-collect
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- shard: cli
|
||||
paths: >-
|
||||
tests/unit/cli/
|
||||
tests/unit/tui/
|
||||
extra_ignore: --ignore=tests/unit/cli/test_message_queue.py
|
||||
- shard: bots-gateway
|
||||
workdir: src/praisonai-bot
|
||||
paths: >-
|
||||
tests/unit/bots/
|
||||
tests/unit/gateway/
|
||||
tests/unit/cli/
|
||||
tests/unit/daemon/
|
||||
extra_ignore: ""
|
||||
pythonpath: >-
|
||||
${{ github.workspace }}/src/praisonai-agents:${{ github.workspace }}/src/praisonai-code
|
||||
- shard: train
|
||||
workdir: src/praisonai-train
|
||||
# tests/unit/data/ (96 tests) was collected by no workflow at all.
|
||||
paths: >-
|
||||
tests/unit/train/ tests/unit/data/
|
||||
extra_ignore: ""
|
||||
pythonpath: >-
|
||||
${{ github.workspace }}/src/praisonai-agents:${{ github.workspace }}/src/praisonai-code
|
||||
- shard: mcp
|
||||
workdir: src/praisonai-mcp
|
||||
paths: >-
|
||||
tests/mcp_server/
|
||||
extra_ignore: ""
|
||||
pythonpath: >-
|
||||
${{ github.workspace }}/src/praisonai-agents
|
||||
- shard: sandbox
|
||||
workdir: src/praisonai-sandbox
|
||||
paths: >-
|
||||
tests/
|
||||
extra_ignore: ""
|
||||
pythonpath: >-
|
||||
${{ github.workspace }}/src/praisonai-agents
|
||||
- shard: deploy
|
||||
workdir: src/praisonai-deploy
|
||||
paths: >-
|
||||
tests/
|
||||
extra_ignore: ""
|
||||
pythonpath: >-
|
||||
${{ github.workspace }}/src/praisonai-agents
|
||||
- shard: subdirs
|
||||
paths: >-
|
||||
tests/unit/scheduler/
|
||||
tests/unit/integrations/
|
||||
tests/unit/mcp/
|
||||
tests/unit/knowledge/
|
||||
tests/unit/llm/
|
||||
tests/unit/doctor/
|
||||
tests/unit/code/
|
||||
tests/unit/acp/
|
||||
tests/unit/agent/
|
||||
tests/unit/tools/
|
||||
tests/unit/persistence/
|
||||
tests/unit/security/
|
||||
tests/unit/recipe/
|
||||
tests/unit/storage/
|
||||
tests/unit/rag/
|
||||
tests/unit/replay/
|
||||
tests/unit/escalation/
|
||||
tests/unit/orchestration/
|
||||
tests/unit/dev/
|
||||
tests/unit/_dev/
|
||||
tests/unit/context/
|
||||
tests/unit/adapters/
|
||||
tests/unit/templates/
|
||||
tests/unit/suite_runner/
|
||||
tests/unit/standardise/
|
||||
extra_ignore: ""
|
||||
- shard: root
|
||||
paths: ""
|
||||
extra_ignore: ""
|
||||
root_glob: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-timeout pytest-xdist
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
with:
|
||||
use-uv: "true"
|
||||
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
- name: Run unit shard
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
WORKDIR="${{ matrix.workdir || 'src/praisonai' }}"
|
||||
if [ "$WORKDIR" = "src/praisonai-sandbox" ]; then
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c13_sandbox_imports.sh"
|
||||
fi
|
||||
if [ "$WORKDIR" = "src/praisonai-deploy" ]; then
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c14_deploy_imports.sh"
|
||||
fi
|
||||
if [ "$WORKDIR" = "src/praisonai-mcp" ]; then
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c12_mcp_imports.sh"
|
||||
fi
|
||||
cd "$WORKDIR"
|
||||
if [ -n "${{ matrix.pythonpath || '' }}" ]; then
|
||||
export PYTHONPATH="${{ matrix.pythonpath }}:${PYTHONPATH:-}"
|
||||
fi
|
||||
PYTEST_ARGS=(
|
||||
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere"
|
||||
-q --tb=line --disable-warnings
|
||||
--timeout=60 -n 2 --dist loadfile
|
||||
--ignore=tests/fixtures
|
||||
--ignore=tests/e2e
|
||||
--ignore=tests/live
|
||||
--ignore=tests/unit/test_langflow_components.py
|
||||
)
|
||||
if [ "${{ matrix.root_glob || 'false' }}" = "true" ]; then
|
||||
shopt -s nullglob
|
||||
ROOT_TESTS=(tests/unit/test_*.py)
|
||||
shopt -u nullglob
|
||||
EXCLUDE=(tests/unit/test_langflow_components.py)
|
||||
FILTERED=()
|
||||
for t in "${ROOT_TESTS[@]}"; do
|
||||
skip=false
|
||||
for e in "${EXCLUDE[@]}"; do
|
||||
if [ "$t" = "$e" ]; then skip=true; break; fi
|
||||
done
|
||||
if [ "$skip" = false ]; then FILTERED+=("$t"); fi
|
||||
done
|
||||
if [ ${#FILTERED[@]} -eq 0 ]; then
|
||||
echo "No root unit tests matched"
|
||||
exit 0
|
||||
fi
|
||||
python -m pytest "${FILTERED[@]}" "${PYTEST_ARGS[@]}"
|
||||
else
|
||||
python -m pytest ${{ matrix.paths }} "${PYTEST_ARGS[@]}" ${{ matrix.extra_ignore }}
|
||||
fi
|
||||
|
||||
test-core-integration:
|
||||
name: test-core-integration
|
||||
needs: test-core-unit
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-timeout
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
with:
|
||||
use-uv: "true"
|
||||
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
- name: Run integration tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src/praisonai
|
||||
python -m pytest tests/integration/ \
|
||||
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
|
||||
-q --tb=line --disable-warnings \
|
||||
--timeout=120 \
|
||||
--ignore=tests/fixtures \
|
||||
--ignore=tests/e2e \
|
||||
--ignore=tests/live \
|
||||
--ignore=tests/integration/test_tracker_complex_tasks.py \
|
||||
--ignore=tests/integration/test_serve_integration.py
|
||||
|
||||
helm-charts:
|
||||
name: helm-charts
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.14.4
|
||||
- name: Lint and template Helm charts
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_helm_charts.sh"
|
||||
|
||||
test-core:
|
||||
name: test-core
|
||||
needs: [test-core-collect, test-core-unit, test-core-integration, helm-charts]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
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 }}'
|
||||
helm='${{ needs.helm-charts.result }}'
|
||||
echo "collect=$collect unit=$unit integration=$integration helm=$helm"
|
||||
if [ "$collect" != "success" ] || [ "$unit" != "success" ] || [ "$integration" != "success" ] || [ "$helm" != "success" ]; then
|
||||
echo "Core Tests failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "Core Tests passed"
|
||||
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
|
||||
@@ -13,8 +13,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -29,12 +31,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: |
|
||||
@@ -80,8 +81,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -96,12 +99,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: |
|
||||
|
||||
@@ -33,8 +33,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -49,12 +51,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,11 +42,11 @@ jobs:
|
||||
# ============================================
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 12
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -64,247 +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 \
|
||||
tests/unit/test_c14_deploy_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: Run praisonai-agents subscription-auth smoke tests
|
||||
env:
|
||||
PRAISONAI_TEST_TIER: smoke
|
||||
PRAISONAI_ALLOW_NETWORK: '0'
|
||||
OPENAI_MODEL_NAME: gpt-4o-mini
|
||||
OPENAI_API_KEY: 'test-key'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src/praisonai-agents
|
||||
python -m pytest tests/smoke/test_subscription_auth.py \
|
||||
tests/unit/auth/test_agent_auth_wiring.py \
|
||||
-q --tb=line --timeout=60 --maxfail=5
|
||||
|
||||
- name: Standalone praisonai-code import smoke
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv /tmp/praisonai-code-standalone
|
||||
/tmp/praisonai-code-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-code
|
||||
/tmp/praisonai-code-standalone/bin/python -c "
|
||||
import praisonai_code
|
||||
from praisonai_code.cli.configuration.resolver import ConfigResolver
|
||||
from praisonai_code._version import get_package_version
|
||||
from praisonai_code.llm.credentials import is_configured
|
||||
from praisonai_code.llm.catalogue import ModelCatalogue
|
||||
import toml
|
||||
assert hasattr(ConfigResolver, '__name__')
|
||||
assert callable(is_configured)
|
||||
assert ModelCatalogue().list_models(provider='openai')
|
||||
import praisonai_code.cli.main
|
||||
import praisonai_code.cli.app
|
||||
from praisonai_code.tool_resolver import ToolResolver
|
||||
assert ToolResolver is not None
|
||||
print('standalone ok', get_package_version(), 'toml', toml.__version__)
|
||||
"
|
||||
/tmp/praisonai-code-standalone/bin/praisonai-code --version
|
||||
/tmp/praisonai-code-standalone/bin/praisonai-code run --help
|
||||
cd src/praisonai-code
|
||||
for f in praisonai_code/cli/main.py praisonai_code/cli/app.py \
|
||||
praisonai_code/cli/commands/run.py praisonai_code/cli/commands/chat.py \
|
||||
praisonai_code/cli/commands/code.py; do
|
||||
if grep -E '^from praisonai\.|^import praisonai' "$f"; then
|
||||
echo "FAIL: module-level wrapper import in $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "C7 hot-path import gate ok"
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c7_imports.sh"
|
||||
# Standalone actions run (no wrapper); default run/chat require praisonai
|
||||
set +e
|
||||
RUN_OUT=$(timeout 90 /tmp/praisonai-code-standalone/bin/praisonai-code run --output actions "Say hello in one word" 2>&1)
|
||||
RUN_CODE=$?
|
||||
set -e
|
||||
echo "$RUN_OUT" | tail -20
|
||||
if echo "$RUN_OUT" | grep -qE 'ImportError|ModuleNotFoundError: No module named .praisonai'; then
|
||||
echo "FAIL: standalone actions run hit wrapper import error"
|
||||
exit 1
|
||||
fi
|
||||
echo "standalone actions run import gate ok (exit $RUN_CODE)"
|
||||
set +e
|
||||
DEFAULT_RUN=$(timeout 15 /tmp/praisonai-code-standalone/bin/praisonai-code run "hi" 2>&1)
|
||||
set -e
|
||||
echo "$DEFAULT_RUN" | tail -10
|
||||
if ! echo "$DEFAULT_RUN" | grep -qi 'install praisonai'; then
|
||||
echo "FAIL: default run should prompt for praisonai wrapper install"
|
||||
exit 1
|
||||
fi
|
||||
echo "default run wrapper gate ok"
|
||||
set +e
|
||||
CHAT_RUN=$(timeout 10 /tmp/praisonai-code-standalone/bin/praisonai-code chat 2>&1)
|
||||
set -e
|
||||
echo "$CHAT_RUN" | tail -10
|
||||
if ! echo "$CHAT_RUN" | grep -qi 'install praisonai'; then
|
||||
echo "FAIL: chat should prompt for praisonai wrapper install"
|
||||
exit 1
|
||||
fi
|
||||
echo "chat wrapper gate ok"
|
||||
|
||||
- name: Standalone praisonai-bot smoke (C9)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv /tmp/praisonai-bot-standalone
|
||||
/tmp/praisonai-bot-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-bot
|
||||
/tmp/praisonai-bot-standalone/bin/python -c "
|
||||
from praisonai_bot._version import __version__ as pkg_version
|
||||
import praisonai_bot
|
||||
from praisonai_bot.bots import Bot
|
||||
from praisonai_bot.bots._registry import resolve_adapter
|
||||
from praisonai_bot.scheduler.executor import ScheduledAgentExecutor
|
||||
from praisonai_bot.gateway import WebSocketGateway
|
||||
assert praisonai_bot.__version__ == pkg_version
|
||||
telegram = resolve_adapter('telegram')
|
||||
assert telegram.__name__ == 'TelegramBot'
|
||||
assert telegram.__module__.startswith('praisonai_bot.bots.')
|
||||
assert ScheduledAgentExecutor.__module__.startswith('praisonai_bot.')
|
||||
print('bot standalone ok', praisonai_bot.__version__, telegram.__name__)
|
||||
"
|
||||
/tmp/praisonai-bot-standalone/bin/praisonai-bot --help
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c9_bot_imports.sh"
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_bot_code_imports.sh"
|
||||
/tmp/praisonai-bot-standalone/bin/python -c "
|
||||
import praisonai_bot.cli.commands.bot as bot_cmd
|
||||
import praisonai_bot.cli.commands.gateway as gw_cmd
|
||||
print('bot command modules import ok', bot_cmd.app, gw_cmd.app)
|
||||
"
|
||||
|
||||
- name: Standalone praisonai-train smoke (C10)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv /tmp/praisonai-train-standalone
|
||||
/tmp/praisonai-train-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-train
|
||||
/tmp/praisonai-train-standalone/bin/python -c "
|
||||
import sys
|
||||
from praisonai_train._version import __version__ as pkg_version
|
||||
import praisonai_train
|
||||
assert praisonai_train.__version__ == pkg_version
|
||||
trainer_cls = praisonai_train.AgentTrainer
|
||||
scenario_cls = praisonai_train.TrainingScenario
|
||||
assert trainer_cls.__module__.startswith('praisonai_train.')
|
||||
for heavy in ('torch', 'unsloth', 'trl', 'datasets'):
|
||||
assert heavy not in sys.modules, f'{heavy} imported eagerly'
|
||||
print('train standalone ok', praisonai_train.__version__, trainer_cls.__name__)
|
||||
"
|
||||
/tmp/praisonai-train-standalone/bin/praisonai-train --help
|
||||
/tmp/praisonai-train-standalone/bin/python -c "
|
||||
import praisonai_train.cli.commands.train as train_cmd
|
||||
print('train command module imports ok', train_cmd.app)
|
||||
"
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c10_train_imports.sh"
|
||||
|
||||
- name: Standalone praisonai-browser smoke (C11)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv /tmp/praisonai-browser-standalone
|
||||
/tmp/praisonai-browser-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-browser
|
||||
/tmp/praisonai-browser-standalone/bin/python -c "
|
||||
import sys
|
||||
from praisonai_browser._version import __version__ as pkg_version
|
||||
import praisonai_browser
|
||||
assert praisonai_browser.__version__ == pkg_version
|
||||
server_cls = praisonai_browser.BrowserServer
|
||||
assert server_cls.__name__ == 'BrowserServer'
|
||||
for heavy in ('fastapi', 'uvicorn', 'playwright'):
|
||||
assert heavy not in sys.modules, f'{heavy} imported eagerly'
|
||||
print('browser standalone ok', praisonai_browser.__version__, server_cls.__name__)
|
||||
"
|
||||
/tmp/praisonai-browser-standalone/bin/praisonai-browser --help
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c11_browser_imports.sh"
|
||||
python -m pytest src/praisonai-browser/tests/browser/test_protocol_compliance.py -q -m "not network"
|
||||
|
||||
- name: Standalone praisonai-mcp smoke (C12)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv /tmp/praisonai-mcp-standalone
|
||||
/tmp/praisonai-mcp-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-mcp
|
||||
/tmp/praisonai-mcp-standalone/bin/python -c "
|
||||
import sys
|
||||
from praisonai_mcp._version import __version__ as pkg_version
|
||||
import praisonai_mcp
|
||||
assert praisonai_mcp.__version__ == pkg_version
|
||||
for heavy in ('fastapi', 'uvicorn'):
|
||||
assert heavy not in sys.modules, f'{heavy} imported eagerly'
|
||||
print('mcp standalone ok', praisonai_mcp.__version__)
|
||||
"
|
||||
/tmp/praisonai-mcp-standalone/bin/praisonai-mcp --help
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c12_mcp_imports.sh"
|
||||
python -m pytest src/praisonai-mcp/tests/mcp_server -q -m "not network"
|
||||
|
||||
- name: Standalone praisonai-sandbox smoke (C13)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv /tmp/praisonai-sandbox-standalone
|
||||
/tmp/praisonai-sandbox-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-sandbox
|
||||
/tmp/praisonai-sandbox-standalone/bin/python -c "
|
||||
from praisonai_sandbox._version import __version__ as pkg_version
|
||||
import praisonai_sandbox
|
||||
assert praisonai_sandbox.__version__ == pkg_version
|
||||
for heavy in ('docker', 'modal', 'e2b'):
|
||||
assert heavy not in __import__('sys').modules
|
||||
print('sandbox standalone ok', praisonai_sandbox.__version__)
|
||||
"
|
||||
/tmp/praisonai-sandbox-standalone/bin/praisonai-sandbox --help
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c13_sandbox_imports.sh"
|
||||
python -m pytest src/praisonai-sandbox/tests -q -m "not network"
|
||||
|
||||
- name: Standalone praisonai-deploy smoke (C14)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv /tmp/praisonai-deploy-standalone
|
||||
/tmp/praisonai-deploy-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-deploy
|
||||
/tmp/praisonai-deploy-standalone/bin/python -c "
|
||||
from praisonai_deploy._version import __version__ as pkg_version
|
||||
import praisonai_deploy
|
||||
assert praisonai_deploy.__version__ == pkg_version
|
||||
assert 'main' not in __import__('sys').modules
|
||||
print('deploy standalone ok', praisonai_deploy.__version__)
|
||||
"
|
||||
/tmp/praisonai-deploy-standalone/bin/praisonai-deploy --help
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c14_deploy_imports.sh"
|
||||
python -m pytest src/praisonai-deploy/tests -q -m "not network"
|
||||
|
||||
- name: Audit hybrid module imports (C8 full stack)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pip install -q -e src/praisonai-agents -e src/praisonai-code -e src/praisonai-bot -e src/praisonai-train -e src/praisonai-browser -e src/praisonai-mcp -e src/praisonai-sandbox -e src/praisonai-deploy -e src/praisonai
|
||||
python scripts/audit_hybrid_modules.py
|
||||
--maxfail=5 \
|
||||
|| echo "Some smoke tests failed"
|
||||
|
||||
- name: Smoke Test Summary
|
||||
if: always()
|
||||
@@ -317,15 +98,15 @@ jobs:
|
||||
main:
|
||||
needs: smoke
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.11']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -343,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'
|
||||
@@ -407,8 +187,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -430,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'
|
||||
@@ -480,8 +259,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -494,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'
|
||||
@@ -540,8 +318,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -554,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'
|
||||
@@ -600,8 +377,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -614,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'
|
||||
@@ -659,8 +436,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -673,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'
|
||||
@@ -718,8 +495,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -732,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'
|
||||
@@ -759,204 +535,11 @@ jobs:
|
||||
if: steps.check-key.outputs.has_key == 'false'
|
||||
run: echo "✅ Cohere tests skipped (no API key available)"
|
||||
|
||||
# ============================================
|
||||
# OLLAMA LIVE TESTS - Real local model, no API key
|
||||
# Nightly / extended only. Installs Ollama on the runner and pulls a small
|
||||
# tools-capable model. Unlike the other live jobs this one needs no secret,
|
||||
# so its required step has no "|| echo" fallback: a failure here is a real
|
||||
# regression in local-model tool calling, not a missing credential.
|
||||
# ============================================
|
||||
extended-ollama:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
# Bump deliberately: this is the version everything in this job was
|
||||
# verified against. Latest at time of writing is v0.33.3.
|
||||
OLLAMA_VERSION: v0.33.2
|
||||
# 522 MB, capabilities: completion, tools, thinking.
|
||||
OLLAMA_TEST_MODEL: qwen3:0.6b
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Cache pip packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-ollama-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Cache Ollama model blobs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
# No restore-keys on purpose: a partial hit from a different model would
|
||||
# silently test something other than OLLAMA_TEST_MODEL.
|
||||
key: ${{ runner.os }}-ollama-models-${{ env.OLLAMA_TEST_MODEL }}
|
||||
|
||||
- name: Install Ollama
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The official installer, but VERSION-PINNED via OLLAMA_VERSION (which it
|
||||
# honours) so this job cannot silently change what it tests. Fetching the
|
||||
# release asset directly is not a workable alternative: since v0.33 the
|
||||
# linux bundle ships as a 1.4 GB .tar.zst with GPU runtimes bundled in,
|
||||
# and the installer already handles the zst/tgz split across versions.
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq zstd
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
ollama --version
|
||||
|
||||
- name: Start Ollama and pull model
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ollama serve > /tmp/ollama-serve.log 2>&1 &
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://127.0.0.1:11434/api/tags > /dev/null; then
|
||||
echo "Ollama is up after ${i}s"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
curl -sf http://127.0.0.1:11434/api/tags > /dev/null
|
||||
ollama pull "$OLLAMA_TEST_MODEL"
|
||||
ollama list
|
||||
|
||||
- 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]"
|
||||
|
||||
- name: Run Ollama minimum contract (required)
|
||||
env:
|
||||
PRAISONAI_TEST_TIER: extended
|
||||
PRAISONAI_ALLOW_NETWORK: '1'
|
||||
PRAISONAI_LIVE_TESTS: '1'
|
||||
PRAISONAI_TEST_PROVIDERS: 'ollama'
|
||||
PRAISONAI_TEST_OLLAMA: '1'
|
||||
PRAISONAI_OLLAMA_TEST_MODEL: ${{ env.OLLAMA_TEST_MODEL }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src/praisonai
|
||||
# No "|| echo" here: no secret is involved, so a failure is a real
|
||||
# regression rather than a missing credential.
|
||||
python -m pytest tests/integration/test_ollama_tool_calling_live.py::TestOllamaCIMinimum \
|
||||
-v --tb=short \
|
||||
--timeout=120
|
||||
|
||||
- name: Run remaining Ollama live tests (informational)
|
||||
if: always()
|
||||
env:
|
||||
PRAISONAI_TEST_TIER: extended
|
||||
PRAISONAI_ALLOW_NETWORK: '1'
|
||||
PRAISONAI_LIVE_TESTS: '1'
|
||||
PRAISONAI_TEST_PROVIDERS: 'ollama'
|
||||
PRAISONAI_TEST_OLLAMA: '1'
|
||||
PRAISONAI_OLLAMA_TEST_MODEL: ${{ env.OLLAMA_TEST_MODEL }}
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/integration/test_ollama_tool_calling_live.py \
|
||||
--deselect tests/integration/test_ollama_tool_calling_live.py::TestOllamaCIMinimum \
|
||||
-v --tb=short \
|
||||
--timeout=180 \
|
||||
|| echo "Ollama exploratory tests completed (small models are non-deterministic)"
|
||||
|
||||
- name: Ollama server log
|
||||
if: failure()
|
||||
run: tail -100 /tmp/ollama-serve.log
|
||||
|
||||
# ============================================
|
||||
# LEGACY UNIT TESTS - Post-C6 re-enabled suite
|
||||
# Nightly / extended tier only (~401 tests)
|
||||
# ============================================
|
||||
legacy-unit:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
with:
|
||||
agents-extras: ".[all]"
|
||||
wrapper-extras: ".[chat,code,crewai,autogen]"
|
||||
|
||||
- name: Run post-C6 legacy unit tests
|
||||
env:
|
||||
PRAISONAI_TEST_TIER: extended
|
||||
PRAISONAI_ALLOW_NETWORK: '0'
|
||||
PRAISONAI_ALLOW_JOB_WORKFLOWS: 'true'
|
||||
PRAISONAI_TOOL_SAFETY: 'off'
|
||||
OPENAI_API_KEY: 'test-key'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src/praisonai
|
||||
LEGACY_FILES=(
|
||||
tests/unit/test_warm_runtime.py
|
||||
tests/unit/test_yaml_cli_backend.py
|
||||
tests/unit/test_tool_resolver.py
|
||||
tests/unit/test_serve_unified.py
|
||||
tests/unit/test_security_fixes_core.py
|
||||
tests/unit/test_query_rewriter_cli.py
|
||||
tests/unit/test_kanban_sqlite_store.py
|
||||
tests/unit/test_job_workflow_agent_steps.py
|
||||
tests/unit/test_hybrid_workflow.py
|
||||
../praisonai-bot/tests/unit/test_gateway_session.py
|
||||
../praisonai-bot/tests/unit/test_gateway_gaps.py
|
||||
tests/unit/test_discord_approval.py
|
||||
tests/unit/test_decorator_simple.py
|
||||
tests/unit/test_db_adapter_tracing_init.py
|
||||
../praisonai-bot/tests/unit/test_bot_wiring.py
|
||||
../praisonai-bot/tests/unit/bots/test_enable_shell.py
|
||||
../praisonai-bot/tests/unit/gateway/test_preflight.py
|
||||
../praisonai-bot/tests/unit/gateway/test_gateway_doctor.py
|
||||
../praisonai-bot/tests/unit/test_bot_history.py
|
||||
../praisonai-bot/tests/unit/test_bot_fixes.py
|
||||
tests/unit/test_auto_lazy_loading.py
|
||||
tests/unit/test_async_daemon_deployment.py
|
||||
tests/unit/test_approval_interactive.py
|
||||
tests/unit/test_approval_agent_integration.py
|
||||
tests/unit/test_agents_yaml_validation.py
|
||||
tests/unit/test_agent_os_wrapper.py
|
||||
tests/unit/test_lazy_imports.py
|
||||
tests/unit/test_profiler.py
|
||||
tests/unit/test_profiler_advanced.py
|
||||
tests/unit/test_serverless_postgres.py
|
||||
)
|
||||
python -m pytest "${LEGACY_FILES[@]}" \
|
||||
-q --tb=line --timeout=120 --maxfail=10
|
||||
|
||||
# ============================================
|
||||
# TEST SUMMARY
|
||||
# ============================================
|
||||
test-summary:
|
||||
needs: [smoke, main, extended-ollama]
|
||||
needs: [smoke, main]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -45,8 +45,10 @@ jobs:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -61,12 +63,11 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
with:
|
||||
use-uv: "true"
|
||||
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
|
||||
@@ -7,20 +7,18 @@ on:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-windows.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-windows.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-windows:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_MODEL_NAME: gpt-4o-mini
|
||||
@@ -35,10 +33,18 @@ jobs:
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install monorepo packages
|
||||
run: python -m pip install --upgrade pip
|
||||
- name: Install praisonaiagents
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e .
|
||||
shell: bash
|
||||
|
||||
- name: Install praisonai wrapper
|
||||
run: |
|
||||
cd src/praisonai
|
||||
pip install -e .
|
||||
shell: bash
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
|
||||
# ==================== CORE IMPORTS ====================
|
||||
- name: Test core imports
|
||||
@@ -176,7 +182,6 @@ jobs:
|
||||
echo "Testing both entry point methods..."
|
||||
python -m praisonai --version
|
||||
praisonai --version
|
||||
praisonai-bot gateway --help
|
||||
|
||||
- name: Test doctor packaging command
|
||||
run: |
|
||||
|
||||
@@ -6,11 +6,7 @@ on:
|
||||
paths:
|
||||
- 'src/praisonai-agents/praisonaiagents/**/*.py'
|
||||
- 'src/praisonai-ts/src/**/*.ts'
|
||||
# The crate lives at src/praisonai-rust/praisonai/src -- the old
|
||||
# 'src/praisonai-rust/src/**' path does not exist, so a Rust change
|
||||
# never triggered either tracker and the Rust numbers only ever
|
||||
# refreshed incidentally, on an unrelated push.
|
||||
- 'src/praisonai-rust/praisonai/src/**/*.rs'
|
||||
- 'src/praisonai-rust/src/**/*.rs'
|
||||
- 'src/praisonai/praisonai/_dev/parity/**/*.py'
|
||||
- '.github/workflows/update-docs-parity.yml'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
name: Update Feature Parity Tracker
|
||||
|
||||
# Regenerates the exported-name parity trackers on push to main and commits them.
|
||||
# Validation on pull requests (fail on drift) lives in parity-gate.yml.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/praisonai-agents/praisonaiagents/**/*.py'
|
||||
- 'src/praisonai-ts/src/**/*.ts'
|
||||
# The crate lives at src/praisonai-rust/praisonai/src -- the old
|
||||
# 'src/praisonai-rust/src/**' path does not exist, so a Rust change
|
||||
# never triggered either tracker and the Rust numbers only ever
|
||||
# refreshed incidentally, on an unrelated push.
|
||||
- 'src/praisonai-rust/praisonai/src/**/*.rs'
|
||||
# The generator's own code and the wrapper features it counts. Change
|
||||
# either and nothing regenerated until an unrelated push landed.
|
||||
- 'src/praisonai/praisonai/_dev/parity/**/*.py'
|
||||
- 'src/praisonai/praisonai/cli/features/**/*.py'
|
||||
- 'src/praisonai-rust/src/**/*.rs'
|
||||
- '.github/workflows/update-parity-tracker.yml'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-parity-tracker:
|
||||
# Push only. On a PR the parity-gate.yml `names` job CHECKS instead --
|
||||
# committing a regenerated tracker onto a contributor's branch is not this
|
||||
# workflow's business, and would fail on a fork besides.
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -46,45 +32,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pip install pyyaml
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install the TypeScript compiler only
|
||||
# The signature extractor needs `typescript` and nothing else;
|
||||
# praisonai-ts's own dependency tree does not install cleanly.
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/parity-ts"
|
||||
npm install --prefix "$RUNNER_TEMP/parity-ts" --no-audit --no-fund [email protected]
|
||||
echo "PARITY_TS_NODE_MODULES=$RUNNER_TEMP/parity-ts/node_modules" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Generate parity trackers
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
python3 src/praisonai/scripts/generate_parity_tracker.py
|
||||
|
||||
- name: Regenerate the signature report
|
||||
# Source locations in the report drift whenever a covered file is edited.
|
||||
# The pull-request gate ignores line-number-only drift so it never reddens
|
||||
# a build over it; refreshing here keeps the committed line numbers true.
|
||||
env:
|
||||
PYTHONPATH: src/praisonai
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
python3 -m praisonai._dev.parity.signatures --write
|
||||
|
||||
- name: Regenerate the behaviour report
|
||||
env:
|
||||
PYTHONPATH: src/praisonai
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
python3 -m praisonai._dev.parity.behaviour --write
|
||||
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if git diff --quiet src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md src/praisonai-ts/SIGNATURE_PARITY.md src/praisonai-ts/signature-parity.json src/praisonai-ts/BEHAVIOUR_PARITY.md src/praisonai-ts/behaviour-parity.json 2>/dev/null; then
|
||||
if git diff --quiet src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md 2>/dev/null; then
|
||||
echo "changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes to parity trackers"
|
||||
else
|
||||
@@ -98,7 +54,8 @@ jobs:
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "docs: auto-update feature parity trackers [skip ci]"
|
||||
file_pattern: 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md src/praisonai-ts/SIGNATURE_PARITY.md src/praisonai-ts/signature-parity.json src/praisonai-ts/BEHAVIOUR_PARITY.md src/praisonai-ts/behaviour-parity.json'
|
||||
file_pattern: 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md'
|
||||
commit_user_name: "MervinPraison"
|
||||
commit_user_email: "[email protected]"
|
||||
commit_author: "MervinPraison <[email protected]>"
|
||||
|
||||
+1
-21
@@ -12,7 +12,6 @@ assets
|
||||
.DS_Store
|
||||
.pytest_cache
|
||||
praisonAI.egg-info
|
||||
src/*/build/
|
||||
flagged
|
||||
test.yaml
|
||||
db
|
||||
@@ -23,24 +22,11 @@ other
|
||||
output
|
||||
!src/praisonai/praisonai/cli/output/
|
||||
!src/praisonai-agents/praisonaiagents/output/
|
||||
!src/praisonai-code/praisonai_code/cli/output/
|
||||
!src/praisonai-bot/praisonai_bot/cli/output/
|
||||
!src/praisonai-ts/src/db/
|
||||
!src/praisonai-ts/src/cli/output/
|
||||
|
||||
.files
|
||||
threads.db
|
||||
threads.db-journal
|
||||
|
||||
# Example run artifacts (generated at runtime, must not be committed)
|
||||
examples/**/knowledge.db
|
||||
examples/**/knowledge.db-journal
|
||||
examples/python/managed-agents/managed_ids.json
|
||||
examples/python/save_output/blog_post.md
|
||||
examples/python/save_output/generated/
|
||||
examples/python/token-metrics/token_metrics_export.json
|
||||
examples/python/usecases/market_research_results.json
|
||||
|
||||
.chainlit
|
||||
!praisonai/ui/config/.chainlit
|
||||
|
||||
@@ -102,9 +88,6 @@ issue_details.json
|
||||
.internal
|
||||
.praisonai
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
|
||||
# Rust / Cargo
|
||||
target/
|
||||
Cargo.lock
|
||||
@@ -115,7 +98,4 @@ Cargo.lock
|
||||
src/praisonai/build*.egg-info/
|
||||
src/praisonai/PraisonAI.egg-info/
|
||||
jobs/*
|
||||
*.egg-info
|
||||
# Local Android SDK/NDK/JDK for building praisonai-mobile on a laptop without
|
||||
# Android Studio (how: .github/workflows/mobile-platforms.yml). Several GB.
|
||||
.android-sdk/
|
||||
*.egg-info
|
||||
@@ -1,28 +0,0 @@
|
||||
# Agent Instructions
|
||||
|
||||
You are working on the PraisonAI project.
|
||||
|
||||
## Project Guidelines
|
||||
- Follow the existing code style and conventions
|
||||
- Be concise and helpful in responses
|
||||
- Test implementation thoroughly
|
||||
- Ensure backward compatibility with existing APIs
|
||||
- Follow protocol-driven design across the nine Python PyPI packages plus the TypeScript SDK: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, deployment in `praisonai-deploy/`, integrations/serve/dashboard in the `praisonai/` wrapper, TypeScript/JavaScript SDK in `src/praisonai-ts/` (`npm: praisonai`)
|
||||
- **TypeScript review routing:** For TS/JS issues and PRs, read [`src/praisonai-ts/AGENTS.md`](src/praisonai-ts/AGENTS.md) (not just this file). Canonical source is `src/praisonai-ts/` in this monorepo; [MervinPraison/praisonai-js](https://github.com/MervinPraison/praisonai-js) is the npm mirror only (sync: monorepo → praisonai-js via workflow **Sync to praisonai-js**). Do not implement TS fixes in praisonai-js for PraisonAI issues.
|
||||
- Preserve old `praisonai.*` import paths via shims when moving code between packages (see §2.3 in `src/praisonai-agents/AGENTS.md`; shim helpers in `src/praisonai/praisonai/cli/_shim.py`)
|
||||
- Package boundaries and dependency rules: `ARCHITECTURE.md` §2 (Tier 2 packages must never PyPI-depend on the wrapper; cross-tier access goes through lazy `_*_bridge` modules)
|
||||
- Boundary manifests: `src/praisonai/tests/PRAISONAI_BOT_MANIFEST.md` (C9), `src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md` (C10), `src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md` (C11), `src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md` (C12), `src/praisonai/tests/PRAISONAI_SANDBOX_MANIFEST.md` (C13), `src/praisonai/tests/PRAISONAI_DEPLOY_MANIFEST.md` (C14)
|
||||
- When reviewing a PR or an issue, evaluate whether the change addresses a framework concern or a user goal, and design its surface (params, naming, defaults) accordingly
|
||||
- The aim of this package is to stay **lightweight and powerful**. Do a critical review at each stage — when triaging an issue, when planning a fix, and when reviewing/implementing a PR. Reject scope creep for the sake of adding features: if a capability already exists (e.g. via existing Agent params like `instructions`/`backstory`/`tools`/`hooks`/`memory`), prefer it over a new API surface. A change must genuinely strengthen the SDK (simpler, more robust, more user-friendly) — do not add knobs, params, modules, or exports that have no live consumer or that merely duplicate existing behaviour.
|
||||
|
||||
## Issue and PR routing
|
||||
|
||||
| Area | Where to implement | AGENTS.md to read | Tests |
|
||||
|------|-------------------|-------------------|-------|
|
||||
| Python core SDK | `src/praisonai-agents/praisonaiagents/` | `src/praisonai-agents/AGENTS.md` | `pytest` in `src/praisonai-agents/tests/` |
|
||||
| Python wrapper / CLI | `src/praisonai/`, `src/praisonai-code/`, etc. | `src/praisonai-agents/AGENTS.md` | `pytest` under `src/praisonai/tests/` |
|
||||
| **TypeScript / JavaScript SDK** | **`src/praisonai-ts/`** | **`src/praisonai-ts/AGENTS.md`** | `cd src/praisonai-ts && npm run build && npm test` |
|
||||
| Agent-callable tools | [PraisonAI-Tools](https://github.com/MervinPraison/PraisonAI-Tools) | — | repo tests |
|
||||
| Lifecycle plugins | [PraisonAI-Plugins](https://github.com/MervinPraison/PraisonAI-Plugins) | — | repo tests |
|
||||
| Documentation | [PraisonAIDocs](https://github.com/MervinPraison/PraisonAIDocs) | — | `nav-check` |
|
||||
| npm mirror (read-only for fixes) | [praisonai-js](https://github.com/MervinPraison/praisonai-js) | mirror of `src/praisonai-ts/AGENTS.md` | CI on praisonai-js |
|
||||
-593
@@ -1,593 +0,0 @@
|
||||
# PraisonAI Architecture
|
||||
|
||||
> **Last updated:** 2026-07-31 (C14 — nine-package tiered model)
|
||||
>
|
||||
> Strategic architecture document for PraisonAI — a multi-agent AI framework.
|
||||
> Covers **Python tiered package model (C7.1 + C9 + C10)**, system design, runtime
|
||||
> architecture, data contracts, reliability, observability, and the road map.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#1-executive-summary)
|
||||
2. [Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13 + C14)](#2-python-tiered-package-model-c71--c9--c10--c11--c12--c13--c14)
|
||||
3. [System Overview](#3-system-overview)
|
||||
4. [Layered Architecture](#4-layered-architecture)
|
||||
5. [Core Data Contracts](#5-core-data-contracts)
|
||||
6. [Reliability by Design](#6-reliability-by-design)
|
||||
7. [Observability & Telemetry](#7-observability--telemetry)
|
||||
8. [Replay & Checkpointing](#8-replay--checkpointing)
|
||||
9. [Implementation Roadmap](#9-implementation-roadmap)
|
||||
10. [Success Metrics](#10-success-metrics)
|
||||
|
||||
**Related boundary docs:** [`src/praisonai/tests/C7.1_BOUNDARIES.md`](src/praisonai/tests/C7.1_BOUNDARIES.md) · [`src/praisonai/tests/C9.1_BOUNDARIES.md`](src/praisonai/tests/C9.1_BOUNDARIES.md) · [`src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md`](src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md) · [`src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md`](src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md) · [`src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md`](src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md) · [`src/praisonai/tests/C7_VERIFICATION.md`](src/praisonai/tests/C7_VERIFICATION.md) · [`src/praisonai/tests/C9_VERIFICATION.md`](src/praisonai/tests/C9_VERIFICATION.md) · [`src/praisonai-agents/AGENTS.md`](src/praisonai-agents/AGENTS.md) §2.4
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
PraisonAI is a multi-agent AI framework with broad capability surface across
|
||||
Python, TypeScript, and Rust SDKs. The framework's core strength is its
|
||||
feature breadth — agent abstractions, integrations, workflows, and active
|
||||
release cadence.
|
||||
|
||||
The strategic bet for the next two quarters is building a **Reliability +
|
||||
Orchestration + Observability** core to unlock adoption and trust:
|
||||
|
||||
| Dimension | Current State | Target |
|
||||
|-----------|--------------|--------|
|
||||
| Reliability | Cross-platform mismatches, optional dep fragility | Deterministic gating, adapter abstraction, parity CI |
|
||||
| Determinism | Flaky tests, global state collisions | Isolated test fixtures, traceable tenant IDs |
|
||||
| Developer UX | Complex onboarding, implicit config | Golden-path CLI, Doctor Auto-Fix |
|
||||
| Observability | Minimal runtime tracing | Structured event bus, replay engine, failure classifier |
|
||||
|
||||
### Design Principles
|
||||
|
||||
- **Deterministic by default**, flexible by opt-in
|
||||
- **Policy-enforced execution boundaries** at every layer
|
||||
- **Structured events** for all lifecycle transitions
|
||||
- **Capability isolation** per agent and per tool
|
||||
|
||||
---
|
||||
|
||||
## 2. Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13 + C14)
|
||||
|
||||
**Release:** v4.6.110+ · `praisonaiagents` · `praisonai-code` · `praisonai-bot` · `praisonai-train` · `praisonai-browser` · `praisonai-mcp` · `praisonai-sandbox` · `praisonai-deploy` · `praisonai`
|
||||
|
||||
The Python monorepo publishes nine packages in three tiers with strict dependency
|
||||
direction. C7 delivered a standalone agentic hot path; C7.1 formalised
|
||||
code/wrapper ownership; C9 extracted bots, gateway, and channel CLI into
|
||||
`praisonai-bot`; C10 extracted LLM fine-tuning and agent training into
|
||||
`praisonai-train`; C11 extracted browser automation into `praisonai-browser`;
|
||||
C12 extracted the heavy MCP host into `praisonai-mcp`.
|
||||
C13 extracted sandbox backends into `praisonai-sandbox`.
|
||||
C14 extracted deployment (API, Docker, cloud) into `praisonai-deploy`.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph tier1 [Tier 1 — Core SDK]
|
||||
Agents["praisonaiagents<br/>Agent, tools, memory, hooks, protocols"]
|
||||
end
|
||||
|
||||
subgraph tier2 [Tier 2 — Terminal + Bot + Train + Browser + MCP + Sandbox + Deploy]
|
||||
Code["praisonai-code<br/>run, chat, code, Typer, runtime, LLM"]
|
||||
Bot["praisonai-bot<br/>bots, gateway, channel CLI, OS daemon"]
|
||||
Train["praisonai-train<br/>LLM fine-tuning, agent training"]
|
||||
Browser["praisonai-browser<br/>extension bridge, CDP, Playwright"]
|
||||
MCP["praisonai-mcp<br/>MCP server host, auth, capability adapters"]
|
||||
Sandbox["praisonai-sandbox<br/>Docker, E2B, Modal, Sandlock backends"]
|
||||
Deploy["praisonai-deploy<br/>API, Docker, AWS/Azure/GCP deploy"]
|
||||
end
|
||||
|
||||
subgraph tier3 [Tier 3 — Wrapper]
|
||||
Wrapper["praisonai<br/>framework_adapters, serve, dashboard"]
|
||||
end
|
||||
|
||||
Agents --> Code
|
||||
Agents --> Bot
|
||||
Agents --> Train
|
||||
Agents --> Browser
|
||||
Agents --> MCP
|
||||
Agents --> Sandbox
|
||||
Agents --> Deploy
|
||||
Agents --> Wrapper
|
||||
Code -.->|"lazy _bot_bridge"| Bot
|
||||
Code -.->|"lazy _train_bridge"| Train
|
||||
Code -.->|"lazy _browser_bridge"| Browser
|
||||
Code -.->|"lazy _mcp_bridge"| MCP
|
||||
Code -.->|"lazy _sandbox_bridge"| Sandbox
|
||||
Code -.->|"lazy _deploy_bridge"| Deploy
|
||||
Code -.->|"lazy _wrapper_bridge"| Wrapper
|
||||
Bot -.->|"lazy _code_bridge / _wrapper_bridge"| Code
|
||||
Bot -.->|"lazy _wrapper_bridge"| Wrapper
|
||||
Train -.->|"lazy _code_bridge / _wrapper_bridge"| Code
|
||||
```
|
||||
|
||||
**PyPI publish order:** `praisonaiagents` → `praisonai-code` + `praisonai-bot` + `praisonai-train` + `praisonai-browser` + `praisonai-mcp` + `praisonai-sandbox` + `praisonai-deploy` → `praisonai`
|
||||
|
||||
**Backward compatibility:** `praisonai.bots`, `praisonai.gateway`, `praisonai.train`,
|
||||
`praisonai.browser`, `praisonai.mcp_server`, `praisonai.sandbox`, `praisonai.deploy`, and related CLI paths remain as
|
||||
`alias_package` shims to `praisonai_bot.*` / `praisonai_train.*` /
|
||||
`praisonai_browser.*` / `praisonai_mcp.*` / `praisonai_sandbox.*` / `praisonai_deploy.*`.
|
||||
|
||||
| Tier | Package | Owns | Must not depend on |
|
||||
|------|---------|------|-------------------|
|
||||
| 1 | `src/praisonai-agents/` | Agent, tools, memory, hooks, `frameworks/` protocols, sandbox protocols | `praisonai`, `praisonai-code`, `praisonai-bot`, `praisonai-train`, `praisonai-browser`, `praisonai-mcp`, `praisonai-sandbox`, `praisonai-deploy` |
|
||||
| 2a | `src/praisonai-code/` | `run`/`chat`/`code`, Typer, runtime, LLM, tool resolution | **`praisonai` as a PyPI dependency** (optional lazy imports via `_wrapper_bridge` only) |
|
||||
| 2b | `src/praisonai-bot/` | Bots, gateway, channel CLI, OS daemon, gateway scheduler tick | **`praisonai` as a PyPI dependency** (optional lazy `_wrapper_bridge` for jobs/UI) |
|
||||
| 2c | `src/praisonai-train/` | LLM fine-tuning (Unsloth), agent training, `train` CLI, conda env setup | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for legacy dispatch) |
|
||||
| 2d | `src/praisonai-browser/` | Extension bridge, CDP/hybrid automation, `browser` CLI | **`praisonai` as a PyPI dependency** (none required; depends on `praisonaiagents` only) |
|
||||
| 2e | `src/praisonai-mcp/` | MCP server host, auth, transports, `mcp` CLI | **`praisonai` as a PyPI dependency** (lazy `_wrapper_bridge` for full capability registry) |
|
||||
| 2f | `src/praisonai-sandbox/` | Sandbox backends (Docker, E2B, Modal, Sandlock, SSH), `SandboxRegistry` | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for `PluginRegistry`) |
|
||||
| 2g | `src/praisonai-deploy/` | Deploy API/Docker/cloud, `deploy` CLI, scheduler integration | **`praisonai` as a PyPI dependency** (lazy `_plugin_registry` via `_code_bridge`) |
|
||||
| 3 | `src/praisonai/` | `framework_adapters/`, serve, dashboard, async jobs API | — |
|
||||
|
||||
### Repo infra (not PyPI)
|
||||
|
||||
Deployment packaging that stays in the git checkout, not in any tier-2 wheel:
|
||||
|
||||
| Path | Runtime owner | Orchestration |
|
||||
|------|---------------|---------------|
|
||||
| `src/praisonai-bot/infra/helm/praisonai-gateway/` | `praisonai-bot` (gateway) | C14 `deploy helm` wrapper |
|
||||
| `src/praisonai-deploy/infra/helm/praisonai-agents-api/` | Generated API from C14 | C14 cross-link |
|
||||
| `src/praisonai-deploy/infra/compose/agents-stack/` | Docker Compose | `praisonai deploy compose up/down` |
|
||||
| `src/praisonai-deploy/infra/starters/` | Starter scaffolds | `praisonai deploy create --template` |
|
||||
| `docker/` | Mixed (wrapper/bot dev stacks) | Not C14 — see bot manifest for `docker/bots/` |
|
||||
|
||||
Same boundary as C14: Helm/K8s manifests are **repo infra**, not `pip install praisonai-deploy` content.
|
||||
|
||||
**Config kernel:** Phase 0 `praisonai/common/` was skipped; shared config lives in
|
||||
`praisonai_code/cli/configuration/` and is reached by the bot tier via lazy
|
||||
`_code_bridge` (see `src/praisonai/tests/CONFIG_KERNEL.md`).
|
||||
|
||||
### Dependency rule (validated)
|
||||
|
||||
| Question | Answer |
|
||||
|----------|--------|
|
||||
| Does `praisonai-code` declare `praisonai` in `pyproject.toml`? | **No** — only `praisonaiagents` + CLI/runtime deps |
|
||||
| Does `praisonai` declare `praisonai-code`? | **Yes** — one-way chain, no PyPI cycle |
|
||||
| Can `praisonai-code` import `praisonai.*` at runtime? | **Only lazily** via `praisonai_code._wrapper_bridge` for optional features; **not** on the agentic hot path |
|
||||
| Does standalone `pip install praisonai-code` work? | **Yes** — CI smoke validates imports + `praisonai-code run` without the wrapper |
|
||||
|
||||
### CLI routing
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
User[User] --> Entry{Entry point}
|
||||
Entry -->|pip install praisonai-code| CodeCLI["praisonai-code run/chat/code"]
|
||||
Entry -->|pip install praisonai| WrapperCLI["praisonai …"]
|
||||
WrapperCLI --> Router["praisonai.__main__"]
|
||||
Router --> CodePath["praisonai_code.cli.app"]
|
||||
Router --> Legacy["praisonai.cli.main"]
|
||||
CodePath --> HotPath["run / chat / code"]
|
||||
CodePath --> Bridge{"wrapper_available?"}
|
||||
Bridge -->|yes| WrapperCmds["bot, gateway, pairing, …"]
|
||||
Bridge -->|no| Hidden["Wrapper commands hidden"]
|
||||
```
|
||||
|
||||
### Import gates (CI enforced)
|
||||
|
||||
- **Hot path:** no module-level `from praisonai` in `main.py`, `app.py`, `run.py`, `chat.py`, `code.py`
|
||||
- **Regression baseline:** 50 direct wrapper import lines max (`scripts/check_c7_imports.sh`; C8 achieved **0**)
|
||||
- **Allowlist:** reviewed files only (`scripts/c7_wrapper_import_allowlist.txt`; empty post-C8)
|
||||
- **Hybrid audit:** `scripts/audit_hybrid_modules.py` — repatriated cross-tier import paths
|
||||
|
||||
### C7 / C7.1 / C8 status
|
||||
|
||||
| Milestone | Status |
|
||||
|-----------|--------|
|
||||
| Standalone agentic hot path | **Complete** |
|
||||
| `_wrapper_bridge` hardening | **Complete** |
|
||||
| Import gate + allowlist | **Complete** |
|
||||
| Boundary tests + CI smoke | **Complete** |
|
||||
| C8 reverse import elimination (225 → 0 direct imports) | **Complete** — see [`C8_BACKLOG.md`](src/praisonai/tests/C8_BACKLOG.md) |
|
||||
| C8.4 main.py physical decomposition | **Deferred** (separate epic) |
|
||||
| PyPI package splits (`praisonai-bot`, etc.) | **Out of scope** |
|
||||
|
||||
---
|
||||
|
||||
## 3. System Overview
|
||||
|
||||
```
|
||||
User/SDK/CLI
|
||||
→ Workflow Compiler (graph + policy + schemas)
|
||||
→ Execution Orchestrator (state machine, retries, fallbacks)
|
||||
→ Tool Runtime (sandbox + approval + capability scope)
|
||||
→ Model Runtime (provider abstraction + rate/timeout policy)
|
||||
→ Observability Bus (trace events + metrics + cost)
|
||||
→ Replay Engine (checkpoint + deterministic re-run)
|
||||
→ Persistence (session, memory, artifacts, run ledger)
|
||||
```
|
||||
|
||||
### Runtime Components
|
||||
|
||||
| Component | Responsibility | Interface |
|
||||
|-----------|---------------|-----------|
|
||||
| API/CLI Gateway | Accepts run requests, validates config/profile | `RunRequest`, `RunProfile` |
|
||||
| Workflow Compiler | Converts YAML/DSL to normalized execution graph | `CompiledGraph` |
|
||||
| Orchestrator | Executes graph state machine with retries/fallbacks | `ExecutionState`, `StepTransition` |
|
||||
| Tool Runtime | Runs tools with approval/sandbox policies | `ToolCall`, `ToolResult` |
|
||||
| Model Runtime | Provider routing, timeout, budget control | `ModelRequest`, `ModelResponse` |
|
||||
| Observability Bus | Streams structured events + metrics | `RunEvent` schema |
|
||||
| Replay Engine | Restarts from checkpoint with deterministic inputs | `ReplayRequest` |
|
||||
|
||||
### Existing Architecture (Python SDK)
|
||||
|
||||
The Python SDK is organised as three publishable tiers (see [§2](#2-python-three-tier-package-model-c71)):
|
||||
|
||||
- `src/praisonai-agents/` — Core SDK (`Agent`, tools, memory, hooks, protocols)
|
||||
- `src/praisonai-code/` — Terminal CLI (`run`, `chat`, `code`, Typer, runtime, LLM)
|
||||
- `src/praisonai/` — Wrapper (gateway, bots, `framework_adapters/`, integrations)
|
||||
|
||||
Core execution modules live in `praisonaiagents`:
|
||||
|
||||
- `src/praisonai-agents/praisonaiagents/agent/` — Agent class, handoff, autonomy
|
||||
- `src/praisonai-agents/praisonaiagents/llm/` — Model runtime with provider routing, rate limiting, failover
|
||||
- `src/praisonai-agents/praisonaiagents/tools/` — Tool runtime with sandbox, approval, retry
|
||||
- `src/praisonai-agents/praisonaiagents/memory/` — Memory runtime (in-memory, SQLite, MongoDB, Mem0 adapters)
|
||||
- `src/praisonai-agents/praisonaiagents/knowledge/` — Knowledge management (indexing, retrieval, chunking)
|
||||
- `src/praisonai-agents/praisonaiagents/workflows/` — Workflow engine (YAML/SDK-based orchestration)
|
||||
- `src/praisonai-agents/praisonaiagents/hooks/`, `src/praisonai-agents/praisonaiagents/bus/` — Hook system and event bus
|
||||
- `src/praisonai-agents/praisonaiagents/mcp/` — MCP protocol support
|
||||
- `src/praisonai-agents/praisonaiagents/ui/a2a/`, `src/praisonai-agents/praisonaiagents/ui/a2ui/` — Agent-to-agent and agent-to-UI protocols
|
||||
|
||||
Wrapper-only surfaces remain in `src/praisonai/`:
|
||||
|
||||
- `gateway/`, `bots/` — Multi-bot orchestration (BotOS)
|
||||
- `framework_adapters/` — CrewAI, AutoGen, PraisonAI adapters
|
||||
- `cli/commands/` — Wrapper commands (`bot`, `gateway`, `pairing`, …)
|
||||
|
||||
### Multi-SDK Layout
|
||||
|
||||
- **Python Core SDK** — `src/praisonai-agents/` (`praisonaiagents`)
|
||||
- **Python Terminal CLI** — `src/praisonai-code/` (`praisonai-code`)
|
||||
- **Python Wrapper** — `src/praisonai/` (`praisonai`)
|
||||
- **TypeScript SDK** — JS/TS runtime (`src/praisonai-ts/`)
|
||||
- **Rust SDK** — High-performance Rust runtime (`src/praisonai-rust/`)
|
||||
- **UI** — Web UI applications (`src/praisonai/praisonai/ui_chat/`, `src/praisonai/praisonai/ui_agents/`, `src/praisonai/praisonai/ui_bot/`, `src/praisonai/praisonai/ui_realtime/`); shared support code lives in `src/praisonai/praisonai/ui/`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Layered Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph UX["Interface Layer"]
|
||||
CLI[CLI]
|
||||
SDK[SDK]
|
||||
API[API Gateway]
|
||||
end
|
||||
|
||||
subgraph Control["Control Plane"]
|
||||
Compiler[Workflow Compiler]
|
||||
Policy[Policy Engine]
|
||||
Orchestrator[Execution Orchestrator]
|
||||
end
|
||||
|
||||
subgraph Runtime["Runtime Plane"]
|
||||
ModelRT[Model Runtime]
|
||||
ToolRT[Tool Runtime]
|
||||
MemoryRT[Memory Runtime]
|
||||
end
|
||||
|
||||
subgraph Observe["Observability Plane"]
|
||||
EventBus[Run Event Bus]
|
||||
Metrics[Metrics + Cost]
|
||||
Replay[Replay Engine]
|
||||
end
|
||||
|
||||
subgraph Data["Data Plane"]
|
||||
Ledger[Run Ledger]
|
||||
Checkpoints[Checkpoints]
|
||||
Artifacts[Artifacts Store]
|
||||
end
|
||||
|
||||
CLI --> API
|
||||
SDK --> API
|
||||
API --> Compiler
|
||||
API --> Policy
|
||||
Compiler --> Orchestrator
|
||||
Policy --> Orchestrator
|
||||
Orchestrator --> ModelRT
|
||||
Orchestrator --> ToolRT
|
||||
Orchestrator --> MemoryRT
|
||||
Orchestrator --> EventBus
|
||||
EventBus --> Metrics
|
||||
EventBus --> Replay
|
||||
Orchestrator --> Ledger
|
||||
Orchestrator --> Checkpoints
|
||||
ToolRT --> Artifacts
|
||||
Replay --> Checkpoints
|
||||
```
|
||||
|
||||
### Interface Layer
|
||||
|
||||
- **CLI** — The `praisonai` command-line entry point
|
||||
- **SDK** — Python library API (`from praisonai import Agent`)
|
||||
- **API Gateway** — HTTP/WebSocket REST API (`praisonai api` or `a2a`/`a2ui`)
|
||||
|
||||
### Control Plane
|
||||
|
||||
- **Workflow Compiler** — Converts YAML workflows and SDK-defined graphs into
|
||||
a normalized `CompiledGraph` with adjacency validation and cycle detection.
|
||||
- **Policy Engine** — Enforces runtime policies (budget, timeout, approval
|
||||
gates, capability validation) before and during execution.
|
||||
- **Execution Orchestrator** — Drives the state machine through graph nodes,
|
||||
handling retry, fallback, and failure classification.
|
||||
|
||||
### Runtime Plane
|
||||
|
||||
- **Model Runtime** — Provider abstraction layer. Routes to OpenAI, Anthropic,
|
||||
Gemini, Ollama, DeepSeek, etc. Handles rate limiting, token tracking, and
|
||||
automatic failover between providers.
|
||||
- **Tool Runtime** — Executes tool calls with configurable sandboxing,
|
||||
approval gates, retry policies, and capability scope validation.
|
||||
- **Memory Runtime** — Manages session memory, persistent memory stores,
|
||||
and knowledge retrieval across in-memory, SQLite, and MongoDB backends.
|
||||
|
||||
### Observability Plane
|
||||
|
||||
- **Run Event Bus** — Structured event streaming for all lifecycle transitions
|
||||
(START, INPUT, MODEL_CALL, TOOL_CALL, ERROR, RETRY, COMPLETE).
|
||||
- **Metrics + Cost** — Token counting, cost tracking, and performance metrics.
|
||||
- **Replay Engine** — Deterministic replay from the last stable checkpoint,
|
||||
with state hash verification.
|
||||
|
||||
### Data Plane
|
||||
|
||||
- **Run Ledger** — Persistent record of all runs with state, metrics, and
|
||||
failure classifications.
|
||||
- **Checkpoints** — Point-in-time snapshots of run state, memory, and
|
||||
artifacts for replay and recovery.
|
||||
- **Artifacts Store** — Tool outputs, generated files, and intermediate
|
||||
results.
|
||||
|
||||
---
|
||||
|
||||
## 5. Core Data Contracts
|
||||
|
||||
### RunRequest
|
||||
|
||||
```text
|
||||
RunRequest {
|
||||
run_id: UUID,
|
||||
workflow_ref: string,
|
||||
inputs: object,
|
||||
profile: {safe_mode, budget, timeout, approval_policy},
|
||||
context: {user_id, workspace_id, env}
|
||||
}
|
||||
```
|
||||
|
||||
### RunEvent
|
||||
|
||||
```text
|
||||
RunEvent {
|
||||
ts: ISO8601,
|
||||
run_id: UUID,
|
||||
step_id: string,
|
||||
type: START | INPUT | MODEL_CALL | TOOL_CALL |
|
||||
ERROR | RETRY | COMPLETE,
|
||||
payload: object,
|
||||
cost: {tokens_in, tokens_out, usd},
|
||||
latency_ms: number
|
||||
}
|
||||
```
|
||||
|
||||
### Checkpoint
|
||||
|
||||
```text
|
||||
Checkpoint {
|
||||
run_id: UUID,
|
||||
step_id: string,
|
||||
state_hash: string,
|
||||
memory_snapshot_ref: string,
|
||||
artifact_refs: string[]
|
||||
}
|
||||
```
|
||||
|
||||
### Execution Sequence
|
||||
|
||||
```text
|
||||
1) Client submits RunRequest
|
||||
2) Gateway validates profile + schema
|
||||
3) Compiler resolves workflow → CompiledGraph
|
||||
4) Orchestrator starts node execution
|
||||
5) Node may call Model Runtime or Tool Runtime
|
||||
6) Every transition emits RunEvent
|
||||
7) On failure: classifier decides retry/fallback/abort
|
||||
8) Checkpoint stored after each critical step
|
||||
9) Final state + artifacts persisted to run ledger
|
||||
10) Replay can resume from last stable checkpoint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Reliability by Design
|
||||
|
||||
### Error Taxonomy
|
||||
|
||||
All failures are classified into one of five categories for automatic
|
||||
remediation:
|
||||
|
||||
| Category | Description | Default Action |
|
||||
|----------|-------------|----------------|
|
||||
| `config` | Invalid configuration or profile | Abort with remediation hint |
|
||||
| `dependency` | Missing optional module or capability | Degrade / skip optional path |
|
||||
| `tool` | Tool execution failure | Retry with backoff and policy cap |
|
||||
| `model` | Model API error or timeout | Fallback to alternate provider/route |
|
||||
| `infra` | Network or resource exhaustion | Circuit-breaker + replay from checkpoint |
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S[Step Failure] --> T{Failure Type?}
|
||||
T -->|Config| C1[Abort + config remediation hint]
|
||||
T -->|Dependency| C2[Degrade capability / skip optional path]
|
||||
T -->|Tool| C3[Retry with backoff and policy cap]
|
||||
T -->|Model| C4[Fallback model/provider route]
|
||||
T -->|Infra| C5[Circuit-breaker + replay from checkpoint]
|
||||
```
|
||||
|
||||
### Hardening Checklist
|
||||
|
||||
- **Dependency gates** — Optional modules declared as capabilities;
|
||||
unavailable capability results in skip/degrade, not crash.
|
||||
- **Cross-platform adapters** — OS-specific implementations behind a single
|
||||
interface (e.g., file lock adapter for Windows/Linux/macOS).
|
||||
- **Idempotent fixtures** — Integration tests use unique tenant/workspace IDs
|
||||
and guaranteed teardown.
|
||||
- **Fail-safe output mode** — CLI rendering falls back to ASCII-safe mode on
|
||||
encoding mismatch.
|
||||
- **CI parity matrix** — Run smoke workflows across Windows, Linux, and macOS.
|
||||
|
||||
### Deterministic Test Pattern
|
||||
|
||||
```python
|
||||
# Integration test pattern — always use unique, traceable IDs
|
||||
import uuid
|
||||
|
||||
def test_agent_workflow():
|
||||
tenant_id = f"test-{uuid.uuid4().hex[:8]}"
|
||||
agent = Agent(name=f"agent-{tenant_id}", ...)
|
||||
result = agent.run("task")
|
||||
assert result.status == "success"
|
||||
# teardown is guaranteed via fixture or context manager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Observability & Telemetry
|
||||
|
||||
### Current Telemetry Stack
|
||||
|
||||
PraisonAI already includes:
|
||||
|
||||
- **OpenTelemetry integration** — Manual and auto-instrumentation for
|
||||
traces, metrics, and logs.
|
||||
- **Token tracking** — Per-call and cumulative token/cost tracking.
|
||||
- **Performance monitoring** — Real-time dashboards and monitoring views.
|
||||
- **LangTrace integration** — LangTrace provider for tracing.
|
||||
- **Run outcomes** — Structured `RunOutcome` objects with status, duration,
|
||||
token usage, and error classification.
|
||||
|
||||
### Planned Enhancements
|
||||
|
||||
- **Run Event Bus** — Stream all lifecycle events as structured `RunEvent`
|
||||
payloads.
|
||||
- **Failure classifier** — Automatic failure category detection with
|
||||
remediation hints.
|
||||
- **Run ledger visualization** — Timeline view of runs with step-by-step
|
||||
event drill-down.
|
||||
- **Replay integration** — Checkpoint-based run replay from the
|
||||
observability dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 8. Replay & Checkpointing
|
||||
|
||||
### Design
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User/Client
|
||||
participant G as Gateway
|
||||
participant C as Compiler
|
||||
participant O as Orchestrator
|
||||
participant R as Model/Tool Runtime
|
||||
participant E as Event Bus
|
||||
participant L as Run Ledger
|
||||
participant K as Checkpoint Store
|
||||
|
||||
U->>G: Submit RunRequest
|
||||
G->>C: Validate + compile workflow
|
||||
C-->>O: CompiledGraph
|
||||
O->>R: Execute next step
|
||||
R-->>O: Step output / error
|
||||
O->>E: Emit structured RunEvent
|
||||
O->>K: Persist checkpoint
|
||||
O->>L: Persist run state + metrics
|
||||
O-->>U: Final result / failure summary
|
||||
```
|
||||
|
||||
### Current State
|
||||
|
||||
- Basic run outcome persistence exists
|
||||
- Session persistence for bot/multi-turn conversations
|
||||
- Token and cost tracking per run
|
||||
|
||||
### Roadmap
|
||||
|
||||
- Full checkpoint creation at configurable step granularity
|
||||
- State hash verification for deterministic replay
|
||||
- Replay API endpoint (`POST /runs/{id}/replay`)
|
||||
- Checkpoint pruning and retention policy
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation Roadmap
|
||||
|
||||
### Completed (C7 / C7.1 — v4.6.110)
|
||||
|
||||
| Project | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| C7 hot path | Standalone `praisonai-code run/chat/code` without wrapper import | **Complete** |
|
||||
| C7.1 boundaries | Three-tier ownership, `_wrapper_bridge`, import gates | **Complete** |
|
||||
| CI parity | Smoke standalone block + pre-existing test fixes (#2560) | **Complete** |
|
||||
|
||||
### Quarter 1: Trust Foundation (Q3 2026)
|
||||
|
||||
| Priority | Project | Description | Status |
|
||||
|----------|---------|-------------|--------|
|
||||
| P0 | Reliability Core | Cross-platform hardening, optional-dep gating, deterministic fixtures | In progress |
|
||||
| P0 | Doctor Auto-Fix | Automated environment diagnosis and repairs | Planned |
|
||||
| P1 | Golden-Path CLI | Single canonical flow: init → run → test → deploy | Planned |
|
||||
| P1 | CI Parity Matrix | Cross-platform smoke tests in CI pipeline | Planned |
|
||||
|
||||
### Quarter 2: Operational Excellence (Q4 2026)
|
||||
|
||||
| Priority | Project | Description | Status |
|
||||
|----------|---------|-------------|--------|
|
||||
| P1 | Trace + Replay | Run timeline, checkpoint replay, failure classifier | Planned |
|
||||
| P1 | Failure Classifier | Automatic failure category detection with remediation | Planned |
|
||||
| P2 | Graph Studio UX | Visual deterministic orchestration editor/inspector | Planned |
|
||||
| P2 | Safe Production Profiles | Policy presets: guardrails, approvals, cost/time caps | Planned |
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title PraisonAI Architecture Program (2 Quarters)
|
||||
dateFormat YYYY-MM-DD
|
||||
section Quarter 1
|
||||
Reliability hardening + parity CI :a1, 2026-07-01, 45d
|
||||
Deterministic test isolation :a2, 2026-07-10, 50d
|
||||
Doctor Auto-Fix MVP :a3, 2026-07-20, 40d
|
||||
Golden-path CLI :a4, 2026-08-01, 45d
|
||||
section Quarter 2
|
||||
Trace bus + run ledger :b1, 2026-10-01, 45d
|
||||
Replay engine :b2, 2026-10-15, 45d
|
||||
Failure classifier :b3, 2026-11-01, 30d
|
||||
Graph UX beta + Safe profiles :b4, 2026-11-10, 50d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Success Metrics
|
||||
|
||||
### KPI Targets
|
||||
|
||||
| KPI | Target | Primary Workstream |
|
||||
|-----|--------|-------------------|
|
||||
| Time-to-first-successful run | 30-40% reduction | Golden-path CLI + Doctor Auto-Fix |
|
||||
| Cross-platform issue rate | 50% reduction | Reliability Core + adapter layer |
|
||||
| Incident triage time | 40% reduction | Trace + Replay + failure classifier |
|
||||
| CI confidence | Flaky test rate <2% | Deterministic tests + fixture isolation |
|
||||
|
||||
### Governance
|
||||
|
||||
- **Architecture Review Council** — Reviews runtime-contract changes
|
||||
- **Release Quality Gate** — Platform matrix + deterministic test thresholds
|
||||
- **Monthly telemetry review** — DX, reliability, adoption trends
|
||||
|
||||
|
||||
---
|
||||
|
||||
*This document is a living reference. Update as the architecture evolves.*
|
||||
@@ -1,26 +0,0 @@
|
||||
# Contributing to PraisonAI
|
||||
|
||||
Thank you for your interest! Here's how to get started:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## Where to report issues
|
||||
|
||||
| Topic | Report here | Where fixes land |
|
||||
|-------|-------------|------------------|
|
||||
| Python SDK, CLI, wrapper | **This repo** | `src/praisonai-agents/`, `src/praisonai/` |
|
||||
| TypeScript / JavaScript (`npm install praisonai`) | **This repo** | `src/praisonai-ts/` |
|
||||
| Cross-language parity | **This repo** | Often both Python and TypeScript paths |
|
||||
| Agent-callable tools | **This repo** | [PraisonAI-Tools](https://github.com/MervinPraison/PraisonAI-Tools) |
|
||||
| Lifecycle plugins (tracing, hooks, guardrails) | **This repo** | [PraisonAI-Plugins](https://github.com/MervinPraison/PraisonAI-Plugins) |
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Follow existing code style
|
||||
- Add tests for new features
|
||||
- Update documentation as needed
|
||||
- TypeScript changes: implement in `src/praisonai-ts/` and run `npm test` there
|
||||
- After merge, sync the npm mirror: `gh workflow run "Sync to praisonai-js" --repo MervinPraison/PraisonAI`
|
||||
@@ -35,25 +35,16 @@ curl -fsSL https://praison.ai/install.sh | bash
|
||||
<img src="https://img.shields.io/badge/Highlighted_by_Elon_Musk-000000?style=for-the-badge&logo=x&logoColor=white" alt="Highlighted by Elon Musk" />
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://github.com/MervinPraison/PraisonAI/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download_for_macOS-000000?style=for-the-badge&logo=apple&logoColor=white" alt="Download for macOS" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/MervinPraison/PraisonAI/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download_for_Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white" alt="Download for Windows" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/MervinPraison/PraisonAI/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download_for_Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Download for Linux" />
|
||||
</a>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/images/dashboard.png" alt="PraisonAI Dashboard" width="800" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/images/agentflow.gif" alt="PraisonAI AgentFlow" width="800" />
|
||||
</p>
|
||||
|
||||
```
|
||||
██████╗ ██████╗ █████╗ ██╗███████╗ ██████╗ ███╗ ██╗ █████╗ ██╗
|
||||
██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██╔═══██╗████╗ ██║ ██╔══██╗██║
|
||||
@@ -65,6 +56,12 @@ curl -fsSL https://praison.ai/install.sh | bash
|
||||
pip install praisonai
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/images/latest_ai_news_and_crawl_each_url_to_find_info.gif" alt="PraisonAI command execution" width="800" />
|
||||
</p>
|
||||
|
||||
\* `export TAVILY_API_KEY=xxxxx`
|
||||
|
||||
<div align="center">
|
||||
<a href="https://docs.praison.ai">
|
||||
<p align="center">
|
||||
@@ -109,232 +106,6 @@ agent.start("Analyze the top 3 tech trends of 2026 and format as a markdown tabl
|
||||
|
||||
---
|
||||
|
||||
## 🧬 The Five-Layer Agent Stack
|
||||
|
||||
Most frameworks hand you one or two layers and leave the rest as homework. PraisonAI covers **all five** — plus the outer layer that decides *where* your agent actually runs.
|
||||
|
||||
Each layer wraps the one inside it. When an agent misbehaves, the layer tells you where to look.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ⬡ MANAGED AGENTS — Where does it actually run? │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 5 · GRAPH — Who runs when, and who checks whom? │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 4 · LOOP — When do we stop? │ │ │
|
||||
│ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
|
||||
│ │ │ │ 3 · HARNESS — Can it act, and be checked? │ │ │ │
|
||||
│ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │
|
||||
│ │ │ │ │ 2 · CONTEXT — Is the right thing in the window? │ │ │ │ │
|
||||
│ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │
|
||||
│ │ │ │ │ │ 1 · PROMPT — Did I say it clearly? │ │ │ │ │ │
|
||||
│ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │
|
||||
│ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │
|
||||
│ │ │ └─────────────────────────────────────────────────────┘ │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Layer | The question it answers | PraisonAI |
|
||||
|:--|:--|:--|
|
||||
| **1 · Prompt** | Did I say it clearly? | `instructions=`, `role`/`goal`/`backstory`, `output=`, `templates=` |
|
||||
| **2 · Context** | Is the right thing in the window? | `memory=`, `knowledge=`, `context=`, handoff `ContextPolicy` |
|
||||
| **3 · Harness** | Can it act, and be checked? | `tools=`, `MCP()`, `guardrails=`, `approval=`, `hooks=`, `sandbox=` |
|
||||
| **4 · Loop** | When do we stop? | `execution=ExecutionConfig(...)`, `reflection=`, `autonomy=`, doom-loop detection |
|
||||
| **5 · Graph** | Who runs when, and who checks whom? | `AgentFlow`, `route()`, `parallel()`, `loop()`, `repeat()` |
|
||||
| **⬡ Managed** | *Where does it actually run?* | `tools_run_on="docker"` — one shared sandbox for the tools, or `run_on="anthropic"` for the whole agent |
|
||||
|
||||
### Layer 1 · Prompt — *Did I say it clearly?*
|
||||
|
||||
Role, instructions, examples, output format.
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Senior Data Analyst",
|
||||
goal="Turn raw numbers into decisions",
|
||||
output="verbose", # markdown-formatted output
|
||||
)
|
||||
agent.start("Summarise Q3 revenue trends")
|
||||
```
|
||||
|
||||
### Layer 2 · Context — *Is the right thing in the window?*
|
||||
|
||||
Write, select, compress, isolate — the four context operations, one parameter each.
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a support engineer.",
|
||||
memory={"user_id": "u-42"}, # write — persists across runs (needs a user_id)
|
||||
knowledge=["docs/"], # select — retrieves only what's relevant
|
||||
context="summarize", # compress — auto-compacts before the limit
|
||||
)
|
||||
```
|
||||
|
||||
> **Isolate** is `handoffs=[specialist]` — a sub-agent inherits the last few messages and the intersection of your tools, not your whole transcript. [📖 Handoffs](https://docs.praison.ai/docs/concepts/handoffs)
|
||||
|
||||
### Layer 3 · Harness — *Can it act, and be checked?*
|
||||
|
||||
*Agent = Model + Harness.* Tool dispatch, plus the guides that steer before acting and the sensors that observe after.
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, MCP, tool
|
||||
|
||||
@tool
|
||||
def deploy(env: str) -> str:
|
||||
"""Deploy the current build to an environment."""
|
||||
return f"Deployed to {env}"
|
||||
|
||||
agent = Agent(
|
||||
name="ReleaseEngineer",
|
||||
instructions="You are a release engineer.",
|
||||
tools=[deploy, MCP("npx -y @modelcontextprotocol/server-filesystem /tmp")],
|
||||
approval=True, # guide — human gate before risky tools run
|
||||
)
|
||||
agent.start("Deploy to staging, then list the files you can read")
|
||||
```
|
||||
|
||||
### Layer 4 · Loop — *When do we stop?*
|
||||
|
||||
Hard iteration caps, budget ceilings, no-progress detection and completion checks — every brake is explicit.
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, ExecutionConfig
|
||||
|
||||
agent = Agent(
|
||||
instructions="Fix the failing tests.",
|
||||
execution=ExecutionConfig(max_iter=30, max_budget=0.50, on_budget_exceeded="stop"),
|
||||
autonomy=True, # required to drive the loop with run_autonomous()
|
||||
)
|
||||
result = agent.run_autonomous("Refactor the auth module", max_iterations=5)
|
||||
|
||||
print(result.completion_reason)
|
||||
# goal | no_tool_calls | max_iterations | timeout | doom_loop | needs_help | error
|
||||
# (with on_budget_exceeded="stop", hitting the cap raises BudgetExceededError,
|
||||
# surfaced here as completion_reason="error")
|
||||
```
|
||||
|
||||
> **Doom-loop detection is on by default.** Repeated identical tool calls and A→B→A→B oscillation get caught — while a poller whose output keeps changing does not. [📖 Doom Loop Detection](https://docs.praison.ai/docs/features/doom-loop-detection)
|
||||
|
||||
### Layer 5 · Graph — *Who runs when, and who checks whom?*
|
||||
|
||||
Topology as a versionable artifact: prompt chaining, routing, parallelisation, orchestrator-worker.
|
||||
|
||||
```python
|
||||
from praisonaiagents import AgentFlow
|
||||
from praisonaiagents.workflows import route, parallel, repeat
|
||||
|
||||
flow = AgentFlow(steps=[
|
||||
classifier,
|
||||
route({"bug": [bug_agent], "feature": [feature_agent], "default": [triage]}),
|
||||
parallel([reviewer, tester]), # fan out, join automatically
|
||||
repeat(editor, until=lambda ctx: "approved" in ctx.previous_result.lower(),
|
||||
max_iterations=3), # evaluator–optimizer
|
||||
])
|
||||
flow.run("Ticket #123: login fails on Safari")
|
||||
```
|
||||
|
||||
> The same graph is expressible in YAML with no Python at all. [📖 AgentFlow](https://docs.praison.ai/docs/concepts/agentflow)
|
||||
|
||||
### ⬡ Outside the stack: Managed Agents — *Where does it actually run?*
|
||||
|
||||
The harness is commoditising; **where** the agent executes is the next multiplier. Rather than burning your laptop's CPU, hand an agent a short-lived cloud sandbox — repo, tools and tests run there.
|
||||
|
||||
```bash
|
||||
pip install praisonai
|
||||
```
|
||||
|
||||
The simplest way in is `tools_run_on=` — one whole team or workflow shares **one** sandbox, so a file written by step 1 is there for step 2. Thinking stays on your machine:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, AgentFlow
|
||||
|
||||
writer = Agent(name="Writer", instructions="You write files.")
|
||||
reader = Agent(name="Reader", instructions="You read files.")
|
||||
|
||||
flow = AgentFlow(tools_run_on="docker", steps=[writer, reader]) # or e2b | modal | daytona | flyio
|
||||
flow.run("Write 'hello' to /workspace/note.txt, then read it back")
|
||||
```
|
||||
|
||||
Same thing with no Python at all:
|
||||
|
||||
```yaml
|
||||
name: remote-demo
|
||||
tools_run_on: docker # every step shares one sandbox
|
||||
agents:
|
||||
writer: {role: Writer, goal: Write files}
|
||||
reader: {role: Reader, goal: Read files}
|
||||
steps:
|
||||
- agent: writer
|
||||
action: "Write 'hello' to /workspace/note.txt"
|
||||
- agent: reader
|
||||
action: "Read /workspace/note.txt"
|
||||
```
|
||||
|
||||
For a single agent, two words cover it — and they answer different questions:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# A. Only the TOOLS move. Thinking stays on your machine.
|
||||
agent = Agent(name="builder", instructions="You build things.",
|
||||
tools_run_on="docker") # docker | e2b | modal | daytona | flyio
|
||||
# tenki | sandlock | ssh | novita | subprocess
|
||||
|
||||
# B. The WHOLE agent moves — model calls, loop and tools
|
||||
agent = Agent(name="teacher", instructions="You teach.", run_on="anthropic") # hosted
|
||||
agent = Agent(name="builder", instructions="You build.", run_on="docker") # self-hosted
|
||||
agent.start("Write a Python script that prints the first 10 primes, then run it")
|
||||
```
|
||||
|
||||
Ask any object where it runs, and it will tell you:
|
||||
|
||||
```python
|
||||
>>> Agent(name="builder", instructions="x", tools_run_on="docker")
|
||||
Agent(name='builder', thinks_on='this machine', tools_run_on='a Docker container')
|
||||
|
||||
>>> agent.where_does_it_run()
|
||||
Thinking (the AI model calls) happens on this machine.
|
||||
Tools run on a Docker container.
|
||||
Your own tools (check_db) still run on this machine -- only shell, file and
|
||||
code tools move. They read and write this machine's files.
|
||||
```
|
||||
|
||||
Naming a place that cannot do the job is a typo, not a preference, so it says so:
|
||||
|
||||
```python
|
||||
>>> Agent(name="x", instructions="i", run_on="e2b")
|
||||
TypeError: Agent(run_on='e2b') is not valid: run_on= places the whole agent
|
||||
-- model calls, loop and tools -- on a managed runtime, and 'e2b' runs
|
||||
commands but cannot host an agent loop.
|
||||
To run only the tools there: Agent(tools_run_on='e2b')
|
||||
```
|
||||
|
||||
To run one block of code somewhere else, name the place on that call:
|
||||
|
||||
```python
|
||||
agent.execute_code_sync("print(6 * 7)", run_in="sandlock") # kernel-enforced
|
||||
```
|
||||
|
||||
See what is running and reclaim strays:
|
||||
|
||||
```bash
|
||||
praisonai managed ps # list running sandboxes
|
||||
praisonai managed stop --all # reclaim them
|
||||
```
|
||||
|
||||
Sandboxes shut themselves down when idle (`auto_shutdown`, `idle_timeout_s`), and a post-setup snapshot is reused so the next run skips the image pull and dependency install. Commit a `.praisonai/environment.yaml` and the environment travels with the repo.
|
||||
|
||||
> 📖 [20 runnable examples](examples/python/managed-agents/) · manage sessions with `praisonai managed sessions list <agent-id>` or `praisonai managed sessions resume <session-id> "<prompt>"`
|
||||
|
||||
<sub>Stack framing adapted from [The Five-Layer Agent Stack](https://mer.vin/2026/07/five-layer-agent-stack-match-bug-to-right-layer/) and [Agent Harnesses vs Orbs](https://mer.vin/2026/08/agent-harnesses-vs-orbs-why-remote-sandboxes-beat-local-agent-loops/).</sub>
|
||||
|
||||
---
|
||||
|
||||
## 🌌 The PraisonAI Ecosystem
|
||||
|
||||
Start simple with the core SDK, or expand to full visual builders and dashboards when you're ready.
|
||||
@@ -429,9 +200,9 @@ Powered by 100+ LLMs (OpenAI, Anthropic, Gemini & local models).
|
||||
| 🧠 | **Planning Mode** — plan → execute → reason | `planning=True` |
|
||||
| 🔍 | **Deep Research** — multi-step autonomous research | [Docs](https://docs.praison.ai/docs/agents/deep-research) |
|
||||
| 🤖 | **External Agents** — orchestrate Claude Code, Gemini CLI, Codex | [Docs](https://docs.praison.ai/docs/code/external-agents) |
|
||||
| 🔄 | **Agent Handoffs** — seamless conversation passing | `handoffs=[other_agent]` |
|
||||
| 🔄 | **Agent Handoffs** — seamless conversation passing | `handoff=True` |
|
||||
| 🛡️ | **Guardrails** — input/output validation | [Docs](https://docs.praison.ai/docs/concepts/guardrails) |
|
||||
| | **Web Search + Fetch** — native browsing | `web=True` |
|
||||
| | **Web Search + Fetch** — native browsing | `web_search=True` |
|
||||
| 🪞 | **Self Reflection** — agent reviews its own output | [Docs](https://docs.praison.ai/docs/concepts/reflection) |
|
||||
| 🔀 | **Workflow Patterns** — route, parallel, loop, repeat | [Docs](https://docs.praison.ai/docs/concepts/agentflow) |
|
||||
| 🧠 | **Memory (zero deps)** — works out of the box | `memory=True` |
|
||||
@@ -441,9 +212,9 @@ Powered by 100+ LLMs (OpenAI, Anthropic, Gemini & local models).
|
||||
|
||||
| | Feature | How |
|
||||
|--|---------|-----|
|
||||
| 💡 | **Prompt Caching** — reduce latency + cost | `caching=True` |
|
||||
| 💡 | **Prompt Caching** — reduce latency + cost | `prompt_caching=True` |
|
||||
| 💾 | **Sessions + Auto-Save** — persistent state across restarts | `auto_save="my-project"` |
|
||||
| 💭 | **Thinking Budgets** — control reasoning depth | `agent.thinking_budget = 1024` |
|
||||
| 💭 | **Thinking Budgets** — control reasoning depth | `thinking_budget=1024` |
|
||||
| 📚 | **RAG + Quality-Based RAG** — auto quality scoring retrieval | [Docs](https://docs.praison.ai/docs/concepts/rag) |
|
||||
| 📊 | **Model Router** — auto-routes to cheapest capable model | [Docs](https://docs.praison.ai/docs/features/model-router) |
|
||||
| 🧊 | **Shadow Git Checkpoints** — auto-rollback on failure | [Docs](https://docs.praison.ai/docs/features/checkpoints) |
|
||||
@@ -570,10 +341,8 @@ from praisonaiagents import Agent, db
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
memory={
|
||||
"db": db(database_url="postgresql://localhost/mydb"),
|
||||
"session_id": "my-session",
|
||||
},
|
||||
db=db(database_url="postgresql://localhost/mydb"),
|
||||
session_id="my-session"
|
||||
)
|
||||
agent.chat("Hello!") # Auto-persists messages, runs, traces
|
||||
```
|
||||
@@ -799,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>
|
||||
@@ -1001,11 +770,11 @@ agent.start('Write a movie script about a robot in Mars');
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
PraisonAI is built for speed, with agent instantiation in around 14μs. This reduces overhead, improves responsiveness, and helps multi-agent systems scale efficiently in real-world production workloads.
|
||||
PraisonAI is built for speed, with agent instantiation in under 4μs. This reduces overhead, improves responsiveness, and helps multi-agent systems scale efficiently in real-world production workloads.
|
||||
|
||||
| Performance Metric | PraisonAI |
|
||||
|--------------------|-----------|
|
||||
| Avg Instantiation Time | **14 μs** |
|
||||
| Avg Instantiation Time | **3.77 μs** |
|
||||
|
||||
---
|
||||
|
||||
@@ -1019,16 +788,6 @@ PraisonAI is built for speed, with agent instantiation in around 14μs. This red
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/images/agentflow.gif" alt="PraisonAI AgentFlow" width="800" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/images/latest_ai_news_and_crawl_each_url_to_find_info.gif" alt="PraisonAI command execution" width="800" />
|
||||
</p>
|
||||
|
||||
\* `export TAVILY_API_KEY=xxxxx`
|
||||
|
||||
## 🔍 Langfuse Tracing
|
||||
|
||||
```bash
|
||||
@@ -1112,39 +871,13 @@ For other providers, see [Models docs](https://docs.praison.ai/docs/models).
|
||||
<summary><strong>How do I use a local model (Ollama)?</strong></summary>
|
||||
|
||||
```bash
|
||||
# Start Ollama and pull a model
|
||||
# Start Ollama server first
|
||||
ollama serve
|
||||
ollama pull llama3.2
|
||||
|
||||
# Set environment variable
|
||||
export OPENAI_BASE_URL=http://localhost:11434/v1
|
||||
```
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(instructions="You are a helpful assistant", llm="ollama/llama3.2")
|
||||
agent.start("Why is the sky blue?")
|
||||
```
|
||||
|
||||
The `ollama/` prefix is what selects Ollama's handling — tool-call repair,
|
||||
tool-result formatting and the streaming rules small local models need. Or set
|
||||
it by environment instead:
|
||||
|
||||
```bash
|
||||
export OPENAI_MODEL_NAME=ollama/llama3.2
|
||||
```
|
||||
|
||||
Setting only `OPENAI_BASE_URL` is not enough: with no model named, the OpenAI
|
||||
default (`gpt-4o-mini`) is sent to Ollama, which answers
|
||||
`404 model 'gpt-4o-mini' not found`. Always name the model.
|
||||
|
||||
Point at a non-default host with `base_url=` or `OLLAMA_HOST`:
|
||||
|
||||
```python
|
||||
agent = Agent(instructions="...", llm="ollama/llama3.2", base_url="http://192.168.1.10:11434")
|
||||
```
|
||||
|
||||
The same shape works for other local runtimes — `lm_studio/`, `vllm/` and
|
||||
`hosted_vllm/` with their server's `base_url`.
|
||||
|
||||
See [Models docs](https://docs.praison.ai/docs/models) for more details.
|
||||
|
||||
</details>
|
||||
@@ -1158,10 +891,8 @@ from praisonaiagents import Agent, db
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
memory={
|
||||
"db": db(database_url="postgresql://localhost/mydb"),
|
||||
"session_id": "my-session",
|
||||
},
|
||||
db=db(database_url="postgresql://localhost/mydb"),
|
||||
session_id="my-session"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -1177,8 +908,8 @@ from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
# Enables file-based memory (no extra deps!)
|
||||
memory={"user_id": "user123"},
|
||||
memory=True, # Enables file-based memory (no extra deps!)
|
||||
user_id="user123"
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
PraisonAI takes security seriously. We appreciate responsible disclosure from researchers and users.
|
||||
|
||||
## Supported packages
|
||||
|
||||
| Package | Registry | Source path |
|
||||
|---------|----------|-------------|
|
||||
| `praisonaiagents` | [PyPI](https://pypi.org/project/praisonaiagents/) | `src/praisonai-agents/` |
|
||||
| `praisonai` | [PyPI](https://pypi.org/project/praisonai/) | `src/praisonai/` |
|
||||
| `praisonai` | [npm](https://www.npmjs.com/package/praisonai) | `src/praisonai-ts/` |
|
||||
|
||||
Report issues against the **specific package** where the vulnerability exists. A platform-only issue does not necessarily affect the core SDK, and vice versa.
|
||||
|
||||
### Packages maintained in their own repositories
|
||||
|
||||
`praisonai-platform` now lives at
|
||||
[MervinPraison/PraisonAI-Platform](https://github.com/MervinPraison/PraisonAI-Platform).
|
||||
Report platform vulnerabilities through
|
||||
[that repository's advisories](https://github.com/MervinPraison/PraisonAI-Platform/security/advisories/new),
|
||||
not here — it is versioned and released independently of the SDK.
|
||||
|
||||
## In scope
|
||||
|
||||
- Authentication, authorisation, and multi-tenant isolation (e.g. workspace boundaries)
|
||||
- Tool execution, sandbox escape, and code execution paths
|
||||
- SSRF, injection, and unsafe deserialisation in shipped code
|
||||
- MCP, gateway, and bot/webhook integrations
|
||||
- Secrets handling, JWT/session configuration, and default credentials
|
||||
- Supply-chain issues in published package artefacts
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Issues in forks or deployments not maintained in [MervinPraison/PraisonAI](https://github.com/MervinPraison/PraisonAI)
|
||||
- Vulnerabilities requiring physical access or fully compromised operator machines
|
||||
- Reports against versions no longer published or without a supported upgrade path
|
||||
- Theoretical issues with no practical impact in documented, supported configurations
|
||||
- Behaviour covered by an explicit, documented opt-out (e.g. dev-only env vars)
|
||||
|
||||
## How to report
|
||||
|
||||
**Preferred:** open a **private** security advisory:
|
||||
|
||||
[https://github.com/MervinPraison/PraisonAI/security/advisories/new](https://github.com/MervinPraison/PraisonAI/security/advisories/new)
|
||||
|
||||
Include:
|
||||
|
||||
1. Affected package and version
|
||||
2. Clear reproduction steps
|
||||
3. Impact assessment (confidentiality, integrity, availability)
|
||||
4. Suggested fix (optional)
|
||||
|
||||
Please allow reasonable time for triage and a patched release before public disclosure.
|
||||
|
||||
## What to expect
|
||||
|
||||
1. **Triage** — we validate the report and assign a disposition (`real`, `duplicate`, `wontfix`, etc.)
|
||||
2. **Fix** — minimal, backward-compatible patch on `main` where appropriate
|
||||
3. **Release** — patched version published to PyPI/npm **before** the advisory is published
|
||||
4. **Advisory** — GitHub Security Advisory with reporter credit and CVE where applicable
|
||||
|
||||
## Resources
|
||||
|
||||
- **Repository:** https://github.com/MervinPraison/PraisonAI
|
||||
- **Documentation:** https://docs.praison.ai
|
||||
- **Website:** https://praison.ai
|
||||
- **Advisories:** https://github.com/MervinPraison/PraisonAI/security/advisories
|
||||
|
||||
## Maintainer workflow
|
||||
|
||||
Contributors and agents handling audits should follow:
|
||||
|
||||
`src/praisonai-agents/.agent/workflows/security-audit.md`
|
||||
@@ -7,7 +7,7 @@ Regenerate with: `praisonai docs api-md --write`
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import ContextPolicy, GuardrailResult, HandoffConfig, HandoffCycleError, HandoffDepthError, HandoffError, HandoffInputData, HandoffResult, HandoffTimeoutError, ReflectionOutput, StepResult, ToolResult, ToolValidationError, WorkflowContext
|
||||
from praisonaiagents import ContextPolicy, GuardrailResult, HandoffConfig, HandoffCycleError, HandoffDepthError, HandoffError, HandoffInputData, HandoffResult, HandoffTimeoutError, ReflectionOutput, StepResult, TaskOutput, ToolResult, ToolValidationError, WorkflowContext
|
||||
```
|
||||
|
||||
Methods:
|
||||
@@ -15,29 +15,31 @@ Methods:
|
||||
* <code title="class GuardrailResult">GuardrailResult.<a href="./src/praisonai-agents/praisonaiagents/guardrails/guardrail_result.py">from_tuple</a>(result: Tuple[bool, Any]) -> 'GuardrailResult'</code>
|
||||
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_dict</a>(data: Dict[str, Any]) -> 'HandoffConfig'</code>
|
||||
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <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 ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">is_multimodal</a>() -> bool</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">to_dict</a>() -> Dict[str, Any]</code>
|
||||
|
||||
# Agents
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PraisonAIAgents, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
|
||||
from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">aclose</a>() -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">add_mcp_server</a>(name: str, mcp: Any) -> Any</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">add_verification</a>(hook: Any) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">agent_id</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">analyze_prompt</a>(prompt: str) -> set</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_history</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_history</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clone_for_channel</a>() -> 'Agent'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">close</a>() -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>() -> Optional[Any]</code>
|
||||
@@ -56,10 +58,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>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_model</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>(value: Optional[str]) -> None</code>
|
||||
@@ -68,29 +66,22 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">query</a>(question: str, **kwargs) -> 'RAGResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag_query</a>(question: str, **kwargs) -> 'RAGResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">reasoning_effort</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">reasoning_effort</a>(value: Optional[str]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">redo</a>() -> bool</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">refresh_tools</a>() -> List[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">remove_mcp_server</a>(name: str) -> bool</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">remove_verification</a>(name: str) -> bool</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieval_config</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieve</a>(query: str, **kwargs) -> 'ContextPack'</code>
|
||||
* <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">set_snapshot_root</a>(project_path: str) -> bool</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>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs: Any) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>(value: Optional[Any]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>() -> Optional[int]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value: Optional[int]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">total_cost</a>() -> float</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">undo</a>() -> bool</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">where_does_it_run</a>() -> str</code>
|
||||
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">astart</a>()</code>
|
||||
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">start</a>()</code>
|
||||
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">achat</a>(message: str, **kwargs) -> str</code>
|
||||
@@ -179,7 +170,6 @@ Methods:
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">get_schema</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">run</a>(**kwargs) -> Any</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">safe_run</a>(**kwargs) -> ToolResult</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_model_output</a>(result: Any) -> Optional[Any]</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate</a>() -> bool</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_class</a>() -> bool</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_schema_roundtrip</a>() -> bool</code>
|
||||
@@ -187,24 +177,21 @@ Methods:
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">check_availability</a>() -> tuple[bool, str]</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">injected_params</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">run</a>(**kwargs) -> Any</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">to_model_output</a>(result: Any) -> Optional[Any]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">clear</a>() -> None</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_plugins</a>() -> int</code>
|
||||
* <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_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_available_tools</a>(context: Optional[Dict[str, Any]] = 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>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools</a>() -> List[str]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools_with_allowed_filter</a>(context: Optional[Dict[str, Any]] = None) -> List[str]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False, trust_level: Optional[str] = None, dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None) -> None</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False) -> None</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">unregister</a>(name: str) -> bool</code>
|
||||
* <code title="class Tools">Tools.<a href="./src/praisonai-agents/praisonaiagents/tools/tools.py">internet_search</a>(*args, **kwargs)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_registry</a>() -> ToolRegistry</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, trust_level: Optional[str] = None, dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None) -> None</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None) -> None</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">tool</a>(func: Optional[Callable] = None) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_tool</a>(tool: Any) -> bool</code>
|
||||
|
||||
@@ -233,6 +220,13 @@ Methods:
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">repeat</a>(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None, max_iterations: int = 10) -> Repeat</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">route</a>(routes: Dict[str, List], default: Optional[List] = None) -> Route</code>
|
||||
|
||||
# DB
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import db
|
||||
```
|
||||
|
||||
# Memory
|
||||
|
||||
Types:
|
||||
@@ -242,12 +236,9 @@ from praisonaiagents import Memory, MemoryBackend, MemoryConfig
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">acommit_memory_batch</a>(writes: List[tuple]) -> None</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_cache_optimized_context</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_cache_boundary: bool = True, include_in_output: Optional[bool] = None) -> Dict[str, str]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_context_for_task</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_in_output: Optional[bool] = None) -> str</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">calculate_quality_metrics</a>(output: str, expected_output: str, llm: Optional[str] = None, custom_prompt: Optional[str] = None) -> Dict[str, float]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">close_connections</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">commit_memory_batch</a>(writes: List[tuple]) -> None</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">compute_quality_score</a>(completeness: float, relevance: float, clarity: float, accuracy: float, weights: Dict[str, float] = None) -> float</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_long_term</a>(memory_id: str) -> bool</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memories</a>(memory_ids: List[str]) -> int</code>
|
||||
@@ -255,13 +246,9 @@ Methods:
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memory</a>(memory_id: str, memory_type: Optional[str] = None) -> bool</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_short_term</a>(memory_id: str) -> bool</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">finalize_task_output</a>(content: str, agent_name: str, quality_score: float, threshold: float = 0.7, metrics: Dict[str, Any] = None, task_id: str = None)</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">forget</a>(**kwargs) -> int</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_all_memories</a>() -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_context</a>(query: Optional[str] = None, **kwargs) -> str</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_learn_context</a>() -> str</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">learn</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">recall</a>(query: str, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">remember</a>(content: str, **kwargs) -> str</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_all</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_entity_only</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_long_term</a>()</code>
|
||||
@@ -269,8 +256,8 @@ Methods:
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_user_memory</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search</a>(query: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_entity</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, metadata_filter: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, metadata_filter: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_user_memory</a>(user_id: str, query: str, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_with_quality</a>(query: str, min_quality: float = 0.0, memory_type: Literal['short', 'long'] = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_entity</a>(name: str, type_: str, desc: str, relations: str)</code>
|
||||
@@ -309,7 +296,7 @@ Methods:
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">normalize_content</a>(content)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">reset</a>()</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">search</a>(query, user_id = None, agent_id = None, run_id = None, rerank = None, **kwargs)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">store</a>(content, user_id = None, agent_id = None, run_id = None, metadata = None, is_content = False)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">store</a>(content, user_id = None, agent_id = None, run_id = None, metadata = None)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">update</a>(memory_id, data)</code>
|
||||
* <code title="class KnowledgeConfig">KnowledgeConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
|
||||
@@ -366,7 +353,7 @@ Methods:
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_tool_function</a>(source_agent: 'Agent') -> Callable</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_description</a>() -> str</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_name</a>() -> str</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None, tool_policy_mode: Optional[Literal['intersect', 'passthrough']] = None, blocked_tools: Optional[List[str]] = None) -> Handoff</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None) -> Handoff</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">compress_history</a>(data: HandoffInputData) -> HandoffInputData</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">keep_last_n_messages</a>(n: int) -> Callable[[HandoffInputData], HandoffInputData]</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_all_tools</a>(data: HandoffInputData) -> HandoffInputData</code>
|
||||
@@ -387,7 +374,6 @@ Methods:
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_input</a>(content: str, **kwargs) -> Tuple[bool, str]</code>
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_output</a>(content: str, **kwargs) -> Tuple[bool, str]</code>
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_tool_call</a>(tool_name: str, arguments: Dict[str, Any], **kwargs) -> Tuple[bool, Dict[str, Any]]</code>
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_tool_result</a>(tool_name: str, result: Any, **kwargs) -> Tuple[bool, Any]</code>
|
||||
|
||||
# Planning
|
||||
|
||||
@@ -471,7 +457,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import SkillLoader, SkillsConfig
|
||||
from praisonaiagents import SkillLoader, SkillManager, SkillMetadata, SkillProperties, SkillsConfig
|
||||
```
|
||||
|
||||
Methods:
|
||||
@@ -483,6 +469,32 @@ Methods:
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_metadata</a>(skill_path: str) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_references</a>(skill: LoadedSkill) -> dict</code>
|
||||
* <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_skill</a>(skill_path: str) -> Optional[LoadedSkill]</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_skill</a>(name: str, content: str, category: str = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">delete_skill</a>(name: str) -> 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">edit_skill</a>(name: str, content: str) -> 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_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>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_user_invocable_skills</a>() -> List[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">invoke</a>(name: str, raw_args: str = '', session_id: Optional[str] = None, shell_exec: bool = False) -> Optional[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">load_resources</a>(name: str) -> bool</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">patch_skill</a>(name: str, old_string: str, new_string: str, file_path: str = None, replace_all: bool = False) -> 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) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skill_names</a>() -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skills</a>() -> List[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">to_prompt</a>() -> str</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">validate_skill_capabilities</a>(skill_name: str, force_refresh: bool = False) -> ValidationResult</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">write_skill_file</a>(name: str, file_path: str, file_content: str) -> dict</code>
|
||||
* <code title="class SkillMetadata">SkillMetadata.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">from_properties</a>(props: SkillProperties) -> 'SkillMetadata'</code>
|
||||
* <code title="class SkillProperties">SkillProperties.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">to_dict</a>() -> dict</code>
|
||||
* <code title="class SkillsConfig">SkillsConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
|
||||
# Session
|
||||
@@ -499,12 +511,10 @@ Methods:
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_memory</a>(text: str, memory_type: str = 'long', **metadata) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">chat</a>(message: str, **kwargs) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">clear_memory</a>(memory_type: str = 'all') -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">close</a>() -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">create_agent</a>(*args, **kwargs) -> 'Agent'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_context</a>(query: str, max_items: int = 3) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">increment_state</a>(key: str, increment: int = 1, default: int = 0) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">is_expired</a>() -> bool</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">knowledge</a>() -> 'Knowledge'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">memory</a>() -> 'Memory'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">restore_state</a>() -> Dict[str, Any]</code>
|
||||
@@ -513,7 +523,6 @@ Methods:
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_memory</a>(query: str, memory_type: str = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">send_message</a>(message: str, **kwargs) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">time_to_expiry</a>() -> Optional[float]</code>
|
||||
|
||||
# MCP
|
||||
|
||||
@@ -524,14 +533,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">list_active_server_names</a>() -> set</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
|
||||
|
||||
@@ -601,7 +605,7 @@ Methods:
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">register_snapshot_callback</a>(callback: Callable[[SnapshotHookData], None]) -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">reset</a>() -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">set_tool_budget</a>(tool_name: str, max_tokens: int, protected: bool = False) -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str, tool_call_id: str = None, run_id: str = None) -> str</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str) -> str</code>
|
||||
|
||||
# UI
|
||||
|
||||
@@ -622,9 +626,6 @@ Methods:
|
||||
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/autonomy.py">effective_track_changes</a>() -> bool</code>
|
||||
* <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>
|
||||
@@ -692,7 +693,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, RetryBackoffConfig, RunOutcome, __version__, aembedding, aembeddings, embedding, embeddings, get_dimensions
|
||||
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, aembedding, aembeddings, embedding, embeddings, get_dimensions
|
||||
```
|
||||
|
||||
Methods:
|
||||
@@ -715,20 +716,14 @@ Methods:
|
||||
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>() -> bool</code>
|
||||
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>(value: bool)</code>
|
||||
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">where_does_it_run</a>() -> str</code>
|
||||
* <code title="class AgentOSProtocol">AgentOSProtocol.<a href="./src/praisonai-agents/praisonaiagents/app/protocols.py">get_app</a>() -> Any</code>
|
||||
* <code title="class AgentOSProtocol">AgentOSProtocol.<a href="./src/praisonai-agents/praisonaiagents/app/protocols.py">serve</a>(host: Optional[str] = None, port: Optional[int] = None, reload: bool = False, **kwargs: Any) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aannounce_completion</a>(agent_id: str, task_id: str, result: Any, success: bool = True, error: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">announce_completion</a>(agent_id: str, task_id: str, result: Any, success: bool = True, error: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aspawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart_for_each</a>(inputs, **kwargs)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">await_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">close</a>() -> None</code>
|
||||
@@ -743,7 +738,6 @@ Methods:
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_spawned_agents</a>() -> List[SpawnedSubAgent]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
|
||||
@@ -752,28 +746,20 @@ Methods:
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '127.0.0.1', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> bool</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">spawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start_for_each</a>(inputs, **kwargs)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">stream_emitter</a>()</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">wait_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">where_does_it_run</a>() -> str</code>
|
||||
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval</a>(request: ApprovalRequest) -> ApprovalDecision</code>
|
||||
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval_sync</a>(request: ApprovalRequest) -> ApprovalDecision</code>
|
||||
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">completed</a>(output: Optional[str] = None) -> 'RunOutcome'</code>
|
||||
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">from_exception</a>(exc: BaseException, output: Optional[str] = None) -> 'RunOutcome'</code>
|
||||
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">succeeded</a>() -> bool</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/dimensions.py">get_dimensions</a>(model_name: str) -> int</code>
|
||||
@@ -782,7 +768,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, 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
|
||||
@@ -790,14 +776,64 @@ from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, HostedAge
|
||||
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 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 audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.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 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 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 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>
|
||||
@@ -808,6 +844,17 @@ Methods:
|
||||
* <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 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>
|
||||
@@ -816,6 +863,28 @@ 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 config <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor db <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor doctor-callback <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 mcp <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 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 tools <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>
|
||||
@@ -828,6 +897,32 @@ 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 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 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>
|
||||
@@ -845,19 +940,41 @@ 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>
|
||||
* <code title="cli">praisonai managed managed-callback <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed multi <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed ps <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed restore <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed resume <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed run <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <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 stop <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 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>
|
||||
@@ -865,6 +982,30 @@ 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 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>
|
||||
@@ -872,6 +1013,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>
|
||||
@@ -886,8 +1029,9 @@ Methods:
|
||||
* <code title="cli">praisonai recipe list <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <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 runs <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>
|
||||
@@ -895,54 +1039,127 @@ 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>
|
||||
* <code title="cli">praisonai schedule delete <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule logs <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule pause <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule remove <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule restart <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule resume <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule run <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule runs <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule schedule-callback <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule start <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule stats <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule stop <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule suggestion-accept <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <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 schedule update <a href="./src/praisonai/praisonai/cli/commands/schedule.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 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 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 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 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
|
||||
|
||||
Types/Exports:
|
||||
```ts
|
||||
export { Agent, AgentTeam, Agents, CodeAgent, EmbeddingAgent, OCRAgent, PraisonAIAgents, RealtimeAgent, Router, TASK_STATUS, VideoAgent, VisionAgent, createCodeAgent, createEmbeddingAgent, createOCRAgent, createRealtimeAgent, createVideoAgent, createVisionAgent } from "./agent";
|
||||
export type { AgentChatCallOptions, AgentChatOptions, AgentEvent, AgentExecuteTask, AgentGuardrailEntry, AgentGuardrailFunction, AgentGuardrailInput, AgentHooksInput, AgentMemoryStore, AgentMessage, AgentRetryConfig, AgentStreamOptions, AgentTaskLike, AgentTeamConfig, AgentTeamProcess, AgentTeamStartDictOptions, AgentTeamStartOptions, AgentTeamStartOptionsInput, AgentWebConfig, PraisonAIAgentsConfig, SimpleAgentConfig, SimpleRouteConfig, SimpleRouterConfig, StopReason, TaskCallback, TaskGuardrail, TaskOnError } from "./agent";
|
||||
export { Agent, AgentTeam, Agents, PraisonAIAgents, Router } from "./agent";
|
||||
export type { AgentTeamConfig, PraisonAIAgentsConfig, SimpleAgentConfig, SimpleRouteConfig, SimpleRouterConfig } from "./agent";
|
||||
export { AudioAgent, createAudioAgent } from "./agent/audio";
|
||||
export type { AudioAgentConfig, AudioConfig, AudioProvider, AudioSpeakOptions, AudioSpeakResult, AudioTranscribeOptions, AudioTranscribeResult } from "./agent/audio";
|
||||
export { ContextAgent, createContextAgent, create_context_agent } from "./agent/context";
|
||||
export { ContextPolicy, DEFAULT_HANDOFF_TOOL_POLICY, Handoff, HandoffCycleError, HandoffDepthError, HandoffError, HandoffTimeoutError, HandoffToolPolicyMode, RECOMMENDED_PROMPT_PREFIX, handoff, handoffFilters, handoffToolName, handoff_filters, listAgentTools, parallelHandoffs, parallel_handoffs, promptWithHandoffInstructions, prompt_with_handoff_instructions, resolveHandoffToolPolicy } from "./agent/handoff";
|
||||
export { Heartbeat, HeartbeatConfig, parseHeartbeatSchedule } from "./agent/heartbeat";
|
||||
export type { HeartbeatAgent, HeartbeatOnError, HeartbeatOptions } from "./agent/heartbeat";
|
||||
export type { AudioAgentConfig, AudioProvider, AudioSpeakOptions, AudioSpeakResult, AudioTranscribeOptions, AudioTranscribeResult } from "./agent/audio";
|
||||
export { ContextAgent, createContextAgent } from "./agent/context";
|
||||
export { ContextPolicy, Handoff, HandoffCycleError, HandoffDepthError, HandoffError, HandoffTimeoutError, RECOMMENDED_PROMPT_PREFIX, handoff, handoffFilters, promptWithHandoffInstructions } from "./agent/handoff";
|
||||
export { ImageAgent, createImageAgent } from "./agent/image";
|
||||
export { PromptExpanderAgent, createPromptExpanderAgent } from "./agent/prompt-expander";
|
||||
export { QueryRewriterAgent, createQueryRewriterAgent } from "./agent/query-rewriter";
|
||||
export { DeepResearchAgent, Provider, createDeepResearchAgent } from "./agent/research";
|
||||
export { RetryBackoffConfig, interruptibleSleep, jitteredBackoff } from "./agent/retry-utils";
|
||||
export type { JitteredBackoffOptions, RetryBackoffConfigOptions } from "./agent/retry-utils";
|
||||
export { DeepResearchAgent, createDeepResearchAgent } from "./agent/research";
|
||||
export { RouterAgent, createRouter, routeConditions } from "./agent/router";
|
||||
export { AGENT_RUN_STATUSES, AgentRunOutcome, PROVIDER_BLOCK_REASONS, RunOutcome, TERMINAL_REASON_PRECEDENCE, TERMINATION_TO_RUN_STATUS, TerminationReason, classifyFinishReason, terminationToRunStatus, termination_to_run_status, validateDecisionString, validate_decision_string } from "./agent/run-outcome";
|
||||
export type { AgentRunOutcomeFailureOptions, AgentRunOutcomeInit, AgentRunOutcomeOptions, AgentRunStatus, RunOutcomeInit, TerminalReason } from "./agent/run-outcome";
|
||||
export { // Agent loop
|
||||
createAgentLoop, // DevTools
|
||||
enableDevTools, // MCP
|
||||
@@ -959,7 +1176,6 @@ export { // Agent loop
|
||||
defineTool, // UI Message (AI SDK v6 parity)
|
||||
convertToModelMessages, AIAgentStep, AIEmbedManyResult, AIEmbedOptions, AIEmbedResult, AIFilePart, AIGenerateImageOptions, AIGenerateImageResult, AIGenerateObjectOptions, AIGenerateObjectResult, AIGenerateTextOptions, AIGenerateTextResult, AIImagePart, AIMiddleware, AIMiddlewareConfig, AIModelMessage, AISpan, AISpanKind, AISpanOptions, AISpanStatus, AIStreamObjectOptions, AIStreamObjectResult, AIStreamTextOptions, AIStreamTextResult, AITelemetryEvent, AITelemetrySettings, AITextPart, AIToolDefinition, AITracer, AgentLoop, DANGEROUS_PATTERNS, MCPClientType, MODEL_ALIASES, SPEECH_MODELS, TRANSCRIPTION_MODELS, ToolApprovalDeniedError, ToolApprovalTimeoutError, aiEmbed, aiEmbedMany, aiGenerateImage, aiGenerateObject, aiGenerateText, aiStreamObject, aiStreamText, applyMiddleware, autoEnableDevTools, base64ToUint8Array, clearAICache, clearEvents, closeAllMCPClients, closeMCPClient, convertToUIMessages, createAILoggingMiddleware, createAISpan, createApprovalResponse, createDangerousPatternChecker, createDevToolsMiddleware, createExpressHandler, createFastifyHandler, createFilePart, createHonoHandler, createMultimodalMessage, createNestHandler, createPagesHandler, createPdfPart, createSystemMessage, createTelemetryMiddleware, createTelemetrySettings, createTextMessage, createTextPart, createToolSet, disableAITelemetry, disableDevTools, enableAITelemetry, functionToTool, getAICacheStats, getApprovalManager, getDevToolsState, getDevToolsUrl, getEvents, getMCPClient, getModel, getTelemetrySettings, getToolsNeedingApproval, getTracer, hasModelAlias, hasPendingApprovals, initOpenTelemetry, isDangerous, isDataUrl, isDevToolsEnabled, isTelemetryEnabled, isUrl, listModelAliases, mcpToolsToAITools, parseModel, pipeUIMessageStreamToResponse, recordEvent, resolveModelAlias, safeValidateUIMessages, setApprovalManager, stopAfterSteps, stopWhen, stopWhenNoToolCalls, toMessageContent, toUIMessageStreamResponse, transcribe, uint8ArrayToBase64, validateUIMessages, withApproval, withSpan, wrapModel } from "./ai";
|
||||
export { AutoAgents, AutoTaskConfig, createAutoAgents } from "./auto";
|
||||
export { BotRunStatus } from "./bots/protocols";
|
||||
export { BaseCache, FileCache, MemoryCache, createFileCache, createMemoryCache } from "./cache";
|
||||
export { CLI_SPEC_VERSION, executeCommand, parseArgs } from "./cli";
|
||||
export { // Autonomy Mode
|
||||
@@ -981,27 +1197,23 @@ export { // Autonomy Mode
|
||||
export { // Classes
|
||||
DictCondition, // Functions
|
||||
evaluateCondition, // Types
|
||||
type ConditionProtocol, ExpressionCondition, FunctionCondition, andConditions, createCondition, evaluate_condition, notCondition, orConditions } from "./conditions";
|
||||
type ConditionProtocol, ExpressionCondition, FunctionCondition, andConditions, createCondition, notCondition, orConditions } from "./conditions";
|
||||
export { // Enums
|
||||
MemoryBackend, // Errors
|
||||
ConfigValidationError, // Parse utilities
|
||||
detect_url_scheme, // Presets
|
||||
MEMORY_PRESETS, // Resolver functions
|
||||
resolve, AUTONOMY_PRESETS, ArrayMode, CACHING_PRESETS, CONTEXT_PRESETS, ChunkingStrategy, EXECUTION_PRESETS, ExecutionPreset, FeatureMemoryConfig, GUARDRAIL_PRESETS, GuardrailAction, KNOWLEDGE_PRESETS, LearnBackend, LearnMode, LearnScope, MEMORY_URL_SCHEMES, MULTI_AGENT_EXECUTION_PRESETS, MULTI_AGENT_OUTPUT_PRESETS, OUTPUT_PRESETS, OutputPreset, PLANNING_PRESETS, PreCompactionMemoryFlushConfig, REFLECTION_PRESETS, RulesConfig, ToolSearchConfig, WEB_PRESETS, WebSearchProvider, apply_config_defaults, clean_triple_backticks, get_config, get_config_path, get_default, get_defaults_config, get_plugins_config, is_path_like, is_policy_string, parse_policy_string, resolve_autonomy, resolve_caching, resolve_context, resolve_execution, resolve_guardrails, resolve_hooks, resolve_knowledge, resolve_memory, resolve_output, resolve_planning, resolve_reflection, resolve_routing, resolve_skills, resolve_web, suggest_similar, validate_config } from "./config";
|
||||
resolve, AUTONOMY_PRESETS, ArrayMode, CACHING_PRESETS, CONTEXT_PRESETS, ChunkingStrategy, EXECUTION_PRESETS, ExecutionPreset, FeatureMemoryConfig, GUARDRAIL_PRESETS, GuardrailAction, KNOWLEDGE_PRESETS, MEMORY_URL_SCHEMES, MULTI_AGENT_EXECUTION_PRESETS, MULTI_AGENT_OUTPUT_PRESETS, OUTPUT_PRESETS, OutputPreset, PLANNING_PRESETS, REFLECTION_PRESETS, WEB_PRESETS, WebSearchProvider, apply_config_defaults, clean_triple_backticks, get_config, get_config_path, get_default, get_defaults_config, get_plugins_config, is_path_like, is_policy_string, parse_policy_string, resolve_autonomy, resolve_caching, resolve_context, resolve_execution, resolve_guardrails, resolve_hooks, resolve_knowledge, resolve_memory, resolve_output, resolve_planning, resolve_reflection, resolve_routing, resolve_skills, resolve_web, suggest_similar, validate_config } from "./config";
|
||||
export { createDbAdapter, db, getDefaultDbAdapter, setDefaultDbAdapter } from "./db";
|
||||
export type { DbAdapter, DbConfig, DbMessage, DbRun, DbTrace } from "./db";
|
||||
export { MemoryPostgresAdapter, NeonPostgresAdapter, PostgresSessionStorage, createMemoryPostgres, createNeonPostgres, createPostgresSessionStorage } from "./db/postgres";
|
||||
export { MemoryRedisAdapter, UpstashRedisAdapter, createMemoryRedis, createUpstashRedis } from "./db/redis";
|
||||
export { SQLiteAdapter, createSQLiteAdapter } from "./db/sqlite";
|
||||
export { // Functions
|
||||
// NB: the unaliased registerDisplayCallback name belongs to the per-type
|
||||
// callback registry (callbacks module, // Types
|
||||
type DisplayCallback, DisplayFlow, DisplayFlowConfig, asyncDisplayCallbacks, async_display_callbacks, clearDisplayCallbacks, clearErrorLogs, displayError, displayGenerating, displayInstruction, displayInteraction, displaySelfReflection, displayToolCall, display_error, display_generating, display_instruction, display_interaction, display_self_reflection, display_tool_call, errorLogs, error_logs, logError, registerDisplay, register_display_callback, syncDisplayCallbacks, sync_display_callbacks } from "./display";
|
||||
export { // Types
|
||||
type DisplayCallback, DisplayFlow, DisplayFlowConfig, asyncDisplayCallbacks, clearDisplayCallbacks, clearErrorLogs, displayError, displayGenerating, displayInstruction, displayInteraction, displaySelfReflection, displayToolCall, errorLogs, logError, registerDisplay, syncDisplayCallbacks } from "./display";
|
||||
export { // Functions
|
||||
embed, // Types
|
||||
type EmbeddingResult, aembed, aembedding, aembeddings, cosineSimilarity, embedding, embeddings, euclideanDistance, getDimensions, get_dimensions, normalizeEmbedding, setEmbeddingConfig } from "./embeddings";
|
||||
export { AGENT_ERROR_KINDS, FailoverDecision, IdleTimeoutBreaker, LEGACY_ERROR_CATEGORY_MAP, LLMError, NetworkError, PraisonAIConfigError, PraisonAIError, ToolExecutionError, ValidationError, isAgentErrorKind, isErrorContext, resolveErrorCategory } from "./errors";
|
||||
export type { AgentErrorKind, ErrorContextProtocol, FailoverAction, FailoverDecisionOptions, LLMErrorOptions, LegacyErrorCategory, NetworkErrorOptions, PraisonAIConfigErrorOptions, PraisonAIErrorOptions, ToolExecutionErrorOptions, ValidationErrorOptions } from "./errors";
|
||||
type EmbeddingResult, aembed, aembedding, aembeddings, cosineSimilarity, embedding, embeddings, euclideanDistance, getDimensions, normalizeEmbedding, setEmbeddingConfig } from "./embeddings";
|
||||
export { // LLM-as-Judge
|
||||
Judge, AccuracyJudge, CriteriaJudge, EvalResults, EvalSuite, Evaluator, RecipeJudge, accuracyEval, addJudge, addOptimizationRule, containsKeywordsCriterion, createDefaultEvaluator, createEvalResults, createEvaluator, getJudge, getOptimizationRule, lengthCriterion, listJudges, listOptimizationRules, noHarmfulContentCriterion, parseJudgeResponse, performanceEval, relevanceCriterion, reliabilityEval, removeJudge, removeOptimizationRule } from "./eval";
|
||||
export { AgentEventBus, AgentEvents, EventEmitterPubSub, PubSub, createEventBus, createPubSub } from "./events";
|
||||
@@ -1010,7 +1222,7 @@ export { // Bot types
|
||||
FailoverManager, // Enums
|
||||
SandboxStatus, // Gateway types
|
||||
type GatewayConfig, // Other types
|
||||
type ProviderStatus, AutonomyLevel, GatewayEventType, RagRetrievalPolicy } from "./gateway";
|
||||
type ProviderStatus, AutonomyLevel, RagRetrievalPolicy } from "./gateway";
|
||||
export { LLMGuardrail, createLLMGuardrail } from "./guardrails/llm-guardrail";
|
||||
export { DisplayTypes, HooksManager, WorkflowHooksExecutor, clearAllCallbacks, clearApprovalCallback, createHooksManager, createLoggingOperationHooks, createLoggingWorkflowHooks, createTimingWorkflowHooks, createValidationOperationHooks, createWorkflowHooks, executeCallback, executeSyncCallback, getRegisteredDisplayTypes, hasApprovalCallback, registerApprovalCallback, registerDisplayCallback, requestApproval, unregisterDisplayCallback } from "./hooks";
|
||||
export { // Computer Use
|
||||
@@ -1022,11 +1234,7 @@ export { // Slack
|
||||
createSlackBot, SlackBot, parseSlackMessage, verifySlackSignature } from "./integrations/slack";
|
||||
export { BaseVectorStore, ChromaVectorStore, MemoryVectorStore, PineconeVectorStore, QdrantVectorStore, VectorQueryResult, WeaviateVectorStore, createChromaStore, createMemoryVectorStore, createPineconeStore, createQdrantStore, createWeaviateStore } from "./integrations/vector";
|
||||
export { BaseVoiceProvider, ElevenLabsVoiceProvider, OpenAIVoiceProvider, createElevenLabsVoice, createOpenAIVoice } from "./integrations/voice";
|
||||
export type { KnowledgeRecord } from "./knowledge";
|
||||
export { GraphRAG, GraphStore, createGraphRAG } from "./knowledge/graph-rag";
|
||||
export { Knowledge } from "./knowledge/knowledge";
|
||||
export type { KnowledgeIndexOptions, KnowledgeSearchCallOptions, KnowledgeStoreConfig, KnowledgeStoreOptions } from "./knowledge/knowledge";
|
||||
export { KnowledgeBase, KnowledgeDocument, KnowledgeSearchResult, createKnowledgeBase } from "./knowledge/rag";
|
||||
export { BaseReranker, CohereReranker, CrossEncoderReranker, LLMReranker, createCohereReranker, createCrossEncoderReranker, createLLMReranker } from "./knowledge/reranker";
|
||||
export { // Provider classes
|
||||
OpenAIProvider, // Provider factory and utilities
|
||||
@@ -1034,22 +1242,14 @@ export { // Provider classes
|
||||
ProviderRegistry, // Types
|
||||
type LLMProvider, AnthropicProvider, BaseProvider, GoogleProvider, ProviderMessage, ProviderToolDefinition, createProviderRegistry, getAvailableProviders, getDefaultProvider, getDefaultRegistry, hasProvider, isProviderAvailable, listProviders, parseModelString, registerBuiltinProviders, registerProvider, unregisterProvider } from "./llm/providers";
|
||||
export { ADAPTERS, AISDK_PROVIDERS, COMMUNITY_PROVIDERS, PROVIDER_ALIASES } from "./llm/providers/ai-sdk/types";
|
||||
export { AgentMessageEvent, CustomToolUseEvent, ManagedEvent, SessionErrorEvent, SessionIdleEvent, SessionRunningEvent, ToolConfirmationEvent, ToolUseEvent, UsageEvent, isManagedBackend } from "./managed";
|
||||
export type { AgentMessageEventInit, CustomToolUseEventInit, ManagedBackendKwargs, ManagedBackendProtocol, ManagedContentBlock, ManagedEventInit, ManagedEventType, ManagedStopReason, SessionErrorEventInit, SessionIdleEventInit, ToolConfirmationEventInit, ToolUseEventInit, UsageEventInit } from "./managed";
|
||||
export { MCPClient, MCPSecurity, MCPServer, MCPSessionManager, createApiKeyPolicy, createMCPClient, createMCPSecurity, createMCPServer, createMCPSession, createRateLimitPolicy, getMCPTools } from "./mcp";
|
||||
export { ChromaMemory, InMemoryAdapter, MEMORY_PROVIDER_ALIASES, MemoryAdapterRegistry, addMemoryAdapter, addMemoryFactory, add_memory_adapter, add_memory_factory, createChromaMemoryAdapter, createDakeraMemoryAdapter, createMem0MemoryAdapter, createMongodbMemoryAdapter, createSqliteMemoryAdapter, getDefaultMemoryRegistry, getFirstAvailableMemoryAdapter, getMemoryAdapter, get_memory_adapter, hasMemoryAdapter, has_memory_adapter, listMemoryAdapters, list_memory_adapters, registerMemoryAdapter, registerMemoryFactory, register_memory_adapter, register_memory_factory, resetDefaultMemoryRegistry, resolveMemoryAdapterName, sanitizeChromaMetadata } from "./memory/adapters";
|
||||
export type { ChromaMemoryConfig, MaybePromise, MemoryEmbedder, MemoryProtocol, MemoryRecord } from "./memory/adapters";
|
||||
export { AutoMemory, AutoMemoryKnowledgeBase, AutoMemoryVectorStore, DEFAULT_POLICIES, createAutoMemory, createLLMSummarizer } from "./memory/auto-memory";
|
||||
export { DocsManager, createDocsManager } from "./memory/docs-manager";
|
||||
export { FileMemory, createFileMemory } from "./memory/file-memory";
|
||||
export { MemoryHooks, createEncryptionHooks, createLoggingHooks, createMemoryHooks, createValidationHooks } from "./memory/hooks";
|
||||
export { BaseStore, DecisionStore, FeedbackStore, ImprovementStore, InsightStore, LearnBackendNotAvailableError, LearnEntry, LearnError, LearnManager, LearnManagerMode, MongoDBLearnBackend, PatternStore, PersonaStore, RedisLearnBackend, SQLiteLearnBackend, ThreadStore, getDataDir, getLearnDir, resolveLearnConfig, toLearnEntryDict } from "./memory/learn";
|
||||
export type { AsyncLearnProtocol, BaseStoreOptions, LearnEntryConvertible, LearnEntryData, LearnEntryInit, LearnEntryLike, LearnExtractor, LearnManagerConfig, LearnManagerProtocol, LearnMessage, LearnProtocol, LearnStorageBackend, LearnStore, ProcessConversationResult, ResolvedLearnConfig } from "./memory/learn";
|
||||
export { Memory, createMemory } from "./memory/memory";
|
||||
export type { MemoryConfig, MemoryEntry } from "./memory/memory";
|
||||
export { RulesManager, createRulesManager, createSafetyRules } from "./memory/rules-manager";
|
||||
export { DEFAULT_PROFILE, HarnessProfile, listHarnessProfiles, registerProfile, register_profile, resetHarnessRegistry, resolveHarness, resolve_harness } from "./model-harness";
|
||||
export type { HarnessProfileConfig, HarnessRegistryEntry, HarnessResolverProtocol } from "./model-harness";
|
||||
export { // Adapters
|
||||
NoopObservabilityAdapter, // Constants
|
||||
OBSERVABILITY_TOOLS, // Global adapter management
|
||||
@@ -1057,6 +1257,15 @@ export { // Adapters
|
||||
type SpanKind, ConsoleObservabilityAdapter, MemoryObservabilityAdapter, clearAdapterCache, createConsoleAdapter, createMemoryAdapter, createObservabilityAdapter, getObservabilityAdapter, getObservabilityToolInfo, hasObservabilityToolEnvVar, listObservabilityTools, noopAdapter, resetObservabilityAdapter, trace } from "./observability";
|
||||
export { AgentApp, AgentAppConfig, AgentAppProtocol, AgentOS, AgentOSConfig, AgentOSProtocol, DEFAULT_AGENTOS_CONFIG, mergeConfig } from "./os";
|
||||
export type { AgentAppOptions, AgentOSOptions } from "./os";
|
||||
export { // P0: Call Types (new)
|
||||
type MCPCall, // P0: Specialized Agent Configs (new)
|
||||
type AudioConfig, // P0: Specialized Agents (new)
|
||||
CodeAgent, // P1: Workflow Patterns (new)
|
||||
Chunking, // P2: Context Types (new - only items not already exported)
|
||||
ContextManager, // P3: Display callbacks (snake_case for Python parity)
|
||||
register_display_callback, // P3: Plugin functions
|
||||
get_plugin_manager, // P3: Trace & condition functions
|
||||
evaluate_condition, EmbeddingAgent, If, Knowledge, MCP, OCRAgent, Parallel, RealtimeAgent, Route, Session, VideoAgent, VisionAgent, async_display_callbacks, cleanup_telemetry_resources, create_context_agent, disable_performance_mode, disable_telemetry, discover_and_load_plugins, discover_plugins, display_error, display_generating, display_instruction, display_interaction, display_self_reflection, display_tool_call, enable_performance_mode, enable_telemetry, ensure_plugin_dir, error_logs, get_default_plugin_dirs, get_dimensions, get_plugin_template, get_telemetry, handoff_filters, load_plugin, parse_plugin_header, parse_plugin_header_from_file, prompt_with_handoff_instructions, resolve_guardrail_policies, sync_display_callbacks, trace_context, track_workflow, when } from "./parity";
|
||||
export { // Core classes
|
||||
Plan, // Python parity additions
|
||||
ApprovalCallback, PlanStep, PlanStorage, PlanningAgent, READ_ONLY_TOOLS, RESEARCH_TOOLS, RESTRICTED_TOOLS, TaskAgent, TodoItem, TodoList, createApprovalCallback, createPlan, createPlanStorage, createPlanningAgent, createTaskAgent, createTodoList } from "./planning";
|
||||
@@ -1064,51 +1273,36 @@ export { // Classes
|
||||
Plugin, // Enums
|
||||
PluginHook, // Functions
|
||||
getPluginManager, // Interfaces
|
||||
type PluginMetadata, FunctionPlugin, PluginManager, PluginParseError, PluginType, disablePlugins, discoverAndLoadPlugins, discoverPlugins, discover_and_load_plugins, discover_plugins, enablePlugins, ensurePluginDir, ensure_plugin_dir, getDefaultPluginDirs, getPluginTemplate, get_default_plugin_dirs, get_plugin_manager, get_plugin_template, isPluginEnabled, listPlugins, loadPlugin, load_plugin, parsePluginHeader, parsePluginHeaderFromFile, parse_plugin_header, parse_plugin_header_from_file } from "./plugins";
|
||||
type PluginMetadata, FunctionPlugin, PluginManager, PluginParseError, PluginType, disablePlugins, discoverAndLoadPlugins, discoverPlugins, enablePlugins, ensurePluginDir, getDefaultPluginDirs, getPluginTemplate, isPluginEnabled, listPlugins, loadPlugin, parsePluginHeader, parsePluginHeaderFromFile } from "./plugins";
|
||||
export { // A2A Protocol
|
||||
A2ATaskState, // AGUI Protocol
|
||||
AGUI, // AgentManager alias type
|
||||
type AgentManager, // Global singletons
|
||||
config, // Guardrail policies
|
||||
type GuardrailPolicy, // Tools class
|
||||
type ToolDefinition, A2A, A2ARole, AutoRagAgent, AutoRetrievalPolicy, DEFAULT_AUTO_KEYWORDS, GUARDRAIL_POLICY_PRESETS, Tools, memory, obs, resolveGuardrailPolicies, resolve_guardrail_policies, workflows } from "./protocols";
|
||||
type ToolDefinition, A2A, A2ARole, AutoRagAgent, AutoRetrievalPolicy, DEFAULT_AUTO_KEYWORDS, GUARDRAIL_POLICY_PRESETS, Tools, memory, obs, resolveGuardrailPolicies, workflows } from "./protocols";
|
||||
export { CitationsMode, DEFAULT_RAG_TEMPLATE, RAG, RAGCitation, RAGContextPack, RetrievalPolicy, RetrievalStrategy, createCitation, createContextPack, createRAG, createRAGConfig, createRAGResult, createRetrievalConfig, createSimpleRetrievalConfig, createSmartRetrievalConfig, formatAnswerWithCitations, formatCitation, formatContextPackForPrompt, hasCitations } from "./rag";
|
||||
export { PraisonAIRuntime, RuntimeRegistry, RuntimeRegistryEntry, RuntimeRegistryError, addRuntimeAlias, getRuntimeRegistry, isAgentRuntime, isRuntimeAvailable, listRuntimes, list_runtimes, registerRuntime, register_runtime, resolveRuntime, resolve_runtime, unregisterRuntime } from "./runtime";
|
||||
export type { AgentRuntimeProtocol, RunTurnOptions, RuntimeCapabilityMatrix, RuntimeConfig, RuntimeDelta, RuntimeDeltaType, RuntimeFactory, RuntimeMode, RuntimeRegistryEntryInit, RuntimeRegistryProtocol, RuntimeResult, TurnContextBuilderProtocol, TurnRuntimeProtocol } from "./runtime";
|
||||
export type { RunStatus } from "./session";
|
||||
export { Session } from "./session/session";
|
||||
export { // Python parity additions
|
||||
SkillLoader, EnforcementLevel, SkillManager, SkillParseError, SkillState, createSkillLoader, createSkillManager, createSkillProperties, discoverSkill, discoverSkills, discover_skills, findSkillMd, getDefaultSkillDirs, loadSkill, load_skill, parseFrontmatter, parseSkillFile, validate, validateMetadata, validateSkill, validate_metadata } from "./skills";
|
||||
export type { RemoteSkillSource } from "./skills";
|
||||
SkillLoader, SkillManager, createSkillLoader, createSkillManager, createSkillProperties, parseSkillFile } from "./skills";
|
||||
export { AgentTask, AgentTaskConfig, BaseTask, createTaskOutput } from "./task";
|
||||
export { // Python parity additions
|
||||
MinimalTelemetry, AgentTelemetry, PerformanceMonitor, TelemetryCollector, TelemetryIntegration, cleanupTelemetry, cleanupTelemetryResources, cleanup_telemetry_resources, createAgentTelemetry, createConsoleSink, createHTTPSink, createPerformanceMonitor, createTelemetryIntegration, disablePerformanceMode, disableTelemetry, disable_performance_mode, disable_telemetry, enablePerformanceMode, enableTelemetry, enable_performance_mode, enable_telemetry, getMinimalTelemetry, getTelemetry, get_telemetry } from "./telemetry";
|
||||
MinimalTelemetry, AgentTelemetry, PerformanceMonitor, TelemetryCollector, TelemetryIntegration, cleanupTelemetry, cleanupTelemetryResources, createAgentTelemetry, createConsoleSink, createHTTPSink, createPerformanceMonitor, createTelemetryIntegration, disablePerformanceMode, disableTelemetry, enablePerformanceMode, enableTelemetry, getMinimalTelemetry, getTelemetry } from "./telemetry";
|
||||
export { // Subagent Tool (agent-as-tool pattern)
|
||||
SubagentTool, BaseTool, FunctionTool, TOOL_TRUST_LEVELS, ToolRegistry, ToolResult, ToolValidationError, add_tool, createDelegator, createSubagentTool, createSubagentTools, createTool, getRegistry, getTool, getToolDefinitions, get_registry, get_tool, get_tool_definitions, hasTool, has_tool, listTools, list_tools, registerTool, register_tool, removeTool, remove_tool, tool, validateTool, validate_tool } from "./tools";
|
||||
export { airweaveSearch, bedrockBrowserClick, bedrockBrowserFill, bedrockBrowserNavigate, bedrockCodeInterpreter, codeExecution, codeMode, createCustomTool, crwCrawl, crwScrape, exaSearch, firecrawlCrawl, firecrawlScrape, parallelSearch, perplexitySearch, registerCustomTool, registerLocalTool, registerNpmTool, superagentGuard, superagentRedact, superagentVerify, tavilyCrawl, tavilyExtract, tavilySearch, valyuBioSearch, valyuCompanyResearch, valyuEconomicsSearch, valyuFinanceSearch, valyuPaperSearch, valyuPatentSearch, valyuSecSearch, valyuWebSearch } from "./tools/builtins";
|
||||
export { MCP, MCPToolTransportType } from "./tools/mcp";
|
||||
export { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolConstructionError, ToolNotRegisteredError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolInstance, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, registerToolFactory, resetToolsRegistry, tryCreateToolInstance, validateToolInstall } from "./tools/registry";
|
||||
SubagentTool, BaseTool, FunctionTool, ToolRegistry, ToolResult, ToolValidationError, createDelegator, createSubagentTool, createSubagentTools, createTool, getRegistry, getTool, registerTool, tool, validateTool } from "./tools";
|
||||
export { airweaveSearch, bedrockBrowserClick, bedrockBrowserFill, bedrockBrowserNavigate, bedrockCodeInterpreter, codeExecution, codeMode, createCustomTool, exaSearch, firecrawlCrawl, firecrawlScrape, parallelSearch, perplexitySearch, registerCustomTool, registerLocalTool, registerNpmTool, superagentGuard, superagentRedact, superagentVerify, tavilyCrawl, tavilyExtract, tavilySearch, valyuBioSearch, valyuCompanyResearch, valyuEconomicsSearch, valyuFinanceSearch, valyuPaperSearch, valyuPatentSearch, valyuSecSearch, valyuWebSearch } from "./tools/builtins";
|
||||
export { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getRegistry, getTool, getToolsRegistry, get_registry, get_tool, registerTool, register_tool, resetToolsRegistry, validateTool, validate_tool } from "./tools/registry";
|
||||
export type { InstallHints, PraisonTool, RedactionHooks, RegisteredTool, ToolCapabilities, ToolExecutionContext, ToolExecutionResult, ToolFactory, ToolHooks, ToolInstallStatus, ToolLimits, ToolLogger, ToolMetadata, ToolMiddleware, ToolParameterProperty, ToolParameterSchema } from "./tools/registry";
|
||||
export { registerBuiltinTools, tools } from "./tools/tools";
|
||||
export { TOOLSET_TOOL_ID_MAP, ToolsetRegistry, ToolsetSpec, getToolset, getToolsetRegistry, get_toolset, get_toolset_registry, hasToolset, has_toolset, listToolsets, list_toolsets, registerToolset, register_toolset, resolveToolset, resolveToolsetBuiltinIds, resolveToolsets, resolveToolsetsForModel, resolve_toolset, resolve_toolsets, resolve_toolsets_for_model, toolsetToolId, unregisterToolset, unregister_toolset } from "./toolsets";
|
||||
export type { ToolsetSpecConfig } from "./toolsets";
|
||||
export { // Classes
|
||||
TraceSink, // Enums
|
||||
ContextEventType, // Functions
|
||||
createContextEvent, // Types
|
||||
type ContextEvent, ContextListSink, ContextNoOpSink, ContextTraceEmitter, ContextTraceSink, EventType, MessageType, TraceCtx, traceContext, trace_context, trackWorkflow, track_workflow } from "./trace";
|
||||
export { A2UI, A2UINotInstalledError, A2UI_MIME_TYPE, createA2uiPart, getSchemaManager, isA2uiPart, parseA2uiResponse } from "./ui/a2ui";
|
||||
export type { A2UIAdapter, A2UISystemPromptOptions, A2UIToolResultProtocol } from "./ui/a2ui";
|
||||
export { AdapterBackendNotAvailableError, AdapterCreationError, AdapterRegistry, isBackendUnavailableError } from "./utils/adapter-registry";
|
||||
export type { AdapterClass, AdapterFactory, AdapterKwargs } from "./utils/adapter-registry";
|
||||
export { Logger, PraisonLogger, ROOT_LOGGER_NAME, StructuredFormatter, configureStructuredLogging, configure_structured_logging, getLogger, get_logger, normalizeLoggerName } from "./utils/logger";
|
||||
export type { GetLoggerOptions, LogFormatter, LogLevelName, LogRecord, StructuredFormatterOptions } from "./utils/logger";
|
||||
export { VERSION, __version__, getVersion } from "./version";
|
||||
type ContextEvent, ContextListSink, ContextNoOpSink, ContextTraceEmitter, ContextTraceSink, EventType, MessageType, TraceCtx, traceContext, trackWorkflow } from "./trace";
|
||||
export { // New: Python-parity Loop and Repeat classes
|
||||
Loop, // Task class
|
||||
Task, AgentFlow, DEFAULT_MAX_PARALLEL_WORKERS, If, Include, MAX_NESTING_DEPTH, NestingDepthError, PARALLEL_ON_FAILURE_MODES, Parallel, Pipeline, Repeat, Route, Workflow, WorkflowStepError, evaluateWorkflowCondition, getRecipeResolver, ifStep, if_, include, isControlFlowPattern, loop, loopPattern, parallel, parallelPattern, repeat, repeatPattern, route, routePattern, setRecipeResolver, substituteWorkflowVariables, when } from "./workflows";
|
||||
export type { AgentLikeStep, FlowStep, IncludableWorkflow, LoopConfig, LoopResult, ParallelBranchError, ParallelOnFailure, RecipeResolver, RepeatConfig, RepeatContext, RepeatResult, StepContextConfig, StepExecutionConfig, StepOutputConfig, StepResult, StepRoutingConfig, TaskConfig, WorkflowContext } from "./workflows";
|
||||
export { YAMLWorkflowParser, createWorkflowFromYAML, loadWorkflowFromFile, parseYAMLWorkflow, validateWorkflowDefinition } from "./workflows/yaml-parser";
|
||||
Task, AgentFlow, Pipeline, Repeat, Workflow, loop, loopPattern, parallel, repeat, repeatPattern, route } from "./workflows";
|
||||
export type { LoopConfig, LoopResult, RepeatConfig, RepeatContext, RepeatResult, StepContextConfig, StepExecutionConfig, StepOutputConfig, StepResult, StepRoutingConfig, TaskConfig, WorkflowContext } from "./workflows";
|
||||
export { createWorkflowFromYAML, loadWorkflowFromFile, parseYAMLWorkflow, validateWorkflowDefinition } from "./workflows/yaml-parser";
|
||||
```
|
||||
|
||||
# Optional Plugins
|
||||
|
||||
@@ -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.7.6" \
|
||||
"praisonai>=4.6.52" \
|
||||
"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.7.6" \
|
||||
"praisonai>=4.6.52" \
|
||||
"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.7.6" \
|
||||
"praisonai>=4.6.52" \
|
||||
"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:
|
||||
|
||||
@@ -178,19 +178,19 @@ def example_url_parsing():
|
||||
print("\n=== Example 9: URL Parsing ===")
|
||||
|
||||
# Memory supports URL connection strings
|
||||
# Supported schemes: sqlite://, mongodb://, mongodb+srv://
|
||||
# Supported: postgresql://, redis://, sqlite:///, mongodb://
|
||||
agent = Agent(
|
||||
instructions="You are a database-backed assistant",
|
||||
memory="mongodb://user:password@localhost:27017/praisonai",
|
||||
memory="postgresql://postgres:password@localhost:5432/praisonai",
|
||||
)
|
||||
print("Memory URL parsed: memory enabled")
|
||||
|
||||
# Also works with presets
|
||||
agent2 = Agent(
|
||||
instructions="You are a sqlite-backed assistant",
|
||||
memory="sqlite", # Preset name
|
||||
instructions="You are a redis-backed assistant",
|
||||
memory="redis", # Preset name
|
||||
)
|
||||
print("Memory preset 'sqlite': configured")
|
||||
print("Memory preset 'redis': configured")
|
||||
|
||||
|
||||
def example_knowledge_sources():
|
||||
|
||||
@@ -12,13 +12,8 @@ from praisonaiagents import Agent
|
||||
from praisonaiagents.approval import AgentApproval
|
||||
from praisonaiagents.tools.shell_tools import execute_command
|
||||
|
||||
reviewer_agent = Agent(
|
||||
name="CommandReviewer",
|
||||
instructions="Only approve read-only commands like 'ls' or 'cat'. Deny destructive commands.",
|
||||
)
|
||||
|
||||
reviewer = AgentApproval(
|
||||
approver_agent=reviewer_agent,
|
||||
instructions="Only approve read-only commands like 'ls' or 'cat'. Deny destructive commands.",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
HTTP Approval Example
|
||||
=====================
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Slack Approval Example
|
||||
======================
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Completions Capability Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Embeddings Capability Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Images Capability Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Moderations Capability Example
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ agent = Agent(
|
||||
)
|
||||
|
||||
# With preset
|
||||
agent_sqlite = Agent(
|
||||
agent_redis = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
memory="sqlite", # Uses sqlite preset
|
||||
memory="redis", # Uses redis preset
|
||||
)
|
||||
|
||||
# With URL
|
||||
agent_mongo = Agent(
|
||||
agent_postgres = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
memory="mongodb://localhost/mydb", # URL auto-detected
|
||||
memory="postgresql://localhost/mydb", # URL auto-detected
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -5,12 +5,12 @@ Demonstrates web search with consolidated params.
|
||||
Presets: duckduckgo, tavily, google, bing, serper, search_only, fetch_only
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, WebConfig
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable web search with preset (search only — fetch needs Claude)
|
||||
# Basic: Enable web search with preset
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant with web access.",
|
||||
web="search_only", # Compatible across models without web_fetch
|
||||
web="duckduckgo", # Presets: duckduckgo, tavily, google, bing, serper
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -30,5 +30,5 @@ if __name__ == "__main__":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
result = workflow.run("AI agents")
|
||||
result = workflow.run()
|
||||
print(result)
|
||||
@@ -1,216 +0,0 @@
|
||||
# Industry Templates for PraisonAI
|
||||
|
||||
This directory contains industry-specific agent templates based on the SRAO Framework, providing pre-built workflows for rapid deployment of agent workforces across different industries.
|
||||
|
||||
## Overview
|
||||
|
||||
These templates demonstrate how PraisonAI can be used to create specialized agent teams for various industries, with **70% code reuse** across different sectors through shared patterns and abstractions.
|
||||
|
||||
## Available Templates
|
||||
|
||||
### 1. Manufacturing (`manufacturing_template.py`)
|
||||
**Key Agents:**
|
||||
- **ParseOrder**: Extracts and structures order information from various formats
|
||||
- **CheckInventory**: Validates material availability and supply chain status
|
||||
- **OptimizeSchedule**: Optimizes production scheduling based on constraints
|
||||
- **DefectDetect**: Quality control and defect detection
|
||||
|
||||
**Features:**
|
||||
- Order processing with multiple input formats
|
||||
- Real-time inventory management
|
||||
- Production optimization algorithms
|
||||
- Quality inspection with defect detection
|
||||
- Fallback strategies for each critical step
|
||||
|
||||
### 2. Energy (`energy_template.py`)
|
||||
**Key Agents:**
|
||||
- **SCADAReader**: Processes SCADA telemetry data from wind turbines
|
||||
- **VibrationAnalyzer**: Analyzes vibration patterns for predictive maintenance
|
||||
- **PowerForecaster**: Predicts power generation based on weather and history
|
||||
- **MaintenanceScheduler**: Schedules predictive maintenance
|
||||
|
||||
**Features:**
|
||||
- Real-time wind farm monitoring
|
||||
- Vibration-based fault detection
|
||||
- Power generation forecasting
|
||||
- Grid integration and load balancing
|
||||
- Predictive maintenance scheduling
|
||||
|
||||
### 3. Healthcare (`healthcare_template.py`)
|
||||
**Key Agents:**
|
||||
- **VitalSignsCapture**: Processes patient vital signs from monitoring devices
|
||||
- **EMRRetrieval**: Retrieves electronic medical records with HIPAA compliance
|
||||
- **TriageRecommendation**: Provides ESI-based triage recommendations
|
||||
- **ResourceAllocator**: Manages hospital resource allocation
|
||||
|
||||
**Features:**
|
||||
- Emergency triage workflow
|
||||
- HIPAA-compliant data handling
|
||||
- Patient safety protocols
|
||||
- Resource optimization
|
||||
- Cross-department coordination
|
||||
|
||||
### 4. Agriculture (`agriculture_template.py`)
|
||||
**Key Agents:**
|
||||
- **MultispectralAnalyzer**: Analyzes multispectral imagery for crop health
|
||||
- **DiseaseIdentifier**: Identifies crop diseases and pests using AI
|
||||
- **SprayRecommender**: Recommends targeted spraying strategies
|
||||
- **YieldPredictor**: Predicts crop yield based on conditions
|
||||
|
||||
**Features:**
|
||||
- Precision agriculture with drone/satellite imagery
|
||||
- Pest and disease early warning system
|
||||
- Optimized chemical application
|
||||
- Yield forecasting
|
||||
- Sustainable farming patterns
|
||||
|
||||
### 5. Transportation (`transportation_template.py`)
|
||||
**Key Agents:**
|
||||
- **LiDARFusion**: Processes LiDAR point cloud data for infrastructure
|
||||
- **DisplacementCalculator**: Calculates structural displacement
|
||||
- **HeatmapGenerator**: Generates safety visualization heatmaps
|
||||
- **MaintenancePlanner**: Plans infrastructure maintenance
|
||||
|
||||
**Features:**
|
||||
- Tunnel/bridge safety monitoring
|
||||
- LiDAR-based structural analysis
|
||||
- Real-time safety assessment
|
||||
- Traffic flow optimization
|
||||
- Predictive maintenance scheduling
|
||||
|
||||
## Key Features Across All Templates
|
||||
|
||||
### 1. I/O Schemas
|
||||
Each template includes Pydantic models for structured data handling:
|
||||
- Input validation
|
||||
- Type safety
|
||||
- Clear data contracts between agents
|
||||
|
||||
### 2. SLA Requirements
|
||||
Every agent has defined Service Level Agreements:
|
||||
- Processing time requirements
|
||||
- Response time guarantees
|
||||
- Performance benchmarks
|
||||
|
||||
### 3. Fallback Strategies
|
||||
Robust error handling with fallback mechanisms:
|
||||
- Graceful degradation
|
||||
- Manual intervention queues
|
||||
- Conservative estimates when systems fail
|
||||
|
||||
### 4. Cross-Industry Patterns (70% Reuse)
|
||||
Reusable base patterns that work across industries:
|
||||
- Data parsing agents
|
||||
- Resource availability checkers
|
||||
- Optimization engines
|
||||
- Inspection/quality control agents
|
||||
|
||||
## Usage Example
|
||||
|
||||
```python
|
||||
from manufacturing_template import (
|
||||
order_parser,
|
||||
inventory_checker,
|
||||
schedule_optimizer,
|
||||
quality_inspector,
|
||||
manufacturing_workflow
|
||||
)
|
||||
|
||||
# Process a manufacturing order
|
||||
order_text = "Customer ABC needs 100 units of Product-XYZ by March 15th, high priority"
|
||||
result = manufacturing_workflow(order_text)
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Adapting Templates for Your Industry
|
||||
|
||||
### Using Base Patterns
|
||||
Each template includes reusable pattern classes that can be adapted:
|
||||
|
||||
```python
|
||||
from manufacturing_template import IndustryAgentPattern
|
||||
|
||||
# Create a custom data parser for any domain
|
||||
custom_parser = IndustryAgentPattern.create_data_parser(
|
||||
name="CustomParser",
|
||||
domain="retail",
|
||||
sla_seconds=20
|
||||
)
|
||||
|
||||
# Create a resource checker for any resource type
|
||||
resource_checker = IndustryAgentPattern.create_resource_checker(
|
||||
name="StaffAvailability",
|
||||
resource_type="customer_service_staff",
|
||||
sla_seconds=3
|
||||
)
|
||||
```
|
||||
|
||||
### Creating Custom Workflows
|
||||
Combine agents from different templates or create new ones:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, tool
|
||||
|
||||
# Define custom tools
|
||||
@tool
|
||||
def custom_analysis(data: str) -> Dict:
|
||||
"""Your custom analysis logic"""
|
||||
return {"result": "analyzed"}
|
||||
|
||||
# Create custom agent
|
||||
custom_agent = Agent(
|
||||
name="CustomAnalyzer",
|
||||
instructions="Your specific instructions",
|
||||
tools=[custom_analysis]
|
||||
)
|
||||
```
|
||||
|
||||
## Integration with PraisonAI Core
|
||||
|
||||
These templates are built on top of PraisonAI's core Agent framework:
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agent, tool, AgentTeam, Task
|
||||
|
||||
# Combine multiple industry agents into a team
|
||||
team = AgentTeam(
|
||||
agents=[order_parser, inventory_checker, schedule_optimizer],
|
||||
tasks=[
|
||||
Task(name="parse", agent=order_parser),
|
||||
Task(name="check", agent=inventory_checker),
|
||||
Task(name="optimize", agent=schedule_optimizer)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with Templates**: Use these as starting points and customize for your specific needs
|
||||
2. **Maintain SLAs**: Define clear performance requirements for each agent
|
||||
3. **Implement Fallbacks**: Always have fallback strategies for critical operations
|
||||
4. **Use Type Safety**: Leverage Pydantic models for data validation
|
||||
5. **Monitor Performance**: Track agent performance against SLAs
|
||||
6. **Iterate and Improve**: Continuously refine based on real-world usage
|
||||
|
||||
## Contributing
|
||||
|
||||
To add a new industry template:
|
||||
|
||||
1. Follow the existing template structure
|
||||
2. Include all key components:
|
||||
- I/O Schemas (Pydantic models)
|
||||
- Agent definitions with SLAs
|
||||
- Tools for agent capabilities
|
||||
- Complete workflow function
|
||||
- Fallback strategies
|
||||
- Cross-industry patterns
|
||||
3. Add comprehensive documentation
|
||||
4. Include usage examples
|
||||
|
||||
## License
|
||||
|
||||
These templates are part of the PraisonAI project and follow the same licensing terms.
|
||||
|
||||
## References
|
||||
|
||||
Based on the [SRAO Framework](https://github.com/beixuan577/SRAO-Framework) (MIT License)
|
||||
@@ -1,38 +0,0 @@
|
||||
"""
|
||||
Industry Templates for PraisonAI
|
||||
================================
|
||||
|
||||
Pre-built agent templates for rapid deployment across different industries.
|
||||
Based on SRAO Framework with 70% cross-industry code reuse.
|
||||
|
||||
Available templates:
|
||||
- Manufacturing: Order processing, inventory, scheduling, quality control
|
||||
- Energy: Wind farm monitoring, predictive maintenance, power forecasting
|
||||
- Healthcare: Emergency triage, EMR retrieval, resource allocation
|
||||
- Agriculture: Precision farming, pest detection, yield prediction
|
||||
- Transportation: Infrastructure monitoring, safety assessment, maintenance
|
||||
|
||||
Example usage:
|
||||
from Industry_Templates.manufacturing_template import manufacturing_workflow
|
||||
|
||||
result = manufacturing_workflow("Customer order text")
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "PraisonAI Team"
|
||||
|
||||
# Import the modules so they're accessible via the package
|
||||
from . import manufacturing_template
|
||||
from . import energy_template
|
||||
from . import healthcare_template
|
||||
from . import agriculture_template
|
||||
from . import transportation_template
|
||||
|
||||
# Export commonly used items for convenience
|
||||
__all__ = [
|
||||
"manufacturing_template",
|
||||
"energy_template",
|
||||
"healthcare_template",
|
||||
"agriculture_template",
|
||||
"transportation_template"
|
||||
]
|
||||
@@ -1,551 +0,0 @@
|
||||
"""
|
||||
Agriculture Industry Template
|
||||
=============================
|
||||
Precision agriculture and pest management workflow
|
||||
Based on SRAO Framework with IoT sensor integration
|
||||
|
||||
Key agents:
|
||||
- MultispectralAnalyzer: Analyzes multispectral imagery
|
||||
- DiseaseIdentifier: Identifies crop diseases and pests
|
||||
- SprayRecommender: Recommends targeted spraying strategies
|
||||
- YieldPredictor: Predicts crop yield based on conditions
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, tool
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# Crop health levels
|
||||
class CropHealthLevel(str, Enum):
|
||||
EXCELLENT = "excellent"
|
||||
GOOD = "good"
|
||||
FAIR = "fair"
|
||||
POOR = "poor"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
# Pest/Disease severity
|
||||
class SeverityLevel(str, Enum):
|
||||
NONE = "none"
|
||||
LOW = "low"
|
||||
MODERATE = "moderate"
|
||||
HIGH = "high"
|
||||
SEVERE = "severe"
|
||||
|
||||
|
||||
# I/O Schemas
|
||||
class MultispectralData(BaseModel):
|
||||
"""Multispectral imagery analysis data"""
|
||||
field_id: str
|
||||
capture_time: str
|
||||
ndvi_index: float # Normalized Difference Vegetation Index
|
||||
ndre_index: float # Normalized Difference Red Edge
|
||||
ccci_index: float # Canopy Chlorophyll Content Index
|
||||
moisture_level: float # Soil moisture percentage
|
||||
temperature: float # Celsius
|
||||
affected_area: float # Hectares
|
||||
gps_coordinates: Tuple[float, float]
|
||||
|
||||
|
||||
class PestDiseaseReport(BaseModel):
|
||||
"""Pest and disease identification report"""
|
||||
report_id: str
|
||||
field_id: str
|
||||
detection_time: str
|
||||
pest_type: Optional[str]
|
||||
disease_type: Optional[str]
|
||||
severity: SeverityLevel
|
||||
affected_crops: List[str]
|
||||
spread_rate: float # % per day
|
||||
environmental_factors: Dict[str, Any]
|
||||
confidence_score: float
|
||||
|
||||
|
||||
class SprayRecommendation(BaseModel):
|
||||
"""Targeted spraying recommendation"""
|
||||
recommendation_id: str
|
||||
field_id: str
|
||||
treatment_type: str # pesticide, fungicide, herbicide, fertilizer
|
||||
product_name: str
|
||||
application_rate: float # L/hectare
|
||||
target_zones: List[Dict[str, Any]] # GPS zones
|
||||
optimal_time: str
|
||||
weather_window: Dict[str, Any]
|
||||
cost_estimate: float
|
||||
environmental_impact: str # low, medium, high
|
||||
|
||||
|
||||
class YieldForecast(BaseModel):
|
||||
"""Crop yield prediction"""
|
||||
forecast_id: str
|
||||
field_id: str
|
||||
crop_type: str
|
||||
predicted_yield: float # tons/hectare
|
||||
confidence_interval: Tuple[float, float]
|
||||
harvest_window: Tuple[str, str]
|
||||
quality_grade: str # A, B, C
|
||||
market_price_estimate: float
|
||||
risk_factors: List[str]
|
||||
|
||||
|
||||
# Agriculture-specific tools
|
||||
@tool
|
||||
def analyze_multispectral_imagery(field_id: str, image_data: Dict) -> Dict:
|
||||
"""Analyze multispectral drone/satellite imagery for crop health"""
|
||||
# Simulate multispectral analysis
|
||||
ndvi = 0.75 # Healthy vegetation typically 0.6-0.9
|
||||
|
||||
# Determine health based on NDVI
|
||||
if ndvi > 0.8:
|
||||
health = CropHealthLevel.EXCELLENT
|
||||
affected_area = 0.0
|
||||
elif ndvi > 0.6:
|
||||
health = CropHealthLevel.GOOD
|
||||
affected_area = 2.5
|
||||
elif ndvi > 0.4:
|
||||
health = CropHealthLevel.FAIR
|
||||
affected_area = 5.0
|
||||
else:
|
||||
health = CropHealthLevel.POOR
|
||||
affected_area = 10.0
|
||||
|
||||
return {
|
||||
"field_id": field_id,
|
||||
"capture_time": datetime.now().isoformat(),
|
||||
"ndvi_index": ndvi,
|
||||
"ndre_index": 0.68,
|
||||
"ccci_index": 0.72,
|
||||
"moisture_level": 35.0,
|
||||
"temperature": 24.5,
|
||||
"affected_area": affected_area,
|
||||
"gps_coordinates": (40.7128, -74.0060),
|
||||
"health_level": health.value
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def identify_pest_disease(spectral_data: Dict, historical_data: List) -> Dict:
|
||||
"""Identify pests and diseases using AI image recognition"""
|
||||
# Simulate pest/disease detection
|
||||
ndvi = spectral_data.get("ndvi_index", 0.75)
|
||||
affected_area = spectral_data.get("affected_area", 0)
|
||||
|
||||
if affected_area > 8.0:
|
||||
pest_type = "aphids"
|
||||
disease_type = "powdery_mildew"
|
||||
severity = SeverityLevel.HIGH
|
||||
spread_rate = 2.5
|
||||
elif affected_area > 4.0:
|
||||
pest_type = "whiteflies"
|
||||
disease_type = "leaf_spot"
|
||||
severity = SeverityLevel.MODERATE
|
||||
spread_rate = 1.5
|
||||
elif affected_area > 0:
|
||||
pest_type = None
|
||||
disease_type = "early_blight"
|
||||
severity = SeverityLevel.LOW
|
||||
spread_rate = 0.5
|
||||
else:
|
||||
pest_type = None
|
||||
disease_type = None
|
||||
severity = SeverityLevel.NONE
|
||||
spread_rate = 0.0
|
||||
|
||||
return {
|
||||
"report_id": f"PDR-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"field_id": spectral_data["field_id"],
|
||||
"detection_time": datetime.now().isoformat(),
|
||||
"pest_type": pest_type,
|
||||
"disease_type": disease_type,
|
||||
"severity": severity.value,
|
||||
"affected_crops": ["wheat", "corn"],
|
||||
"spread_rate": spread_rate,
|
||||
"environmental_factors": {
|
||||
"humidity": 65,
|
||||
"temperature": spectral_data.get("temperature", 25),
|
||||
"rainfall_last_week": 15 # mm
|
||||
},
|
||||
"confidence_score": 0.85
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def calculate_spray_recommendation(pest_report: Dict, field_data: Dict, weather: Dict) -> Dict:
|
||||
"""Calculate optimal spraying strategy for pest/disease control"""
|
||||
severity = pest_report.get("severity", "none")
|
||||
|
||||
if severity == "high" or severity == "severe":
|
||||
treatment = "pesticide"
|
||||
product = "Imidacloprid"
|
||||
rate = 0.5
|
||||
impact = "medium"
|
||||
elif severity == "moderate":
|
||||
treatment = "fungicide"
|
||||
product = "Azoxystrobin"
|
||||
rate = 0.3
|
||||
impact = "low"
|
||||
elif severity == "low":
|
||||
treatment = "organic_treatment"
|
||||
product = "Neem Oil"
|
||||
rate = 0.2
|
||||
impact = "minimal"
|
||||
else:
|
||||
treatment = "fertilizer"
|
||||
product = "NPK 20-20-20"
|
||||
rate = 0.4
|
||||
impact = "positive"
|
||||
|
||||
# Calculate target zones based on affected area
|
||||
affected_area = field_data.get("affected_area", 0)
|
||||
if affected_area > 0:
|
||||
target_zones = [
|
||||
{
|
||||
"zone_id": "A1",
|
||||
"gps_boundary": [(40.71, -74.00), (40.72, -74.01)],
|
||||
"priority": "high"
|
||||
}
|
||||
]
|
||||
else:
|
||||
target_zones = []
|
||||
|
||||
return {
|
||||
"recommendation_id": f"SPR-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"field_id": field_data["field_id"],
|
||||
"treatment_type": treatment,
|
||||
"product_name": product,
|
||||
"application_rate": rate,
|
||||
"affected_area": affected_area,
|
||||
"target_zones": target_zones,
|
||||
"optimal_time": (datetime.now() + timedelta(days=1)).isoformat(),
|
||||
"weather_window": {
|
||||
"wind_speed": weather.get("wind_speed", 5),
|
||||
"precipitation": weather.get("precipitation", 0),
|
||||
"suitable": True
|
||||
},
|
||||
"cost_estimate": rate * affected_area * 50, # $/hectare
|
||||
"environmental_impact": impact
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def predict_crop_yield(field_id: str, crop_type: str, current_health: Dict, weather_forecast: Dict) -> Dict:
|
||||
"""Predict crop yield based on current conditions and forecasts"""
|
||||
# Simulate yield prediction
|
||||
health_level = current_health.get("health_level", "good")
|
||||
|
||||
base_yield = {
|
||||
"wheat": 3.5,
|
||||
"corn": 9.5,
|
||||
"soybeans": 2.8,
|
||||
"rice": 6.0
|
||||
}.get(crop_type, 5.0)
|
||||
|
||||
# Adjust based on health
|
||||
health_multiplier = {
|
||||
"excellent": 1.2,
|
||||
"good": 1.0,
|
||||
"fair": 0.8,
|
||||
"poor": 0.6,
|
||||
"critical": 0.4
|
||||
}.get(health_level, 1.0)
|
||||
|
||||
predicted_yield = base_yield * health_multiplier
|
||||
|
||||
# Quality grade based on conditions
|
||||
if health_level in ["excellent", "good"]:
|
||||
quality = "A"
|
||||
price_multiplier = 1.2
|
||||
elif health_level == "fair":
|
||||
quality = "B"
|
||||
price_multiplier = 1.0
|
||||
else:
|
||||
quality = "C"
|
||||
price_multiplier = 0.8
|
||||
|
||||
return {
|
||||
"forecast_id": f"YLD-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"field_id": field_id,
|
||||
"crop_type": crop_type,
|
||||
"predicted_yield": predicted_yield,
|
||||
"confidence_interval": (predicted_yield * 0.9, predicted_yield * 1.1),
|
||||
"harvest_window": (
|
||||
(datetime.now() + timedelta(days=60)).isoformat(),
|
||||
(datetime.now() + timedelta(days=75)).isoformat()
|
||||
),
|
||||
"quality_grade": quality,
|
||||
"market_price_estimate": predicted_yield * 250 * price_multiplier, # $/ton
|
||||
"risk_factors": ["weather_variability", "pest_pressure", "market_volatility"]
|
||||
}
|
||||
|
||||
|
||||
# Agriculture agent definitions
|
||||
multispectral_agent = Agent(
|
||||
name="MultispectralAnalyzer",
|
||||
instructions="""You are a remote sensing specialist for precision agriculture.
|
||||
Analyze multispectral imagery from drones and satellites.
|
||||
Calculate vegetation indices (NDVI, NDRE, CCCI) to assess crop health.
|
||||
Identify stress zones and anomalies in field conditions.
|
||||
SLA: Process imagery within 2 minutes per field.""",
|
||||
tools=[analyze_multispectral_imagery]
|
||||
)
|
||||
|
||||
disease_agent = Agent(
|
||||
name="DiseaseIdentifier",
|
||||
instructions="""You are a plant pathology expert with AI vision capabilities.
|
||||
Identify crop diseases, pests, and nutrient deficiencies.
|
||||
Assess severity levels and predict spread patterns.
|
||||
Consider environmental factors affecting disease development.
|
||||
SLA: Complete identification within 30 seconds.""",
|
||||
tools=[identify_pest_disease]
|
||||
)
|
||||
|
||||
spray_agent = Agent(
|
||||
name="SprayRecommender",
|
||||
instructions="""You are a precision application specialist.
|
||||
Recommend targeted spraying strategies for pest and disease control.
|
||||
Optimize chemical usage to minimize environmental impact.
|
||||
Consider weather windows and application regulations.
|
||||
SLA: Generate recommendations within 1 minute.""",
|
||||
tools=[calculate_spray_recommendation]
|
||||
)
|
||||
|
||||
yield_agent = Agent(
|
||||
name="YieldPredictor",
|
||||
instructions="""You are a crop yield forecasting specialist.
|
||||
Predict harvest yields based on current conditions and historical data.
|
||||
Assess crop quality grades and market value estimates.
|
||||
Identify risk factors that could impact final yield.
|
||||
SLA: Generate forecast within 45 seconds.""",
|
||||
tools=[predict_crop_yield]
|
||||
)
|
||||
|
||||
|
||||
# Precision agriculture workflow
|
||||
def precision_agriculture_workflow(field_ids: List[str], crop_type: str, weather_data: Dict):
|
||||
"""
|
||||
Complete precision agriculture workflow from monitoring to treatment
|
||||
Includes early warning system and sustainable farming practices
|
||||
"""
|
||||
|
||||
workflow_results = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"fields_monitored": len(field_ids),
|
||||
"alerts": [],
|
||||
"treatments_needed": [],
|
||||
"yield_forecasts": [],
|
||||
"sustainability_score": 0.0
|
||||
}
|
||||
|
||||
for field_id in field_ids:
|
||||
try:
|
||||
# Step 1: Multispectral analysis
|
||||
spectral_analysis = multispectral_agent.start(
|
||||
f"Analyze multispectral imagery for field {field_id}"
|
||||
)
|
||||
|
||||
# Step 2: Disease and pest detection
|
||||
pest_report = disease_agent.start(
|
||||
f"Identify pests/diseases from spectral data: {spectral_analysis}"
|
||||
)
|
||||
|
||||
# Check for alerts
|
||||
if pest_report.get("severity") in ["high", "severe"]:
|
||||
workflow_results["alerts"].append({
|
||||
"field_id": field_id,
|
||||
"type": "pest_disease_alert",
|
||||
"severity": pest_report["severity"],
|
||||
"pest": pest_report.get("pest_type"),
|
||||
"disease": pest_report.get("disease_type"),
|
||||
"action": "immediate_treatment_required"
|
||||
})
|
||||
|
||||
# Step 3: Generate spray recommendation
|
||||
spray_recommendation = spray_agent.start(
|
||||
f"Calculate spray strategy for pest report: {pest_report}, "
|
||||
f"field: {spectral_analysis}, weather: {weather_data}"
|
||||
)
|
||||
workflow_results["treatments_needed"].append(spray_recommendation)
|
||||
|
||||
# Step 4: Yield prediction
|
||||
yield_forecast = yield_agent.start(
|
||||
f"Predict yield for field {field_id}, crop {crop_type}, "
|
||||
f"health: {spectral_analysis}, weather: {weather_data}"
|
||||
)
|
||||
workflow_results["yield_forecasts"].append(yield_forecast)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: Schedule manual inspection
|
||||
workflow_results["alerts"].append({
|
||||
"field_id": field_id,
|
||||
"type": "monitoring_failure",
|
||||
"error": str(e),
|
||||
"action": "schedule_manual_inspection"
|
||||
})
|
||||
|
||||
# Calculate sustainability score
|
||||
total_chemical = sum(
|
||||
t.get("application_rate", 0) * t.get("affected_area", 0)
|
||||
for t in workflow_results["treatments_needed"]
|
||||
)
|
||||
workflow_results["sustainability_score"] = max(0, 100 - (total_chemical * 10))
|
||||
|
||||
return workflow_results
|
||||
|
||||
|
||||
# Sustainable farming patterns
|
||||
class SustainableFarmingPatterns:
|
||||
"""
|
||||
Patterns for sustainable and regenerative agriculture
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def integrated_pest_management(pest_level: str) -> Dict:
|
||||
"""IPM strategy based on pest pressure"""
|
||||
strategies = {
|
||||
"low": {
|
||||
"method": "biological_control",
|
||||
"actions": ["release_beneficial_insects", "plant_trap_crops"],
|
||||
"chemical_use": 0
|
||||
},
|
||||
"moderate": {
|
||||
"method": "targeted_intervention",
|
||||
"actions": ["spot_treatment", "pheromone_traps", "minimal_spray"],
|
||||
"chemical_use": 30
|
||||
},
|
||||
"high": {
|
||||
"method": "integrated_approach",
|
||||
"actions": ["systematic_spray", "crop_rotation", "resistant_varieties"],
|
||||
"chemical_use": 70
|
||||
}
|
||||
}
|
||||
return strategies.get(pest_level, strategies["moderate"])
|
||||
|
||||
@staticmethod
|
||||
def precision_irrigation(moisture_level: float, crop_stage: str) -> Dict:
|
||||
"""Calculate optimal irrigation based on soil moisture and crop stage"""
|
||||
# Water requirements by growth stage (mm/day)
|
||||
stage_requirements = {
|
||||
"germination": 3.0,
|
||||
"vegetative": 5.0,
|
||||
"flowering": 7.0,
|
||||
"grain_filling": 6.0,
|
||||
"maturation": 2.0
|
||||
}
|
||||
|
||||
required = stage_requirements.get(crop_stage, 5.0)
|
||||
deficit = max(0, 40 - moisture_level) # Target 40% moisture
|
||||
|
||||
return {
|
||||
"irrigation_needed": deficit > 10,
|
||||
"amount_mm": deficit * 2,
|
||||
"method": "drip" if deficit < 20 else "sprinkler",
|
||||
"schedule": "night_irrigation" if deficit > 15 else "morning_irrigation",
|
||||
"water_saved": (100 - deficit) / 100 * 1000 # Liters per hectare
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def crop_rotation_optimizer(current_crop: str, soil_data: Dict) -> str:
|
||||
"""Recommend next crop for rotation based on soil health"""
|
||||
rotations = {
|
||||
"wheat": ["soybeans", "canola"], # Nitrogen fixation
|
||||
"corn": ["soybeans", "alfalfa"], # Break disease cycle
|
||||
"soybeans": ["wheat", "corn"], # Utilize fixed nitrogen
|
||||
"rice": ["legumes", "vegetables"], # Soil structure improvement
|
||||
}
|
||||
|
||||
return rotations.get(current_crop, ["cover_crops"])[0]
|
||||
|
||||
|
||||
# IoT sensor integration patterns
|
||||
class IoTSensorPatterns:
|
||||
"""
|
||||
Patterns for integrating IoT sensors in smart farming
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_sensor_network_agent(sensor_types: List[str]) -> Agent:
|
||||
"""Create agent for managing IoT sensor networks"""
|
||||
return Agent(
|
||||
name="SensorNetworkManager",
|
||||
instructions=f"""Manage IoT sensor network with {', '.join(sensor_types)}.
|
||||
Collect real-time data from field sensors.
|
||||
Detect sensor anomalies and maintenance needs.
|
||||
Aggregate data for precision agriculture decisions.
|
||||
SLA: Data collection every 15 minutes."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def process_sensor_data(sensor_readings: List[Dict]) -> Dict:
|
||||
"""Process and aggregate IoT sensor data"""
|
||||
aggregated = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"sensor_count": len(sensor_readings),
|
||||
"averages": {},
|
||||
"alerts": []
|
||||
}
|
||||
|
||||
# Calculate averages
|
||||
for reading in sensor_readings:
|
||||
for key, value in reading.items():
|
||||
if isinstance(value, (int, float)):
|
||||
if key not in aggregated["averages"]:
|
||||
aggregated["averages"][key] = []
|
||||
aggregated["averages"][key].append(value)
|
||||
|
||||
# Convert to means
|
||||
for key in aggregated["averages"]:
|
||||
values = aggregated["averages"][key]
|
||||
aggregated["averages"][key] = sum(values) / len(values)
|
||||
|
||||
# Check for anomalies
|
||||
if key == "soil_ph" and (aggregated["averages"][key] < 6.0 or aggregated["averages"][key] > 7.5):
|
||||
aggregated["alerts"].append({
|
||||
"type": "soil_ph_anomaly",
|
||||
"value": aggregated["averages"][key],
|
||||
"action": "adjust_soil_treatment"
|
||||
})
|
||||
|
||||
return aggregated
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Monitor multiple fields
|
||||
fields = ["FIELD-001", "FIELD-002", "FIELD-003"]
|
||||
weather = {
|
||||
"temperature": 25,
|
||||
"humidity": 60,
|
||||
"wind_speed": 8,
|
||||
"precipitation": 0,
|
||||
"forecast": "partly_cloudy"
|
||||
}
|
||||
|
||||
result = precision_agriculture_workflow(fields, "wheat", weather)
|
||||
print("Agriculture workflow result:", result)
|
||||
|
||||
# Sustainable farming example
|
||||
ipm_strategy = SustainableFarmingPatterns.integrated_pest_management("moderate")
|
||||
print("IPM Strategy:", ipm_strategy)
|
||||
|
||||
irrigation_plan = SustainableFarmingPatterns.precision_irrigation(25.0, "flowering")
|
||||
print("Irrigation Plan:", irrigation_plan)
|
||||
|
||||
next_crop = SustainableFarmingPatterns.crop_rotation_optimizer("wheat", {})
|
||||
print("Recommended next crop:", next_crop)
|
||||
|
||||
# IoT integration example
|
||||
sensor_agent = IoTSensorPatterns.create_sensor_network_agent(
|
||||
["soil_moisture", "temperature", "humidity", "light", "ph"]
|
||||
)
|
||||
|
||||
sensor_data = [
|
||||
{"sensor_id": "S001", "soil_moisture": 35, "temperature": 24, "soil_ph": 6.8},
|
||||
{"sensor_id": "S002", "soil_moisture": 32, "temperature": 25, "soil_ph": 7.2},
|
||||
{"sensor_id": "S003", "soil_moisture": 38, "temperature": 23, "soil_ph": 5.8}
|
||||
]
|
||||
|
||||
processed_data = IoTSensorPatterns.process_sensor_data(sensor_data)
|
||||
print("Processed sensor data:", processed_data)
|
||||
@@ -1,373 +0,0 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Energy Industry Template
|
||||
========================
|
||||
Wind farm monitoring and power optimization workflow
|
||||
Based on SRAO Framework patterns with predictive maintenance
|
||||
|
||||
Key agents:
|
||||
- SCADAReader: Processes SCADA telemetry data
|
||||
- VibrationAnalyzer: Analyzes turbine vibration patterns
|
||||
- PowerForecaster: Predicts power generation
|
||||
- MaintenanceScheduler: Schedules predictive maintenance
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, tool
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
# I/O Schemas
|
||||
class TurbineData(BaseModel):
|
||||
"""Wind turbine telemetry data"""
|
||||
turbine_id: str
|
||||
timestamp: str
|
||||
wind_speed: float # m/s
|
||||
power_output: float # MW
|
||||
rotor_speed: float # RPM
|
||||
blade_pitch: float # degrees
|
||||
nacelle_temperature: float # Celsius
|
||||
vibration_level: float # mm/s
|
||||
operational_status: str # running, stopped, maintenance
|
||||
|
||||
|
||||
class VibrationAnalysis(BaseModel):
|
||||
"""Vibration analysis results"""
|
||||
turbine_id: str
|
||||
analysis_time: str
|
||||
vibration_rms: float
|
||||
frequency_peaks: List[float]
|
||||
anomaly_detected: bool
|
||||
failure_probability: float
|
||||
recommended_action: str
|
||||
|
||||
|
||||
class PowerForecast(BaseModel):
|
||||
"""Power generation forecast"""
|
||||
forecast_id: str
|
||||
start_time: str
|
||||
end_time: str
|
||||
predicted_output: List[float] # MW per hour
|
||||
confidence_interval: float
|
||||
weather_factors: Dict[str, Any]
|
||||
grid_demand: float
|
||||
|
||||
|
||||
class MaintenancePlan(BaseModel):
|
||||
"""Maintenance scheduling plan"""
|
||||
plan_id: str
|
||||
turbine_id: str
|
||||
maintenance_type: str # preventive, predictive, corrective
|
||||
scheduled_date: str
|
||||
estimated_duration: int # hours
|
||||
required_parts: List[str]
|
||||
technician_team: str
|
||||
production_loss: float # MW
|
||||
|
||||
|
||||
# Tools for energy management
|
||||
@tool
|
||||
def read_scada_telemetry(turbine_id: str, time_range: str = "last_hour") -> Dict:
|
||||
"""Read SCADA system telemetry for wind turbines"""
|
||||
# Simulate SCADA data reading
|
||||
return {
|
||||
"turbine_id": turbine_id,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"wind_speed": 12.5,
|
||||
"power_output": 2.3,
|
||||
"rotor_speed": 16.8,
|
||||
"blade_pitch": 8.5,
|
||||
"nacelle_temperature": 45.2,
|
||||
"vibration_level": 3.2,
|
||||
"operational_status": "running"
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def analyze_vibration_patterns(turbine_data: Dict) -> Dict:
|
||||
"""Analyze vibration patterns for early fault detection"""
|
||||
# Simulate vibration analysis
|
||||
vibration_level = turbine_data.get("vibration_level", 0)
|
||||
anomaly = vibration_level > 5.0 # Threshold for anomaly
|
||||
|
||||
return {
|
||||
"turbine_id": turbine_data["turbine_id"],
|
||||
"analysis_time": datetime.now().isoformat(),
|
||||
"vibration_rms": vibration_level,
|
||||
"frequency_peaks": [10.5, 25.3, 50.0], # Hz
|
||||
"anomaly_detected": anomaly,
|
||||
"failure_probability": min(vibration_level / 10.0, 1.0),
|
||||
"recommended_action": "immediate_inspection" if anomaly else "continue_monitoring"
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def forecast_power_generation(weather_data: Dict, historical_data: List) -> Dict:
|
||||
"""Forecast power generation based on weather and historical patterns"""
|
||||
# Simulate power forecasting
|
||||
base_forecast = [2.0 + (i * 0.1) for i in range(24)] # 24-hour forecast
|
||||
|
||||
return {
|
||||
"forecast_id": f"FCST-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"start_time": datetime.now().isoformat(),
|
||||
"end_time": (datetime.now() + timedelta(hours=24)).isoformat(),
|
||||
"predicted_output": base_forecast,
|
||||
"confidence_interval": 0.85,
|
||||
"weather_factors": weather_data,
|
||||
"grid_demand": sum(base_forecast)
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def schedule_maintenance(turbine_id: str, maintenance_type: str, urgency: str = "normal") -> Dict:
|
||||
"""Schedule maintenance based on condition monitoring"""
|
||||
# Calculate maintenance window
|
||||
if urgency == "urgent":
|
||||
scheduled_date = (datetime.now() + timedelta(days=1)).isoformat()
|
||||
else:
|
||||
scheduled_date = (datetime.now() + timedelta(days=7)).isoformat()
|
||||
|
||||
return {
|
||||
"plan_id": f"MNT-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"turbine_id": turbine_id,
|
||||
"maintenance_type": maintenance_type,
|
||||
"scheduled_date": scheduled_date,
|
||||
"estimated_duration": 4,
|
||||
"required_parts": ["bearing_set", "lubricant", "filter"],
|
||||
"technician_team": "Team-A",
|
||||
"production_loss": 8.0 # MW lost during maintenance
|
||||
}
|
||||
|
||||
|
||||
# Agent Definitions with SLA requirements
|
||||
scada_reader = Agent(
|
||||
name="SCADAReader",
|
||||
instructions="""You are a SCADA system specialist for wind farms.
|
||||
Process real-time telemetry data from wind turbines.
|
||||
Identify operational anomalies and performance deviations.
|
||||
SLA: Process telemetry within 1 second for real-time monitoring.""",
|
||||
tools=[read_scada_telemetry]
|
||||
)
|
||||
|
||||
vibration_analyzer = Agent(
|
||||
name="VibrationAnalyzer",
|
||||
instructions="""You are a vibration analysis expert for rotating machinery.
|
||||
Analyze vibration patterns to detect early signs of mechanical failure.
|
||||
Use FFT analysis and pattern recognition for fault diagnosis.
|
||||
SLA: Complete analysis within 5 seconds per turbine.""",
|
||||
tools=[analyze_vibration_patterns]
|
||||
)
|
||||
|
||||
power_forecaster = Agent(
|
||||
name="PowerForecaster",
|
||||
instructions="""You are a power generation forecasting specialist.
|
||||
Predict power output based on weather forecasts and historical patterns.
|
||||
Consider grid demand and optimize generation schedules.
|
||||
SLA: Generate 24-hour forecast within 30 seconds.""",
|
||||
tools=[forecast_power_generation]
|
||||
)
|
||||
|
||||
maintenance_scheduler = Agent(
|
||||
name="MaintenanceScheduler",
|
||||
instructions="""You are a predictive maintenance planning expert.
|
||||
Schedule maintenance based on condition monitoring and failure predictions.
|
||||
Minimize production loss while ensuring turbine reliability.
|
||||
SLA: Create maintenance plan within 1 minute.""",
|
||||
tools=[schedule_maintenance]
|
||||
)
|
||||
|
||||
|
||||
# Energy workflow with grid integration
|
||||
def energy_monitoring_workflow(turbine_ids: List[str], weather_forecast: Dict):
|
||||
"""
|
||||
Complete energy monitoring and optimization workflow
|
||||
Includes predictive maintenance and grid integration
|
||||
"""
|
||||
|
||||
results = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"turbines_monitored": len(turbine_ids),
|
||||
"alerts": [],
|
||||
"maintenance_required": [],
|
||||
"power_forecast": None
|
||||
}
|
||||
|
||||
# Step 1: Monitor all turbines
|
||||
for turbine_id in turbine_ids:
|
||||
try:
|
||||
# Read SCADA data
|
||||
scada_data = scada_reader.start(
|
||||
f"Read telemetry for turbine {turbine_id}"
|
||||
)
|
||||
|
||||
# Analyze vibrations
|
||||
vibration_result = vibration_analyzer.start(
|
||||
f"Analyze vibrations for data: {scada_data}"
|
||||
)
|
||||
|
||||
# Check for anomalies
|
||||
if vibration_result.get("anomaly_detected"):
|
||||
results["alerts"].append({
|
||||
"turbine_id": turbine_id,
|
||||
"type": "vibration_anomaly",
|
||||
"severity": "high" if vibration_result.get("failure_probability", 0) > 0.7 else "medium"
|
||||
})
|
||||
|
||||
# Schedule maintenance if needed
|
||||
maintenance_plan = maintenance_scheduler.start(
|
||||
f"Schedule maintenance for turbine {turbine_id} with urgency based on {vibration_result}"
|
||||
)
|
||||
results["maintenance_required"].append(maintenance_plan)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: Mark turbine for manual inspection
|
||||
results["alerts"].append({
|
||||
"turbine_id": turbine_id,
|
||||
"type": "monitoring_failure",
|
||||
"error": str(e),
|
||||
"action": "manual_inspection_required"
|
||||
})
|
||||
|
||||
# Step 2: Power generation forecast
|
||||
try:
|
||||
historical_data = [] # Would be fetched from database
|
||||
power_forecast = power_forecaster.start(
|
||||
f"Forecast power with weather {weather_forecast} and history {historical_data}"
|
||||
)
|
||||
results["power_forecast"] = power_forecast
|
||||
|
||||
# Grid integration check
|
||||
if power_forecast.get("predicted_output"):
|
||||
total_predicted = sum(power_forecast["predicted_output"])
|
||||
grid_demand = power_forecast.get("grid_demand", 0)
|
||||
|
||||
if total_predicted < grid_demand * 0.9:
|
||||
results["alerts"].append({
|
||||
"type": "supply_shortage",
|
||||
"predicted": total_predicted,
|
||||
"demand": grid_demand,
|
||||
"action": "consider_backup_sources"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: Use conservative forecast
|
||||
results["power_forecast"] = {
|
||||
"status": "fallback_mode",
|
||||
"conservative_estimate": len(turbine_ids) * 1.5, # MW
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# Cross-industry pattern adaptation
|
||||
class EnergyPatternAdapter:
|
||||
"""
|
||||
Adapts manufacturing patterns for energy sector
|
||||
Demonstrates 70% code reuse across industries
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def adapt_for_solar_farm():
|
||||
"""Adapt wind farm agents for solar farm monitoring"""
|
||||
return Agent(
|
||||
name="SolarMonitor",
|
||||
instructions="""Monitor solar panel efficiency and temperature.
|
||||
Track irradiance levels and predict power output.
|
||||
Detect panel degradation and soiling issues.
|
||||
SLA: Real-time monitoring with 2-second updates."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def adapt_for_power_grid():
|
||||
"""Adapt for power grid management"""
|
||||
return Agent(
|
||||
name="GridBalancer",
|
||||
instructions="""Balance power supply and demand across the grid.
|
||||
Manage load distribution and prevent blackouts.
|
||||
Coordinate with renewable and traditional sources.
|
||||
SLA: Sub-second response for grid stability."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def adapt_for_battery_storage():
|
||||
"""Adapt for battery energy storage systems"""
|
||||
return Agent(
|
||||
name="BatteryOptimizer",
|
||||
instructions="""Optimize battery charge/discharge cycles.
|
||||
Predict battery degradation and schedule replacements.
|
||||
Maximize energy arbitrage opportunities.
|
||||
SLA: Optimization decision within 10 seconds."""
|
||||
)
|
||||
|
||||
|
||||
# Fallback strategies for critical operations
|
||||
class EnergyFallbackStrategies:
|
||||
"""
|
||||
Fallback strategies for maintaining grid stability
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def turbine_failure_fallback(failed_turbine_id: str, available_turbines: List[str]):
|
||||
"""Handle turbine failure by redistributing load"""
|
||||
return {
|
||||
"strategy": "load_redistribution",
|
||||
"failed_turbine": failed_turbine_id,
|
||||
"compensating_turbines": available_turbines[:3], # Use top 3 turbines
|
||||
"increase_output_by": 0.3, # MW per turbine
|
||||
"duration": "until_maintenance_complete"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def forecast_failure_fallback(last_known_forecast: Optional[Dict]):
|
||||
"""Handle forecasting system failure"""
|
||||
if last_known_forecast:
|
||||
return {
|
||||
"strategy": "use_last_known",
|
||||
"forecast": last_known_forecast,
|
||||
"confidence": 0.6,
|
||||
"validity": "4_hours"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"strategy": "seasonal_average",
|
||||
"estimated_output": 50.0, # MW based on seasonal average
|
||||
"confidence": 0.4,
|
||||
"recommendation": "increase_reserves"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def communication_failure_fallback():
|
||||
"""Handle SCADA communication failure"""
|
||||
return {
|
||||
"strategy": "local_control_mode",
|
||||
"actions": [
|
||||
"Switch turbines to autonomous mode",
|
||||
"Use local safety thresholds",
|
||||
"Log data locally for later sync",
|
||||
"Alert maintenance teams"
|
||||
],
|
||||
"recovery_check_interval": 300 # seconds
|
||||
}
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Monitor wind farm
|
||||
turbine_list = [f"WT-{i:03d}" for i in range(1, 11)] # 10 turbines
|
||||
weather = {"wind_speed": 14.0, "direction": "NW", "temperature": 15.0}
|
||||
|
||||
result = energy_monitoring_workflow(turbine_list, weather)
|
||||
print("Energy monitoring result:", result)
|
||||
|
||||
# Demonstrate cross-industry adaptation
|
||||
solar_monitor = EnergyPatternAdapter.adapt_for_solar_farm()
|
||||
grid_balancer = EnergyPatternAdapter.adapt_for_power_grid()
|
||||
battery_optimizer = EnergyPatternAdapter.adapt_for_battery_storage()
|
||||
|
||||
# Fallback examples
|
||||
turbine_fallback = EnergyFallbackStrategies.turbine_failure_fallback(
|
||||
"WT-003", ["WT-001", "WT-002", "WT-004", "WT-005"]
|
||||
)
|
||||
print("Turbine failure fallback:", turbine_fallback)
|
||||
@@ -1,478 +0,0 @@
|
||||
"""
|
||||
Healthcare Industry Template
|
||||
============================
|
||||
Emergency triage and patient care coordination workflow
|
||||
Based on SRAO Framework with HIPAA-compliant patterns
|
||||
|
||||
Key agents:
|
||||
- VitalSignsCapture: Processes patient vital signs
|
||||
- EMRRetrieval: Retrieves electronic medical records
|
||||
- TriageRecommendation: Provides triage recommendations
|
||||
- ResourceAllocator: Manages hospital resource allocation
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, tool
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# Triage levels following Emergency Severity Index (ESI)
|
||||
class TriageLevel(str, Enum):
|
||||
RESUSCITATION = "1_resuscitation" # Immediate life-saving intervention
|
||||
EMERGENT = "2_emergent" # High risk, severe pain/distress
|
||||
URGENT = "3_urgent" # Stable but needs multiple resources
|
||||
LESS_URGENT = "4_less_urgent" # Stable, one resource needed
|
||||
NON_URGENT = "5_non_urgent" # Stable, no resources needed
|
||||
|
||||
|
||||
# I/O Schemas
|
||||
class VitalSigns(BaseModel):
|
||||
"""Patient vital signs data"""
|
||||
patient_id: str
|
||||
timestamp: str
|
||||
heart_rate: int # bpm
|
||||
blood_pressure_systolic: int # mmHg
|
||||
blood_pressure_diastolic: int # mmHg
|
||||
respiratory_rate: int # breaths/min
|
||||
temperature: float # Celsius
|
||||
oxygen_saturation: int # percentage
|
||||
pain_scale: int # 0-10
|
||||
consciousness_level: str # alert, verbal, pain, unresponsive (AVPU)
|
||||
|
||||
|
||||
class MedicalHistory(BaseModel):
|
||||
"""Patient medical history from EMR"""
|
||||
patient_id: str
|
||||
allergies: List[str]
|
||||
chronic_conditions: List[str]
|
||||
current_medications: List[Dict[str, str]]
|
||||
recent_visits: List[Dict[str, Any]]
|
||||
emergency_contacts: List[Dict[str, str]]
|
||||
insurance_status: str
|
||||
|
||||
|
||||
class TriageAssessment(BaseModel):
|
||||
"""Triage assessment and recommendations"""
|
||||
assessment_id: str
|
||||
patient_id: str
|
||||
triage_level: TriageLevel
|
||||
chief_complaint: str
|
||||
recommended_department: str
|
||||
estimated_wait_time: int # minutes
|
||||
required_resources: List[str]
|
||||
clinical_notes: str
|
||||
red_flags: List[str]
|
||||
|
||||
|
||||
class ResourceAllocation(BaseModel):
|
||||
"""Hospital resource allocation"""
|
||||
allocation_id: str
|
||||
patient_id: str
|
||||
assigned_bed: Optional[str]
|
||||
assigned_staff: List[str]
|
||||
equipment_needed: List[str]
|
||||
department: str
|
||||
priority_score: float
|
||||
estimated_treatment_time: int # minutes
|
||||
|
||||
|
||||
# Healthcare-specific tools
|
||||
@tool
|
||||
def capture_vital_signs(patient_id: str) -> Dict:
|
||||
"""Capture patient vital signs from monitoring devices"""
|
||||
# Simulate vital signs capture
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"heart_rate": 78,
|
||||
"blood_pressure_systolic": 130,
|
||||
"blood_pressure_diastolic": 85,
|
||||
"respiratory_rate": 16,
|
||||
"temperature": 37.2,
|
||||
"oxygen_saturation": 98,
|
||||
"pain_scale": 6,
|
||||
"consciousness_level": "alert"
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def retrieve_medical_history(patient_id: str, consent_verified: bool = True) -> Dict:
|
||||
"""Retrieve patient medical history from EMR system"""
|
||||
if not consent_verified:
|
||||
return {"error": "consent_required", "message": "Patient consent required for EMR access"}
|
||||
|
||||
# Simulate EMR retrieval
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"allergies": ["penicillin", "latex"],
|
||||
"chronic_conditions": ["hypertension", "diabetes_type2"],
|
||||
"current_medications": [
|
||||
{"name": "metformin", "dose": "500mg", "frequency": "twice daily"},
|
||||
{"name": "lisinopril", "dose": "10mg", "frequency": "once daily"}
|
||||
],
|
||||
"recent_visits": [
|
||||
{"date": "2024-02-15", "reason": "routine_checkup", "department": "primary_care"}
|
||||
],
|
||||
"emergency_contacts": [
|
||||
{"name": "Jane Doe", "relationship": "spouse", "phone": "555-0123"}
|
||||
],
|
||||
"insurance_status": "active"
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def calculate_triage_priority(vital_signs: Dict, medical_history: Dict, chief_complaint: str) -> Dict:
|
||||
"""Calculate triage priority based on clinical indicators"""
|
||||
# Simulate triage algorithm
|
||||
priority_score = 0
|
||||
red_flags = []
|
||||
|
||||
# Check vital signs
|
||||
if vital_signs["heart_rate"] > 120 or vital_signs["heart_rate"] < 50:
|
||||
priority_score += 3
|
||||
red_flags.append("abnormal_heart_rate")
|
||||
|
||||
if vital_signs["oxygen_saturation"] < 92:
|
||||
priority_score += 4
|
||||
red_flags.append("low_oxygen")
|
||||
|
||||
if vital_signs["consciousness_level"] != "alert":
|
||||
priority_score += 5
|
||||
red_flags.append("altered_consciousness")
|
||||
|
||||
# Determine triage level
|
||||
if priority_score >= 8:
|
||||
triage_level = TriageLevel.RESUSCITATION
|
||||
department = "emergency"
|
||||
wait_time = 0
|
||||
elif priority_score >= 5:
|
||||
triage_level = TriageLevel.EMERGENT
|
||||
department = "emergency"
|
||||
wait_time = 10
|
||||
elif priority_score >= 3:
|
||||
triage_level = TriageLevel.URGENT
|
||||
department = "urgent_care"
|
||||
wait_time = 30
|
||||
else:
|
||||
triage_level = TriageLevel.LESS_URGENT
|
||||
department = "primary_care"
|
||||
wait_time = 60
|
||||
|
||||
return {
|
||||
"assessment_id": f"TRG-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
"patient_id": vital_signs["patient_id"],
|
||||
"triage_level": triage_level.value,
|
||||
"chief_complaint": chief_complaint,
|
||||
"recommended_department": department,
|
||||
"estimated_wait_time": wait_time,
|
||||
"required_resources": ["physician", "nurse", "monitoring"],
|
||||
"clinical_notes": f"Priority score: {priority_score}",
|
||||
"red_flags": red_flags
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def allocate_resources(patient_id: str, triage_level: str, department: str) -> Dict:
|
||||
"""Allocate hospital resources based on triage assessment"""
|
||||
# Simulate resource allocation
|
||||
if "resuscitation" in triage_level:
|
||||
bed = "ER-01"
|
||||
staff = ["Dr. Smith", "Nurse Johnson", "Nurse Williams"]
|
||||
equipment = ["cardiac_monitor", "defibrillator", "crash_cart"]
|
||||
priority = 10.0
|
||||
elif "emergent" in triage_level:
|
||||
bed = "ER-05"
|
||||
staff = ["Dr. Brown", "Nurse Davis"]
|
||||
equipment = ["vital_signs_monitor", "IV_pump"]
|
||||
priority = 8.0
|
||||
else:
|
||||
bed = "UC-12"
|
||||
staff = ["PA Miller", "Nurse Garcia"]
|
||||
equipment = ["basic_monitor"]
|
||||
priority = 5.0
|
||||
|
||||
return {
|
||||
"allocation_id": f"RES-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
"patient_id": patient_id,
|
||||
"assigned_bed": bed,
|
||||
"assigned_staff": staff,
|
||||
"equipment_needed": equipment,
|
||||
"department": department,
|
||||
"priority_score": priority,
|
||||
"estimated_treatment_time": 45
|
||||
}
|
||||
|
||||
|
||||
# Healthcare agent definitions with SLA requirements
|
||||
vital_signs_agent = Agent(
|
||||
name="VitalSignsCapture",
|
||||
instructions="""You are a clinical vital signs specialist.
|
||||
Capture and interpret patient vital signs from various monitoring devices.
|
||||
Identify abnormal values and trends that require immediate attention.
|
||||
Follow HIPAA guidelines for patient data handling.
|
||||
SLA: Capture and process within 30 seconds.""",
|
||||
tools=[capture_vital_signs]
|
||||
)
|
||||
|
||||
emr_agent = Agent(
|
||||
name="EMRRetrieval",
|
||||
instructions="""You are an EMR system specialist with HIPAA compliance expertise.
|
||||
Retrieve relevant patient medical history while ensuring data privacy.
|
||||
Verify patient consent and maintain audit trails.
|
||||
Highlight relevant allergies and contraindications.
|
||||
SLA: Retrieve records within 5 seconds.""",
|
||||
tools=[retrieve_medical_history]
|
||||
)
|
||||
|
||||
triage_agent = Agent(
|
||||
name="TriageRecommendation",
|
||||
instructions="""You are an emergency triage specialist following ESI guidelines.
|
||||
Assess patient severity and provide triage recommendations.
|
||||
Consider vital signs, medical history, and presenting symptoms.
|
||||
Identify red flags that require immediate intervention.
|
||||
SLA: Complete assessment within 1 minute.""",
|
||||
tools=[calculate_triage_priority]
|
||||
)
|
||||
|
||||
resource_agent = Agent(
|
||||
name="ResourceAllocator",
|
||||
instructions="""You are a hospital resource management specialist.
|
||||
Allocate beds, staff, and equipment based on patient needs and priority.
|
||||
Optimize resource utilization while maintaining quality of care.
|
||||
Coordinate between departments for efficient patient flow.
|
||||
SLA: Allocate resources within 30 seconds.""",
|
||||
tools=[allocate_resources]
|
||||
)
|
||||
|
||||
|
||||
# Healthcare workflow with safety protocols
|
||||
def emergency_triage_workflow(patient_id: str, chief_complaint: str):
|
||||
"""
|
||||
Complete emergency triage workflow from arrival to resource allocation
|
||||
Includes safety checks and fallback protocols
|
||||
"""
|
||||
|
||||
workflow_result = {
|
||||
"patient_id": patient_id,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"status": "in_progress",
|
||||
"safety_checks": [],
|
||||
"clinical_pathway": []
|
||||
}
|
||||
|
||||
# Step 1: Vital signs capture with validation
|
||||
try:
|
||||
vital_signs = vital_signs_agent.start(
|
||||
f"Capture vital signs for patient {patient_id}"
|
||||
)
|
||||
workflow_result["clinical_pathway"].append({
|
||||
"step": "vital_signs",
|
||||
"status": "completed",
|
||||
"data": vital_signs
|
||||
})
|
||||
|
||||
# Safety check: Validate vital signs are within possible ranges
|
||||
if vital_signs.get("heart_rate", 0) < 20 or vital_signs.get("heart_rate", 0) > 300:
|
||||
workflow_result["safety_checks"].append({
|
||||
"type": "vital_signs_validation",
|
||||
"issue": "impossible_heart_rate",
|
||||
"action": "manual_verification_required"
|
||||
})
|
||||
except Exception as e:
|
||||
# Fallback: Manual vital signs entry
|
||||
workflow_result["clinical_pathway"].append({
|
||||
"step": "vital_signs",
|
||||
"status": "manual_entry_required",
|
||||
"error": str(e)
|
||||
})
|
||||
vital_signs = {"manual_entry": True}
|
||||
|
||||
# Step 2: EMR retrieval with consent check
|
||||
try:
|
||||
medical_history = emr_agent.start(
|
||||
f"Retrieve medical history for patient {patient_id} with verified consent"
|
||||
)
|
||||
workflow_result["clinical_pathway"].append({
|
||||
"step": "emr_retrieval",
|
||||
"status": "completed",
|
||||
"records_retrieved": True
|
||||
})
|
||||
|
||||
# Safety check: Allergy alerts
|
||||
if medical_history.get("allergies"):
|
||||
workflow_result["safety_checks"].append({
|
||||
"type": "allergy_alert",
|
||||
"allergies": medical_history["allergies"],
|
||||
"action": "notify_all_providers"
|
||||
})
|
||||
except Exception as e:
|
||||
# Fallback: Proceed with limited information
|
||||
medical_history = {
|
||||
"patient_id": patient_id,
|
||||
"allergies": [],
|
||||
"note": "EMR_unavailable_proceed_with_caution"
|
||||
}
|
||||
workflow_result["safety_checks"].append({
|
||||
"type": "emr_unavailable",
|
||||
"action": "collect_history_manually"
|
||||
})
|
||||
|
||||
# Step 3: Triage assessment
|
||||
try:
|
||||
triage_assessment = triage_agent.start(
|
||||
f"Assess triage for patient with vitals {vital_signs}, "
|
||||
f"history {medical_history}, complaint: {chief_complaint}"
|
||||
)
|
||||
workflow_result["clinical_pathway"].append({
|
||||
"step": "triage_assessment",
|
||||
"status": "completed",
|
||||
"triage_level": triage_assessment.get("triage_level")
|
||||
})
|
||||
|
||||
# Safety check: Red flags requiring immediate action
|
||||
if triage_assessment.get("red_flags"):
|
||||
workflow_result["safety_checks"].append({
|
||||
"type": "clinical_red_flags",
|
||||
"flags": triage_assessment["red_flags"],
|
||||
"action": "immediate_physician_notification"
|
||||
})
|
||||
except Exception as e:
|
||||
# Fallback: Conservative triage (assume urgent)
|
||||
triage_assessment = {
|
||||
"triage_level": TriageLevel.URGENT.value,
|
||||
"recommended_department": "emergency",
|
||||
"fallback": True
|
||||
}
|
||||
|
||||
# Step 4: Resource allocation
|
||||
try:
|
||||
resource_allocation = resource_agent.start(
|
||||
f"Allocate resources for patient {patient_id} "
|
||||
f"with triage {triage_assessment.get('triage_level')} "
|
||||
f"to {triage_assessment.get('recommended_department')}"
|
||||
)
|
||||
workflow_result["clinical_pathway"].append({
|
||||
"step": "resource_allocation",
|
||||
"status": "completed",
|
||||
"bed_assigned": resource_allocation.get("assigned_bed"),
|
||||
"staff_assigned": resource_allocation.get("assigned_staff")
|
||||
})
|
||||
except Exception as e:
|
||||
# Fallback: Queue for next available
|
||||
workflow_result["clinical_pathway"].append({
|
||||
"step": "resource_allocation",
|
||||
"status": "queued",
|
||||
"queue_position": "next_available",
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
workflow_result["status"] = "completed"
|
||||
return workflow_result
|
||||
|
||||
|
||||
# HIPAA-compliant data handling patterns
|
||||
class HIPAACompliancePatterns:
|
||||
"""
|
||||
Reusable patterns for HIPAA-compliant healthcare workflows
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def anonymize_patient_data(data: Dict) -> Dict:
|
||||
"""Remove PHI for analytics and reporting"""
|
||||
safe_fields = ["triage_level", "department", "wait_time", "resource_type"]
|
||||
return {k: v for k, v in data.items() if k in safe_fields}
|
||||
|
||||
@staticmethod
|
||||
def audit_log_access(user_id: str, patient_id: str, action: str, reason: str):
|
||||
"""Create audit trail for EMR access"""
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"user_id": user_id,
|
||||
"patient_id": patient_id,
|
||||
"action": action,
|
||||
"reason": reason,
|
||||
"ip_address": "10.0.0.1", # Would be actual IP
|
||||
"session_id": "SEC-" + datetime.now().strftime('%Y%m%d%H%M%S')
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def encrypt_sensitive_data(data: Dict) -> Dict:
|
||||
"""Encrypt sensitive fields (placeholder for real encryption)"""
|
||||
sensitive_fields = ["ssn", "dob", "address", "phone"]
|
||||
encrypted = data.copy()
|
||||
for field in sensitive_fields:
|
||||
if field in encrypted:
|
||||
encrypted[field] = f"ENCRYPTED_{field.upper()}"
|
||||
return encrypted
|
||||
|
||||
|
||||
# Cross-department coordination patterns
|
||||
class HealthcareCoordinationPatterns:
|
||||
"""
|
||||
Patterns for coordinating across healthcare departments
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def coordinate_lab_orders(patient_id: str, triage_level: str) -> Agent:
|
||||
"""Create agent for lab order coordination"""
|
||||
return Agent(
|
||||
name="LabCoordinator",
|
||||
instructions=f"""Coordinate laboratory orders for patient {patient_id}.
|
||||
Priority level: {triage_level}.
|
||||
Ensure STAT orders for critical patients.
|
||||
Track specimen collection and result delivery.
|
||||
SLA: Order placement within 2 minutes."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def coordinate_radiology(patient_id: str, imaging_type: str) -> Agent:
|
||||
"""Create agent for radiology coordination"""
|
||||
return Agent(
|
||||
name="RadiologyCoordinator",
|
||||
instructions=f"""Schedule {imaging_type} imaging for patient {patient_id}.
|
||||
Check for contrast allergies and pregnancy status.
|
||||
Coordinate with transport for patient movement.
|
||||
SLA: Schedule within 5 minutes for urgent cases."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def coordinate_pharmacy(patient_id: str, medications: List[str]) -> Agent:
|
||||
"""Create agent for pharmacy coordination"""
|
||||
return Agent(
|
||||
name="PharmacyCoordinator",
|
||||
instructions=f"""Verify and dispense medications for patient {patient_id}.
|
||||
Check for drug interactions and allergies.
|
||||
Ensure proper dosing based on patient parameters.
|
||||
SLA: Medication verification within 3 minutes."""
|
||||
)
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Process emergency patient
|
||||
patient_id = "PT-2024-00123"
|
||||
chief_complaint = "severe chest pain radiating to left arm"
|
||||
|
||||
result = emergency_triage_workflow(patient_id, chief_complaint)
|
||||
print("Triage workflow result:", result)
|
||||
|
||||
# Create specialized coordinators
|
||||
lab_coordinator = HealthcareCoordinationPatterns.coordinate_lab_orders(
|
||||
patient_id, TriageLevel.EMERGENT.value
|
||||
)
|
||||
|
||||
radiology_coordinator = HealthcareCoordinationPatterns.coordinate_radiology(
|
||||
patient_id, "chest_xray"
|
||||
)
|
||||
|
||||
pharmacy_coordinator = HealthcareCoordinationPatterns.coordinate_pharmacy(
|
||||
patient_id, ["aspirin", "nitroglycerin"]
|
||||
)
|
||||
|
||||
# HIPAA compliance example
|
||||
audit_log = HIPAACompliancePatterns.audit_log_access(
|
||||
"DR-001", patient_id, "EMR_ACCESS", "emergency_triage"
|
||||
)
|
||||
print("Audit log created:", audit_log)
|
||||
@@ -1,298 +0,0 @@
|
||||
"""
|
||||
Manufacturing Industry Template
|
||||
================================
|
||||
Order scheduling, inventory management, and quality inspection workflow
|
||||
Based on SRAO Framework patterns with 70% cross-industry reuse
|
||||
|
||||
Key agents:
|
||||
- ParseOrder: Extracts order details from various formats
|
||||
- CheckInventory: Validates material availability
|
||||
- OptimizeSchedule: Optimizes production scheduling
|
||||
- DefectDetect: Quality control and defect detection
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, tool
|
||||
from typing import Dict, List, Any
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# I/O Schemas
|
||||
class OrderDetails(BaseModel):
|
||||
"""Structured order information"""
|
||||
order_id: str
|
||||
customer_id: str
|
||||
product_id: str
|
||||
quantity: int
|
||||
priority: str = "normal" # normal, high, urgent
|
||||
due_date: str
|
||||
requirements: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class InventoryStatus(BaseModel):
|
||||
"""Material availability status"""
|
||||
material_id: str
|
||||
available_quantity: int
|
||||
reorder_level: int
|
||||
supplier_lead_time: int
|
||||
location: str
|
||||
|
||||
|
||||
class ProductionSchedule(BaseModel):
|
||||
"""Optimized production schedule"""
|
||||
schedule_id: str
|
||||
production_line: str
|
||||
start_time: str
|
||||
end_time: str
|
||||
assigned_workers: List[str]
|
||||
estimated_completion: str
|
||||
bottleneck_analysis: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class QualityReport(BaseModel):
|
||||
"""Quality inspection results"""
|
||||
batch_id: str
|
||||
inspection_time: str
|
||||
defect_rate: float
|
||||
defect_types: List[str]
|
||||
passed: bool
|
||||
recommendations: List[str]
|
||||
|
||||
|
||||
# Tools for agents
|
||||
@tool
|
||||
def extract_order_details(text: str) -> Dict:
|
||||
"""Extract structured order information from unstructured text"""
|
||||
# Simulate order parsing logic
|
||||
return {
|
||||
"order_id": "ORD-2024-001",
|
||||
"customer_id": "CUST-123",
|
||||
"product_id": "PROD-456",
|
||||
"quantity": 100,
|
||||
"priority": "high",
|
||||
"due_date": "2024-03-15",
|
||||
"requirements": {"material": "steel", "finish": "polished"}
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def check_material_availability(product_id: str, quantity: int) -> Dict:
|
||||
"""Check if materials are available for production"""
|
||||
# Simulate inventory check
|
||||
return {
|
||||
"material_id": "MAT-789",
|
||||
"available_quantity": 500,
|
||||
"reorder_level": 100,
|
||||
"supplier_lead_time": 7,
|
||||
"location": "Warehouse-A"
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def calculate_optimal_schedule(order: Dict, inventory: Dict) -> Dict:
|
||||
"""Calculate optimal production schedule based on constraints"""
|
||||
# Simulate scheduling optimization
|
||||
return {
|
||||
"schedule_id": "SCH-2024-001",
|
||||
"production_line": "Line-3",
|
||||
"start_time": "2024-03-10T08:00:00",
|
||||
"end_time": "2024-03-14T17:00:00",
|
||||
"assigned_workers": ["Worker-001", "Worker-002"],
|
||||
"estimated_completion": "2024-03-14T16:30:00",
|
||||
"bottleneck_analysis": {
|
||||
"critical_path": "Assembly",
|
||||
"buffer_time": 2.5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def perform_quality_inspection(batch_id: str) -> Dict:
|
||||
"""Perform automated quality inspection on production batch"""
|
||||
# Simulate quality control
|
||||
return {
|
||||
"batch_id": batch_id,
|
||||
"inspection_time": "2024-03-14T17:00:00",
|
||||
"defect_rate": 0.02,
|
||||
"defect_types": ["surface_scratch", "dimension_variance"],
|
||||
"passed": True,
|
||||
"recommendations": [
|
||||
"Adjust machine calibration on Line-3",
|
||||
"Increase inspection frequency for next batch"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Agent Definitions with SLA requirements
|
||||
order_parser = Agent(
|
||||
name="ParseOrder",
|
||||
instructions="""You are an order parsing specialist. Extract and structure order information
|
||||
from various formats (email, PDF, API). Ensure all required fields are captured and validated.
|
||||
SLA: Process within 30 seconds per order.""",
|
||||
tools=[extract_order_details]
|
||||
)
|
||||
|
||||
inventory_checker = Agent(
|
||||
name="CheckInventory",
|
||||
instructions="""You are an inventory management specialist. Verify material availability,
|
||||
check reorder points, and identify potential supply chain issues.
|
||||
SLA: Real-time response within 5 seconds.""",
|
||||
tools=[check_material_availability]
|
||||
)
|
||||
|
||||
schedule_optimizer = Agent(
|
||||
name="OptimizeSchedule",
|
||||
instructions="""You are a production scheduling expert. Optimize production schedules
|
||||
based on order priority, resource availability, and constraints. Minimize idle time
|
||||
and maximize throughput.
|
||||
SLA: Generate schedule within 2 minutes for complex orders.""",
|
||||
tools=[calculate_optimal_schedule]
|
||||
)
|
||||
|
||||
quality_inspector = Agent(
|
||||
name="DefectDetect",
|
||||
instructions="""You are a quality control specialist. Perform automated inspections,
|
||||
detect defects using vision analysis, and provide improvement recommendations.
|
||||
SLA: Complete inspection within 1 minute per batch.""",
|
||||
tools=[perform_quality_inspection]
|
||||
)
|
||||
|
||||
|
||||
# Workflow with fallback strategies
|
||||
def manufacturing_workflow(order_text: str):
|
||||
"""
|
||||
Complete manufacturing workflow from order to quality control
|
||||
Includes fallback strategies for each critical step
|
||||
"""
|
||||
|
||||
# Step 1: Parse order with fallback
|
||||
try:
|
||||
order_details = order_parser.start(f"Parse this order: {order_text}")
|
||||
if not order_details:
|
||||
# Fallback: Manual review queue
|
||||
return {"status": "manual_review_required", "reason": "order_parsing_failed"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "stage": "order_parsing", "error": str(e)}
|
||||
|
||||
# Step 2: Check inventory with fallback
|
||||
try:
|
||||
inventory_status = inventory_checker.start(
|
||||
f"Check materials for order: {order_details}"
|
||||
)
|
||||
if not inventory_status:
|
||||
# Fallback: Alternative supplier check
|
||||
return {"status": "alternative_supplier_needed", "order": order_details}
|
||||
except Exception as e:
|
||||
# Fallback: Conservative estimate with safety margin
|
||||
inventory_status = {"estimated": True, "safety_margin": 1.5}
|
||||
|
||||
# Step 3: Optimize schedule with fallback
|
||||
try:
|
||||
schedule = schedule_optimizer.start(
|
||||
f"Create schedule for order {order_details} with inventory {inventory_status}"
|
||||
)
|
||||
if not schedule:
|
||||
# Fallback: Standard scheduling template
|
||||
schedule = {"template": "standard", "priority": "normal"}
|
||||
except Exception as e:
|
||||
# Fallback: Queue for next available slot
|
||||
schedule = {"status": "queued", "estimated_start": "next_available"}
|
||||
|
||||
# Step 4: Quality inspection with fallback
|
||||
try:
|
||||
quality_report = quality_inspector.start(
|
||||
f"Inspect batch for schedule {schedule}"
|
||||
)
|
||||
if not quality_report:
|
||||
# Fallback: Manual inspection required
|
||||
quality_report = {"requires_manual": True, "inspector_assigned": "QC-Team-1"}
|
||||
except Exception as e:
|
||||
# Fallback: Quarantine for review
|
||||
quality_report = {"status": "quarantined", "review_required": True}
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"order": order_details,
|
||||
"inventory": inventory_status,
|
||||
"schedule": schedule,
|
||||
"quality": quality_report
|
||||
}
|
||||
|
||||
|
||||
# Cross-industry reusable patterns (70% reuse)
|
||||
class IndustryAgentPattern:
|
||||
"""
|
||||
Base pattern reusable across industries
|
||||
Provides common agent capabilities that can be specialized
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_data_parser(name: str, domain: str, sla_seconds: int = 30):
|
||||
"""Create a data parsing agent for any domain"""
|
||||
return Agent(
|
||||
name=name,
|
||||
instructions=f"""You are a {domain} data parsing specialist.
|
||||
Extract and structure information from various formats.
|
||||
Validate all required fields according to {domain} standards.
|
||||
SLA: Process within {sla_seconds} seconds."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_resource_checker(name: str, resource_type: str, sla_seconds: int = 5):
|
||||
"""Create a resource availability checker for any resource type"""
|
||||
return Agent(
|
||||
name=name,
|
||||
instructions=f"""You are a {resource_type} availability specialist.
|
||||
Verify {resource_type} availability and identify constraints.
|
||||
Provide real-time status and alternative options if needed.
|
||||
SLA: Response within {sla_seconds} seconds."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_optimizer(name: str, optimization_target: str, sla_minutes: int = 2):
|
||||
"""Create an optimization agent for any target metric"""
|
||||
return Agent(
|
||||
name=name,
|
||||
instructions=f"""You are an optimization expert for {optimization_target}.
|
||||
Find optimal solutions considering all constraints and priorities.
|
||||
Minimize waste and maximize efficiency.
|
||||
SLA: Generate solution within {sla_minutes} minutes."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_inspector(name: str, inspection_type: str, sla_minutes: int = 1):
|
||||
"""Create an inspection agent for any quality metric"""
|
||||
return Agent(
|
||||
name=name,
|
||||
instructions=f"""You are a {inspection_type} inspection specialist.
|
||||
Perform automated analysis and detect anomalies.
|
||||
Provide actionable recommendations for improvement.
|
||||
SLA: Complete inspection within {sla_minutes} minute(s)."""
|
||||
)
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Create a manufacturing workflow instance
|
||||
order = "Customer ABC needs 100 units of Product-XYZ by March 15th, high priority"
|
||||
|
||||
# Option 1: Use specialized agents
|
||||
result = manufacturing_workflow(order)
|
||||
print("Manufacturing workflow result:", result)
|
||||
|
||||
# Option 2: Use reusable patterns for other industries
|
||||
# These patterns can be adapted for Energy, Healthcare, Agriculture, Transportation
|
||||
energy_parser = IndustryAgentPattern.create_data_parser(
|
||||
"EnergyDataParser", "wind farm telemetry", sla_seconds=10
|
||||
)
|
||||
|
||||
healthcare_checker = IndustryAgentPattern.create_resource_checker(
|
||||
"BedAvailability", "hospital bed", sla_seconds=3
|
||||
)
|
||||
|
||||
agriculture_optimizer = IndustryAgentPattern.create_optimizer(
|
||||
"IrrigationOptimizer", "water usage", sla_minutes=5
|
||||
)
|
||||
|
||||
transport_inspector = IndustryAgentPattern.create_inspector(
|
||||
"TunnelSafety", "structural integrity", sla_minutes=2
|
||||
)
|
||||
@@ -1,600 +0,0 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Transportation Industry Template
|
||||
================================
|
||||
Tunnel safety monitoring and infrastructure management workflow
|
||||
Based on SRAO Framework with LiDAR and sensor fusion
|
||||
|
||||
Key agents:
|
||||
- LiDARFusion: Processes LiDAR point cloud data
|
||||
- DisplacementCalculator: Calculates structural displacement
|
||||
- HeatmapGenerator: Generates safety heatmaps
|
||||
- MaintenancePlanner: Plans infrastructure maintenance
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, tool
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
import math
|
||||
|
||||
|
||||
# Safety levels for infrastructure
|
||||
class SafetyLevel(str, Enum):
|
||||
SAFE = "safe"
|
||||
CAUTION = "caution"
|
||||
WARNING = "warning"
|
||||
DANGER = "danger"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
# Infrastructure types
|
||||
class InfrastructureType(str, Enum):
|
||||
TUNNEL = "tunnel"
|
||||
BRIDGE = "bridge"
|
||||
HIGHWAY = "highway"
|
||||
RAILWAY = "railway"
|
||||
AIRPORT = "airport"
|
||||
|
||||
|
||||
# I/O Schemas
|
||||
class LiDARScan(BaseModel):
|
||||
"""LiDAR scan data for infrastructure"""
|
||||
scan_id: str
|
||||
infrastructure_id: str
|
||||
scan_time: str
|
||||
point_cloud_size: int # Number of points
|
||||
scan_resolution: float # Points per square meter
|
||||
coverage_area: float # Square meters
|
||||
anomaly_points: List[Tuple[float, float, float]] # XYZ coordinates
|
||||
reference_baseline: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class DisplacementAnalysis(BaseModel):
|
||||
"""Structural displacement analysis"""
|
||||
analysis_id: str
|
||||
infrastructure_id: str
|
||||
analysis_time: str
|
||||
max_displacement: float # millimeters
|
||||
displacement_vector: Tuple[float, float, float] # XYZ displacement
|
||||
critical_zones: List[Dict[str, Any]]
|
||||
settlement_rate: float # mm/month
|
||||
tilt_angle: float # degrees
|
||||
safety_factor: float
|
||||
|
||||
|
||||
class SafetyHeatmap(BaseModel):
|
||||
"""Infrastructure safety heatmap"""
|
||||
heatmap_id: str
|
||||
infrastructure_id: str
|
||||
generation_time: str
|
||||
grid_resolution: float # meters
|
||||
safety_scores: List[List[float]] # 2D grid of safety scores
|
||||
critical_areas: List[Dict[str, Any]]
|
||||
overall_safety: SafetyLevel
|
||||
risk_trends: Dict[str, Any]
|
||||
|
||||
|
||||
class MaintenanceSchedule(BaseModel):
|
||||
"""Maintenance planning schedule"""
|
||||
schedule_id: str
|
||||
infrastructure_id: str
|
||||
maintenance_type: str # preventive, corrective, emergency
|
||||
priority_level: str # low, medium, high, critical
|
||||
scheduled_date: str
|
||||
estimated_duration: int # hours
|
||||
required_resources: List[str]
|
||||
traffic_impact: str
|
||||
cost_estimate: float
|
||||
|
||||
|
||||
# Transportation-specific tools
|
||||
@tool
|
||||
def process_lidar_scan(infrastructure_id: str, scan_type: str = "full") -> Dict:
|
||||
"""Process LiDAR point cloud data for structural analysis"""
|
||||
# Simulate LiDAR processing
|
||||
return {
|
||||
"scan_id": f"LIDAR-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"infrastructure_id": infrastructure_id,
|
||||
"scan_time": datetime.now().isoformat(),
|
||||
"point_cloud_size": 5000000, # 5 million points
|
||||
"scan_resolution": 100.0, # points/m²
|
||||
"coverage_area": 50000.0, # m²
|
||||
"anomaly_points": [
|
||||
(10.5, 20.3, 5.1),
|
||||
(15.2, 25.8, 5.3),
|
||||
(20.1, 30.5, 5.5)
|
||||
],
|
||||
"reference_baseline": {
|
||||
"date": "2024-01-01",
|
||||
"baseline_id": "BSL-001"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def calculate_displacement(current_scan: Dict, baseline_scan: Dict) -> Dict:
|
||||
"""Calculate structural displacement from LiDAR scans"""
|
||||
# Simulate displacement calculation
|
||||
anomalies = current_scan.get("anomaly_points", [])
|
||||
|
||||
if len(anomalies) > 5:
|
||||
max_displacement = 15.0
|
||||
safety_factor = 0.6
|
||||
safety_level = SafetyLevel.WARNING
|
||||
elif len(anomalies) > 2:
|
||||
max_displacement = 8.0
|
||||
safety_factor = 0.8
|
||||
safety_level = SafetyLevel.CAUTION
|
||||
else:
|
||||
max_displacement = 3.0
|
||||
safety_factor = 0.95
|
||||
safety_level = SafetyLevel.SAFE
|
||||
|
||||
return {
|
||||
"analysis_id": f"DISP-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"infrastructure_id": current_scan["infrastructure_id"],
|
||||
"analysis_time": datetime.now().isoformat(),
|
||||
"max_displacement": max_displacement,
|
||||
"displacement_vector": (2.5, 1.8, max_displacement),
|
||||
"critical_zones": [
|
||||
{
|
||||
"zone_id": "CZ-001",
|
||||
"location": "tunnel_crown",
|
||||
"displacement": max_displacement * 0.8,
|
||||
"risk": "high" if max_displacement > 10 else "medium"
|
||||
}
|
||||
],
|
||||
"settlement_rate": max_displacement / 30, # mm/month
|
||||
"tilt_angle": math.degrees(math.atan(max_displacement / 1000)),
|
||||
"safety_factor": safety_factor
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def generate_safety_heatmap(displacement_data: Dict, sensor_data: List[Dict]) -> Dict:
|
||||
"""Generate safety heatmap visualization for infrastructure"""
|
||||
# Simulate heatmap generation
|
||||
safety_factor = displacement_data.get("safety_factor", 1.0)
|
||||
|
||||
# Create grid (simplified 10x10)
|
||||
grid_size = 10
|
||||
safety_grid = []
|
||||
critical_areas = []
|
||||
|
||||
for i in range(grid_size):
|
||||
row = []
|
||||
for j in range(grid_size):
|
||||
# Generate safety score based on position and displacement
|
||||
base_score = safety_factor
|
||||
distance_factor = math.sqrt((i-5)**2 + (j-5)**2) / 10
|
||||
safety_score = min(1.0, base_score + (0.2 * distance_factor))
|
||||
row.append(safety_score)
|
||||
|
||||
if safety_score < 0.7:
|
||||
critical_areas.append({
|
||||
"grid_position": (i, j),
|
||||
"safety_score": safety_score,
|
||||
"recommended_action": "immediate_inspection"
|
||||
})
|
||||
safety_grid.append(row)
|
||||
|
||||
# Determine overall safety
|
||||
avg_safety = sum(sum(row) for row in safety_grid) / (grid_size * grid_size)
|
||||
if avg_safety > 0.9:
|
||||
overall = SafetyLevel.SAFE
|
||||
elif avg_safety > 0.8:
|
||||
overall = SafetyLevel.CAUTION
|
||||
elif avg_safety > 0.7:
|
||||
overall = SafetyLevel.WARNING
|
||||
else:
|
||||
overall = SafetyLevel.DANGER
|
||||
|
||||
return {
|
||||
"heatmap_id": f"HEAT-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"infrastructure_id": displacement_data["infrastructure_id"],
|
||||
"generation_time": datetime.now().isoformat(),
|
||||
"grid_resolution": 5.0, # meters
|
||||
"safety_scores": safety_grid,
|
||||
"critical_areas": critical_areas,
|
||||
"overall_safety": overall.value,
|
||||
"risk_trends": {
|
||||
"trend": "deteriorating" if avg_safety < 0.8 else "stable",
|
||||
"rate": -0.02 if avg_safety < 0.8 else 0.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@tool
|
||||
def plan_maintenance(infrastructure_id: str, safety_analysis: Dict, traffic_data: Dict) -> Dict:
|
||||
"""Plan maintenance based on safety analysis and traffic patterns"""
|
||||
safety_level = safety_analysis.get("overall_safety", "safe")
|
||||
|
||||
# Determine maintenance urgency
|
||||
if safety_level in ["danger", "critical"]:
|
||||
maintenance_type = "emergency"
|
||||
priority = "critical"
|
||||
days_until = 1
|
||||
duration = 48
|
||||
elif safety_level == "warning":
|
||||
maintenance_type = "corrective"
|
||||
priority = "high"
|
||||
days_until = 7
|
||||
duration = 24
|
||||
elif safety_level == "caution":
|
||||
maintenance_type = "preventive"
|
||||
priority = "medium"
|
||||
days_until = 30
|
||||
duration = 12
|
||||
else:
|
||||
maintenance_type = "preventive"
|
||||
priority = "low"
|
||||
days_until = 90
|
||||
duration = 8
|
||||
|
||||
# Traffic impact assessment
|
||||
peak_traffic = traffic_data.get("peak_volume", 1000)
|
||||
if peak_traffic > 5000:
|
||||
traffic_impact = "severe"
|
||||
cost_multiplier = 2.0
|
||||
elif peak_traffic > 2000:
|
||||
traffic_impact = "moderate"
|
||||
cost_multiplier = 1.5
|
||||
else:
|
||||
traffic_impact = "minimal"
|
||||
cost_multiplier = 1.0
|
||||
|
||||
return {
|
||||
"schedule_id": f"MAINT-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"infrastructure_id": infrastructure_id,
|
||||
"maintenance_type": maintenance_type,
|
||||
"priority_level": priority,
|
||||
"scheduled_date": (datetime.now() + timedelta(days=days_until)).isoformat(),
|
||||
"estimated_duration": duration,
|
||||
"required_resources": [
|
||||
"inspection_team",
|
||||
"repair_crew",
|
||||
"safety_equipment",
|
||||
"traffic_control"
|
||||
],
|
||||
"traffic_impact": traffic_impact,
|
||||
"cost_estimate": 50000 * cost_multiplier * (duration / 24)
|
||||
}
|
||||
|
||||
|
||||
# Transportation agent definitions
|
||||
lidar_agent = Agent(
|
||||
name="LiDARFusion",
|
||||
instructions="""You are a LiDAR data processing specialist for infrastructure.
|
||||
Process point cloud data from mobile and terrestrial LiDAR scanners.
|
||||
Perform data fusion from multiple sensor sources.
|
||||
Identify structural anomalies and deformations.
|
||||
SLA: Process scan data within 5 minutes.""",
|
||||
tools=[process_lidar_scan]
|
||||
)
|
||||
|
||||
displacement_agent = Agent(
|
||||
name="DisplacementCalculator",
|
||||
instructions="""You are a structural displacement analysis expert.
|
||||
Calculate precise displacement measurements from temporal scans.
|
||||
Analyze settlement patterns and structural movements.
|
||||
Predict future displacement trends using ML models.
|
||||
SLA: Complete analysis within 2 minutes.""",
|
||||
tools=[calculate_displacement]
|
||||
)
|
||||
|
||||
heatmap_agent = Agent(
|
||||
name="HeatmapGenerator",
|
||||
instructions="""You are a safety visualization specialist.
|
||||
Generate intuitive safety heatmaps for infrastructure monitoring.
|
||||
Integrate multiple data sources for comprehensive risk assessment.
|
||||
Highlight critical areas requiring immediate attention.
|
||||
SLA: Generate heatmap within 1 minute.""",
|
||||
tools=[generate_safety_heatmap]
|
||||
)
|
||||
|
||||
maintenance_agent = Agent(
|
||||
name="MaintenancePlanner",
|
||||
instructions="""You are an infrastructure maintenance planning expert.
|
||||
Schedule maintenance based on structural health and traffic patterns.
|
||||
Optimize maintenance windows to minimize disruption.
|
||||
Coordinate resources and estimate costs.
|
||||
SLA: Create maintenance plan within 3 minutes.""",
|
||||
tools=[plan_maintenance]
|
||||
)
|
||||
|
||||
|
||||
# Transportation infrastructure monitoring workflow
|
||||
def infrastructure_monitoring_workflow(
|
||||
infrastructure_ids: List[str],
|
||||
infrastructure_type: InfrastructureType,
|
||||
traffic_data: Dict
|
||||
):
|
||||
"""
|
||||
Complete infrastructure monitoring workflow from scanning to maintenance
|
||||
Includes safety assessment and predictive maintenance
|
||||
"""
|
||||
|
||||
monitoring_results = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"infrastructure_type": infrastructure_type.value,
|
||||
"structures_monitored": len(infrastructure_ids),
|
||||
"safety_assessments": [],
|
||||
"maintenance_required": [],
|
||||
"emergency_alerts": [],
|
||||
"network_health_score": 0.0
|
||||
}
|
||||
|
||||
total_safety_score = 0.0
|
||||
|
||||
for infra_id in infrastructure_ids:
|
||||
try:
|
||||
# Step 1: LiDAR scanning
|
||||
lidar_scan = lidar_agent.start(
|
||||
f"Process LiDAR scan for {infrastructure_type.value} {infra_id}"
|
||||
)
|
||||
|
||||
# Step 2: Displacement analysis
|
||||
# Get baseline (would be from database in real scenario)
|
||||
baseline = {"scan_id": "BSL-001", "point_cloud": []}
|
||||
|
||||
displacement_analysis = displacement_agent.start(
|
||||
f"Calculate displacement for scan: {lidar_scan} against baseline: {baseline}"
|
||||
)
|
||||
|
||||
# Step 3: Generate safety heatmap
|
||||
sensor_data = [] # Would include IoT sensor readings
|
||||
safety_heatmap = heatmap_agent.start(
|
||||
f"Generate heatmap for displacement: {displacement_analysis} "
|
||||
f"with sensors: {sensor_data}"
|
||||
)
|
||||
|
||||
monitoring_results["safety_assessments"].append({
|
||||
"infrastructure_id": infra_id,
|
||||
"safety_level": safety_heatmap.get("overall_safety"),
|
||||
"max_displacement": displacement_analysis.get("max_displacement"),
|
||||
"critical_zones": len(safety_heatmap.get("critical_areas", []))
|
||||
})
|
||||
|
||||
# Check for emergency conditions
|
||||
if safety_heatmap.get("overall_safety") in ["danger", "critical"]:
|
||||
monitoring_results["emergency_alerts"].append({
|
||||
"infrastructure_id": infra_id,
|
||||
"alert_level": "emergency",
|
||||
"safety_status": safety_heatmap["overall_safety"],
|
||||
"immediate_action": "close_to_traffic",
|
||||
"notification_sent": True
|
||||
})
|
||||
|
||||
# Step 4: Emergency maintenance planning
|
||||
maintenance_plan = maintenance_agent.start(
|
||||
f"Plan emergency maintenance for {infra_id} "
|
||||
f"with safety: {safety_heatmap} and traffic: {traffic_data}"
|
||||
)
|
||||
monitoring_results["maintenance_required"].append(maintenance_plan)
|
||||
|
||||
elif safety_heatmap.get("overall_safety") in ["warning", "caution"]:
|
||||
# Regular maintenance planning
|
||||
maintenance_plan = maintenance_agent.start(
|
||||
f"Plan maintenance for {infra_id} "
|
||||
f"with safety: {safety_heatmap} and traffic: {traffic_data}"
|
||||
)
|
||||
monitoring_results["maintenance_required"].append(maintenance_plan)
|
||||
|
||||
# Calculate safety score contribution
|
||||
safety_mapping = {
|
||||
"safe": 1.0, "caution": 0.8, "warning": 0.6,
|
||||
"danger": 0.3, "critical": 0.1
|
||||
}
|
||||
total_safety_score += safety_mapping.get(
|
||||
safety_heatmap.get("overall_safety", "safe"), 0.5
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: Manual inspection
|
||||
monitoring_results["emergency_alerts"].append({
|
||||
"infrastructure_id": infra_id,
|
||||
"alert_level": "system_failure",
|
||||
"error": str(e),
|
||||
"action": "dispatch_manual_inspection_team"
|
||||
})
|
||||
|
||||
# Calculate network health score
|
||||
if infrastructure_ids:
|
||||
monitoring_results["network_health_score"] = (
|
||||
total_safety_score / len(infrastructure_ids) * 100
|
||||
)
|
||||
|
||||
return monitoring_results
|
||||
|
||||
|
||||
# Multi-modal transportation patterns
|
||||
class MultiModalTransportPatterns:
|
||||
"""
|
||||
Patterns for different transportation infrastructure types
|
||||
Demonstrates cross-industry reuse for transportation sector
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def adapt_for_bridge_monitoring() -> Agent:
|
||||
"""Adapt tunnel monitoring for bridge structures"""
|
||||
return Agent(
|
||||
name="BridgeMonitor",
|
||||
instructions="""Monitor bridge structural health using sensors.
|
||||
Track deck deflection, cable tension, and vibration patterns.
|
||||
Detect fatigue cracks and corrosion.
|
||||
Consider wind and seismic loads.
|
||||
SLA: Real-time monitoring with 10-second updates."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def adapt_for_railway_tracks() -> Agent:
|
||||
"""Adapt for railway track monitoring"""
|
||||
return Agent(
|
||||
name="RailwayMonitor",
|
||||
instructions="""Monitor railway track geometry and condition.
|
||||
Detect rail defects, gauge variations, and ballast degradation.
|
||||
Analyze wheel-rail interaction forces.
|
||||
Predict track maintenance needs.
|
||||
SLA: Process track inspection data within 3 minutes per km."""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def adapt_for_airport_runways() -> Agent:
|
||||
"""Adapt for airport runway monitoring"""
|
||||
return Agent(
|
||||
name="RunwayMonitor",
|
||||
instructions="""Monitor airport runway surface conditions.
|
||||
Detect FOD (Foreign Object Debris) and surface irregularities.
|
||||
Assess pavement condition and friction levels.
|
||||
Monitor drainage and lighting systems.
|
||||
SLA: Complete runway scan within 10 minutes."""
|
||||
)
|
||||
|
||||
|
||||
# Intelligent traffic management integration
|
||||
class TrafficManagementPatterns:
|
||||
"""
|
||||
Patterns for integrating with intelligent traffic systems
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def optimize_traffic_flow(maintenance_window: Dict, traffic_patterns: Dict) -> Dict:
|
||||
"""Optimize traffic flow during maintenance"""
|
||||
peak_hours = traffic_patterns.get("peak_hours", [7, 9, 17, 19])
|
||||
maintenance_start = maintenance_window.get("start_time", "02:00")
|
||||
|
||||
# Calculate optimal diversion routes
|
||||
return {
|
||||
"strategy": "dynamic_routing",
|
||||
"diversion_routes": [
|
||||
{"route_id": "ALT-1", "capacity": 0.6, "added_time": 15},
|
||||
{"route_id": "ALT-2", "capacity": 0.3, "added_time": 20}
|
||||
],
|
||||
"lane_management": {
|
||||
"reversible_lanes": True,
|
||||
"peak_direction": "inbound" if int(maintenance_start[:2]) < 12 else "outbound"
|
||||
},
|
||||
"public_transport": {
|
||||
"increase_frequency": True,
|
||||
"additional_services": 4
|
||||
},
|
||||
"estimated_delay": 10 # minutes
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def incident_response_plan(incident_type: str, location: Dict) -> Dict:
|
||||
"""Generate incident response plan"""
|
||||
response_times = {
|
||||
"accident": 5,
|
||||
"breakdown": 10,
|
||||
"infrastructure_failure": 2,
|
||||
"weather_hazard": 15
|
||||
}
|
||||
|
||||
return {
|
||||
"incident_id": f"INC-{datetime.now().strftime('%Y%m%d%H%M')}",
|
||||
"response_time": response_times.get(incident_type, 10),
|
||||
"resources_dispatched": [
|
||||
r for r in [
|
||||
"emergency_response_team",
|
||||
"traffic_control_unit",
|
||||
"maintenance_crew" if "infrastructure" in incident_type else None
|
||||
] if r is not None
|
||||
],
|
||||
"traffic_management": {
|
||||
"lane_closures": 2,
|
||||
"speed_reduction": 30, # km/h
|
||||
"message_signs": ["Incident ahead", "Reduce speed", "Merge left"]
|
||||
},
|
||||
"estimated_clearance": 45 # minutes
|
||||
}
|
||||
|
||||
|
||||
# Predictive maintenance patterns
|
||||
class PredictiveMaintenancePatterns:
|
||||
"""
|
||||
ML-based predictive maintenance for transportation infrastructure
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def predict_failure_probability(
|
||||
displacement_trend: List[float],
|
||||
environmental_factors: Dict
|
||||
) -> float:
|
||||
"""Predict probability of structural failure"""
|
||||
# Simplified prediction model
|
||||
trend_slope = (displacement_trend[-1] - displacement_trend[0]) / len(displacement_trend)
|
||||
temperature_factor = abs(environmental_factors.get("temp_variation", 0)) / 50
|
||||
moisture_factor = environmental_factors.get("humidity", 50) / 100
|
||||
|
||||
base_probability = 0.1
|
||||
trend_contribution = max(0, trend_slope * 10)
|
||||
env_contribution = (temperature_factor + moisture_factor) * 0.2
|
||||
|
||||
return min(1.0, base_probability + trend_contribution + env_contribution)
|
||||
|
||||
@staticmethod
|
||||
def optimize_maintenance_schedule(
|
||||
infrastructure_conditions: List[Dict],
|
||||
budget_constraint: float
|
||||
) -> List[Dict]:
|
||||
"""Optimize maintenance schedule across infrastructure network"""
|
||||
# Sort by urgency (safety_factor * cost_efficiency)
|
||||
prioritized = sorted(
|
||||
infrastructure_conditions,
|
||||
key=lambda x: (1 - x.get("safety_factor", 1)) * (1 / x.get("cost", 1)),
|
||||
reverse=True
|
||||
)
|
||||
|
||||
scheduled = []
|
||||
total_cost = 0
|
||||
|
||||
for infra in prioritized:
|
||||
if total_cost + infra.get("cost", 0) <= budget_constraint:
|
||||
scheduled.append({
|
||||
"infrastructure_id": infra["id"],
|
||||
"scheduled_date": infra["recommended_date"],
|
||||
"priority": "high" if infra["safety_factor"] < 0.7 else "medium"
|
||||
})
|
||||
total_cost += infra.get("cost", 0)
|
||||
|
||||
return scheduled
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Monitor tunnel network
|
||||
tunnel_ids = [f"TUN-{i:03d}" for i in range(1, 6)]
|
||||
traffic = {
|
||||
"peak_volume": 3000,
|
||||
"off_peak_volume": 800,
|
||||
"peak_hours": [7, 9, 17, 19]
|
||||
}
|
||||
|
||||
result = infrastructure_monitoring_workflow(
|
||||
tunnel_ids,
|
||||
InfrastructureType.TUNNEL,
|
||||
traffic
|
||||
)
|
||||
print("Infrastructure monitoring result:", result)
|
||||
|
||||
# Multi-modal adaptation examples
|
||||
bridge_monitor = MultiModalTransportPatterns.adapt_for_bridge_monitoring()
|
||||
railway_monitor = MultiModalTransportPatterns.adapt_for_railway_tracks()
|
||||
runway_monitor = MultiModalTransportPatterns.adapt_for_airport_runways()
|
||||
|
||||
# Traffic management example
|
||||
maintenance = {"start_time": "22:00", "duration": 8}
|
||||
traffic_plan = TrafficManagementPatterns.optimize_traffic_flow(maintenance, traffic)
|
||||
print("Traffic optimization plan:", traffic_plan)
|
||||
|
||||
# Predictive maintenance example
|
||||
displacement_history = [2.0, 2.3, 2.8, 3.5, 4.3] # mm over months
|
||||
environment = {"temp_variation": 30, "humidity": 75}
|
||||
|
||||
failure_prob = PredictiveMaintenancePatterns.predict_failure_probability(
|
||||
displacement_history, environment
|
||||
)
|
||||
print(f"Failure probability: {failure_prob:.2%}")
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CI Integration Example - Using Doctor in CI/CD pipelines
|
||||
@@ -20,9 +19,7 @@ def run_doctor_ci():
|
||||
[sys.executable, "-m", "praisonai", "doctor", "ci"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=120,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
# Parse JSON output
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
"""
|
||||
Agent-centric Embedding Example
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Endpoints Example
|
||||
|
||||
@@ -36,12 +36,6 @@ if __name__ == "__main__":
|
||||
print(f"Custom agent max_rpm: {agent.max_rpm}")
|
||||
print(f"Custom agent max_execution_time: {agent.max_execution_time}")
|
||||
print(f"Custom agent max_retry_limit: {agent.max_retry_limit}")
|
||||
|
||||
# max_rpm auto-creates a live RateLimiter (not just a stored number)
|
||||
assert agent_rate_limited.max_rpm == 10
|
||||
assert agent_rate_limited._rate_limiter is not None
|
||||
assert agent_rate_limited._rate_limiter.requests_per_minute == 10
|
||||
print(f"Rate-limited agent limiter: {agent_rate_limited._rate_limiter}")
|
||||
|
||||
# Test custom agent
|
||||
print("\n--- Custom Execution ---")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fixed example demonstrating proper guardrail usage with PraisonAI Agents.
|
||||
|
||||
@@ -149,28 +149,21 @@ def main():
|
||||
|
||||
print(f"\nOriginal document length: {len(content)} characters")
|
||||
|
||||
chunks = [{"text": content, "metadata": {"source": doc_path}}]
|
||||
target_tokens = 2000
|
||||
|
||||
# Compress with different ratios
|
||||
for ratio in [0.3, 0.5, 0.7]:
|
||||
compressor = ContextCompressor(verbose=False)
|
||||
result = compressor.compress(
|
||||
chunks,
|
||||
query="security verification code",
|
||||
target_tokens=target_tokens,
|
||||
compression_ratio=ratio,
|
||||
compressor = ContextCompressor(
|
||||
max_tokens=2000,
|
||||
target_ratio=ratio,
|
||||
)
|
||||
result = compressor.compress([content], query="security verification code")
|
||||
|
||||
print(f"\nCompression ratio {ratio}:")
|
||||
print(f" Original tokens: {result.original_tokens}")
|
||||
print(f" Compressed tokens: {result.compressed_tokens}")
|
||||
print(f" Actual ratio: {result.compression_ratio:.2f}")
|
||||
print(f" Actual ratio: {result.compressed_tokens / max(result.original_tokens, 1):.2f}")
|
||||
|
||||
# Check if secret code is preserved
|
||||
compressed_text = " ".join(
|
||||
c.get("text", "") for c in result.chunks
|
||||
) if result.chunks else ""
|
||||
compressed_text = " ".join(result.chunks) if result.chunks else ""
|
||||
if SECRET_CODE in compressed_text:
|
||||
print(f" ✅ Secret code preserved in compressed output")
|
||||
else:
|
||||
|
||||
@@ -80,7 +80,7 @@ Be precise and quote exact values when asked about codes or numbers.""",
|
||||
print(f"\nAnswer: {response}\n")
|
||||
|
||||
# Verify the answer contains the unique code
|
||||
if "ZEBRA-71" in str(response or "").upper():
|
||||
if "ZEBRA-71" in response.upper():
|
||||
print("✅ VERIFIED: Agent correctly retrieved the unique code from context!\n")
|
||||
else:
|
||||
print("❌ WARNING: Agent did not find the code - check retrieval\n")
|
||||
@@ -104,7 +104,7 @@ Be precise and quote exact values when asked about codes or numbers.""",
|
||||
response = agent.chat("What is the security incident code?")
|
||||
print(f"\nAnswer: {response}\n")
|
||||
|
||||
if "HAWK-88" in str(response or "").upper():
|
||||
if "HAWK-88" in response.upper():
|
||||
print("✅ VERIFIED: Agent correctly retrieved the security code!\n")
|
||||
else:
|
||||
print("❌ WARNING: Agent did not find the code - check retrieval\n")
|
||||
|
||||
@@ -65,6 +65,7 @@ def main():
|
||||
name="Policy_Expert",
|
||||
instructions="You are a policy expert.",
|
||||
knowledge=[temp_dir],
|
||||
agent_id="policy_agent_v1", # Knowledge scoped to this agent
|
||||
)
|
||||
|
||||
response = shared_agent.chat("What are the expense report rules?")
|
||||
@@ -80,6 +81,7 @@ def main():
|
||||
instructions="You are a personal HR assistant.",
|
||||
knowledge=[temp_dir],
|
||||
memory={"user_id": "user_bob"},
|
||||
agent_id="hr_bot_v2",
|
||||
)
|
||||
|
||||
response = combined_agent.chat("What training is required?")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + ClickHouse Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + MongoDB Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + MySQL Persistence — Real conversation with session resume.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# praisonai: skip=true
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + PostgreSQL Persistence — Real conversation with session resume.
|
||||
|
||||
Loaded 100 of 3980 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user