mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 03:39:37 -10:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e9826bda7 | ||
|
|
fc298049e2 | ||
|
|
0b7bf10820 | ||
|
|
cbb7c726aa | ||
|
|
21d1986d0c | ||
|
|
26b5792192 | ||
|
|
afcea5800e |
No files matched your search
@@ -227,10 +227,9 @@ runs:
|
||||
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
|
||||
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.
|
||||
|
||||
@@ -261,24 +260,16 @@ 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")
|
||||
@@ -289,36 +280,11 @@ runs:
|
||||
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..."
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Install PraisonAI monorepo packages
|
||||
description: Install praisonai-agents, praisonai-code, and praisonai wrapper from local paths (mirrors PyPI three-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"
|
||||
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 "$WRAPPER_PKG"
|
||||
uv pip install --system "${WRAPPER_SUFFIX}"
|
||||
else
|
||||
cd "$AGENTS_PKG"
|
||||
pip install -e "${AGENTS_SUFFIX}"
|
||||
cd "$CODE_PKG"
|
||||
pip install -e .
|
||||
cd "$WRAPPER_PKG"
|
||||
pip install -e "${WRAPPER_SUFFIX}"
|
||||
fi
|
||||
@@ -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]']);
|
||||
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,185 +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'));
|
||||
|
||||
// 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 noise = [{ user: { login: 'MervinPraison' }, body: '**Merge gate scan** — wait for `@claude`', created_at: new Date().toISOString() }];
|
||||
assert('diagnostic comment not a trigger', !mg.hasRecentClaudeTrigger(noise, 35));
|
||||
|
||||
// Cooldown skip requires an actual verdict on HEAD, not just a FINAL trigger comment
|
||||
const recentFinalOnHead = [
|
||||
{
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude You are the FINAL architecture reviewer.',
|
||||
created_at: '2026-06-27T10:00:00Z',
|
||||
},
|
||||
];
|
||||
assert(
|
||||
'final trigger alone does not count as verdict on head',
|
||||
mg.findMergeGateVerdict(recentFinalOnHead, null, '2026-06-27T09:55:00Z') === null
|
||||
);
|
||||
const recentVerdictOnHead = [
|
||||
...recentFinalOnHead,
|
||||
{
|
||||
user: { login: 'github-actions[bot]' },
|
||||
body: 'MERGE_GATE_VERDICT: APPROVE',
|
||||
created_at: '2026-06-27T10:05:00Z',
|
||||
},
|
||||
];
|
||||
assert(
|
||||
'verdict on head skips cooldown gate',
|
||||
mg.findMergeGateVerdict(recentVerdictOnHead, null, '2026-06-27T09:55:00Z') === 'APPROVE'
|
||||
);
|
||||
|
||||
// Sensitive + secrets
|
||||
assert('workflow path sensitive', mg.sensitivePathReasons([{ filename: '.github/workflows/foo.yml' }]).length === 1);
|
||||
assert('ci-only label exempts workflows', mg.sensitivePathReasons(
|
||||
[{ filename: '.github/workflows/foo.yml' }],
|
||||
[mg.WORKFLOW_ONLY_LABEL]
|
||||
).length === 0);
|
||||
assert('ci-only label not exempt mixed changes', mg.sensitivePathReasons(
|
||||
[{ filename: '.github/workflows/foo.yml' }, { filename: 'src/foo.py' }],
|
||||
[mg.WORKFLOW_ONLY_LABEL]
|
||||
).length === 1);
|
||||
assert('secret in patch', mg.secretScanReasons([{ filename: 'x.py', patch: '+key = "ghp_abcdefghijklmnopqrstuvwxyz1234567890"' }]).length === 1);
|
||||
|
||||
// Claude run scoping — other PR branches must not block
|
||||
assert('other branch claude does not block', !mg.hasBlockingClaudeRunForPr(
|
||||
[{ event: 'issue_comment', head_branch: 'other-branch' }],
|
||||
'my-branch'
|
||||
));
|
||||
assert('same branch claude blocks', mg.hasBlockingClaudeRunForPr(
|
||||
[{ event: 'issue_comment', head_branch: 'my-branch' }],
|
||||
'my-branch'
|
||||
));
|
||||
assert('issues event never blocks', !mg.hasBlockingClaudeRunForPr(
|
||||
[{ event: 'issues', head_branch: 'my-branch' }],
|
||||
'my-branch'
|
||||
));
|
||||
|
||||
// Conflict rebase clears after bot completion + FINAL on HEAD
|
||||
const conflictTrigger = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude this PR has merge conflicts with main. Please rebase',
|
||||
created_at: '2026-06-26T10:30:38Z',
|
||||
};
|
||||
const rebaseDone = {
|
||||
user: { login: 'praisonai-triage-agent[bot]' },
|
||||
body: 'Rebase complete — PR #2308 onto latest main',
|
||||
created_at: '2026-06-26T10:30:55Z',
|
||||
};
|
||||
const finalAfterRebase = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@claude You are the FINAL architecture reviewer.',
|
||||
created_at: '2026-06-26T10:39:43Z',
|
||||
};
|
||||
const headAfterRebase = '2026-06-26T10:39:25Z';
|
||||
const rebaseComments = [conflictTrigger, rebaseDone, finalAfterRebase];
|
||||
assert('conflict blocks before rebase done', mg.hasRecentConflictComment([conflictTrigger], headAfterRebase));
|
||||
assert('conflict clears after rebase + FINAL on HEAD', !mg.hasRecentConflictComment(rebaseComments, headAfterRebase));
|
||||
assert('conflict still blocks without FINAL on HEAD', mg.hasRecentConflictComment(
|
||||
[conflictTrigger, rebaseDone],
|
||||
headAfterRebase
|
||||
));
|
||||
|
||||
// Tests heuristic
|
||||
assert('sdk without tests', mg.missingTestsReason([{ filename: 'src/praisonai-agents/a/b.py', additions: 3 }]) !== null);
|
||||
assert('sdk with tests ok', mg.missingTestsReason([
|
||||
{ filename: 'src/praisonai-agents/a/b.py', additions: 3 },
|
||||
{ filename: 'src/praisonai-agents/tests/test_x.py', additions: 10 },
|
||||
]) === null);
|
||||
|
||||
// PR size
|
||||
assert('large PR blocked', mg.prSizeReasons([{ additions: 900 }]).length > 0);
|
||||
|
||||
assert('internal PR link accepted', mg.isInternalPullRequestLink(
|
||||
{ base: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
|
||||
'MervinPraison',
|
||||
'PraisonAI'
|
||||
));
|
||||
assert('fork sync PR link rejected', !mg.isInternalPullRequestLink(
|
||||
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAI' } } },
|
||||
'MervinPraison',
|
||||
'PraisonAI'
|
||||
));
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -1,802 +0,0 @@
|
||||
/**
|
||||
* Shared merge-gate helpers for claude-merge-gate.yml
|
||||
* @see .github/workflows/claude-merge-gate.yml
|
||||
*/
|
||||
|
||||
const CLAUDE_TRIGGER_LOGINS = ['MervinPraison', 'github-actions[bot]'];
|
||||
const AUTO_ACTORS = CLAUDE_TRIGGER_LOGINS;
|
||||
const CONFLICT_COOLDOWN_MS = 12 * 60 * 60 * 1000;
|
||||
const CLAUDE_ACTIVE_MS = 35 * 60 * 1000;
|
||||
const STALE_FINAL_RECOVERY_WINDOW_MS = 60 * 60 * 1000;
|
||||
const STALE_FINAL_MAX_PER_WINDOW = 2;
|
||||
const PUSH_AFTER_FINAL_DEBOUNCE_MS = 15 * 60 * 1000;
|
||||
const ALLOWED_MERGE_STATES = new Set(['CLEAN', 'UNSTABLE']);
|
||||
const AGENT_PY_MAX_AUTO_LINES = 100;
|
||||
const PR_MAX_AUTO_ADDITIONS = 800;
|
||||
const PR_MAX_AUTO_FILES = 30;
|
||||
const MANUAL_ONLY_LABELS = new Set(['security', 'breaking-change', 'needs-manual-review', 'release']);
|
||||
const WORKFLOW_ONLY_LABEL = 'merge-gate-ci-only';
|
||||
const CI_ONLY_PATH_PREFIXES = ['.github/workflows/', '.github/actions/', '.github/scripts/merge-gate'];
|
||||
const SDK_PATH_PREFIXES = ['src/praisonai-agents/', 'src/praisonai/'];
|
||||
const SENSITIVE_PATH_PATTERNS = [
|
||||
/^\.github\/workflows\//,
|
||||
/praisonaiagents\/(auth|approval|policy|sandbox)\//,
|
||||
/^src\/praisonai-agents\/pyproject\.toml$/,
|
||||
/^src\/praisonai\/pyproject\.toml$/,
|
||||
/\.env(\.|$)/,
|
||||
/credentials\.json$/i,
|
||||
];
|
||||
const REQUIRED_SDK_CHECK_PATTERNS = [/test/i, /smoke/i, /core/i, /python package/i, /comprehensive/i, /optimized/i];
|
||||
const SECRET_PATTERNS = [
|
||||
/sk-[a-zA-Z0-9]{20,}/,
|
||||
/AKIA[0-9A-Z]{16}/,
|
||||
/ghp_[a-zA-Z0-9]{36,}/,
|
||||
/github_pat_/,
|
||||
/Bearer eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/,
|
||||
/-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
||||
];
|
||||
const BLOCK_LABELS = new Set([
|
||||
'claude-conflict-pending',
|
||||
'claude-merge-gate-active',
|
||||
'no-auto-merge',
|
||||
'auto-merged-by-gate',
|
||||
]);
|
||||
const BOT_REVIEWER_PATTERNS = [
|
||||
'coderabbit',
|
||||
'qodo',
|
||||
'gemini',
|
||||
'copilot',
|
||||
'greptile',
|
||||
];
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
function isFinalClaudeTriggerComment(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 isClaudeTriggerNoise(c) {
|
||||
const body = c.body || '';
|
||||
if (body.includes('Merge gate scan')) return true;
|
||||
if (body.includes('MERGE_GATE_VERDICT')) return true;
|
||||
if (body.includes('Merged by **Claude PR merge gate**')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasRecentClaudeTrigger(comments, minutes = 35) {
|
||||
const cutoff = Date.now() - minutes * 60 * 1000;
|
||||
return comments.some((c) => {
|
||||
if (!CLAUDE_TRIGGER_LOGINS.includes(c.user.login)) return false;
|
||||
if (isClaudeTriggerNoise(c)) return false;
|
||||
if (!(c.body || '').includes('@claude')) return false;
|
||||
return new Date(c.created_at).getTime() > cutoff;
|
||||
});
|
||||
}
|
||||
|
||||
function isConflictRebaseTriggerComment(c) {
|
||||
if (!AUTO_ACTORS.includes(c.user.login)) return false;
|
||||
const body = (c.body || '').toLowerCase();
|
||||
return body.includes('@claude') && body.includes('merge conflict');
|
||||
}
|
||||
|
||||
function isConflictRebaseCompletionComment(c) {
|
||||
const login = (c.user?.login || '').toLowerCase();
|
||||
if (!login.includes('praisonai-triage') && !login.includes('github-actions')) return false;
|
||||
const body = (c.body || '').toLowerCase();
|
||||
return (
|
||||
body.includes('rebase complete') ||
|
||||
body.includes('rebase onto') ||
|
||||
(body.includes('conflict') && body.includes('resolved'))
|
||||
);
|
||||
}
|
||||
|
||||
function conflictRebaseQuiescent(comments, headPushedAt) {
|
||||
const conflictTriggers = comments.filter(isConflictRebaseTriggerComment);
|
||||
if (conflictTriggers.length === 0) return true;
|
||||
|
||||
const latestConflict = conflictTriggers.reduce((a, b) =>
|
||||
new Date(a.created_at) > new Date(b.created_at) ? a : b
|
||||
);
|
||||
const conflictTime = new Date(latestConflict.created_at).getTime();
|
||||
|
||||
const rebaseDone = comments.some(
|
||||
(c) =>
|
||||
new Date(c.created_at).getTime() > conflictTime && isConflictRebaseCompletionComment(c)
|
||||
);
|
||||
if (!rebaseDone) return false;
|
||||
|
||||
return finalClaudeCompletedOnSha(comments, headPushedAt);
|
||||
}
|
||||
|
||||
function hasRecentConflictComment(comments, headPushedAt = null) {
|
||||
const cutoff = Date.now() - CONFLICT_COOLDOWN_MS;
|
||||
const hasRecentTrigger = comments.some((c) => {
|
||||
if (!isConflictRebaseTriggerComment(c)) return false;
|
||||
return new Date(c.created_at).getTime() > cutoff;
|
||||
});
|
||||
if (!hasRecentTrigger) return false;
|
||||
|
||||
if (headPushedAt && conflictRebaseQuiescent(comments, headPushedAt)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isBotReviewer(login, userType) {
|
||||
const lower = (login || '').toLowerCase();
|
||||
if (userType === 'Bot') return true;
|
||||
if (lower.endsWith('[bot]')) return true;
|
||||
return ['coderabbit', 'qodo', 'gemini', 'copilot', 'greptile'].some((p) => lower.includes(p));
|
||||
}
|
||||
|
||||
function latestReviewsByUser(reviews) {
|
||||
const latestByUser = new Map();
|
||||
for (const r of reviews) {
|
||||
const login = r.user?.login;
|
||||
if (!login) continue;
|
||||
const prev = latestByUser.get(login);
|
||||
if (!prev || new Date(r.submitted_at) > new Date(prev.submitted_at)) {
|
||||
latestByUser.set(login, r);
|
||||
}
|
||||
}
|
||||
return latestByUser;
|
||||
}
|
||||
|
||||
function hasHumanChangesRequested(reviews) {
|
||||
for (const [login, review] of latestReviewsByUser(reviews)) {
|
||||
if (review.state !== 'CHANGES_REQUESTED') continue;
|
||||
if (!isBotReviewer(login, review.user?.type)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasAnyChangesRequested(reviews) {
|
||||
for (const [, review] of latestReviewsByUser(reviews)) {
|
||||
if (review.state === 'CHANGES_REQUESTED') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasFinalClaudeReviewTrigger(comments) {
|
||||
return comments.some(isFinalClaudeTriggerComment);
|
||||
}
|
||||
|
||||
function isStaleFinalAfterPush(comments, headPushedAt) {
|
||||
if (!headPushedAt) return false;
|
||||
const headTime = new Date(headPushedAt).getTime();
|
||||
const finals = comments.filter(isFinalClaudeTriggerComment);
|
||||
if (finals.length === 0) return true;
|
||||
const latestFinal = finals.reduce((a, b) =>
|
||||
new Date(a.created_at) > new Date(b.created_at) ? a : b
|
||||
);
|
||||
const finalTime = new Date(latestFinal.created_at).getTime();
|
||||
if (headTime <= finalTime + 60000) return false;
|
||||
const claudeSinceHead = comments.some((c) => {
|
||||
if (!CLAUDE_TRIGGER_LOGINS.includes(c.user.login)) return false;
|
||||
if (isClaudeTriggerNoise(c)) return false;
|
||||
if (!(c.body || '').includes('@claude')) return false;
|
||||
return new Date(c.created_at).getTime() >= headTime - 60000;
|
||||
});
|
||||
return !claudeSinceHead;
|
||||
}
|
||||
|
||||
function needsStaleFinalRecovery(comments, headPushedAt) {
|
||||
return (
|
||||
hasFinalClaudeReviewTrigger(comments) &&
|
||||
isStaleFinalAfterPush(comments, headPushedAt)
|
||||
);
|
||||
}
|
||||
|
||||
function shouldSkipFinalRecovery(comments, headPushedAt) {
|
||||
const isStale = isStaleFinalAfterPush(comments, headPushedAt);
|
||||
if (isStale) return false;
|
||||
return hasRecentClaudeTrigger(comments, 35);
|
||||
}
|
||||
|
||||
function countFinalTriggersSince(comments, sinceMs) {
|
||||
return comments.filter(
|
||||
(c) => isFinalClaudeTriggerComment(c) && new Date(c.created_at).getTime() > sinceMs
|
||||
).length;
|
||||
}
|
||||
|
||||
function isClaudeAutomationLogin(login) {
|
||||
const lower = (login || '').toLowerCase();
|
||||
return lower.includes('praisonai-triage') || lower === 'github-actions[bot]';
|
||||
}
|
||||
|
||||
function isPushSoonAfterLatestFinal(comments, headPushedAt) {
|
||||
if (!headPushedAt) return false;
|
||||
const finals = comments.filter(isFinalClaudeTriggerComment);
|
||||
if (finals.length === 0) return false;
|
||||
const latestFinal = finals.reduce((a, b) =>
|
||||
new Date(a.created_at) > new Date(b.created_at) ? a : b
|
||||
);
|
||||
const finalTime = new Date(latestFinal.created_at).getTime();
|
||||
const headTime = new Date(headPushedAt).getTime();
|
||||
if (headTime <= finalTime) return false;
|
||||
return headTime - finalTime < PUSH_AFTER_FINAL_DEBOUNCE_MS;
|
||||
}
|
||||
|
||||
/** Returns { skip: true, reason } when stale-FINAL recovery should not post. */
|
||||
function shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusherLogin = null) {
|
||||
if (!isStaleFinalAfterPush(comments, headPushedAt)) {
|
||||
return { skip: true, reason: 'not stale' };
|
||||
}
|
||||
if (headPusherLogin && isClaudeAutomationLogin(headPusherLogin)) {
|
||||
return { skip: true, reason: 'head pushed by Claude automation' };
|
||||
}
|
||||
if (isPushSoonAfterLatestFinal(comments, headPushedAt)) {
|
||||
return {
|
||||
skip: true,
|
||||
reason: 'head pushed soon after FINAL (wait for CI / batched fixes)',
|
||||
};
|
||||
}
|
||||
const windowStart = Date.now() - STALE_FINAL_RECOVERY_WINDOW_MS;
|
||||
const finalsInWindow = countFinalTriggersSince(comments, windowStart);
|
||||
if (finalsInWindow >= STALE_FINAL_MAX_PER_WINDOW) {
|
||||
return {
|
||||
skip: true,
|
||||
reason: `stale-FINAL capped (${finalsInWindow} FINAL triggers in last hour)`,
|
||||
};
|
||||
}
|
||||
return { skip: false, reason: '' };
|
||||
}
|
||||
|
||||
function finalClaudeCompletedOnSha(comments, headPushedAt) {
|
||||
if (!hasFinalClaudeReviewTrigger(comments)) return false;
|
||||
if (isStaleFinalAfterPush(comments, headPushedAt)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const FINAL_CLAUDE_REVIEW_BODY =
|
||||
'@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items';
|
||||
|
||||
async function getMergeState(github, owner, repo, prNumber) {
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
mergeStateStatus
|
||||
maintainerCanModify
|
||||
isDraft
|
||||
headRefOid
|
||||
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,
|
||||
headSha: prGql.headRefOid,
|
||||
maintainerCanModify: prGql.maintainerCanModify === true,
|
||||
};
|
||||
}
|
||||
if (attempt < 2) await sleep(10000);
|
||||
}
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
return {
|
||||
status: (pr.mergeable_state || '').toUpperCase(),
|
||||
isDraft: pr.draft,
|
||||
headRepo: pr.head.repo?.full_name,
|
||||
headSha: pr.head.sha,
|
||||
maintainerCanModify: pr.maintainer_can_modify === true,
|
||||
};
|
||||
}
|
||||
|
||||
async function allChecksGreenOnSha(github, owner, repo, sha, core) {
|
||||
const { data } = await github.rest.checks.listForRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: sha,
|
||||
per_page: 100,
|
||||
});
|
||||
const runs = (data.check_runs || []).filter((r) => r.head_sha === sha);
|
||||
if (runs.length === 0) {
|
||||
core?.info?.(`No check runs on ${sha.slice(0, 7)} — allowing (e.g. docs-only PR)`);
|
||||
return true;
|
||||
}
|
||||
for (const run of runs) {
|
||||
if (run.status !== 'completed') {
|
||||
core?.info?.(`Check pending: ${run.name} (${run.status})`);
|
||||
return false;
|
||||
}
|
||||
const ok = ['success', 'neutral', 'skipped'].includes(run.conclusion);
|
||||
if (!ok) {
|
||||
core?.info?.(`Check failed: ${run.name} (${run.conclusion})`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function claudeRunBlocksPr(run, headRef) {
|
||||
if (!run || run.event === 'issues') return false;
|
||||
if (!headRef) return true;
|
||||
return run.head_branch === headRef;
|
||||
}
|
||||
|
||||
function hasBlockingClaudeRunForPr(runs, headRef) {
|
||||
return (runs || []).some((r) => claudeRunBlocksPr(r, headRef));
|
||||
}
|
||||
|
||||
async function hasInProgressClaudeAssistant(github, owner, repo, prNumber = null) {
|
||||
try {
|
||||
const { data } = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'claude.yml',
|
||||
status: 'in_progress',
|
||||
per_page: 20,
|
||||
});
|
||||
const runs = (data.workflow_runs || []).filter((r) => r.event !== 'issues');
|
||||
if (runs.length === 0) return false;
|
||||
if (prNumber == null) return runs.length > 0;
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
const headRef = pr.head.ref;
|
||||
return hasBlockingClaudeRunForPr(runs, headRef);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isCiOnlyChange(files) {
|
||||
if (!files.length) return false;
|
||||
return files.every((f) => CI_ONLY_PATH_PREFIXES.some((p) => f.filename.startsWith(p)));
|
||||
}
|
||||
|
||||
function isInternalPullRequestLink(link, owner, repo) {
|
||||
if (link?.base?.repo?.full_name === `${owner}/${repo}`) return true;
|
||||
const baseUrl = link?.base?.repo?.url || '';
|
||||
return baseUrl.endsWith(`/repos/${owner}/${repo}`);
|
||||
}
|
||||
|
||||
async function resolvePrNumberFromLinkedPullRequests(github, owner, repo, linked) {
|
||||
const repoFull = `${owner}/${repo}`;
|
||||
const internal = (linked || []).filter((l) => isInternalPullRequestLink(l, owner, repo));
|
||||
for (const link of internal) {
|
||||
if (!link.number) continue;
|
||||
try {
|
||||
const { data } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: link.number,
|
||||
});
|
||||
if (data.state === 'open' && data.base?.repo?.full_name === repoFull) {
|
||||
return data.number;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.status !== 404) throw err;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolvePrNumberFromHeadBranch(github, owner, repo, branch) {
|
||||
if (!branch || branch === 'main' || branch === 'master') return null;
|
||||
const { data } = await github.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
head: `${owner}:${branch}`,
|
||||
per_page: 1,
|
||||
});
|
||||
return data[0]?.number || null;
|
||||
}
|
||||
|
||||
async function resolvePrNumberFromWorkflowRun(github, owner, repo, workflowRun) {
|
||||
const fromLinked = await resolvePrNumberFromLinkedPullRequests(
|
||||
github, owner, repo, workflowRun.pull_requests
|
||||
);
|
||||
if (fromLinked) return fromLinked;
|
||||
|
||||
const fromBranch = await resolvePrNumberFromHeadBranch(
|
||||
github, owner, repo, workflowRun.head_branch
|
||||
);
|
||||
if (fromBranch) return fromBranch;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function listPullFiles(github, owner, repo, prNumber) {
|
||||
if (typeof github.paginate === 'function') {
|
||||
return github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
const { data } = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
function countNewAgentParams(patch) {
|
||||
if (!patch) return 0;
|
||||
return patch
|
||||
.split('\n')
|
||||
.filter((l) => l.startsWith('+') && !l.startsWith('+++'))
|
||||
.filter((l) => {
|
||||
const t = l.slice(1).trim();
|
||||
if (!t || t.startsWith('#') || t.startsWith('"""') || t.startsWith("'''")) return false;
|
||||
if (/^(def|class|@|return\b|if\b|elif\b|else\b|for\b|while\b)/.test(t)) return false;
|
||||
if (/^\w+\s*[:=]/.test(t) && !t.startsWith('self.')) return true;
|
||||
return false;
|
||||
}).length;
|
||||
}
|
||||
|
||||
function getAgentPyChangeFromFiles(files) {
|
||||
const agentFile = files.find((f) => f.filename.endsWith('praisonaiagents/agent/agent.py'));
|
||||
if (!agentFile) {
|
||||
return { touched: false, additions: 0, newParams: 0 };
|
||||
}
|
||||
return {
|
||||
touched: true,
|
||||
additions: agentFile.additions || 0,
|
||||
newParams: countNewAgentParams(agentFile.patch || ''),
|
||||
};
|
||||
}
|
||||
|
||||
async function getAgentPyChange(github, owner, repo, prNumber) {
|
||||
const files = await listPullFiles(github, owner, repo, prNumber);
|
||||
return getAgentPyChangeFromFiles(files);
|
||||
}
|
||||
|
||||
function touchesSdk(files) {
|
||||
return files.some((f) => SDK_PATH_PREFIXES.some((p) => f.filename.startsWith(p)));
|
||||
}
|
||||
|
||||
function hasManualOnlyLabel(labels) {
|
||||
return labels.some((l) => MANUAL_ONLY_LABELS.has(l));
|
||||
}
|
||||
|
||||
function sensitivePathReasons(files, labels = []) {
|
||||
if (labels.includes(WORKFLOW_ONLY_LABEL) && isCiOnlyChange(files)) {
|
||||
return [];
|
||||
}
|
||||
const reasons = [];
|
||||
for (const f of files) {
|
||||
if (SENSITIVE_PATH_PATTERNS.some((p) => p.test(f.filename))) {
|
||||
reasons.push(`sensitive path: ${f.filename}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return reasons;
|
||||
}
|
||||
|
||||
function prSizeReasons(files) {
|
||||
const reasons = [];
|
||||
const totalAdditions = files.reduce((sum, f) => sum + (f.additions || 0), 0);
|
||||
if (totalAdditions > PR_MAX_AUTO_ADDITIONS) {
|
||||
reasons.push(`PR +${totalAdditions} lines (>${PR_MAX_AUTO_ADDITIONS}) requires manual review`);
|
||||
}
|
||||
if (files.length > PR_MAX_AUTO_FILES) {
|
||||
reasons.push(`${files.length} files changed (>${PR_MAX_AUTO_FILES}) requires manual review`);
|
||||
}
|
||||
return reasons;
|
||||
}
|
||||
|
||||
function missingTestsReason(files) {
|
||||
const sdkAdds = files.filter(
|
||||
(f) =>
|
||||
f.filename.startsWith('src/praisonai-agents/') &&
|
||||
f.filename.endsWith('.py') &&
|
||||
!f.filename.endsWith('__init__.py') &&
|
||||
(f.additions || 0) > 0
|
||||
);
|
||||
if (sdkAdds.length === 0) return null;
|
||||
const hasTestChange = files.some(
|
||||
(f) => /\/tests?\//.test(f.filename) || /test_.*\.py$/.test(f.filename) || /_test\.py$/.test(f.filename)
|
||||
);
|
||||
if (!hasTestChange) return 'SDK code added without test file changes — requires manual review';
|
||||
return null;
|
||||
}
|
||||
|
||||
function secretScanReasons(files) {
|
||||
for (const f of files) {
|
||||
if (/\/tests?\//.test(f.filename) || /test_.*\.py$/.test(f.filename)) continue;
|
||||
const patch = f.patch || '';
|
||||
if (!patch) continue;
|
||||
for (const pattern of SECRET_PATTERNS) {
|
||||
if (pattern.test(patch)) {
|
||||
return [`possible secret in diff (${f.filename}) — requires manual review`];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function sdkTestChecksReason(github, owner, repo, sha, files, core) {
|
||||
if (!touchesSdk(files)) return null;
|
||||
const { data } = await github.rest.checks.listForRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: sha,
|
||||
per_page: 100,
|
||||
});
|
||||
const runs = (data.check_runs || []).filter((r) => r.head_sha === sha);
|
||||
if (runs.length === 0) {
|
||||
return 'SDK code changed but no CI checks on HEAD';
|
||||
}
|
||||
const testRuns = runs.filter((r) => REQUIRED_SDK_CHECK_PATTERNS.some((p) => p.test(r.name || '')));
|
||||
if (testRuns.length === 0) {
|
||||
return 'SDK code changed but no test check runs on HEAD';
|
||||
}
|
||||
core?.info?.(`SDK test checks on HEAD: ${testRuns.map((r) => r.name).join(', ')}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
function manualReviewReasonForAgentPy(agentChange) {
|
||||
if (!agentChange.touched) return null;
|
||||
if (agentChange.additions > AGENT_PY_MAX_AUTO_LINES) {
|
||||
return `agent.py +${agentChange.additions} lines (>${AGENT_PY_MAX_AUTO_LINES}) requires manual review`;
|
||||
}
|
||||
if (agentChange.newParams > 0) {
|
||||
return `agent.py adds ${agentChange.newParams} new Agent param(s) — requires manual review`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function getHeadCommitDate(github, owner, repo, prNumber) {
|
||||
try {
|
||||
let commits;
|
||||
if (typeof github.paginate === 'function') {
|
||||
commits = await github.paginate(github.rest.pulls.listCommits, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
} else {
|
||||
const { data } = await github.rest.pulls.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
commits = data;
|
||||
}
|
||||
const last = commits[commits.length - 1];
|
||||
return last?.commit?.committer?.date || last?.commit?.author?.date || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPrContext(github, owner, repo, prNumber) {
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: prNumber });
|
||||
const comments = await listAllComments(github, owner, repo, prNumber);
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const mergeState = await getMergeState(github, owner, repo, prNumber);
|
||||
const headSha = mergeState.headSha || pr.head.sha;
|
||||
const headCommitDate = await getHeadCommitDate(github, owner, repo, prNumber);
|
||||
const headPushedAt = headCommitDate || pr.updated_at;
|
||||
|
||||
return {
|
||||
pr,
|
||||
issue,
|
||||
comments,
|
||||
reviews,
|
||||
mergeState,
|
||||
headSha,
|
||||
headPushedAt,
|
||||
labels: issue.labels.map((l) => l.name),
|
||||
baseRepo: `${owner}/${repo}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, options = {}) {
|
||||
const {
|
||||
forMergeStep = false,
|
||||
skipGlobalClaudeRunCheck = false,
|
||||
skipRecentClaudeCooldown = false,
|
||||
} = options;
|
||||
const ctx = await loadPrContext(github, owner, repo, prNumber);
|
||||
const reasons = [];
|
||||
|
||||
if (ctx.pr.draft) reasons.push('draft');
|
||||
if (ctx.pr.state !== 'open') reasons.push('not open');
|
||||
if (ctx.labels.includes('auto-merged-by-gate')) reasons.push('already merged by gate');
|
||||
if (ctx.labels.includes('no-auto-merge')) reasons.push('no-auto-merge label');
|
||||
if (ctx.labels.includes('claude-conflict-pending')) reasons.push('claude-conflict-pending');
|
||||
if (!forMergeStep && ctx.labels.includes('claude-merge-gate-active')) {
|
||||
reasons.push('claude-merge-gate-active');
|
||||
}
|
||||
|
||||
const { status, headRepo, maintainerCanModify } = ctx.mergeState;
|
||||
if (!ALLOWED_MERGE_STATES.has(status)) reasons.push(`mergeState=${status}`);
|
||||
|
||||
if (headRepo && headRepo !== ctx.baseRepo) {
|
||||
reasons.push('fork PR');
|
||||
}
|
||||
|
||||
if (hasRecentConflictComment(ctx.comments, ctx.headPushedAt)) {
|
||||
reasons.push('recent merge-conflict @claude');
|
||||
}
|
||||
if (!skipRecentClaudeCooldown && hasRecentClaudeTrigger(ctx.comments, 35)) {
|
||||
const verdictOnHead = findMergeGateVerdict(ctx.comments, null, ctx.headPushedAt) !== null;
|
||||
if (!verdictOnHead) reasons.push('recent @claude within 35min');
|
||||
}
|
||||
|
||||
if (!skipGlobalClaudeRunCheck && (await hasInProgressClaudeAssistant(github, owner, repo, prNumber))) {
|
||||
reasons.push('claude.yml in progress');
|
||||
}
|
||||
|
||||
const checksOk = await allChecksGreenOnSha(github, owner, repo, ctx.headSha, core);
|
||||
if (!checksOk) reasons.push('CI not green on HEAD');
|
||||
|
||||
if (!finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt)) {
|
||||
if (!hasFinalClaudeReviewTrigger(ctx.comments)) {
|
||||
reasons.push('no FINAL Claude review trigger');
|
||||
} else if (isStaleFinalAfterPush(ctx.comments, ctx.headPushedAt)) {
|
||||
reasons.push('stale FINAL after new commits (needs @claude re-review)');
|
||||
} else {
|
||||
reasons.push('FINAL Claude not complete on HEAD');
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAnyChangesRequested(ctx.reviews)) reasons.push('CHANGES_REQUESTED review');
|
||||
|
||||
if (hasManualOnlyLabel(ctx.labels)) {
|
||||
const manualLabel = ctx.labels.find((l) => MANUAL_ONLY_LABELS.has(l));
|
||||
reasons.push(`manual-only label: ${manualLabel}`);
|
||||
}
|
||||
|
||||
const pullFiles = await listPullFiles(github, owner, repo, prNumber);
|
||||
|
||||
const agentChange = getAgentPyChangeFromFiles(pullFiles);
|
||||
const agentManual = manualReviewReasonForAgentPy(agentChange);
|
||||
if (agentManual) reasons.push(agentManual);
|
||||
|
||||
reasons.push(...sensitivePathReasons(pullFiles, ctx.labels));
|
||||
reasons.push(...prSizeReasons(pullFiles));
|
||||
reasons.push(...secretScanReasons(pullFiles));
|
||||
|
||||
const testsReason = missingTestsReason(pullFiles);
|
||||
if (testsReason) reasons.push(testsReason);
|
||||
|
||||
const sdkCheckReason = await sdkTestChecksReason(github, owner, repo, ctx.headSha, pullFiles, core);
|
||||
if (sdkCheckReason) reasons.push(sdkCheckReason);
|
||||
|
||||
if (ctx.pr.mergeable === false) reasons.push('not mergeable');
|
||||
|
||||
const ready = reasons.length === 0;
|
||||
return {
|
||||
ready,
|
||||
reasons,
|
||||
headSha: ctx.headSha,
|
||||
prNumber,
|
||||
};
|
||||
}
|
||||
|
||||
async function selectMergeGateCandidates(github, owner, repo, prNumbers, maxCandidates, core) {
|
||||
const readyList = [];
|
||||
const skipped = [];
|
||||
for (const num of prNumbers) {
|
||||
const result = await evaluatePipelineQuiescent(github, owner, repo, num, core);
|
||||
if (result.ready) {
|
||||
readyList.push({ pr_number: num, head_sha: result.headSha });
|
||||
} else {
|
||||
skipped.push({ pr: num, reasons: result.reasons });
|
||||
}
|
||||
}
|
||||
readyList.sort((a, b) => a.pr_number - b.pr_number);
|
||||
return {
|
||||
candidates: readyList.slice(0, maxCandidates),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
function findMergeGateVerdict(comments, minCreatedAt = null, headPushedAt = null) {
|
||||
const minTime = minCreatedAt ? new Date(minCreatedAt).getTime() : 0;
|
||||
const headTime = headPushedAt ? new Date(headPushedAt).getTime() - 60000 : 0;
|
||||
const gateComments = comments
|
||||
.filter((c) => {
|
||||
if (!(c.body || '').includes('MERGE_GATE_VERDICT:')) return false;
|
||||
const created = new Date(c.created_at).getTime();
|
||||
if (minTime && created < minTime) return false;
|
||||
if (headTime && created < headTime) return false;
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
if (gateComments.length === 0) return null;
|
||||
const body = gateComments[0].body || '';
|
||||
if (body.includes('MERGE_GATE_VERDICT: APPROVE')) return 'APPROVE';
|
||||
if (body.includes('MERGE_GATE_VERDICT: BLOCK')) return 'BLOCK';
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CLAUDE_TRIGGER_LOGINS,
|
||||
AUTO_ACTORS,
|
||||
isFinalClaudeTriggerComment,
|
||||
isClaudeTriggerNoise,
|
||||
hasRecentClaudeTrigger,
|
||||
hasRecentConflictComment,
|
||||
isConflictRebaseTriggerComment,
|
||||
isConflictRebaseCompletionComment,
|
||||
conflictRebaseQuiescent,
|
||||
hasHumanChangesRequested,
|
||||
hasAnyChangesRequested,
|
||||
hasFinalClaudeReviewTrigger,
|
||||
finalClaudeCompletedOnSha,
|
||||
getMergeState,
|
||||
allChecksGreenOnSha,
|
||||
hasInProgressClaudeAssistant,
|
||||
claudeRunBlocksPr,
|
||||
hasBlockingClaudeRunForPr,
|
||||
isCiOnlyChange,
|
||||
isInternalPullRequestLink,
|
||||
resolvePrNumberFromLinkedPullRequests,
|
||||
resolvePrNumberFromHeadBranch,
|
||||
resolvePrNumberFromWorkflowRun,
|
||||
WORKFLOW_ONLY_LABEL,
|
||||
getAgentPyChange,
|
||||
getAgentPyChangeFromFiles,
|
||||
manualReviewReasonForAgentPy,
|
||||
countNewAgentParams,
|
||||
listPullFiles,
|
||||
touchesSdk,
|
||||
hasManualOnlyLabel,
|
||||
sensitivePathReasons,
|
||||
prSizeReasons,
|
||||
missingTestsReason,
|
||||
secretScanReasons,
|
||||
sdkTestChecksReason,
|
||||
listAllComments,
|
||||
getHeadCommitDate,
|
||||
isStaleFinalAfterPush,
|
||||
needsStaleFinalRecovery,
|
||||
shouldSkipFinalRecovery,
|
||||
shouldSkipStaleFinalRecovery,
|
||||
isClaudeAutomationLogin,
|
||||
isPushSoonAfterLatestFinal,
|
||||
countFinalTriggersSince,
|
||||
STALE_FINAL_RECOVERY_WINDOW_MS,
|
||||
STALE_FINAL_MAX_PER_WINDOW,
|
||||
FINAL_CLAUDE_REVIEW_BODY,
|
||||
loadPrContext,
|
||||
evaluatePipelineQuiescent,
|
||||
selectMergeGateCandidates,
|
||||
findMergeGateVerdict,
|
||||
};
|
||||
@@ -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,245 +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 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(', ')}]`);
|
||||
return {
|
||||
synced: true,
|
||||
stage,
|
||||
blockers,
|
||||
ready: evalResult.ready,
|
||||
reasons: evalResult.reasons,
|
||||
labels: ctx.labels,
|
||||
createdAt: new Date(ctx.pr.created_at).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
async function dispatchMergeGateForOldestReady(github, owner, repo, readyCandidates, core) {
|
||||
if (!readyCandidates.length) return 0;
|
||||
readyCandidates.sort((a, b) => a.createdAt - b.createdAt);
|
||||
for (const cand of readyCandidates) {
|
||||
if ((cand.labels || []).includes('claude-merge-gate-active')) {
|
||||
core?.info?.(`Skip dispatch PR #${cand.prNumber}: merge gate already active`);
|
||||
continue;
|
||||
}
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner,
|
||||
repo,
|
||||
event_type: 'claude-merge-gate',
|
||||
client_payload: { pr_number: cand.prNumber },
|
||||
});
|
||||
core?.info?.(`Dispatched merge gate for ready PR #${cand.prNumber}`);
|
||||
return cand.prNumber;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function syncOpenPullRequests(github, owner, repo, options, core) {
|
||||
const { maxPrs = 20, dispatchMergeGate = true } = options || {};
|
||||
await ensurePipelineLabels(github, owner, repo, core);
|
||||
let prs;
|
||||
if (maxPrs <= 100) {
|
||||
({ data: prs } = await github.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
}));
|
||||
} else {
|
||||
prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
let synced = 0;
|
||||
const readyCandidates = [];
|
||||
for (const pr of prs) {
|
||||
if (synced >= maxPrs) break;
|
||||
if (pr.draft) continue;
|
||||
if (pr.head?.repo?.full_name && pr.head.repo.full_name !== `${owner}/${repo}`) continue;
|
||||
const result = await syncPipelineLabels(github, owner, repo, pr.number, core);
|
||||
if (result.ready) {
|
||||
readyCandidates.push({
|
||||
prNumber: pr.number,
|
||||
createdAt: result.createdAt || new Date(pr.created_at).getTime(),
|
||||
labels: result.labels || [],
|
||||
});
|
||||
}
|
||||
synced += 1;
|
||||
}
|
||||
let dispatched = 0;
|
||||
if (dispatchMergeGate && readyCandidates.length) {
|
||||
dispatched = await dispatchMergeGateForOldestReady(
|
||||
github, owner, repo, readyCandidates, core
|
||||
);
|
||||
}
|
||||
core?.info?.(`Pipeline label sync complete (${synced} PR(s), dispatched=${dispatched || 'none'})`);
|
||||
return synced;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
STAGE_LABELS,
|
||||
BLOCKER_LABELS,
|
||||
ALL_PIPELINE_LABELS,
|
||||
deriveStage,
|
||||
deriveBlockerLabels,
|
||||
computePipelineLabels,
|
||||
ensurePipelineLabels,
|
||||
syncPipelineLabels,
|
||||
syncOpenPullRequests,
|
||||
dispatchMergeGateForOldestReady,
|
||||
};
|
||||
@@ -1,20 +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'));
|
||||
|
||||
(async () => {
|
||||
assert.strictEqual(await rg.pypiVersionExists('praisonaiagents', '0.0.0'), false);
|
||||
console.log('ok: bumpPatch');
|
||||
console.log('ok: readVersionsFromTree');
|
||||
console.log('ok: pypiVersionExists missing version');
|
||||
})();
|
||||
@@ -1,203 +0,0 @@
|
||||
/**
|
||||
* Release gate preflight — path changes, CI SHA, dedupe, PyPI version checks.
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const PACKAGE_PATHS = ['src/praisonai', 'src/praisonai-agents', 'src/praisonai-code'];
|
||||
const ACTIVE_RELEASE_STATUSES = new Set([
|
||||
'queued', 'in_progress', 'waiting', 'pending', 'requested',
|
||||
]);
|
||||
|
||||
function bumpPatch(version) {
|
||||
const parts = version.split('.');
|
||||
if (parts.length !== 3) throw new Error(`Invalid version: ${version}`);
|
||||
return `${parts[0]}.${parts[1]}.${Number(parts[2]) + 1}`;
|
||||
}
|
||||
|
||||
function readVersionsFromTree(root = '.') {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const agentsToml = fs.readFileSync(
|
||||
path.join(root, 'src/praisonai-agents/pyproject.toml'), 'utf8'
|
||||
);
|
||||
const agentsMatch = agentsToml.match(/^version\s*=\s*"([^"]+)"/m);
|
||||
if (!agentsMatch) throw new Error('Could not read agents version');
|
||||
|
||||
const codeToml = fs.readFileSync(
|
||||
path.join(root, 'src/praisonai-code/pyproject.toml'), 'utf8'
|
||||
);
|
||||
const codeMatch = codeToml.match(/^version\s*=\s*"([^"]+)"/m);
|
||||
if (!codeMatch) throw new Error('Could not read praisonai-code version');
|
||||
|
||||
const wrapperPy = fs.readFileSync(
|
||||
path.join(root, 'src/praisonai/praisonai/version.py'), 'utf8'
|
||||
);
|
||||
const wrapperMatch = wrapperPy.match(/__version__ = "([^"]+)"/);
|
||||
if (!wrapperMatch) throw new Error('Could not read wrapper version');
|
||||
return {
|
||||
currentAgents: agentsMatch[1],
|
||||
currentCode: codeMatch[1],
|
||||
currentWrapper: wrapperMatch[1],
|
||||
targetAgents: bumpPatch(agentsMatch[1]),
|
||||
targetCode: bumpPatch(codeMatch[1]),
|
||||
targetWrapper: bumpPatch(wrapperMatch[1]),
|
||||
};
|
||||
}
|
||||
|
||||
function pypiVersionExists(packageName, version) {
|
||||
return new Promise((resolve) => {
|
||||
const req = https.get(
|
||||
`https://pypi.org/pypi/${packageName}/${version}/json`,
|
||||
{ timeout: 15000 },
|
||||
(res) => {
|
||||
res.resume();
|
||||
resolve(res.statusCode === 200);
|
||||
}
|
||||
);
|
||||
req.on('error', () => resolve(false));
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function hasActiveReleaseRun(github, owner, repo) {
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'pypi-release.yml',
|
||||
per_page: 20,
|
||||
});
|
||||
return runs.data.workflow_runs.some(
|
||||
(r) => ACTIVE_RELEASE_STATUSES.has(r.status) && !r.conclusion
|
||||
);
|
||||
}
|
||||
|
||||
async function lastGreenCoreTestsSha(github, owner, repo) {
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'test-core.yml',
|
||||
branch: 'main',
|
||||
status: 'completed',
|
||||
per_page: 30,
|
||||
});
|
||||
const hit = runs.data.workflow_runs.find((r) => r.conclusion === 'success');
|
||||
return hit ? hit.head_sha : '';
|
||||
}
|
||||
|
||||
async function evaluateReleasePreflight(github, owner, repo, options, core) {
|
||||
const {
|
||||
headSha,
|
||||
isCiTrigger = false,
|
||||
bump = 'patch',
|
||||
} = options;
|
||||
|
||||
const reasons = [];
|
||||
const out = {
|
||||
ready: false,
|
||||
reasons,
|
||||
headSha: headSha || '',
|
||||
lastTag: '',
|
||||
targetAgents: '',
|
||||
targetCode: '',
|
||||
targetWrapper: '',
|
||||
};
|
||||
|
||||
if (bump !== 'patch') {
|
||||
reasons.push('only patch auto-release supported');
|
||||
return out;
|
||||
}
|
||||
|
||||
if (await hasActiveReleaseRun(github, owner, repo)) {
|
||||
reasons.push('PyPI Release already in progress or awaiting approval');
|
||||
return out;
|
||||
}
|
||||
|
||||
let versions;
|
||||
try {
|
||||
versions = readVersionsFromTree();
|
||||
} catch (err) {
|
||||
reasons.push(err.message);
|
||||
return out;
|
||||
}
|
||||
out.targetAgents = versions.targetAgents;
|
||||
out.targetCode = versions.targetCode;
|
||||
out.targetWrapper = versions.targetWrapper;
|
||||
|
||||
const agentsOnPypi = await pypiVersionExists('praisonaiagents', versions.targetAgents);
|
||||
const codeOnPypi = await pypiVersionExists('praisonai-code', versions.targetCode);
|
||||
const wrapperOnPypi = await pypiVersionExists('praisonai', versions.targetWrapper);
|
||||
if (agentsOnPypi && codeOnPypi && wrapperOnPypi) {
|
||||
reasons.push(
|
||||
`already published: praisonaiagents==${versions.targetAgents}, `
|
||||
+ `praisonai-code==${versions.targetCode}, praisonai==${versions.targetWrapper}`
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
let lastTag = '';
|
||||
try {
|
||||
lastTag = execSync('git describe --tags --match "v*" --abbrev=0', { encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
reasons.push('no v* tag found');
|
||||
return out;
|
||||
}
|
||||
out.lastTag = lastTag;
|
||||
|
||||
let changed = '';
|
||||
try {
|
||||
changed = execSync(
|
||||
`git diff --name-only ${lastTag} HEAD -- ${PACKAGE_PATHS.join(' ')}`,
|
||||
{ encoding: 'utf8' }
|
||||
).trim();
|
||||
} catch {
|
||||
reasons.push('git diff failed');
|
||||
return out;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
reasons.push(`no changes in ${PACKAGE_PATHS.join(' or ')} since ${lastTag}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
if (isCiTrigger) {
|
||||
let mainSha = '';
|
||||
try {
|
||||
mainSha = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
reasons.push('could not resolve origin/main');
|
||||
return out;
|
||||
}
|
||||
if (headSha !== mainSha) {
|
||||
reasons.push(`superseded: green SHA ${headSha.slice(0, 7)} != main ${mainSha.slice(0, 7)}`);
|
||||
return out;
|
||||
}
|
||||
out.headSha = headSha;
|
||||
} else {
|
||||
const evalSha = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
|
||||
out.headSha = evalSha;
|
||||
const greenSha = await lastGreenCoreTestsSha(github, owner, repo);
|
||||
if (greenSha !== evalSha) {
|
||||
reasons.push(`CI not green on HEAD (last green: ${greenSha ? greenSha.slice(0, 7) : 'none'})`);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
out.ready = true;
|
||||
out.reasons = ['ready'];
|
||||
if (core) core.info(`Release preflight passed for ${out.headSha || headSha}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bumpPatch,
|
||||
readVersionsFromTree,
|
||||
pypiVersionExists,
|
||||
evaluateReleasePreflight,
|
||||
ACTIVE_RELEASE_STATUSES,
|
||||
PACKAGE_PATHS,
|
||||
};
|
||||
@@ -15,10 +15,7 @@ on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
schedule:
|
||||
- cron: '30 3,9,15,21 * * *'
|
||||
workflow_dispatch:
|
||||
types: [opened]
|
||||
|
||||
env:
|
||||
MAX_POLL_ATTEMPTS: 20
|
||||
@@ -146,12 +143,9 @@ jobs:
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
core.setOutput('comments_count', comments.data.length.toString());
|
||||
|
||||
@@ -192,8 +186,7 @@ jobs:
|
||||
const claudeBody = '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. ' + reviewSummary + '.\n\n' +
|
||||
'Review Context: ' + comments.data.length + ' total comments, ' +
|
||||
(reviews.coderabbit.length + reviews.qodo.length + reviews.gemini.length + reviews.copilot.length + reviews.greptile.length) + ' bot reviews.\n\n' +
|
||||
'Phase 1: Review per AGENTS.md principles (protocol-driven, backward compatible, no perf impact, SDK value gate — no feature bloat).\n' +
|
||||
'Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n' +
|
||||
'Phase 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});
|
||||
@@ -216,10 +209,6 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Wait for Copilot to respond (poll)
|
||||
id: poll
|
||||
uses: actions/github-script@v7
|
||||
@@ -306,12 +295,9 @@ jobs:
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
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
|
||||
@@ -327,23 +313,10 @@ jobs:
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
|
||||
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}`);
|
||||
|
||||
- name: Sync pipeline status labels
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
|
||||
# ================================================================
|
||||
# Claude after Qodo fallback path (same polling approach)
|
||||
# ================================================================
|
||||
@@ -359,10 +332,6 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Wait for Copilot to respond (poll)
|
||||
id: poll
|
||||
uses: actions/github-script@v7
|
||||
@@ -430,12 +399,9 @@ jobs:
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
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
|
||||
@@ -451,23 +417,10 @@ jobs:
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
|
||||
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}`);
|
||||
|
||||
- name: Sync pipeline status labels
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
|
||||
# ================================================================
|
||||
# Claude after Gemini — restores the trigger lost from the deleted
|
||||
# chain-claude-after-copilot.yml workflow.
|
||||
@@ -500,12 +453,9 @@ jobs:
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Post Claude analysis request (Lead Engineer)
|
||||
@@ -518,147 +468,9 @@ jobs:
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the Lead Engineer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL analysis and reviews above carefully (Gemini, CodeRabbit, Qodo, Copilot, etc).\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
|
||||
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'
|
||||
});
|
||||
|
||||
# ================================================================
|
||||
# Recovery: post FINAL @claude if merge-conflict comment blocked the chain
|
||||
# (e.g. conflict fired before claude-after-copilot could post review prompt)
|
||||
# ================================================================
|
||||
claude-review-recovery:
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'synchronize'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Re-post FINAL Claude review when stale or missing
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const pr = context.payload.pull_request.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
const comments = await mergeGate.listAllComments(github, owner, repo, pr);
|
||||
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr))
|
||||
|| context.payload.pull_request.updated_at;
|
||||
const headPusher = context.payload.sender?.login || null;
|
||||
|
||||
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
|
||||
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
|
||||
|
||||
if (mergeGate.shouldSkipFinalRecovery(comments, headPushedAt)) {
|
||||
core.info('Recent @claude within 35min and FINAL not stale — skip recovery');
|
||||
return;
|
||||
}
|
||||
|
||||
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr)) {
|
||||
core.info('Claude workflow in progress — skip recovery');
|
||||
return;
|
||||
}
|
||||
|
||||
const finalBody = mergeGate.FINAL_CLAUDE_REVIEW_BODY;
|
||||
|
||||
if (hasFinal && isStale) {
|
||||
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusher);
|
||||
if (gate.skip) {
|
||||
core.info(`Skip stale-FINAL recovery: ${gate.reason}`);
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.createComment({ issue_number: pr, owner, repo, body: finalBody });
|
||||
core.info(`Posted stale-FINAL recovery @claude on PR #${pr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasFinal) {
|
||||
core.info('FINAL Claude review current on HEAD — skip recovery');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasReviewerActivity = comments.some(c => {
|
||||
const login = c.user.login.toLowerCase();
|
||||
return login.includes('coderabbit') || login.includes('copilot') ||
|
||||
login.includes('greptile') || login.includes('qodo') || login.includes('gemini');
|
||||
});
|
||||
if (!hasReviewerActivity) {
|
||||
core.info('No bot reviewer activity yet, skipping recovery');
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({ issue_number: pr, owner, repo, body: finalBody });
|
||||
core.info(`Posted missing FINAL Claude review on PR #${pr}`);
|
||||
|
||||
- name: Sync pipeline status labels
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const pr = context.payload.pull_request.number;
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
|
||||
|
||||
# Scheduled: re-post @claude when FINAL is stale (Claude pushed after last FINAL trigger)
|
||||
stale-final-recovery-scheduled:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Re-post stale FINAL @claude on open internal PRs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner, repo, state: 'open', per_page: 100,
|
||||
});
|
||||
let posted = 0;
|
||||
for (const pr of prs) {
|
||||
if (pr.draft) continue;
|
||||
if (pr.head?.repo?.full_name && pr.head.repo.full_name !== `${owner}/${repo}`) continue;
|
||||
const comments = await mergeGate.listAllComments(github, owner, repo, pr.number);
|
||||
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr.number)) || pr.updated_at;
|
||||
if (!mergeGate.needsStaleFinalRecovery(comments, headPushedAt)) continue;
|
||||
if (mergeGate.hasRecentClaudeTrigger(comments, 10)) {
|
||||
core.info(`PR #${pr.number}: stale FINAL but @claude within 10min — skip`);
|
||||
continue;
|
||||
}
|
||||
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr.number)) {
|
||||
core.info(`PR #${pr.number}: Claude workflow in progress — skip`);
|
||||
continue;
|
||||
}
|
||||
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt);
|
||||
if (gate.skip) {
|
||||
core.info(`PR #${pr.number}: skip stale-FINAL — ${gate.reason}`);
|
||||
continue;
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr.number, owner, repo,
|
||||
body: mergeGate.FINAL_CLAUDE_REVIEW_BODY,
|
||||
});
|
||||
posted++;
|
||||
core.info(`Posted scheduled stale-FINAL recovery on PR #${pr.number}`);
|
||||
if (posted >= 5) break;
|
||||
}
|
||||
core.info(`Scheduled stale-FINAL recovery complete (${posted} posted)`);
|
||||
|
||||
# ================================================================
|
||||
# BOT PRs: CodeRabbit/Qodo skip bot-authored PRs by default.
|
||||
# Trigger them explicitly. Do NOT trigger Copilot here — it will
|
||||
@@ -676,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,504 +0,0 @@
|
||||
name: Claude PR Merge Gate
|
||||
|
||||
# Read-only Claude assesses merge readiness; GH_TOKEN merges (no @claude, no gh pr review).
|
||||
# Does not overlap with merge-conflict-claude.yml or auto-pr-comment @claude chains.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Scan for merge-ready PRs every 10 minutes (assess runs only when candidates exist).
|
||||
- cron: '*/10 * * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Optional PR number (scan all open internal PRs if empty)'
|
||||
required: false
|
||||
type: string
|
||||
merge_method:
|
||||
description: 'Merge method'
|
||||
required: false
|
||||
type: choice
|
||||
default: merge
|
||||
options:
|
||||
- merge
|
||||
- squash
|
||||
- rebase
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Claude Assistant
|
||||
- Core Tests
|
||||
types:
|
||||
- completed
|
||||
repository_dispatch:
|
||||
types:
|
||||
- claude-merge-gate
|
||||
|
||||
concurrency:
|
||||
group: claude-merge-gate-${{ github.repository }}-${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || github.event.workflow_run.id || 'scan' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
MAX_CANDIDATES: 1
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Dispatch merge gate when PR pipeline is ready
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PAT: ${{ secrets.GH_TOKEN }}
|
||||
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 wr = context.payload.workflow_run;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
if (!process.env.PAT) {
|
||||
core.setFailed('GH_TOKEN secret is required for merge gate auto-dispatch (needs repo + workflow scope).');
|
||||
return;
|
||||
}
|
||||
|
||||
if (wr.conclusion !== 'success') {
|
||||
core.info(`Skip dispatch: ${wr.name} conclusion=${wr.conclusion}`);
|
||||
return;
|
||||
}
|
||||
const allowed = ['Claude Assistant', 'Core Tests'];
|
||||
if (!allowed.includes(wr.name)) {
|
||||
core.info(`Skip dispatch: workflow ${wr.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const prNumber = await mergeGate.resolvePrNumberFromWorkflowRun(github, owner, repo, wr);
|
||||
if (!prNumber) {
|
||||
core.info('Skip dispatch: could not resolve PR from workflow run');
|
||||
return;
|
||||
}
|
||||
|
||||
const fromClaude = wr.name === 'Claude Assistant';
|
||||
const check = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core, {
|
||||
skipGlobalClaudeRunCheck: fromClaude,
|
||||
skipRecentClaudeCooldown: fromClaude,
|
||||
}
|
||||
);
|
||||
if (!check.ready) {
|
||||
core.info(`PR #${prNumber} not ready: ${check.reasons.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner,
|
||||
repo,
|
||||
event_type: 'claude-merge-gate',
|
||||
client_payload: { pr_number: prNumber },
|
||||
});
|
||||
core.info(`Dispatched merge gate for PR #${prNumber} after ${wr.name}`);
|
||||
|
||||
scan-candidates:
|
||||
if: github.event_name != 'workflow_run'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: write
|
||||
actions: read
|
||||
outputs:
|
||||
matrix: ${{ steps.scan.outputs.matrix }}
|
||||
has_candidates: ${{ steps.scan.outputs.has_candidates }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Scan open PRs for merge gate eligibility
|
||||
id: scan
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ env.GH_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 prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner, repo, state: 'open', per_page: 100, sort: 'created', direction: 'asc',
|
||||
});
|
||||
const prNumbers = prs.map((p) => p.number);
|
||||
const selected = await mergeGate.selectMergeGateCandidates(
|
||||
github, owner, repo, prNumbers, maxCandidates, core
|
||||
);
|
||||
candidates.push(...selected.candidates);
|
||||
skipped.push(...selected.skipped);
|
||||
}
|
||||
|
||||
core.info('Skipped: ' + JSON.stringify(skipped));
|
||||
core.setOutput('matrix', JSON.stringify({ include: candidates }));
|
||||
core.setOutput('has_candidates', candidates.length > 0 ? 'true' : 'false');
|
||||
|
||||
core.summary.addRaw('## Merge gate scan\n');
|
||||
core.summary.addRaw(`Candidates: ${candidates.length}\n`);
|
||||
core.summary.addRaw('```json\n' + JSON.stringify({ candidates, skipped }, null, 2) + '\n```');
|
||||
await core.summary.write();
|
||||
|
||||
if (inputPr && skipped.length === 1 && skipped[0].pr === parseInt(inputPr, 10)) {
|
||||
const reasons = skipped[0].reasons;
|
||||
const body = [
|
||||
'**Merge gate scan** — not eligible for auto-merge.',
|
||||
'',
|
||||
...reasons.map((r) => `- \`${r}\``),
|
||||
'',
|
||||
'**Actions:** wait for CI and the Claude review chain, or add label `needs-manual-review` and merge manually.',
|
||||
'**Opt out:** label `no-auto-merge`.',
|
||||
].join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: skipped[0].pr, body,
|
||||
});
|
||||
} else if (inputPr && candidates.length === 1) {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: candidates[0].pr_number,
|
||||
body: '**Merge gate scan** — eligible for assessment. Claude merge gate will assess and may auto-merge if `MERGE_GATE_VERDICT: APPROVE`.',
|
||||
});
|
||||
}
|
||||
|
||||
await ps.syncOpenPullRequests(github, owner, repo, { maxPrs: 20 }, 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: ${{ env.GH_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 praisonaiagents (no scope creep).
|
||||
4. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
|
||||
5. If agent.py adds >100 lines or new Agent __init__ params, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
|
||||
6. BLOCK if labels security/breaking-change/needs-manual-review/release, sensitive paths (.github/workflows, auth, pyproject.toml), secrets in diff, PR >800 lines or >30 files, or SDK changes without tests. Label `merge-gate-ci-only` exempts CI-only changes under `.github/workflows/`, `.github/actions/`, or merge-gate scripts.
|
||||
7. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
|
||||
8. If ready to merge: MERGE_GATE_VERDICT: APPROVE. Otherwise: MERGE_GATE_VERDICT: BLOCK with reasons.
|
||||
|
||||
Do not approve or merge via GitHub UI/API — only post the verdict comment.
|
||||
disallowed_tools: |
|
||||
Bash
|
||||
Edit
|
||||
Write
|
||||
Replace
|
||||
allowed_tools: |
|
||||
View
|
||||
GlobTool
|
||||
GrepTool
|
||||
Read
|
||||
timeout_minutes: 15
|
||||
|
||||
- name: Post fallback verdict if Claude did not comment
|
||||
id: fallback_verdict
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ env.GH_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);
|
||||
if (existing) {
|
||||
core.info(`Verdict already posted: ${existing}`);
|
||||
core.setOutput('verdict', existing);
|
||||
return;
|
||||
}
|
||||
|
||||
const check = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core, { forMergeStep: true }
|
||||
);
|
||||
const verdict = check.ready ? 'APPROVE' : 'BLOCK';
|
||||
const lines = [
|
||||
`MERGE_GATE_VERDICT: ${verdict}`,
|
||||
'',
|
||||
'Automated fallback — Claude assess did not post a verdict comment (e.g. GitHub MCP unavailable).',
|
||||
];
|
||||
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}`);
|
||||
core.setOutput('verdict', verdict);
|
||||
|
||||
- name: Verify verdict posted
|
||||
id: verdict
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ env.GH_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);
|
||||
core.setOutput('verdict', verdict || 'NONE');
|
||||
if (!verdict) core.warning('No recent MERGE_GATE_VERDICT comment found after assessment');
|
||||
|
||||
- name: Remove merge gate active label
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ env.GH_TOKEN }}
|
||||
script: |
|
||||
const prNumber = ${{ matrix.pr_number }};
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
name: 'claude-merge-gate-active',
|
||||
});
|
||||
core.info(`Removed claude-merge-gate-active from PR #${prNumber}`);
|
||||
} catch (e) {
|
||||
core.info(`Label claude-merge-gate-active already removed or missing on PR #${prNumber}`);
|
||||
}
|
||||
|
||||
- name: Fail job if assessment failed
|
||||
if: steps.assess.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
merge-only:
|
||||
needs: [scan-candidates, claude-assess]
|
||||
if: |
|
||||
always() &&
|
||||
needs.scan-candidates.outputs.has_candidates == 'true' &&
|
||||
needs.claude-assess.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.scan-candidates.outputs.matrix) }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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: ${{ env.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 = ${{ 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);
|
||||
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();
|
||||
}
|
||||
+40
-144
@@ -123,8 +123,6 @@ jobs:
|
||||
github.event.action != 'opened' &&
|
||||
(github.event.action != 'labeled' || github.event.label.name == 'claude') &&
|
||||
(github.event_name != 'issue_comment' || contains(github.event.comment.body, '@claude')) &&
|
||||
(github.event_name != 'pull_request_review' || contains(github.event.review.body, '@claude')) &&
|
||||
(github.event_name != 'pull_request_review_comment' || contains(github.event.comment.body, '@claude')) &&
|
||||
(
|
||||
!contains(github.actor, '[bot]') ||
|
||||
github.actor == 'github-actions[bot]' ||
|
||||
@@ -133,9 +131,6 @@ jobs:
|
||||
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
|
||||
@@ -156,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
|
||||
@@ -226,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):
|
||||
@@ -282,16 +227,21 @@ jobs:
|
||||
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).
|
||||
- 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.
|
||||
|
||||
${{ 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.
|
||||
@@ -317,7 +267,10 @@ jobs:
|
||||
- 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:*)
|
||||
@@ -339,60 +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
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
|
||||
const issueNumber = context.payload.issue.number;
|
||||
await chain.kickReviewChainForIssue(
|
||||
github, context.repo.owner, context.repo.repo, issueNumber, core
|
||||
);
|
||||
|
||||
- name: Sync pipeline status labels for PR
|
||||
if: steps.check_fork.outputs.is_pr == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
|
||||
const prNumber = parseInt('${{ steps.check_fork.outputs.pr_number }}', 10);
|
||||
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
|
||||
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, prNumber, core);
|
||||
|
||||
- name: Dispatch merge gate when PR pipeline ready
|
||||
if: steps.check_fork.outputs.is_pr == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = parseInt('${{ steps.check_fork.outputs.pr_number }}', 10);
|
||||
const check = await mergeGate.evaluatePipelineQuiescent(
|
||||
github, owner, repo, prNumber, core, {
|
||||
skipGlobalClaudeRunCheck: true,
|
||||
skipRecentClaudeCooldown: true,
|
||||
}
|
||||
);
|
||||
if (!check.ready) {
|
||||
core.info(`PR #${prNumber} not ready for merge gate: ${check.reasons.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner,
|
||||
repo,
|
||||
event_type: 'claude-merge-gate',
|
||||
client_payload: { pr_number: prNumber },
|
||||
});
|
||||
core.info(`Dispatched merge gate for PR #${prNumber}`);
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Deploy install scripts to praison.ai
|
||||
|
||||
# When install.sh or install.ps1 change on main, trigger praison.ai CI which
|
||||
# syncs scripts at build time and deploys to Azure AKS (existing azure-pipeline.yaml).
|
||||
|
||||
on:
|
||||
push:
|
||||
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:
|
||||
trigger-website-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger praison.ai CI
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${GH_TOKEN:-}" ]; then
|
||||
echo "SECRETS_ADMIN_PAT is not configured in this repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh workflow run CI \
|
||||
--repo MervinPraison/praison.ai \
|
||||
--ref master
|
||||
|
||||
echo "Triggered praison.ai CI for PraisonAI commit ${SOURCE_SHA}"
|
||||
echo "Monitor: https://github.com/MervinPraison/praison.ai/actions/workflows/azure-pipeline.yaml"
|
||||
|
||||
sleep 10
|
||||
RUN_URL=$(gh run list \
|
||||
--repo MervinPraison/praison.ai \
|
||||
--workflow CI \
|
||||
--limit 1 \
|
||||
--json url,status,createdAt \
|
||||
--jq '.[0].url' 2>/dev/null || true)
|
||||
if [ -n "${RUN_URL}" ]; then
|
||||
echo "Latest praison.ai CI run: ${RUN_URL}"
|
||||
fi
|
||||
@@ -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,184 +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 }}
|
||||
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.',
|
||||
].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,138 +0,0 @@
|
||||
name: Nightly Release Gate
|
||||
|
||||
# Preflight before PyPI Release (patch bump).
|
||||
# Primary: Core Tests success on main (workflow_run).
|
||||
# Backstop: nightly cron at 00:00 UTC.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
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'
|
||||
)
|
||||
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
|
||||
|
||||
- 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,33 +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
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sync pipeline labels on open PRs
|
||||
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'));
|
||||
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:
|
||||
|
||||
@@ -1,491 +0,0 @@
|
||||
name: PyPI Release
|
||||
|
||||
# Environments (Settings → Environments):
|
||||
# pypi-auto — patch releases from ci/nightly (no reviewers, main branch only)
|
||||
# pypi — manual workflow_dispatch and major/minor (required reviewers)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: 'Version bump type for all packages (agents, code, wrapper)'
|
||||
required: true
|
||||
default: patch
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
dry_run:
|
||||
description: 'Preview versions and steps without publishing or pushing'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_agents:
|
||||
description: 'Skip agents publish (recovery if already on PyPI)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_wrapper:
|
||||
description: 'Skip wrapper publish (agents and/or code only)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_code:
|
||||
description: 'Skip praisonai-code publish (recovery if already on PyPI)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
agents_version:
|
||||
description: 'Override agents version (empty = auto bump from pyproject.toml)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
wrapper_version:
|
||||
description: 'Override wrapper version (empty = auto bump from version.py)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
code_version:
|
||||
description: 'Override praisonai-code version (empty = auto bump from pyproject.toml)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
source:
|
||||
description: 'Trigger source (audit trail)'
|
||||
required: false
|
||||
default: manual
|
||||
type: choice
|
||||
options:
|
||||
- manual
|
||||
- nightly
|
||||
- ci
|
||||
trigger_sha:
|
||||
description: 'Main commit SHA that passed release gate (audit/dedupe)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: pypi-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
environment: >-
|
||||
${{
|
||||
(inputs.source == 'ci' || inputs.source == 'nightly') &&
|
||||
inputs.bump == 'patch' &&
|
||||
'pypi-auto' || 'pypi'
|
||||
}}
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN || secrets.PYPI_API_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "MervinPraison"
|
||||
git config user.email "[email protected]"
|
||||
gh auth setup-git
|
||||
|
||||
- name: Release summary
|
||||
run: |
|
||||
echo "Source: ${{ inputs.source }}"
|
||||
echo "Trigger SHA: ${{ inputs.trigger_sha }}"
|
||||
echo "Bump: ${{ inputs.bump }}"
|
||||
echo "Dry run: ${{ inputs.dry_run }}"
|
||||
echo "Environment: ${{ (inputs.source == 'ci' || inputs.source == 'nightly') && inputs.bump == 'patch' && 'pypi-auto' || 'pypi' }}"
|
||||
|
||||
- name: Compute release versions
|
||||
id: versions
|
||||
env:
|
||||
BUMP: ${{ inputs.bump }}
|
||||
AGENTS_OVERRIDE: ${{ inputs.agents_version }}
|
||||
WRAPPER_OVERRIDE: ${{ inputs.wrapper_version }}
|
||||
CODE_OVERRIDE: ${{ inputs.code_version }}
|
||||
SKIP_AGENTS: ${{ inputs.skip_agents }}
|
||||
SKIP_CODE: ${{ inputs.skip_code }}
|
||||
run: |
|
||||
python <<'PY'
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SEMVER = re.compile(r"^\d+\.\d+\.\d+$")
|
||||
root = Path(".")
|
||||
bump = os.environ["BUMP"]
|
||||
|
||||
def bump_version(version: str) -> str:
|
||||
parts = version.split(".")
|
||||
if len(parts) != 3:
|
||||
print(f"Invalid version: {version}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
maj, min_, patch = (int(parts[0]), int(parts[1]), int(parts[2]))
|
||||
if bump == "major":
|
||||
return f"{maj + 1}.0.0"
|
||||
if bump == "minor":
|
||||
return f"{maj}.{min_ + 1}.0"
|
||||
return f"{maj}.{min_}.{patch + 1}"
|
||||
|
||||
def validate_version(label: str, value: str) -> None:
|
||||
if value and not SEMVER.match(value):
|
||||
print(f"Invalid {label} override: {value!r} (expected X.Y.Z)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
agents_override = os.environ.get("AGENTS_OVERRIDE", "").strip()
|
||||
wrapper_override = os.environ.get("WRAPPER_OVERRIDE", "").strip()
|
||||
code_override = os.environ.get("CODE_OVERRIDE", "").strip()
|
||||
validate_version("agents_version", agents_override)
|
||||
validate_version("wrapper_version", wrapper_override)
|
||||
validate_version("code_version", code_override)
|
||||
|
||||
agents_pyproject = root / "src/praisonai-agents/pyproject.toml"
|
||||
agents_content = agents_pyproject.read_text()
|
||||
agents_match = re.search(r'^version\s*=\s*"([^"]+)"', agents_content, re.MULTILINE)
|
||||
if not agents_match:
|
||||
print("Could not read agents version from pyproject.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_agents = agents_match.group(1)
|
||||
|
||||
code_pyproject = root / "src/praisonai-code/pyproject.toml"
|
||||
code_content = code_pyproject.read_text()
|
||||
code_match = re.search(r'^version\s*=\s*"([^"]+)"', code_content, re.MULTILINE)
|
||||
if not code_match:
|
||||
print("Could not read praisonai-code version from pyproject.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_code = code_match.group(1)
|
||||
|
||||
wrapper_version_py = root / "src/praisonai/praisonai/version.py"
|
||||
wrapper_content = wrapper_version_py.read_text()
|
||||
wrapper_match = re.search(r'__version__ = "([^"]+)"', wrapper_content)
|
||||
if not wrapper_match:
|
||||
print("Could not read wrapper version from version.py", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
current_wrapper = wrapper_match.group(1)
|
||||
|
||||
for label, value in (
|
||||
("agents", current_agents),
|
||||
("code", current_code),
|
||||
("wrapper", current_wrapper),
|
||||
):
|
||||
if not SEMVER.match(value):
|
||||
print(f"Invalid current {label} version: {value}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
skip_agents = os.environ.get("SKIP_AGENTS", "false").lower() == "true"
|
||||
skip_code = os.environ.get("SKIP_CODE", "false").lower() == "true"
|
||||
|
||||
if agents_override:
|
||||
agents_version = agents_override
|
||||
elif skip_agents:
|
||||
agents_version = current_agents
|
||||
else:
|
||||
agents_version = bump_version(current_agents)
|
||||
|
||||
if code_override:
|
||||
code_version = code_override
|
||||
elif skip_code:
|
||||
code_version = current_code
|
||||
else:
|
||||
code_version = bump_version(current_code)
|
||||
|
||||
if wrapper_override:
|
||||
wrapper_version = wrapper_override
|
||||
else:
|
||||
wrapper_version = bump_version(current_wrapper)
|
||||
|
||||
github_output = os.environ["GITHUB_OUTPUT"]
|
||||
with open(github_output, "a", encoding="utf-8") as fh:
|
||||
fh.write(f"agents_version={agents_version}\n")
|
||||
fh.write(f"code_version={code_version}\n")
|
||||
fh.write(f"wrapper_version={wrapper_version}\n")
|
||||
fh.write(f"current_agents={current_agents}\n")
|
||||
fh.write(f"current_code={current_code}\n")
|
||||
fh.write(f"current_wrapper={current_wrapper}\n")
|
||||
|
||||
print(f"Agents: {current_agents} -> {agents_version}")
|
||||
print(f"Code: {current_code} -> {code_version}")
|
||||
print(f"Wrapper: {current_wrapper} -> {wrapper_version}")
|
||||
PY
|
||||
|
||||
- name: Detect existing PyPI versions
|
||||
id: pypi_exists
|
||||
if: inputs.dry_run != true
|
||||
env:
|
||||
AGENTS_VERSION: ${{ steps.versions.outputs.agents_version }}
|
||||
CODE_VERSION: ${{ steps.versions.outputs.code_version }}
|
||||
WRAPPER_VERSION: ${{ steps.versions.outputs.wrapper_version }}
|
||||
INPUT_SKIP_AGENTS: ${{ inputs.skip_agents }}
|
||||
INPUT_SKIP_CODE: ${{ inputs.skip_code }}
|
||||
INPUT_SKIP_WRAPPER: ${{ inputs.skip_wrapper }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
skip_agents="${INPUT_SKIP_AGENTS}"
|
||||
skip_code="${INPUT_SKIP_CODE}"
|
||||
skip_wrapper="${INPUT_SKIP_WRAPPER}"
|
||||
if [ "$skip_agents" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonaiagents/${AGENTS_VERSION}/json" >/dev/null && skip_agents=true || true
|
||||
fi
|
||||
if [ "$skip_code" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai-code/${CODE_VERSION}/json" >/dev/null && skip_code=true || true
|
||||
fi
|
||||
if [ "$skip_wrapper" != "true" ]; then
|
||||
curl -fsSL "https://pypi.org/pypi/praisonai/${WRAPPER_VERSION}/json" >/dev/null && skip_wrapper=true || true
|
||||
fi
|
||||
echo "skip_agents=${skip_agents}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_code=${skip_code}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip_wrapper=${skip_wrapper}" >> "$GITHUB_OUTPUT"
|
||||
if [ "$skip_agents" = "true" ] && [ "$skip_code" = "true" ] && [ "$skip_wrapper" = "true" ]; then
|
||||
echo "All target versions already on PyPI — nothing to publish."
|
||||
fi
|
||||
|
||||
- name: Exit if already published
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
steps.pypi_exists.outputs.skip_agents == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_code == 'true' &&
|
||||
steps.pypi_exists.outputs.skip_wrapper == 'true'
|
||||
run: echo "Idempotent skip — target versions already on PyPI."
|
||||
|
||||
- name: Dry run summary
|
||||
if: inputs.dry_run
|
||||
run: |
|
||||
echo "Dry run — no publish, commit, or push."
|
||||
echo "Agents version: ${{ steps.versions.outputs.agents_version }}"
|
||||
echo "Code version: ${{ steps.versions.outputs.code_version }}"
|
||||
echo "Wrapper version: ${{ steps.versions.outputs.wrapper_version }}"
|
||||
echo ""
|
||||
if [ "${{ inputs.skip_agents }}" != "true" ]; then
|
||||
echo "Would publish praisonaiagents ${{ steps.versions.outputs.agents_version }}"
|
||||
fi
|
||||
if [ "${{ inputs.skip_code }}" != "true" ]; then
|
||||
echo "Would publish praisonai-code ${{ steps.versions.outputs.code_version }}"
|
||||
fi
|
||||
if [ "${{ inputs.skip_wrapper }}" != "true" ]; then
|
||||
echo "Would run bump_and_release.py ${{ steps.versions.outputs.wrapper_version }} --agents ${{ steps.versions.outputs.agents_version }} --code-pin ${{ steps.versions.outputs.code_version }}"
|
||||
echo "Would uv publish praisonai ${{ steps.versions.outputs.wrapper_version }}"
|
||||
fi
|
||||
|
||||
- name: Publish praisonaiagents
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_agents != true &&
|
||||
steps.pypi_exists.outputs.skip_agents != 'true'
|
||||
working-directory: src/praisonai-agents
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.agents_version }}
|
||||
CURRENT: ${{ steps.versions.outputs.current_agents }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
path = Path("pyproject.toml")
|
||||
content = path.read_text()
|
||||
current = os.environ["CURRENT"]
|
||||
new = os.environ["NEW_VERSION"]
|
||||
path.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
|
||||
PY
|
||||
|
||||
rm -rf dist
|
||||
uv lock
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonaiagents on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_agents != true &&
|
||||
steps.pypi_exists.outputs.skip_agents != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonaiagents"
|
||||
VERSION="${{ steps.versions.outputs.agents_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Commit agents version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_agents != true &&
|
||||
steps.pypi_exists.outputs.skip_agents != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-agents/pyproject.toml src/praisonai-agents/uv.lock
|
||||
git diff --cached --quiet && { echo "No agents files to commit"; exit 0; }
|
||||
git commit -m "Bump praisonaiagents to ${{ steps.versions.outputs.agents_version }}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Publish praisonai-code
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_code != true &&
|
||||
steps.pypi_exists.outputs.skip_code != 'true'
|
||||
working-directory: src/praisonai-code
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.versions.outputs.code_version }}
|
||||
CURRENT: ${{ steps.versions.outputs.current_code }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
new = os.environ["NEW_VERSION"]
|
||||
current = os.environ["CURRENT"]
|
||||
pyproject = Path("pyproject.toml")
|
||||
content = pyproject.read_text()
|
||||
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
|
||||
init_py = Path("praisonai_code/__init__.py")
|
||||
init_content = init_py.read_text()
|
||||
init_py.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new}"', init_content, count=1))
|
||||
PY
|
||||
|
||||
rm -rf dist
|
||||
uv lock
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Wait for praisonai-code on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_code != true &&
|
||||
steps.pypi_exists.outputs.skip_code != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai-code"
|
||||
VERSION="${{ steps.versions.outputs.code_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Commit praisonai-code version bump
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_code != true &&
|
||||
steps.pypi_exists.outputs.skip_code != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add src/praisonai-code/pyproject.toml src/praisonai-code/uv.lock src/praisonai-code/praisonai_code/__init__.py
|
||||
git diff --cached --quiet && { echo "No code files to commit"; exit 0; }
|
||||
git commit -m "Bump praisonai-code to ${{ steps.versions.outputs.code_version }}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
|
||||
- name: Bump and release wrapper
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_wrapper != true &&
|
||||
steps.pypi_exists.outputs.skip_wrapper != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python src/praisonai/scripts/bump_and_release.py "${{ steps.versions.outputs.wrapper_version }}" \
|
||||
--agents "${{ steps.versions.outputs.agents_version }}" \
|
||||
--code-pin "${{ steps.versions.outputs.code_version }}" \
|
||||
--wait --max-wait 600 --no-add-all
|
||||
|
||||
- name: Publish praisonai wrapper to PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_wrapper != true &&
|
||||
steps.pypi_exists.outputs.skip_wrapper != 'true'
|
||||
working-directory: src/praisonai
|
||||
run: uv publish
|
||||
|
||||
- name: Wait for praisonai on PyPI
|
||||
if: >-
|
||||
inputs.dry_run != true &&
|
||||
inputs.skip_wrapper != true &&
|
||||
steps.pypi_exists.outputs.skip_wrapper != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACKAGE="praisonai"
|
||||
VERSION="${{ steps.versions.outputs.wrapper_version }}"
|
||||
MAX_WAIT=600
|
||||
INTERVAL=30
|
||||
START=$(date +%s)
|
||||
|
||||
while true; do
|
||||
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
|
||||
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
|
||||
exit 0
|
||||
fi
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
- name: Verify PyPI versions
|
||||
if: inputs.dry_run != true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AGENTS="${{ steps.versions.outputs.agents_version }}"
|
||||
CODE="${{ steps.versions.outputs.code_version }}"
|
||||
WRAPPER="${{ steps.versions.outputs.wrapper_version }}"
|
||||
|
||||
if [ "${{ inputs.skip_agents }}" != "true" ]; then
|
||||
echo "✅ praisonaiagents==${AGENTS}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_code }}" != "true" ]; then
|
||||
echo "✅ praisonai-code==${CODE}"
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.skip_wrapper }}" != "true" ]; then
|
||||
echo "✅ praisonai==${WRAPPER}"
|
||||
fi
|
||||
@@ -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,37 +0,0 @@
|
||||
name: Sync Claude Secrets
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_repo:
|
||||
description: 'Destination repo (owner/name)'
|
||||
required: true
|
||||
default: 'MervinPraison/PraisonAI-Plugins'
|
||||
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 }}
|
||||
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"
|
||||
echo "Synced CLAUDE_APP_ID, CLAUDE_APP_PRIVATE_KEY, CLAUDE_CODE_OAUTH_TOKEN, GH_TOKEN → $TARGET"
|
||||
@@ -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
-235
@@ -7,251 +7,68 @@ on:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-core.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-core.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}/src/praisonai-agents
|
||||
PRAISONAI_ALLOW_NETWORK: '0'
|
||||
PRAISONAI_LIVE_TESTS: '0'
|
||||
PRAISONAI_TEST_TIER: main
|
||||
|
||||
jobs:
|
||||
test-core-collect:
|
||||
name: test-core-collect
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-timeout pytest-xdist
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
with:
|
||||
use-uv: "true"
|
||||
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
- name: Verify unit test collection
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src/praisonai
|
||||
python -m pytest tests/unit/ \
|
||||
-m "not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
|
||||
--ignore=tests/fixtures \
|
||||
--collect-only -q --disable-warnings
|
||||
|
||||
test-core-unit:
|
||||
name: test-core (${{ matrix.shard }})
|
||||
needs: test-core-collect
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- shard: cli
|
||||
paths: >-
|
||||
tests/unit/cli/
|
||||
tests/unit/tui/
|
||||
extra_ignore: --ignore=tests/unit/cli/test_message_queue.py
|
||||
- shard: bots-gateway
|
||||
paths: >-
|
||||
tests/unit/bots/
|
||||
tests/unit/gateway/
|
||||
extra_ignore: ""
|
||||
- shard: subdirs
|
||||
paths: >-
|
||||
tests/unit/scheduler/
|
||||
tests/unit/integrations/
|
||||
tests/unit/deploy/
|
||||
tests/unit/mcp_server/
|
||||
tests/unit/mcp/
|
||||
tests/unit/knowledge/
|
||||
tests/unit/train/
|
||||
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/daemon/
|
||||
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
|
||||
cd src/praisonai
|
||||
PYTEST_ARGS=(
|
||||
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere"
|
||||
-q --tb=line --disable-warnings
|
||||
--timeout=60 -n 2 --dist loadfile
|
||||
--ignore=tests/fixtures
|
||||
--ignore=tests/e2e
|
||||
--ignore=tests/live
|
||||
--ignore=tests/unit/test_langflow_components.py
|
||||
)
|
||||
if [ "${{ matrix.root_glob || 'false' }}" = "true" ]; then
|
||||
shopt -s nullglob
|
||||
ROOT_TESTS=(tests/unit/test_*.py)
|
||||
shopt -u nullglob
|
||||
EXCLUDE=(tests/unit/test_langflow_components.py)
|
||||
FILTERED=()
|
||||
for t in "${ROOT_TESTS[@]}"; do
|
||||
skip=false
|
||||
for e in "${EXCLUDE[@]}"; do
|
||||
if [ "$t" = "$e" ]; then skip=true; break; fi
|
||||
done
|
||||
if [ "$skip" = false ]; then FILTERED+=("$t"); fi
|
||||
done
|
||||
if [ ${#FILTERED[@]} -eq 0 ]; then
|
||||
echo "No root unit tests matched"
|
||||
exit 0
|
||||
fi
|
||||
python -m pytest "${FILTERED[@]}" "${PYTEST_ARGS[@]}"
|
||||
else
|
||||
python -m pytest ${{ matrix.paths }} "${PYTEST_ARGS[@]}" ${{ matrix.extra_ignore }}
|
||||
fi
|
||||
|
||||
test-core-integration:
|
||||
name: test-core-integration
|
||||
needs: test-core-unit
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-timeout
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
with:
|
||||
use-uv: "true"
|
||||
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
- name: Run integration tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src/praisonai
|
||||
python -m pytest tests/integration/ \
|
||||
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
|
||||
-q --tb=line --disable-warnings \
|
||||
--timeout=120 \
|
||||
--ignore=tests/fixtures \
|
||||
--ignore=tests/e2e \
|
||||
--ignore=tests/live \
|
||||
--ignore=tests/integration/test_tracker_complex_tasks.py \
|
||||
--ignore=tests/integration/test_serve_integration.py
|
||||
|
||||
test-core:
|
||||
name: test-core
|
||||
needs: [test-core-collect, test-core-unit, test-core-integration]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Report Core Tests result
|
||||
shell: bash
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install UV
|
||||
run: |
|
||||
set -euo pipefail
|
||||
collect='${{ needs.test-core-collect.result }}'
|
||||
unit='${{ needs.test-core-unit.result }}'
|
||||
integration='${{ needs.test-core-integration.result }}'
|
||||
echo "collect=$collect unit=$unit integration=$integration"
|
||||
if [ "$collect" != "success" ] || [ "$unit" != "success" ] || [ "$integration" != "success" ]; then
|
||||
echo "Core Tests failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "Core Tests passed"
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings \
|
||||
--cov=praisonai --cov-report=xml --cov-branch \
|
||||
--timeout=60 || echo "Some unit tests failed"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings \
|
||||
--timeout=60 \
|
||||
--ignore=tests/integration/test_tracker_complex_tasks.py \
|
||||
--ignore=tests/integration/test_serve_integration.py \
|
||||
|| echo "Some integration tests failed"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN || '' }}
|
||||
files: src/praisonai/coverage.xml
|
||||
flags: core-tests
|
||||
fail_ci_if_error: false
|
||||
@@ -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: 8
|
||||
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,78 +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 \
|
||||
-q --tb=line --timeout=30 --maxfail=5
|
||||
# Run only CLI tests for smoke - fast and critical path
|
||||
python -m pytest tests/unit/cli/ \
|
||||
-m "not slow and not network" \
|
||||
-q --tb=line \
|
||||
--timeout=30 \
|
||||
--ignore=tests/fixtures \
|
||||
--ignore=tests/unit/cli/test_message_queue.py \
|
||||
--maxfail=5
|
||||
|
||||
- name: Standalone praisonai-code import smoke
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv /tmp/praisonai-code-standalone
|
||||
/tmp/praisonai-code-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-code
|
||||
/tmp/praisonai-code-standalone/bin/python -c "
|
||||
import praisonai_code
|
||||
from praisonai_code.cli.configuration.resolver import ConfigResolver
|
||||
from praisonai_code._version import get_package_version
|
||||
from praisonai_code.llm.credentials import is_configured
|
||||
from praisonai_code.llm.catalogue import ModelCatalogue
|
||||
import toml
|
||||
assert hasattr(ConfigResolver, '__name__')
|
||||
assert callable(is_configured)
|
||||
assert ModelCatalogue().list_models(provider='openai')
|
||||
import praisonai_code.cli.main
|
||||
import praisonai_code.cli.app
|
||||
from praisonai_code.tool_resolver import ToolResolver
|
||||
assert ToolResolver is not None
|
||||
print('standalone ok', get_package_version(), 'toml', toml.__version__)
|
||||
"
|
||||
/tmp/praisonai-code-standalone/bin/praisonai-code --version
|
||||
/tmp/praisonai-code-standalone/bin/praisonai-code run --help
|
||||
cd src/praisonai-code
|
||||
for f in praisonai_code/cli/main.py praisonai_code/cli/app.py \
|
||||
praisonai_code/cli/commands/run.py praisonai_code/cli/commands/chat.py \
|
||||
praisonai_code/cli/commands/code.py; do
|
||||
if grep -E '^from praisonai\.|^import praisonai' "$f"; then
|
||||
echo "FAIL: module-level wrapper import in $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "C7 hot-path import gate ok"
|
||||
bash "$GITHUB_WORKSPACE/scripts/check_c7_imports.sh"
|
||||
# Standalone run must not fail on wrapper ImportError (API auth failure is OK)
|
||||
set +e
|
||||
RUN_OUT=$(timeout 90 /tmp/praisonai-code-standalone/bin/praisonai-code run "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 run hit wrapper import error"
|
||||
exit 1
|
||||
fi
|
||||
echo "standalone run import gate ok (exit $RUN_CODE)"
|
||||
--maxfail=5 \
|
||||
|| echo "Some smoke tests failed"
|
||||
|
||||
- name: Smoke Test Summary
|
||||
if: always()
|
||||
@@ -148,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:
|
||||
@@ -174,38 +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/sandbox \
|
||||
--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'
|
||||
@@ -239,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
|
||||
@@ -262,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'
|
||||
@@ -312,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
|
||||
@@ -326,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'
|
||||
@@ -372,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
|
||||
@@ -386,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'
|
||||
@@ -432,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
|
||||
@@ -446,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'
|
||||
@@ -491,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
|
||||
@@ -505,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'
|
||||
@@ -550,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
|
||||
@@ -564,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'
|
||||
@@ -591,76 +535,6 @@ jobs:
|
||||
if: steps.check-key.outputs.has_key == 'false'
|
||||
run: echo "✅ Cohere tests skipped (no API key available)"
|
||||
|
||||
# ============================================
|
||||
# LEGACY UNIT TESTS - Post-C6 re-enabled suite
|
||||
# Nightly / extended tier only (~401 tests)
|
||||
# ============================================
|
||||
legacy-unit:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
with:
|
||||
agents-extras: ".[all]"
|
||||
wrapper-extras: ".[chat,code,crewai,autogen]"
|
||||
|
||||
- name: Run post-C6 legacy unit tests
|
||||
env:
|
||||
PRAISONAI_TEST_TIER: extended
|
||||
PRAISONAI_ALLOW_NETWORK: '0'
|
||||
PRAISONAI_ALLOW_JOB_WORKFLOWS: 'true'
|
||||
PRAISONAI_TOOL_SAFETY: 'off'
|
||||
OPENAI_API_KEY: 'test-key'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src/praisonai
|
||||
LEGACY_FILES=(
|
||||
tests/unit/test_warm_runtime.py
|
||||
tests/unit/test_yaml_cli_backend.py
|
||||
tests/unit/test_tool_resolver.py
|
||||
tests/unit/test_serve_unified.py
|
||||
tests/unit/test_security_fixes_core.py
|
||||
tests/unit/test_query_rewriter_cli.py
|
||||
tests/unit/test_kanban_sqlite_store.py
|
||||
tests/unit/test_job_workflow_agent_steps.py
|
||||
tests/unit/test_hybrid_workflow.py
|
||||
tests/unit/test_gateway_session.py
|
||||
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
|
||||
tests/unit/test_bot_wiring.py
|
||||
tests/unit/test_bot_history.py
|
||||
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
|
||||
# ============================================
|
||||
|
||||
@@ -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,14 +7,12 @@ on:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-windows.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-windows.yml'
|
||||
- '.github/actions/install-monorepo-packages/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -35,10 +33,18 @@ jobs:
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install monorepo packages
|
||||
run: python -m pip install --upgrade pip
|
||||
- name: Install praisonaiagents
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install -e .
|
||||
shell: bash
|
||||
|
||||
- name: Install praisonai wrapper
|
||||
run: |
|
||||
cd src/praisonai
|
||||
pip install -e .
|
||||
shell: bash
|
||||
- uses: ./.github/actions/install-monorepo-packages
|
||||
|
||||
# ==================== CORE IMPORTS ====================
|
||||
- name: Test core imports
|
||||
|
||||
@@ -22,7 +22,6 @@ other
|
||||
output
|
||||
!src/praisonai/praisonai/cli/output/
|
||||
!src/praisonai-agents/praisonaiagents/output/
|
||||
!src/praisonai-code/praisonai_code/cli/output/
|
||||
|
||||
.files
|
||||
threads.db
|
||||
@@ -89,9 +88,6 @@ issue_details.json
|
||||
.internal
|
||||
.praisonai
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
|
||||
# Rust / Cargo
|
||||
target/
|
||||
Cargo.lock
|
||||
|
||||
@@ -1,11 +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: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bot/channel and heavy implementations in `praisonai/`
|
||||
- Preserve old `praisonai.cli.*` import paths via shims when changing moved CLI code (see §2.3 in `src/praisonai-agents/AGENTS.md`)
|
||||
-535
@@ -1,535 +0,0 @@
|
||||
# PraisonAI Architecture
|
||||
|
||||
> **Last updated:** 2026-07-02 (v4.6.110 — C7.1 package boundaries)
|
||||
>
|
||||
> Strategic architecture document for PraisonAI — a multi-agent AI framework.
|
||||
> Covers **Python three-tier package model (C7.1)**, system design, runtime
|
||||
> architecture, data contracts, reliability, observability, and the road map.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#1-executive-summary)
|
||||
2. [Python Three-Tier Package Model (C7.1)](#2-python-three-tier-package-model-c71)
|
||||
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/C7_VERIFICATION.md`](src/praisonai/tests/C7_VERIFICATION.md) · [`src/praisonai/tests/C8_BACKLOG.md`](src/praisonai/tests/C8_BACKLOG.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 Three-Tier Package Model (C7.1)
|
||||
|
||||
**Release:** v4.6.110 · `praisonaiagents` 1.6.110 · `praisonai` 4.6.110 · `praisonai-code` 0.0.8
|
||||
|
||||
The Python monorepo publishes three tiers with strict dependency direction. C7
|
||||
delivered a standalone agentic hot path; C7.1 formalised ownership and import
|
||||
gates.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph tier1 [Tier 1 — Core SDK]
|
||||
Agents["praisonaiagents<br/>Agent, tools, memory, hooks, protocols"]
|
||||
end
|
||||
|
||||
subgraph tier2 [Tier 2 — Terminal CLI]
|
||||
Code["praisonai-code<br/>run, chat, code, Typer, runtime, LLM, tools"]
|
||||
end
|
||||
|
||||
subgraph tier3 [Tier 3 — Wrapper]
|
||||
Wrapper["praisonai<br/>gateway, bots, framework_adapters, train, serve"]
|
||||
end
|
||||
|
||||
Agents --> Code
|
||||
Agents --> Wrapper
|
||||
Code -.->|"lazy bridge only<br/>no PyPI dep"| Wrapper
|
||||
```
|
||||
|
||||
| Tier | Package | Owns | Must not depend on |
|
||||
|------|---------|------|-------------------|
|
||||
| 1 | `src/praisonai-agents/` | Agent, tools, memory, hooks, `frameworks/` protocols | `praisonai`, `praisonai-code` |
|
||||
| 2 | `src/praisonai-code/` | `run`/`chat`/`code`, Typer, runtime, LLM, tool resolution | **`praisonai` as a PyPI dependency** (optional lazy imports via `_wrapper_bridge` only) |
|
||||
| 3 | `src/praisonai/` | Gateway, bots, `framework_adapters/`, capabilities, train, serve | — |
|
||||
|
||||
**Publish order:** `praisonaiagents` → `praisonai-code` → `praisonai`
|
||||
|
||||
### 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:** 225 wrapper import lines max (`scripts/check_c7_imports.sh`)
|
||||
- **Allowlist:** reviewed files only (`scripts/c7_wrapper_import_allowlist.txt`)
|
||||
|
||||
### C7 / C7.1 status
|
||||
|
||||
| Milestone | Status |
|
||||
|-----------|--------|
|
||||
| Standalone agentic hot path | **Complete** |
|
||||
| `_wrapper_bridge` hardening | **Complete** |
|
||||
| Import gate + allowlist | **Complete** |
|
||||
| Boundary tests + CI smoke | **Complete** |
|
||||
| C8+ (zero lazy imports, package splits) | **Future** — see [`C8_BACKLOG.md`](src/praisonai/tests/C8_BACKLOG.md) |
|
||||
|
||||
---
|
||||
|
||||
## 3. System Overview
|
||||
|
||||
```
|
||||
User/SDK/CLI
|
||||
→ Workflow Compiler (graph + policy + schemas)
|
||||
→ Execution Orchestrator (state machine, retries, fallbacks)
|
||||
→ Tool Runtime (sandbox + approval + capability scope)
|
||||
→ Model Runtime (provider abstraction + rate/timeout policy)
|
||||
→ Observability Bus (trace events + metrics + cost)
|
||||
→ Replay Engine (checkpoint + deterministic re-run)
|
||||
→ Persistence (session, memory, artifacts, run ledger)
|
||||
```
|
||||
|
||||
### Runtime Components
|
||||
|
||||
| Component | Responsibility | Interface |
|
||||
|-----------|---------------|-----------|
|
||||
| API/CLI Gateway | Accepts run requests, validates config/profile | `RunRequest`, `RunProfile` |
|
||||
| Workflow Compiler | Converts YAML/DSL to normalized execution graph | `CompiledGraph` |
|
||||
| Orchestrator | Executes graph state machine with retries/fallbacks | `ExecutionState`, `StepTransition` |
|
||||
| Tool Runtime | Runs tools with approval/sandbox policies | `ToolCall`, `ToolResult` |
|
||||
| Model Runtime | Provider routing, timeout, budget control | `ModelRequest`, `ModelResponse` |
|
||||
| Observability Bus | Streams structured events + metrics | `RunEvent` schema |
|
||||
| Replay Engine | Restarts from checkpoint with deterministic inputs | `ReplayRequest` |
|
||||
|
||||
### Existing Architecture (Python SDK)
|
||||
|
||||
The Python SDK is organised as three publishable tiers (see [§2](#2-python-three-tier-package-model-c71)):
|
||||
|
||||
- `src/praisonai-agents/` — Core SDK (`Agent`, tools, memory, hooks, protocols)
|
||||
- `src/praisonai-code/` — Terminal CLI (`run`, `chat`, `code`, Typer, runtime, LLM)
|
||||
- `src/praisonai/` — Wrapper (gateway, bots, `framework_adapters/`, integrations)
|
||||
|
||||
Core execution modules live in `praisonaiagents`:
|
||||
|
||||
- `agent/` — Agent class, handoff, autonomy
|
||||
- `llm/` — Model runtime with provider routing, rate limiting, failover
|
||||
- `tools/` — Tool runtime with sandbox, approval, retry
|
||||
- `memory/` — Memory runtime (in-memory, SQLite, MongoDB, Mem0 adapters)
|
||||
- `knowledge/` — Knowledge management (indexing, retrieval, chunking)
|
||||
- `workflows/` — Workflow engine (YAML/SDK-based orchestration)
|
||||
- `hooks/`, `bus/` — Hook system and event bus
|
||||
- `mcp/` — MCP protocol support
|
||||
- `a2a/`, `a2ui/` — Agent-to-agent and agent-to-UI protocols (wrapper/SDK)
|
||||
|
||||
Wrapper-only surfaces remain in `src/praisonai/`:
|
||||
|
||||
- `gateway/`, `bots/` — Multi-bot orchestration (BotOS)
|
||||
- `framework_adapters/` — CrewAI, AutoGen, PraisonAI adapters
|
||||
- `cli/commands/` — Wrapper commands (`bot`, `gateway`, `pairing`, …)
|
||||
|
||||
### Multi-SDK Layout
|
||||
|
||||
- **Python Core SDK** — `src/praisonai-agents/` (`praisonaiagents`)
|
||||
- **Python Terminal CLI** — `src/praisonai-code/` (`praisonai-code`)
|
||||
- **Python Wrapper** — `src/praisonai/` (`praisonai`)
|
||||
- **TypeScript SDK** — JS/TS runtime (`ts-sdk/`)
|
||||
- **Rust SDK** — High-performance Rust runtime (`rust-sdk/`)
|
||||
- **UI** — Web UI (`ui/`)
|
||||
|
||||
---
|
||||
|
||||
## 4. Layered Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph UX["Interface Layer"]
|
||||
CLI[CLI]
|
||||
SDK[SDK]
|
||||
API[API Gateway]
|
||||
end
|
||||
|
||||
subgraph Control["Control Plane"]
|
||||
Compiler[Workflow Compiler]
|
||||
Policy[Policy Engine]
|
||||
Orchestrator[Execution Orchestrator]
|
||||
end
|
||||
|
||||
subgraph Runtime["Runtime Plane"]
|
||||
ModelRT[Model Runtime]
|
||||
ToolRT[Tool Runtime]
|
||||
MemoryRT[Memory Runtime]
|
||||
end
|
||||
|
||||
subgraph Observe["Observability Plane"]
|
||||
EventBus[Run Event Bus]
|
||||
Metrics[Metrics + Cost]
|
||||
Replay[Replay Engine]
|
||||
end
|
||||
|
||||
subgraph Data["Data Plane"]
|
||||
Ledger[Run Ledger]
|
||||
Checkpoints[Checkpoints]
|
||||
Artifacts[Artifacts Store]
|
||||
end
|
||||
|
||||
CLI --> API
|
||||
SDK --> API
|
||||
API --> Compiler
|
||||
API --> Policy
|
||||
Compiler --> Orchestrator
|
||||
Policy --> Orchestrator
|
||||
Orchestrator --> ModelRT
|
||||
Orchestrator --> ToolRT
|
||||
Orchestrator --> MemoryRT
|
||||
Orchestrator --> EventBus
|
||||
EventBus --> Metrics
|
||||
EventBus --> Replay
|
||||
Orchestrator --> Ledger
|
||||
Orchestrator --> Checkpoints
|
||||
ToolRT --> Artifacts
|
||||
Replay --> Checkpoints
|
||||
```
|
||||
|
||||
### Interface Layer
|
||||
|
||||
- **CLI** — The `praisonai` command-line entry point
|
||||
- **SDK** — Python library API (`from praisonai import Agent`)
|
||||
- **API Gateway** — HTTP/WebSocket REST API (`praisonai api` or `a2a`/`a2ui`)
|
||||
|
||||
### Control Plane
|
||||
|
||||
- **Workflow Compiler** — Converts YAML workflows and SDK-defined graphs into
|
||||
a normalized `CompiledGraph` with adjacency validation and cycle detection.
|
||||
- **Policy Engine** — Enforces runtime policies (budget, timeout, approval
|
||||
gates, capability validation) before and during execution.
|
||||
- **Execution Orchestrator** — Drives the state machine through graph nodes,
|
||||
handling retry, fallback, and failure classification.
|
||||
|
||||
### Runtime Plane
|
||||
|
||||
- **Model Runtime** — Provider abstraction layer. Routes to OpenAI, Anthropic,
|
||||
Gemini, Ollama, DeepSeek, etc. Handles rate limiting, token tracking, and
|
||||
automatic failover between providers.
|
||||
- **Tool Runtime** — Executes tool calls with configurable sandboxing,
|
||||
approval gates, retry policies, and capability scope validation.
|
||||
- **Memory Runtime** — Manages session memory, persistent memory stores,
|
||||
and knowledge retrieval across in-memory, SQLite, and MongoDB backends.
|
||||
|
||||
### Observability Plane
|
||||
|
||||
- **Run Event Bus** — Structured event streaming for all lifecycle transitions
|
||||
(START, INPUT, MODEL_CALL, TOOL_CALL, ERROR, RETRY, COMPLETE).
|
||||
- **Metrics + Cost** — Token counting, cost tracking, and performance metrics.
|
||||
- **Replay Engine** — Deterministic replay from the last stable checkpoint,
|
||||
with state hash verification.
|
||||
|
||||
### Data Plane
|
||||
|
||||
- **Run Ledger** — Persistent record of all runs with state, metrics, and
|
||||
failure classifications.
|
||||
- **Checkpoints** — Point-in-time snapshots of run state, memory, and
|
||||
artifacts for replay and recovery.
|
||||
- **Artifacts Store** — Tool outputs, generated files, and intermediate
|
||||
results.
|
||||
|
||||
---
|
||||
|
||||
## 5. Core Data Contracts
|
||||
|
||||
### RunRequest
|
||||
|
||||
```text
|
||||
RunRequest {
|
||||
run_id: UUID,
|
||||
workflow_ref: string,
|
||||
inputs: object,
|
||||
profile: {safe_mode, budget, timeout, approval_policy},
|
||||
context: {user_id, workspace_id, env}
|
||||
}
|
||||
```
|
||||
|
||||
### RunEvent
|
||||
|
||||
```text
|
||||
RunEvent {
|
||||
ts: ISO8601,
|
||||
run_id: UUID,
|
||||
step_id: string,
|
||||
type: START | INPUT | MODEL_CALL | TOOL_CALL |
|
||||
ERROR | RETRY | COMPLETE,
|
||||
payload: object,
|
||||
cost: {tokens_in, tokens_out, usd},
|
||||
latency_ms: number
|
||||
}
|
||||
```
|
||||
|
||||
### Checkpoint
|
||||
|
||||
```text
|
||||
Checkpoint {
|
||||
run_id: UUID,
|
||||
step_id: string,
|
||||
state_hash: string,
|
||||
memory_snapshot_ref: string,
|
||||
artifact_refs: string[]
|
||||
}
|
||||
```
|
||||
|
||||
### Execution Sequence
|
||||
|
||||
```text
|
||||
1) Client submits RunRequest
|
||||
2) Gateway validates profile + schema
|
||||
3) Compiler resolves workflow → CompiledGraph
|
||||
4) Orchestrator starts node execution
|
||||
5) Node may call Model Runtime or Tool Runtime
|
||||
6) Every transition emits RunEvent
|
||||
7) On failure: classifier decides retry/fallback/abort
|
||||
8) Checkpoint stored after each critical step
|
||||
9) Final state + artifacts persisted to run ledger
|
||||
10) Replay can resume from last stable checkpoint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Reliability by Design
|
||||
|
||||
### Error Taxonomy
|
||||
|
||||
All failures are classified into one of five categories for automatic
|
||||
remediation:
|
||||
|
||||
| Category | Description | Default Action |
|
||||
|----------|-------------|----------------|
|
||||
| `config` | Invalid configuration or profile | Abort with remediation hint |
|
||||
| `dependency` | Missing optional module or capability | Degrade / skip optional path |
|
||||
| `tool` | Tool execution failure | Retry with backoff and policy cap |
|
||||
| `model` | Model API error or timeout | Fallback to alternate provider/route |
|
||||
| `infra` | Network or resource exhaustion | Circuit-breaker + replay from checkpoint |
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S[Step Failure] --> T{Failure Type?}
|
||||
T -->|Config| C1[Abort + config remediation hint]
|
||||
T -->|Dependency| C2[Degrade capability / skip optional path]
|
||||
T -->|Tool| C3[Retry with backoff and policy cap]
|
||||
T -->|Model| C4[Fallback model/provider route]
|
||||
T -->|Infra| C5[Circuit-breaker + replay from checkpoint]
|
||||
```
|
||||
|
||||
### Hardening Checklist
|
||||
|
||||
- **Dependency gates** — Optional modules declared as capabilities;
|
||||
unavailable capability results in skip/degrade, not crash.
|
||||
- **Cross-platform adapters** — OS-specific implementations behind a single
|
||||
interface (e.g., file lock adapter for Windows/Linux/macOS).
|
||||
- **Idempotent fixtures** — Integration tests use unique tenant/workspace IDs
|
||||
and guaranteed teardown.
|
||||
- **Fail-safe output mode** — CLI rendering falls back to ASCII-safe mode on
|
||||
encoding mismatch.
|
||||
- **CI parity matrix** — Run smoke workflows across Windows, Linux, and macOS.
|
||||
|
||||
### Deterministic Test Pattern
|
||||
|
||||
```python
|
||||
# Integration test pattern — always use unique, traceable IDs
|
||||
import uuid
|
||||
|
||||
def test_agent_workflow():
|
||||
tenant_id = f"test-{uuid.uuid4().hex[:8]}"
|
||||
agent = Agent(name=f"agent-{tenant_id}", ...)
|
||||
result = agent.run("task")
|
||||
assert result.status == "success"
|
||||
# teardown is guaranteed via fixture or context manager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Observability & Telemetry
|
||||
|
||||
### Current Telemetry Stack
|
||||
|
||||
PraisonAI already includes:
|
||||
|
||||
- **OpenTelemetry integration** — Manual and auto-instrumentation for
|
||||
traces, metrics, and logs.
|
||||
- **Token tracking** — Per-call and cumulative token/cost tracking.
|
||||
- **Performance monitoring** — Real-time dashboards and monitoring views.
|
||||
- **LangTrace integration** — LangTrace provider for tracing.
|
||||
- **Run outcomes** — Structured `RunOutcome` objects with status, duration,
|
||||
token usage, and error classification.
|
||||
|
||||
### Planned Enhancements
|
||||
|
||||
- **Run Event Bus** — Stream all lifecycle events as structured `RunEvent`
|
||||
payloads.
|
||||
- **Failure classifier** — Automatic failure category detection with
|
||||
remediation hints.
|
||||
- **Run ledger visualization** — Timeline view of runs with step-by-step
|
||||
event drill-down.
|
||||
- **Replay integration** — Checkpoint-based run replay from the
|
||||
observability dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 8. Replay & Checkpointing
|
||||
|
||||
### Design
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User/Client
|
||||
participant G as Gateway
|
||||
participant C as Compiler
|
||||
participant O as Orchestrator
|
||||
participant R as Model/Tool Runtime
|
||||
participant E as Event Bus
|
||||
participant L as Run Ledger
|
||||
participant K as Checkpoint Store
|
||||
|
||||
U->>G: Submit RunRequest
|
||||
G->>C: Validate + compile workflow
|
||||
C-->>O: CompiledGraph
|
||||
O->>R: Execute next step
|
||||
R-->>O: Step output / error
|
||||
O->>E: Emit structured RunEvent
|
||||
O->>K: Persist checkpoint
|
||||
O->>L: Persist run state + metrics
|
||||
O-->>U: Final result / failure summary
|
||||
```
|
||||
|
||||
### Current State
|
||||
|
||||
- Basic run outcome persistence exists
|
||||
- Session persistence for bot/multi-turn conversations
|
||||
- Token and cost tracking per run
|
||||
|
||||
### Roadmap
|
||||
|
||||
- Full checkpoint creation at configurable step granularity
|
||||
- State hash verification for deterministic replay
|
||||
- Replay API endpoint (`POST /runs/{id}/replay`)
|
||||
- Checkpoint pruning and retention policy
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation Roadmap
|
||||
|
||||
### Completed (C7 / C7.1 — v4.6.110)
|
||||
|
||||
| Project | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| C7 hot path | Standalone `praisonai-code run/chat/code` without wrapper import | **Complete** |
|
||||
| C7.1 boundaries | Three-tier ownership, `_wrapper_bridge`, import gates | **Complete** |
|
||||
| CI parity | Smoke standalone block + pre-existing test fixes (#2560) | **Complete** |
|
||||
|
||||
### Quarter 1: Trust Foundation (Q3 2026)
|
||||
|
||||
| Priority | Project | Description | Status |
|
||||
|----------|---------|-------------|--------|
|
||||
| P0 | Reliability Core | Cross-platform hardening, optional-dep gating, deterministic fixtures | In progress |
|
||||
| P0 | Doctor Auto-Fix | Automated environment diagnosis and repairs | Planned |
|
||||
| P1 | Golden-Path CLI | Single canonical flow: init → run → test → deploy | Planned |
|
||||
| P1 | CI Parity Matrix | Cross-platform smoke tests in CI pipeline | Planned |
|
||||
|
||||
### Quarter 2: Operational Excellence (Q4 2026)
|
||||
|
||||
| Priority | Project | Description | Status |
|
||||
|----------|---------|-------------|--------|
|
||||
| P1 | Trace + Replay | Run timeline, checkpoint replay, failure classifier | Planned |
|
||||
| P1 | Failure Classifier | Automatic failure category detection with remediation | Planned |
|
||||
| P2 | Graph Studio UX | Visual deterministic orchestration editor/inspector | Planned |
|
||||
| P2 | Safe Production Profiles | Policy presets: guardrails, approvals, cost/time caps | Planned |
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title PraisonAI Architecture Program (2 Quarters)
|
||||
dateFormat YYYY-MM-DD
|
||||
section Quarter 1
|
||||
Reliability hardening + parity CI :a1, 2026-07-01, 45d
|
||||
Deterministic test isolation :a2, 2026-07-10, 50d
|
||||
Doctor Auto-Fix MVP :a3, 2026-07-20, 40d
|
||||
Golden-path CLI :a4, 2026-08-01, 45d
|
||||
section Quarter 2
|
||||
Trace bus + run ledger :b1, 2026-10-01, 45d
|
||||
Replay engine :b2, 2026-10-15, 45d
|
||||
Failure classifier :b3, 2026-11-01, 30d
|
||||
Graph UX beta + Safe profiles :b4, 2026-11-10, 50d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Success Metrics
|
||||
|
||||
### KPI Targets
|
||||
|
||||
| KPI | Target | Primary Workstream |
|
||||
|-----|--------|-------------------|
|
||||
| Time-to-first-successful run | 30-40% reduction | Golden-path CLI + Doctor Auto-Fix |
|
||||
| Cross-platform issue rate | 50% reduction | Reliability Core + adapter layer |
|
||||
| Incident triage time | 40% reduction | Trace + Replay + failure classifier |
|
||||
| CI confidence | Flaky test rate <2% | Deterministic tests + fixture isolation |
|
||||
|
||||
### Governance
|
||||
|
||||
- **Architecture Review Council** — Reviews runtime-contract changes
|
||||
- **Release Quality Gate** — Platform matrix + deterministic test thresholds
|
||||
- **Monthly telemetry review** — DX, reliability, adoption trends
|
||||
|
||||
|
||||
---
|
||||
|
||||
*This document is a living reference. Update as the architecture evolves.*
|
||||
@@ -568,7 +568,7 @@ praisonai agents.yaml
|
||||
| Query Rewriter Agent | [Example](examples/python/agents/query-rewriter-agent.py) | [📖](https://docs.praison.ai/docs/agents/query-rewriter) |
|
||||
| Native Web Search | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/docs/agents/websearch) |
|
||||
| Built-in Search Tools | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/docs/tools/tavily) |
|
||||
| Unified Web Search | [Example](examples/python/web_search_example.py) | [📖](https://docs.praison.ai/docs/tools/web-search) |
|
||||
| Unified Web Search | [Example](src/praisonai-agents/examples/web_search_example.py) | [📖](https://docs.praison.ai/docs/tools/web-search) |
|
||||
| Web Fetch (Anthropic) | [Example](examples/python/agents/web-fetch-agent.py) | [📖](https://docs.praison.ai/docs/features/model-capabilities) |
|
||||
|
||||
</details>
|
||||
@@ -770,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** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
-66
@@ -1,66 +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-platform` | [PyPI](https://pypi.org/project/praisonai-platform/) | `src/praisonai-platform/` |
|
||||
| `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.
|
||||
|
||||
## 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`
|
||||
@@ -18,7 +18,6 @@ Methods:
|
||||
* <code title="class HandoffResult">HandoffResult.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_outcome</a>(outcome: AgentRunOutcome, target_agent: Optional[str] = None, source_agent: Optional[str] = None, handoff_depth: int = 0) -> 'HandoffResult'</code>
|
||||
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">json</a>() -> Optional[str]</code>
|
||||
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">to_dict</a>() -> dict</code>
|
||||
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">is_multimodal</a>() -> bool</code>
|
||||
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
|
||||
# Agents
|
||||
@@ -31,7 +30,6 @@ from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepR
|
||||
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">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>
|
||||
@@ -61,9 +59,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">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>
|
||||
@@ -73,8 +68,6 @@ Methods:
|
||||
* <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">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">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>
|
||||
@@ -247,7 +240,6 @@ from praisonaiagents import Memory, MemoryBackend, MemoryConfig
|
||||
|
||||
Methods:
|
||||
|
||||
* <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>
|
||||
@@ -483,51 +475,29 @@ Methods:
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_scripts</a>(skill: LoadedSkill) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate</a>(skill: LoadedSkill) -> bool</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate_by_name</a>(name: str) -> bool</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_bundle</a>(manifest: 'BundleManifest') -> None</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill</a>(skill_path: str) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill_by_name</a>(name: str, skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">approve</a>(identifier: str) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">archive_skill</a>(name: str) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">bundle_names</a>() -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">bundles</a>() -> List['BundleManifest']</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">clear</a>() -> None</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">create</a>(name: str, content: str, category: Optional[str] = None, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">create_skill</a>(name: str, content: str, category: str = None, agent_created: bool = True, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">delete</a>(name: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">delete_skill</a>(name: str, hard: bool = False, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">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">discover_bundles</a>(skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> int</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">edit</a>(name: str, content: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">edit_skill</a>(name: str, content: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">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_bundle</a>(name: str) -> Optional['BundleManifest']</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_bundle_instructions</a>() -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_instructions</a>(name: str) -> Optional[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skill</a>(name: str) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skills_diagnostics</a>() -> Dict[str, ValidationResult]</code>
|
||||
* <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">list_archived_skills</a>() -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">list_pending</a>() -> List[dict]</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</a>(name: str, old_string: str, new_string: str, file_path: Optional[str] = None, replace_all: bool = False, propose: Optional[bool] = None) -> dict</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, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">reject</a>(identifier: str) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">remove_file</a>(name: str, file_path: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">remove_skill_file</a>(name: str, file_path: str, propose: Optional[bool] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">resolve</a>(selectors: List[str]) -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">restore_skill</a>(name: str, skill_dir: Optional[str] = None) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">rollback_skill</a>(name: str) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">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_file</a>(name: str, file_path: str, file_content: str, propose: Optional[bool] = None) -> dict</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, propose: Optional[bool] = None) -> dict</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">idle_days</a>() -> Optional[float]</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>
|
||||
|
||||
@@ -545,12 +515,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>
|
||||
@@ -559,7 +527,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
|
||||
|
||||
@@ -642,7 +609,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
|
||||
|
||||
@@ -731,7 +698,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, RetryBackoffConfig, __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:
|
||||
@@ -813,7 +780,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, CloudProvider, DB, Deploy, DeployConfig, DeployType, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, __version__, arun, run, run_integrated_gateway
|
||||
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, CloudProvider, DB, Deploy, DeployConfig, DeployType, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, PraisonAI, __version__, arun, run, run_integrated_gateway
|
||||
```
|
||||
|
||||
# CLI
|
||||
@@ -821,6 +788,24 @@ from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, CloudProv
|
||||
Methods:
|
||||
|
||||
* <code title="cli">praisonai <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai acp acp-main <a href="./src/praisonai/praisonai/cli/commands/acp.py">--help</a></code>
|
||||
* <code title="cli">praisonai 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>
|
||||
@@ -834,28 +819,110 @@ Methods:
|
||||
* <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>
|
||||
* <code title="cli">praisonai context export <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context grep <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context list <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context show <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context stats <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context tail <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai 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>
|
||||
* <code title="cli">praisonai docs report <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs run <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <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>
|
||||
* <code title="cli">praisonai examples report <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples run <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples run-all <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples stats <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai flow export <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
|
||||
* <code title="cli">praisonai flow flow-start <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
|
||||
* <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 doctor <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway gateway-callback <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway install <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway logs <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway mint-link <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway pause <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway reconnect <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway resume <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway stop <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway uninstall <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai identity import <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
|
||||
* <code title="cli">praisonai identity link <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
|
||||
* <code title="cli">praisonai identity list <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
|
||||
* <code title="cli">praisonai identity unlink <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
|
||||
* <code title="cli">praisonai 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>
|
||||
@@ -868,12 +935,227 @@ Methods:
|
||||
* <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>
|
||||
* <code title="cli">praisonai knowledge search <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai langextract render <a href="./src/praisonai/praisonai/cli/commands/langextract.py">--help</a></code>
|
||||
* <code title="cli">praisonai langextract view <a href="./src/praisonai/praisonai/cli/commands/langextract.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse config <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse connect <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse init <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse langfuse-start <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse sessions <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse show <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse status <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse stop <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <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 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 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>
|
||||
* <code title="cli">praisonai n8n preview <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
|
||||
* <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>
|
||||
* <code title="cli">praisonai profile query <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <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>
|
||||
* <code title="cli">praisonai rag query <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag serve <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai realtime realtime-main <a href="./src/praisonai/praisonai/cli/commands/realtime.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe apply <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe create <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe info <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe install <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe judge <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <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 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>
|
||||
* <code title="cli">praisonai replay delete <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
|
||||
* <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 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 restart <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 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
|
||||
|
||||
@@ -1019,8 +1301,8 @@ export { // Python parity additions
|
||||
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, ToolRegistry, ToolResult, ToolValidationError, createDelegator, createSubagentTool, createSubagentTools, createTool, getRegistry, getTool, registerTool, tool, validateTool } 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 { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, get_registry, get_tool, register_tool, resetToolsRegistry, validate_tool } from "./tools/registry";
|
||||
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 { // Classes
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.113" \
|
||||
"praisonai>=4.6.55" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.113" \
|
||||
"praisonai>=4.6.55" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.113" \
|
||||
"praisonai>=4.6.55" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ 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
|
||||
|
||||
@@ -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,372 +0,0 @@
|
||||
"""
|
||||
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,599 +0,0 @@
|
||||
"""
|
||||
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%}")
|
||||
@@ -214,13 +214,8 @@ def example_subagent_delegation():
|
||||
assert hasattr(agent, "delegate"), "Agent should have delegate method"
|
||||
assert callable(agent.delegate), "delegate should be callable"
|
||||
|
||||
# Create a subagent using the public profile API
|
||||
from praisonaiagents.agents.profiles import BUILTIN_PROFILES, get_profile
|
||||
explorer_profile = get_profile("explorer") if "explorer" in BUILTIN_PROFILES else None
|
||||
subagent = Agent(
|
||||
name="subagent_explorer",
|
||||
instructions=explorer_profile.system_prompt if explorer_profile else "You are an explorer assistant.",
|
||||
)
|
||||
# Check _create_subagent method
|
||||
subagent = agent._create_subagent("explorer")
|
||||
print(f"Created subagent: {subagent.name}")
|
||||
print(f"Subagent instructions: {subagent.instructions[:50]}...")
|
||||
|
||||
|
||||
@@ -6,18 +6,10 @@ It includes self-reflection for improving code quality and multiple specialized
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.tools import CodeInterpreter
|
||||
|
||||
def run_python_code(code: str) -> str:
|
||||
"""Execute Python code safely and return stdout or errors."""
|
||||
import io
|
||||
import contextlib
|
||||
try:
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
exec(code, {"__builtins__": __builtins__})
|
||||
return buf.getvalue() or "Executed successfully (no output)"
|
||||
except Exception as e:
|
||||
return f"Execution error: {e}"
|
||||
# Initialize code execution tool
|
||||
code_interpreter = CodeInterpreter()
|
||||
|
||||
# Custom code analysis tool
|
||||
def analyze_code_complexity(code: str) -> str:
|
||||
@@ -40,7 +32,9 @@ code_writer = Agent(
|
||||
goal="Write clean, efficient, and well-documented Python code",
|
||||
backstory="You are a senior Python developer with 10+ years of experience in writing production-quality code.",
|
||||
instructions="Write Python code that follows PEP 8 standards, includes proper error handling, and has clear documentation.",
|
||||
output="silent",
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
code_reviewer = Agent(
|
||||
@@ -58,7 +52,7 @@ code_executor = Agent(
|
||||
goal="Safely execute and test Python code",
|
||||
backstory="You are responsible for running code and reporting results or errors.",
|
||||
instructions="Execute the provided code safely and report the output or any errors encountered.",
|
||||
tools=[run_python_code]
|
||||
tools=[code_interpreter]
|
||||
)
|
||||
|
||||
# Example tasks
|
||||
@@ -93,7 +87,7 @@ def demonstrate_code_agent():
|
||||
workflow = AgentTeam(
|
||||
agents=[code_writer, code_reviewer, code_executor],
|
||||
tasks=[write_task, review_task, execute_task],
|
||||
process="sequential", output="silent"
|
||||
process="sequential", output="verbose"
|
||||
)
|
||||
|
||||
# Run the workflow
|
||||
@@ -112,8 +106,10 @@ general_code_agent = Agent(
|
||||
3. Explain code concepts
|
||||
4. Refactor code for better performance
|
||||
5. Add tests and documentation""",
|
||||
tools=[run_python_code, analyze_code_complexity],
|
||||
output="silent",
|
||||
tools=[code_interpreter, analyze_code_complexity],
|
||||
reflection=True,
|
||||
|
||||
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -41,25 +41,21 @@ def calculate_statistics(numbers: list) -> str:
|
||||
"""Calculate basic statistics for a list of numbers."""
|
||||
if not numbers:
|
||||
return "No numbers provided"
|
||||
try:
|
||||
nums = [float(x) for x in numbers]
|
||||
except (TypeError, ValueError):
|
||||
return "Error: all values must be numbers"
|
||||
|
||||
n = len(nums)
|
||||
mean = sum(nums) / n
|
||||
sorted_nums = sorted(nums)
|
||||
|
||||
n = len(numbers)
|
||||
mean = sum(numbers) / n
|
||||
sorted_nums = sorted(numbers)
|
||||
median = sorted_nums[n//2] if n % 2 else (sorted_nums[n//2-1] + sorted_nums[n//2]) / 2
|
||||
variance = sum((x - mean)**2 for x in nums) / n
|
||||
variance = sum((x - mean)**2 for x in numbers) / n
|
||||
std_dev = math.sqrt(variance)
|
||||
|
||||
|
||||
return f"""Statistics:
|
||||
- Count: {n}
|
||||
- Mean: {mean:.4f}
|
||||
- Median: {median:.4f}
|
||||
- Standard Deviation: {std_dev:.4f}
|
||||
- Min: {min(nums)}
|
||||
- Max: {max(nums)}"""
|
||||
- Min: {min(numbers)}
|
||||
- Max: {max(numbers)}"""
|
||||
|
||||
# Create specialized math agents
|
||||
calculation_agent = Agent(
|
||||
@@ -70,7 +66,8 @@ calculation_agent = Agent(
|
||||
instructions="Perform the requested calculations step by step, showing your work clearly.",
|
||||
tools=[basic_calculator, solve_quadratic, calculate_statistics],
|
||||
reflection=True,
|
||||
output="silent",
|
||||
|
||||
|
||||
)
|
||||
|
||||
problem_solver_agent = Agent(
|
||||
@@ -85,7 +82,8 @@ problem_solver_agent = Agent(
|
||||
4. Show each step of the solution
|
||||
5. Verify the answer makes sense""",
|
||||
reflection=True,
|
||||
output="silent",
|
||||
|
||||
|
||||
)
|
||||
|
||||
math_tutor_agent = Agent(
|
||||
@@ -93,8 +91,7 @@ math_tutor_agent = Agent(
|
||||
role="Mathematics tutor and explainer",
|
||||
goal="Explain mathematical concepts clearly and help users understand math",
|
||||
backstory="You are a patient math tutor who can explain concepts at any level from elementary to advanced.",
|
||||
instructions="Explain mathematical concepts using simple language, provide examples, and check for understanding.",
|
||||
output="silent",
|
||||
instructions="Explain mathematical concepts using simple language, provide examples, and check for understanding."
|
||||
)
|
||||
|
||||
# Example workflow for solving complex problems
|
||||
@@ -129,8 +126,7 @@ def solve_math_problem_workflow():
|
||||
workflow = AgentTeam(
|
||||
agents=[problem_solver_agent, calculation_agent, math_tutor_agent],
|
||||
tasks=[understand_task, solve_task, verify_task],
|
||||
process="sequential",
|
||||
output="silent",
|
||||
process="sequential", output="verbose"
|
||||
)
|
||||
|
||||
return workflow.start()
|
||||
@@ -152,7 +148,8 @@ math_assistant = Agent(
|
||||
Always show your work step by step.""",
|
||||
tools=[basic_calculator, solve_quadratic, calculate_statistics],
|
||||
reflection=True,
|
||||
output="silent",
|
||||
|
||||
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -67,11 +67,7 @@ def main():
|
||||
|
||||
# 4. Subagent delegation
|
||||
print("\n4. Subagent Delegation:")
|
||||
explorer_profile = get_profile("explorer") if "explorer" in BUILTIN_PROFILES else None
|
||||
subagent = Agent(
|
||||
name="subagent_explorer",
|
||||
instructions=explorer_profile.system_prompt if explorer_profile else "You are an explorer assistant.",
|
||||
)
|
||||
subagent = agent._create_subagent("explorer")
|
||||
print(f" Created subagent: {subagent.name}")
|
||||
print(f" Profile: explorer (read-only)")
|
||||
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Bot In-Flight Run Control
|
||||
|
||||
Demonstrates how to use SessionRunControl to provide better UX for bots
|
||||
during long-running agent operations. Shows busy feedback, pending message
|
||||
handling, and /stop command support.
|
||||
|
||||
This example shows the "before" and "after" behavior described in issue #1914.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
# Mock agent for demonstration
|
||||
class MockAgent:
|
||||
"""Simple mock agent that simulates long-running tasks."""
|
||||
|
||||
def __init__(self, name="assistant"):
|
||||
self.name = name
|
||||
self.chat_history = []
|
||||
self._interrupt_controller = None
|
||||
|
||||
def chat(self, prompt: str) -> str:
|
||||
"""Simulate a long-running chat response."""
|
||||
print(f"[Agent] Starting to process: '{prompt[:50]}...'")
|
||||
|
||||
# Simulate long processing with interrupt checking
|
||||
for i in range(30): # 3 second task (30 x 0.1s)
|
||||
time.sleep(0.1)
|
||||
|
||||
# Check for interruption
|
||||
if (self._interrupt_controller and
|
||||
hasattr(self._interrupt_controller, 'is_set') and
|
||||
self._interrupt_controller.is_set()):
|
||||
reason = getattr(self._interrupt_controller, 'reason', 'unknown')
|
||||
print(f"[Agent] ⚠️ Interrupted: {reason}")
|
||||
raise InterruptedError(f"Task cancelled: {reason}")
|
||||
|
||||
response = f"Completed analysis of: {prompt}"
|
||||
print(f"[Agent] ✅ Finished: {response}")
|
||||
return response
|
||||
|
||||
|
||||
async def demo_without_run_control():
|
||||
"""Demonstrate the old behavior (silent blocking)."""
|
||||
print("\n" + "="*60)
|
||||
print("🔴 BEFORE: Without Run Control (Silent Blocking)")
|
||||
print("="*60)
|
||||
|
||||
from praisonai.bots._session import BotSessionManager
|
||||
|
||||
# Standard session manager (no run control)
|
||||
session_mgr = BotSessionManager()
|
||||
agent = MockAgent("research-agent")
|
||||
user_id = "user123"
|
||||
|
||||
print("\n1. User sends long task:")
|
||||
print(" User: 'research quantum computing trends'")
|
||||
|
||||
# Start first task (this will run for 3 seconds)
|
||||
task1 = asyncio.create_task(
|
||||
session_mgr.chat(agent, user_id, "research quantum computing trends")
|
||||
)
|
||||
|
||||
# Simulate user sending follow-up messages during execution
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n2. User sends follow-up (this will block silently):")
|
||||
print(" User: 'actually focus on business impact'")
|
||||
|
||||
task2 = asyncio.create_task(
|
||||
session_mgr.chat(agent, user_id, "actually focus on business impact")
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n3. User tries to send /stop (this will also block):")
|
||||
print(" User: '/stop'")
|
||||
|
||||
task3 = asyncio.create_task(
|
||||
session_mgr.chat(agent, user_id, "/stop")
|
||||
)
|
||||
|
||||
print("\n⏳ Waiting for all messages to complete...")
|
||||
results = await asyncio.gather(task1, task2, task3)
|
||||
|
||||
print("\n📊 Results (all processed sequentially with no feedback):")
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f" {i}. {result}")
|
||||
|
||||
print("\n❌ Problems with this approach:")
|
||||
print(" - No immediate feedback on follow-up messages")
|
||||
print(" - No way to cancel long-running tasks")
|
||||
print(" - User doesn't know if bot is working or broken")
|
||||
|
||||
|
||||
async def demo_with_run_control():
|
||||
"""Demonstrate the new behavior with run control."""
|
||||
print("\n" + "="*60)
|
||||
print("🟢 AFTER: With Run Control (Better UX)")
|
||||
print("="*60)
|
||||
|
||||
from praisonai.bots._run_control import SessionRunControl
|
||||
from praisonai.bots._session import BotSessionManager
|
||||
from praisonai.bots._commands import handle_stop_command
|
||||
|
||||
# Session manager with run control
|
||||
run_control = SessionRunControl(
|
||||
busy_mode="queue",
|
||||
busy_ack_template="⏳ {action} — will process after current task finishes"
|
||||
)
|
||||
session_mgr = BotSessionManager(run_control=run_control)
|
||||
agent = MockAgent("research-agent")
|
||||
user_id = "user123"
|
||||
|
||||
print("\n1. User sends long task:")
|
||||
print(" User: 'research quantum computing trends'")
|
||||
|
||||
# Start first task
|
||||
task1 = asyncio.create_task(
|
||||
session_mgr.chat_with_run_control(agent, user_id, "research quantum computing trends")
|
||||
)
|
||||
|
||||
# Immediate follow-up messages get acknowledgments
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n2. User sends follow-up (gets immediate feedback):")
|
||||
print(" User: 'actually focus on business impact'")
|
||||
|
||||
result2 = await session_mgr.chat_with_run_control(
|
||||
agent, user_id, "actually focus on business impact"
|
||||
)
|
||||
print(f" Bot: {result2['response']}")
|
||||
print(f" Metadata: {result2['metadata']}")
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n3. User decides to stop current task:")
|
||||
print(" User: '/stop'")
|
||||
|
||||
stop_response = await handle_stop_command(user_id, run_control)
|
||||
print(f" Bot: {stop_response}")
|
||||
|
||||
# Wait for first task to complete (it should be cancelled)
|
||||
try:
|
||||
result1 = await task1
|
||||
print(f"\n📊 Task 1 result: {result1['response']}")
|
||||
print(f" Metadata: {result1['metadata']}")
|
||||
except asyncio.CancelledError:
|
||||
print("\n📊 Task 1 was cancelled as expected")
|
||||
|
||||
print("\n4. User sends fresh request after stopping:")
|
||||
print(" User: 'what's the weather like?'")
|
||||
|
||||
result4 = await session_mgr.chat_with_run_control(
|
||||
agent, user_id, "what's the weather like?"
|
||||
)
|
||||
print(f" Bot: {result4['response']}")
|
||||
|
||||
print("\n✅ Benefits of this approach:")
|
||||
print(" - Immediate feedback on all messages")
|
||||
print(" - Can cancel long-running tasks with /stop")
|
||||
print(" - Pending messages are queued and processed in order")
|
||||
print(" - User always knows what's happening")
|
||||
|
||||
|
||||
async def demo_interrupt_mode():
|
||||
"""Demonstrate interrupt mode."""
|
||||
print("\n" + "="*60)
|
||||
print("⚡ BONUS: Interrupt Mode (Cancel and Restart)")
|
||||
print("="*60)
|
||||
|
||||
from praisonai.bots._run_control import SessionRunControl
|
||||
from praisonai.bots._session import BotSessionManager
|
||||
|
||||
# Use interrupt mode instead of queue mode
|
||||
run_control = SessionRunControl(busy_mode="interrupt")
|
||||
session_mgr = BotSessionManager(run_control=run_control)
|
||||
agent = MockAgent("research-agent")
|
||||
user_id = "user123"
|
||||
|
||||
print("\n1. User starts long task:")
|
||||
print(" User: 'research renewable energy for 30 pages'")
|
||||
|
||||
task1 = asyncio.create_task(
|
||||
session_mgr.chat_with_run_control(agent, user_id, "research renewable energy for 30 pages")
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n2. User changes mind (interrupts and restarts):")
|
||||
print(" User: 'actually just summarize solar panel efficiency'")
|
||||
|
||||
result2 = await session_mgr.chat_with_run_control(
|
||||
agent, user_id, "actually just summarize solar panel efficiency"
|
||||
)
|
||||
print(f" Bot: {result2['response']}")
|
||||
|
||||
# Wait for original task (should be interrupted)
|
||||
try:
|
||||
result1 = await task1
|
||||
print(f"\n📊 Original task result: {result1['response']}")
|
||||
if result1['metadata'].get('interrupted'):
|
||||
print(" ✅ Original task was properly interrupted")
|
||||
except:
|
||||
print("\n📊 Original task was cancelled")
|
||||
|
||||
print("\n✅ Interrupt mode is great for:")
|
||||
print(" - When users frequently change direction")
|
||||
print(" - Interactive sessions where latest intent matters most")
|
||||
print(" - Real-time collaboration scenarios")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all demonstrations."""
|
||||
print("🚀 Bot In-Flight Run Control Demo")
|
||||
print("Solving the silent lock problem described in issue #1914")
|
||||
|
||||
# Show the problem
|
||||
await demo_without_run_control()
|
||||
|
||||
# Show the solution
|
||||
await demo_with_run_control()
|
||||
|
||||
# Show advanced feature
|
||||
await demo_interrupt_mode()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🎯 Summary")
|
||||
print("="*60)
|
||||
print("SessionRunControl solves the silent blocking problem by providing:")
|
||||
print("1. Immediate feedback for mid-run messages")
|
||||
print("2. /stop command support via InterruptController")
|
||||
print("3. Configurable policies (queue/interrupt/steer)")
|
||||
print("4. Run generation tracking to prevent race conditions")
|
||||
print("5. Pending message merging and ordering")
|
||||
print("\nThis makes bot interactions feel responsive and predictable!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Example third-party framework adapter (entry-point template)."""
|
||||
|
||||
from praisonaiagents.frameworks.base import BaseFrameworkAdapter
|
||||
|
||||
|
||||
class MyFrameworkAdapter(BaseFrameworkAdapter):
|
||||
name = "my_framework"
|
||||
install_hint = "pip install my-framework-bridge"
|
||||
requires_tools_extra = False
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return True
|
||||
|
||||
def run(
|
||||
self,
|
||||
config,
|
||||
llm_config,
|
||||
topic,
|
||||
*,
|
||||
tools_dict=None,
|
||||
agent_callback=None,
|
||||
task_callback=None,
|
||||
cli_config=None,
|
||||
) -> str:
|
||||
return f"my_framework ran: {topic}"
|
||||
@@ -1,14 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "my-framework-bridge"
|
||||
version = "0.0.1"
|
||||
dependencies = ["praisonaiagents"]
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["my_framework_adapter"]
|
||||
|
||||
[project.entry-points."praisonai.framework_adapters"]
|
||||
my_framework = "my_framework_adapter:MyFrameworkAdapter"
|
||||
@@ -1,201 +0,0 @@
|
||||
"""
|
||||
Prototype implementation of Gateway Server with backpressure and flow control.
|
||||
|
||||
This demonstrates how the bounded queues and slow consumer detection would be
|
||||
implemented in the wrapper layer (praisonai package) using the configuration
|
||||
from the core SDK (praisonaiagents).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# For the prototype, we'll define minimal config inline
|
||||
# from praisonaiagents.gateway import GatewayConfig
|
||||
|
||||
@dataclass
|
||||
class SessionConfig:
|
||||
max_inbox: int = 256
|
||||
|
||||
@dataclass
|
||||
class GatewayConfig:
|
||||
session_config: SessionConfig = field(default_factory=SessionConfig)
|
||||
max_buffered_bytes: int = 1024 * 1024 # 1MB default
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GatewaySession:
|
||||
"""Gateway session with bounded inbox for backpressure."""
|
||||
|
||||
_session_id: str
|
||||
_agent_id: str
|
||||
_client_id: Optional[str] = None
|
||||
_inbox: asyncio.Queue = field(default_factory=asyncio.Queue)
|
||||
_max_inbox: int = 256
|
||||
_is_executing: bool = False
|
||||
|
||||
def setup_inbox(self, max_inbox: int = 256) -> None:
|
||||
"""Set up the inbox with proper bounds."""
|
||||
self._max_inbox = max_inbox
|
||||
if max_inbox > 0:
|
||||
self._inbox = asyncio.Queue(maxsize=max_inbox)
|
||||
else:
|
||||
self._inbox = asyncio.Queue() # Unbounded for backward compat if 0
|
||||
|
||||
async def queue_message(self, message: str) -> bool:
|
||||
"""Queue a user message with backpressure handling.
|
||||
|
||||
Returns:
|
||||
True if message was queued, False if inbox is full.
|
||||
"""
|
||||
if self._max_inbox > 0:
|
||||
# Bounded queue - check if full
|
||||
try:
|
||||
self._inbox.put_nowait(message)
|
||||
return True
|
||||
except asyncio.QueueFull:
|
||||
return False
|
||||
else:
|
||||
# Unbounded queue for backward compatibility
|
||||
await self._inbox.put(message)
|
||||
return True
|
||||
|
||||
|
||||
class GatewayServer:
|
||||
"""Gateway server with flow control."""
|
||||
|
||||
def __init__(self, config: GatewayConfig):
|
||||
self.config = config
|
||||
self._sessions: Dict[str, GatewaySession] = {}
|
||||
self._clients: Dict[str, Any] = {} # client_id -> WebSocket
|
||||
|
||||
def create_session(self, session_id: str, agent_id: str, client_id: str) -> GatewaySession:
|
||||
"""Create a new session with bounded inbox."""
|
||||
session = GatewaySession(
|
||||
_session_id=session_id,
|
||||
_agent_id=agent_id,
|
||||
_client_id=client_id,
|
||||
)
|
||||
# Set up inbox with configured bounds
|
||||
session.setup_inbox(self.config.session_config.max_inbox)
|
||||
self._sessions[session_id] = session
|
||||
return session
|
||||
|
||||
async def handle_message(self, session: GatewaySession, message: str, client_id: str) -> str:
|
||||
"""Handle incoming message with inbox overflow protection."""
|
||||
# Try to queue the message
|
||||
queued = await session.queue_message(message)
|
||||
if not queued:
|
||||
# Inbox is full, send error response
|
||||
await self._send_to_client(
|
||||
client_id,
|
||||
{
|
||||
"type": "error",
|
||||
"code": "inbox_full",
|
||||
"message": "Message queue is full. Please wait for current messages to be processed."
|
||||
}
|
||||
)
|
||||
return "Inbox full - message rejected."
|
||||
|
||||
# Start processing if not already running
|
||||
if not session._is_executing:
|
||||
session._is_executing = True
|
||||
asyncio.create_task(self._drain_session_queue(session, client_id))
|
||||
return "Processing started."
|
||||
|
||||
return "Message queued."
|
||||
|
||||
async def _drain_session_queue(self, session: GatewaySession, client_id: str) -> None:
|
||||
"""Process all messages from the session's inbox queue."""
|
||||
try:
|
||||
while not session._inbox.empty():
|
||||
msg = await session._inbox.get()
|
||||
# Process message here (simplified for prototype)
|
||||
logger.info(f"Processing message for session {session._session_id}: {msg}")
|
||||
|
||||
# Simulate processing delay
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Send response to client (simplified)
|
||||
await self._send_to_client(
|
||||
client_id,
|
||||
{
|
||||
"type": "response",
|
||||
"message": f"Processed: {msg}"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing queue for session {session._session_id}: {e}")
|
||||
finally:
|
||||
session._is_executing = False
|
||||
|
||||
async def _send_to_client(self, client_id: str, data: Dict[str, Any]) -> None:
|
||||
"""Send data to client with slow consumer detection."""
|
||||
ws = self._clients.get(client_id)
|
||||
if ws:
|
||||
try:
|
||||
# Check for slow consumer (buffered bytes)
|
||||
if self.config.max_buffered_bytes > 0:
|
||||
# Try to get transport buffer size
|
||||
transport = getattr(ws, 'transport', None) or getattr(ws, '_transport', None)
|
||||
if transport and hasattr(transport, 'get_write_buffer_size'):
|
||||
buffered_bytes = transport.get_write_buffer_size()
|
||||
if buffered_bytes > self.config.max_buffered_bytes:
|
||||
# Check if this is a droppable event type
|
||||
event_type = data.get("type", "")
|
||||
droppable_types = {"presence", "typing", "status"}
|
||||
|
||||
if event_type in droppable_types:
|
||||
# Drop the message for slow consumers
|
||||
logger.warning(f"Dropping {event_type} event for slow consumer {client_id} (buffered: {buffered_bytes} bytes)")
|
||||
return
|
||||
else:
|
||||
# Critical event - close the connection
|
||||
logger.warning(f"Closing slow consumer {client_id} (buffered: {buffered_bytes} bytes > {self.config.max_buffered_bytes})")
|
||||
await ws.close(code=1013, reason="slow consumer")
|
||||
return
|
||||
|
||||
# Send the message
|
||||
await ws.send_json(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending to client {client_id}: {e}", exc_info=True)
|
||||
|
||||
|
||||
# Example usage demonstrating the flow control
|
||||
async def demo():
|
||||
"""Demonstrate the flow control implementation."""
|
||||
|
||||
# Create config with bounded inbox and slow consumer detection
|
||||
config = GatewayConfig()
|
||||
config.session_config.max_inbox = 10 # Small inbox for demo
|
||||
config.max_buffered_bytes = 1024 # 1KB for demo
|
||||
|
||||
# Create server and session
|
||||
server = GatewayServer(config)
|
||||
session = server.create_session("sess1", "agent1", "client1")
|
||||
|
||||
# Simulate filling up the inbox
|
||||
print(f"Inbox max size: {session._inbox.maxsize}")
|
||||
|
||||
# Fill the inbox
|
||||
for i in range(10):
|
||||
success = await session.queue_message(f"Message {i}")
|
||||
print(f"Queued message {i}: {success}")
|
||||
|
||||
# Try to add one more - should fail
|
||||
success = await session.queue_message("Overflow message")
|
||||
print(f"Overflow message queued: {success}")
|
||||
|
||||
# Process messages
|
||||
while not session._inbox.empty():
|
||||
msg = await session._inbox.get()
|
||||
print(f"Processing: {msg}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(demo())
|
||||
@@ -4,70 +4,43 @@ Basic handoff example demonstrating agent-to-agent delegation.
|
||||
This example shows how agents can hand off tasks to specialized agents.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import TypedDict, Optional
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Define typed payload contracts for each handoff
|
||||
class BillingPayload(BaseModel):
|
||||
account_id: str
|
||||
invoice_amount: Optional[float] = None
|
||||
issue_type: str = "billing"
|
||||
|
||||
class RefundPayload(TypedDict):
|
||||
transaction_id: str
|
||||
amount: float
|
||||
reason: str
|
||||
|
||||
class TechnicalPayload(BaseModel):
|
||||
error_code: Optional[str] = None
|
||||
device_info: str
|
||||
user_agent: str
|
||||
|
||||
# Create specialized agents that expect typed payloads from the triage
|
||||
# (In a real scenario, the agents would validate and use the payload.)
|
||||
# Create specialized agents
|
||||
billing_agent = Agent(
|
||||
name="Billing Agent",
|
||||
role="Billing Specialist",
|
||||
goal="Handle all billing-related inquiries and tasks using structured data",
|
||||
backstory="I am an expert in billing systems, payment processing, and invoice management.",
|
||||
input_payload_schema=BillingPayload # hypothetical but consistent with design
|
||||
goal="Handle all billing-related inquiries and tasks",
|
||||
backstory="I am an expert in billing systems, payment processing, and invoice management."
|
||||
)
|
||||
|
||||
refund_agent = Agent(
|
||||
name="Refund Agent",
|
||||
role="Refund Specialist",
|
||||
goal="Process refund requests and handle refund-related issues with validated data",
|
||||
backstory="I specialize in processing refunds, evaluating refund eligibility, and ensuring customer satisfaction.",
|
||||
input_payload_schema=RefundPayload
|
||||
goal="Process refund requests and handle refund-related issues",
|
||||
backstory="I specialize in processing refunds, evaluating refund eligibility, and ensuring customer satisfaction."
|
||||
)
|
||||
|
||||
technical_support_agent = Agent(
|
||||
name="Technical Support",
|
||||
role="Technical Support Specialist",
|
||||
goal="Resolve technical issues and provide technical assistance with structured context",
|
||||
backstory="I am skilled in troubleshooting technical problems and providing solutions.",
|
||||
input_payload_schema=TechnicalPayload
|
||||
goal="Resolve technical issues and provide technical assistance",
|
||||
backstory="I am skilled in troubleshooting technical problems and providing solutions."
|
||||
)
|
||||
|
||||
# Create a triage agent with typed handoffs
|
||||
# Create a triage agent with handoffs to specialized agents
|
||||
triage_agent = Agent(
|
||||
name="Triage Agent",
|
||||
role="Customer Service Triage",
|
||||
goal="Understand customer needs and route them to the appropriate specialist with a validated payload",
|
||||
goal="Understand customer needs and route them to the appropriate specialist",
|
||||
backstory="I analyze customer requests and direct them to the most suitable specialist for efficient resolution.",
|
||||
instructions="""Analyze the customer's request and determine which specialist can best help:
|
||||
- For billing questions, payment issues, or invoices, transfer to the Billing Agent
|
||||
- For refund requests or refund status inquiries, transfer to the Refund Agent
|
||||
- For technical problems or product issues, transfer to Technical Support
|
||||
|
||||
Always explain why you're transferring the customer before doing so.
|
||||
When handing off, populate the appropriate payload with extracted data.""",
|
||||
handoffs=[
|
||||
(billing_agent, BillingPayload),
|
||||
(refund_agent, RefundPayload),
|
||||
(technical_support_agent, TechnicalPayload)
|
||||
]
|
||||
Always explain why you're transferring the customer before doing so.""",
|
||||
handoffs=[billing_agent, refund_agent, technical_support_agent]
|
||||
)
|
||||
|
||||
# Example usage
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
"""
|
||||
Example: Using MuAPI with PraisonAI for image generation.
|
||||
|
||||
MuAPI (https://muapi.ai) provides unified access to 400+ generative media
|
||||
models — Flux, Midjourney, GPT-4o Image, Google Imagen, Seedream, Veo3,
|
||||
Kling, Wan, and more — through a single API.
|
||||
|
||||
This example creates a PraisonAI agent that generates images on demand
|
||||
using the MuAPI image generation API as a custom tool.
|
||||
|
||||
Setup:
|
||||
pip install praisonaiagents requests
|
||||
export MUAPI_API_KEY=your_key_here
|
||||
# Get an API key at https://muapi.ai/dashboard/api-keys
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import requests
|
||||
from praisonaiagents import Agent, Tool
|
||||
|
||||
|
||||
MUAPI_BASE_URL = "https://api.muapi.ai/api/v1"
|
||||
|
||||
ALLOWED_MODELS = {
|
||||
"flux-schnell", "flux-dev", "flux-kontext-dev", "flux-kontext-pro",
|
||||
"midjourney", "gpt4o", "imagen4", "imagen4-fast", "seedream",
|
||||
"hidream-fast", "hidream-dev", "reve", "ideogram", "hunyuan",
|
||||
}
|
||||
|
||||
|
||||
def generate_image(prompt: str, model: str = "flux-schnell") -> str:
|
||||
"""Generate an image using MuAPI and return the output URL.
|
||||
|
||||
Args:
|
||||
prompt: Text description of the image to generate.
|
||||
model: MuAPI model ID. Options include flux-schnell, flux-dev,
|
||||
midjourney, gpt4o, imagen4, seedream, hidream-fast, reve.
|
||||
|
||||
Returns:
|
||||
URL of the generated image.
|
||||
"""
|
||||
if model not in ALLOWED_MODELS:
|
||||
raise ValueError(
|
||||
f"Unsupported model '{model}'. Choose from: {sorted(ALLOWED_MODELS)}"
|
||||
)
|
||||
|
||||
api_key = os.environ.get("MUAPI_API_KEY", "")
|
||||
if not api_key:
|
||||
raise ValueError("MUAPI_API_KEY environment variable not set")
|
||||
|
||||
headers = {"x-api-key": api_key, "Content-Type": "application/json"}
|
||||
|
||||
# Submit the generation request
|
||||
submit_resp = requests.post(
|
||||
f"{MUAPI_BASE_URL}/{model}",
|
||||
headers=headers,
|
||||
json={"prompt": prompt},
|
||||
timeout=30,
|
||||
)
|
||||
submit_resp.raise_for_status()
|
||||
submit_data = submit_resp.json()
|
||||
request_id = submit_data.get("request_id")
|
||||
if not request_id:
|
||||
raise RuntimeError(f"MuAPI did not return a request_id: {submit_data}")
|
||||
|
||||
# Poll until completion
|
||||
for _ in range(120):
|
||||
time.sleep(3)
|
||||
poll_resp = requests.get(
|
||||
f"{MUAPI_BASE_URL}/predictions/{request_id}/result",
|
||||
headers={"x-api-key": api_key},
|
||||
timeout=15,
|
||||
)
|
||||
poll_resp.raise_for_status()
|
||||
data = poll_resp.json()
|
||||
status = data.get("status")
|
||||
if status == "completed":
|
||||
outputs = data.get("outputs") or []
|
||||
if not outputs:
|
||||
raise RuntimeError(f"Generation completed but returned no outputs: {data}")
|
||||
return outputs[0]
|
||||
if status in ("failed", "cancelled"):
|
||||
raise RuntimeError(f"Image generation {status}: {data.get('error', '')}")
|
||||
|
||||
raise TimeoutError("Image generation timed out after 6 minutes")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
image_tool = Tool(
|
||||
name="generate_image",
|
||||
description=(
|
||||
"Generate an image from a text description using MuAPI's 400+ model library. "
|
||||
"Supported models: flux-schnell (fast), flux-dev, midjourney, gpt4o, imagen4, "
|
||||
"seedream, hidream-fast, reve. Returns a URL to the generated image."
|
||||
),
|
||||
function=generate_image,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Image Generator",
|
||||
instructions=(
|
||||
"You are a creative image generation assistant. "
|
||||
"When asked to create an image, use the generate_image tool. "
|
||||
"Pick the best model for the task: flux-schnell for speed, "
|
||||
"midjourney for artistic quality, gpt4o for photorealism."
|
||||
),
|
||||
tools=[image_tool],
|
||||
)
|
||||
|
||||
# Example: generate a product photo
|
||||
response = agent.start(
|
||||
"Generate a photorealistic product photo of a sleek black coffee mug "
|
||||
"on a white marble table with soft morning light. Use the best model for photorealism."
|
||||
)
|
||||
print(response)
|
||||
|
||||
# Example: generate concept art
|
||||
response = agent.start(
|
||||
"Create a cyberpunk cityscape at night with neon reflections on wet streets. "
|
||||
"Use midjourney for artistic quality."
|
||||
)
|
||||
print(response)
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Simple test script for 24/7 Agent Scheduler.
|
||||
Simple test script for 24/7 Agent Scheduler
|
||||
|
||||
This script demonstrates the basic usage of AgentScheduler
|
||||
for running agents periodically.
|
||||
@@ -8,56 +8,42 @@ Usage:
|
||||
python simple_test.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../.."))
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..'))
|
||||
|
||||
from unittest.mock import Mock
|
||||
from praisonai.scheduler import AgentScheduler
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Create a mock agent
|
||||
#
|
||||
# The scheduler now supports both asynchronous (astart) and synchronous
|
||||
# (start) execution. This example exposes both methods so either dispatch
|
||||
# path can be exercised.
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
mock_agent = Mock(spec=["name", "start", "astart"])
|
||||
# Create a mock agent for testing
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
mock_agent.astart = AsyncMock(
|
||||
return_value="Test execution successful!"
|
||||
)
|
||||
mock_agent.start = Mock(
|
||||
return_value="Test execution successful!"
|
||||
)
|
||||
mock_agent.start = Mock(return_value="Test execution successful!")
|
||||
|
||||
# Create scheduler
|
||||
scheduler = AgentScheduler(
|
||||
agent=mock_agent,
|
||||
task="Test task execution",
|
||||
task="Test task execution"
|
||||
)
|
||||
|
||||
print("🤖 Testing AgentScheduler")
|
||||
print("=" * 60)
|
||||
|
||||
# Execute once
|
||||
# Test 1: Execute once
|
||||
print("\n✅ Test 1: Execute once")
|
||||
result = scheduler.execute_once()
|
||||
print(f"Result: {result}")
|
||||
|
||||
# Statistics
|
||||
# Test 2: Get stats
|
||||
print("\n✅ Test 2: Get statistics")
|
||||
stats = scheduler.get_stats()
|
||||
print(f"Stats: {stats}")
|
||||
|
||||
# Periodic execution
|
||||
# Test 3: Start scheduler (will run every 5 seconds)
|
||||
print("\n✅ Test 3: Start scheduler (*/5s)")
|
||||
scheduler.start("*/5s", run_immediately=True)
|
||||
|
||||
import time
|
||||
print("Running for 12 seconds...")
|
||||
time.sleep(12)
|
||||
|
||||
@@ -65,10 +51,11 @@ time.sleep(12)
|
||||
print("\n✅ Test 4: Stop scheduler")
|
||||
scheduler.stop()
|
||||
|
||||
# Final statistics
|
||||
# Final stats
|
||||
print("\n📊 Final Statistics:")
|
||||
for key, value in scheduler.get_stats().items():
|
||||
final_stats = scheduler.get_stats()
|
||||
for key, value in final_stats.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print("\n✅ All tests passed!")
|
||||
print("=" * 60)
|
||||
print("=" * 60)
|
||||
@@ -179,7 +179,7 @@ def main():
|
||||
|
||||
context = memory.build_context_for_task(
|
||||
task_descr="Explain AI alignment challenges",
|
||||
memory={"user_id": "quality_demo_user"},
|
||||
user_id="quality_demo_user",
|
||||
max_items=3
|
||||
)
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JIRA Agent Watch Example
|
||||
|
||||
This example demonstrates how to use the JIRA watch tools with PraisonAI agents.
|
||||
The agent can monitor JIRA issues and projects for changes, search for issues,
|
||||
and get detailed issue information.
|
||||
|
||||
Setup:
|
||||
1. Install JIRA library: pip install jira
|
||||
2. Set environment variables:
|
||||
- [email protected] (for cloud JIRA)
|
||||
- JIRA_API_TOKEN=your_api_token
|
||||
- Or use JIRA_USERNAME + JIRA_API_TOKEN for server JIRA
|
||||
|
||||
Usage:
|
||||
python jira_agent_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import jira_tools
|
||||
|
||||
|
||||
def main():
|
||||
"""Demonstrate JIRA agent watch capabilities."""
|
||||
|
||||
# Check if JIRA credentials are available
|
||||
jira_email = os.getenv('JIRA_EMAIL')
|
||||
jira_token = os.getenv('JIRA_API_TOKEN')
|
||||
jira_username = os.getenv('JIRA_USERNAME')
|
||||
|
||||
if not jira_token:
|
||||
print("❌ JIRA_API_TOKEN environment variable not set")
|
||||
print("Please set your JIRA API token:")
|
||||
print("export JIRA_API_TOKEN='your_token_here'")
|
||||
return
|
||||
|
||||
if not (jira_email or jira_username):
|
||||
print("❌ JIRA credentials incomplete")
|
||||
print("Please set either:")
|
||||
print("- JIRA_EMAIL (for cloud JIRA)")
|
||||
print("- JIRA_USERNAME (for server JIRA)")
|
||||
return
|
||||
|
||||
# Create agent with JIRA tools
|
||||
agent = Agent(
|
||||
name="JIRA Monitor Agent",
|
||||
instructions="""
|
||||
You are a JIRA monitoring agent. You can:
|
||||
|
||||
1. Watch specific JIRA issues for changes
|
||||
2. Monitor JIRA projects for new issues and updates
|
||||
3. Search for issues using JQL queries
|
||||
4. Get detailed information about specific issues
|
||||
|
||||
When using JIRA tools, always provide the full JIRA URL
|
||||
(e.g., https://yourcompany.atlassian.net).
|
||||
|
||||
Be helpful and provide clear summaries of JIRA activity.
|
||||
""",
|
||||
tools=jira_tools(), # Add all JIRA tools
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("🎯 JIRA Agent Watch Example")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print("Available JIRA tools:")
|
||||
print("- jira_watch_issue: Monitor a specific issue for changes")
|
||||
print("- jira_watch_project: Monitor a project for new/updated issues")
|
||||
print("- jira_get_issue_info: Get detailed info about an issue")
|
||||
print("- jira_search_issues: Search issues using JQL")
|
||||
print()
|
||||
|
||||
# Example interactions
|
||||
example_prompts = [
|
||||
"Get information about JIRA issue DEMO-1 from https://praisonai.atlassian.net",
|
||||
"Search for open issues in project DEMO from https://praisonai.atlassian.net using JQL: 'project = DEMO AND status = Open'",
|
||||
"Watch JIRA issue DEMO-1 from https://praisonai.atlassian.net for 2 minutes (check every 60 seconds, max 2 checks)",
|
||||
]
|
||||
|
||||
print("📝 Example prompts you can try:")
|
||||
for i, prompt in enumerate(example_prompts, 1):
|
||||
print(f"{i}. {prompt}")
|
||||
print()
|
||||
|
||||
# Interactive mode
|
||||
print("💬 Interactive JIRA Agent (type 'quit' to exit)")
|
||||
print("-" * 50)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n🧑 You: ").strip()
|
||||
|
||||
if user_input.lower() in ['quit', 'exit', 'q']:
|
||||
print("👋 Goodbye!")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
print("🤖 Agent: ", end="", flush=True)
|
||||
response = agent.start(user_input)
|
||||
print(response)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Goodbye!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,6 +6,7 @@ to different next steps based on output content.
|
||||
"""
|
||||
|
||||
from praisonaiagents import AgentFlow, Task
|
||||
from praisonaiagents import AgentFlowManager
|
||||
|
||||
# Create a workflow with branching
|
||||
workflow = AgentFlow(
|
||||
@@ -35,9 +36,16 @@ workflow = AgentFlow(
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
|
||||
# Create manager and register workflow
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Decision Workflow"] = workflow
|
||||
|
||||
# Execute - will branch to success_handler since 42 is positive
|
||||
result = manager.execute(
|
||||
"Decision Workflow",
|
||||
default_llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("Workflow completed!")
|
||||
for step in result.get("steps", []):
|
||||
name = step.get("step", step.get("name", "unknown"))
|
||||
status = step.get("status", "completed")
|
||||
print(f" {name}: {status}")
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
@@ -6,8 +6,9 @@ functions that return StepResult with stop_workflow=True.
|
||||
"""
|
||||
|
||||
from praisonaiagents import (
|
||||
AgentFlow, Task, WorkflowContext, StepResult
|
||||
Workflow, Task, WorkflowContext, StepResult
|
||||
)
|
||||
from praisonaiagents import AgentFlowManager
|
||||
|
||||
# Custom validator that can stop the workflow
|
||||
def validate_data(context: WorkflowContext) -> StepResult:
|
||||
@@ -49,13 +50,20 @@ workflow = AgentFlow(
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Data Processing"] = workflow
|
||||
|
||||
# Test with invalid data (will stop early)
|
||||
print("=== Testing with invalid data ===")
|
||||
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
|
||||
for step in result.get("steps", []):
|
||||
print(f" {step.get('step', 'step')}: {step.get('output', step.get('status'))}")
|
||||
|
||||
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
|
||||
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['output']}")
|
||||
|
||||
# Test with valid data (will complete)
|
||||
print("\n=== Testing with valid data ===")
|
||||
workflow.variables["data"] = {"value": 42}
|
||||
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
|
||||
for step in result.get("steps", []):
|
||||
print(f" {step.get('step', 'step')}: {step.get('status', 'completed')}")
|
||||
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
|
||||
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
@@ -6,6 +6,7 @@ Each item is processed individually with the loop variable.
|
||||
"""
|
||||
|
||||
from praisonaiagents import AgentFlow, Task
|
||||
from praisonaiagents import AgentFlowManager
|
||||
|
||||
# Create a workflow with loop
|
||||
workflow = AgentFlow(
|
||||
@@ -29,12 +30,18 @@ workflow = AgentFlow(
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
|
||||
# Create manager and register workflow
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Process Items"] = workflow
|
||||
|
||||
# Execute - will process each fruit in the list
|
||||
result = manager.execute(
|
||||
"Process Items",
|
||||
default_llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("Workflow completed!")
|
||||
for step in result.get("steps", []):
|
||||
name = step.get("step", step.get("name", "unknown"))
|
||||
status = step.get("status", "completed")
|
||||
print(f" {name}: {status}")
|
||||
out = step.get("output")
|
||||
if isinstance(out, list):
|
||||
print(f" Loop processed {len(out)} items")
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
if isinstance(step_result.get("output"), list):
|
||||
print(f" Loop processed {len(step_result['output'])} items")
|
||||
@@ -5,7 +5,7 @@ Demonstrates running multiple agents concurrently and
|
||||
combining their results with an aggregator agent.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, AgentFlow
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents.workflows import parallel
|
||||
|
||||
# Create parallel research agents
|
||||
|
||||
@@ -5,7 +5,7 @@ Demonstrates decision-based routing where an agent classifier
|
||||
routes to specialized handler agents based on the request type.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, AgentFlow
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents.workflows import route
|
||||
|
||||
# Create classifier agent
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
"""Use Xquik tweet search as a PraisonAI custom tool.
|
||||
|
||||
Set XQUIK_API_KEY before running this example. The tool calls the public
|
||||
Xquik tweet search endpoint and returns compact tweet summaries for the agent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from praisonaiagents import Agent, tool
|
||||
|
||||
|
||||
XQUIK_SEARCH_URL = "https://xquik.com/api/v1/x/tweets/search"
|
||||
|
||||
|
||||
def _xquik_api_key_available() -> tuple[bool, str]:
|
||||
return bool(os.environ.get("XQUIK_API_KEY")), "Set XQUIK_API_KEY before using search_x_posts."
|
||||
|
||||
|
||||
def _as_record(value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
return {}
|
||||
|
||||
|
||||
def _as_text(value: Any) -> str:
|
||||
if isinstance(value, str):
|
||||
return " ".join(value.split())
|
||||
if value is None:
|
||||
return ""
|
||||
return " ".join(str(value).split())
|
||||
|
||||
|
||||
def _tweet_line(tweet: dict[str, Any]) -> str:
|
||||
author_record = _as_record(tweet.get("author") or tweet.get("user"))
|
||||
author = _as_text(
|
||||
tweet.get("author_username")
|
||||
or tweet.get("username")
|
||||
or author_record.get("username")
|
||||
or author_record.get("screen_name")
|
||||
or author_record.get("handle")
|
||||
)
|
||||
text = _as_text(tweet.get("text") or tweet.get("full_text") or tweet.get("content"))
|
||||
tweet_id = _as_text(tweet.get("id") or tweet.get("tweet_id") or tweet.get("tweetId"))
|
||||
prefix = f"@{author}: " if author else ""
|
||||
suffix = f" ({tweet_id})" if tweet_id else ""
|
||||
return f"{prefix}{text}{suffix}".strip()
|
||||
|
||||
|
||||
def _bounded_limit(value: Any) -> int:
|
||||
try:
|
||||
parsed = int(float(value))
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return 5
|
||||
return max(1, min(parsed, 10))
|
||||
|
||||
|
||||
@tool(availability=_xquik_api_key_available)
|
||||
def search_x_posts(query: str, limit: int = 5) -> str:
|
||||
"""Search recent public X posts with Xquik."""
|
||||
api_key = os.environ.get("XQUIK_API_KEY")
|
||||
if not api_key:
|
||||
return "Set XQUIK_API_KEY before using search_x_posts."
|
||||
|
||||
query_text = _as_text(query)
|
||||
if not query_text:
|
||||
return "Provide a non-empty query before using search_x_posts."
|
||||
|
||||
bounded_limit = _bounded_limit(limit)
|
||||
params = {"q": query_text, "limit": bounded_limit, "queryType": "Latest"}
|
||||
url = f"{XQUIK_SEARCH_URL}?{urlencode(params)}"
|
||||
request = Request(
|
||||
url,
|
||||
headers={"X-API-Key": api_key, "Accept": "application/json"},
|
||||
)
|
||||
|
||||
try:
|
||||
with urlopen(request, timeout=20) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
except HTTPError as error:
|
||||
return f"Xquik returned HTTP {error.code}."
|
||||
except (URLError, TimeoutError, json.JSONDecodeError) as error:
|
||||
return f"Xquik search failed: {error.__class__.__name__}."
|
||||
|
||||
tweets = _as_record(payload).get("tweets")
|
||||
if not isinstance(tweets, list) or not tweets:
|
||||
return "No posts found."
|
||||
|
||||
lines = [_tweet_line(_as_record(tweet)) for tweet in tweets[:bounded_limit]]
|
||||
return "\n".join(line for line in lines if line) or "No readable posts found."
|
||||
|
||||
|
||||
agent = Agent(
|
||||
instructions="You find recent public X posts and summarize the useful patterns.",
|
||||
tools=[search_x_posts],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent.start("Search X for recent AI agent framework discussions.")
|
||||
@@ -1,5 +0,0 @@
|
||||
# Files allowed to contain lazy ``from praisonai.*`` imports in praisonai-code.
|
||||
# C8 complete — reverse imports eliminated; bridge-only access remains.
|
||||
# Regenerate counts: bash scripts/check_c7_imports.sh
|
||||
#
|
||||
# If a new direct import is unavoidable, add path below after Architecture Review.
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert direct ``from praisonai`` imports to bridge calls in a file."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Match: from praisonai.foo.bar import A, B
|
||||
FROM_IMPORT = re.compile(
|
||||
r"^(\s*)from praisonai(\.[\w.]+)? import (.+)$"
|
||||
)
|
||||
# Match: from praisonai import foo
|
||||
FROM_ROOT = re.compile(
|
||||
r"^(\s*)from praisonai import (.+)$"
|
||||
)
|
||||
|
||||
|
||||
def _convert_from_line(line: str) -> str | None:
|
||||
m = FROM_ROOT.match(line)
|
||||
if m:
|
||||
indent, names = m.groups()
|
||||
mod = "praisonai"
|
||||
return (
|
||||
f"{indent}from praisonai_code._wrapper_bridge import import_wrapper_module\n"
|
||||
f"{indent}_mod = import_wrapper_module({mod!r})\n"
|
||||
f"{indent}({names}) = tuple(getattr(_mod, n.strip()) for n in {names!r}.split(','))"
|
||||
)
|
||||
m = FROM_IMPORT.match(line)
|
||||
if not m:
|
||||
return None
|
||||
indent, suffix, names = m.groups()
|
||||
mod = f"praisonai{suffix or ''}"
|
||||
# Handle ``import x as y``
|
||||
parts = [p.strip() for p in names.split(",")]
|
||||
assigns = []
|
||||
for part in parts:
|
||||
if " as " in part:
|
||||
orig, alias = part.split(" as ", 1)
|
||||
orig, alias = orig.strip(), alias.strip()
|
||||
assigns.append(f"{alias} = getattr(_mod, {orig!r})")
|
||||
else:
|
||||
assigns.append(f"{part} = getattr(_mod, {part!r})")
|
||||
body = "\n".join(f"{indent}{a}" for a in assigns)
|
||||
return (
|
||||
f"{indent}from praisonai_code._wrapper_bridge import import_wrapper_module\n"
|
||||
f"{indent}_mod = import_wrapper_module({mod!r})\n"
|
||||
f"{body}"
|
||||
)
|
||||
|
||||
|
||||
def convert_file(path: Path) -> int:
|
||||
text = path.read_text()
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
changed = 0
|
||||
for line in lines:
|
||||
if line.strip().startswith("#") and "from praisonai" in line:
|
||||
out.append(line)
|
||||
continue
|
||||
if "from praisonai" in line and "praisonai_code" not in line and "praisonaiagents" not in line:
|
||||
new = _convert_from_line(line)
|
||||
if new:
|
||||
out.append(new)
|
||||
changed += 1
|
||||
continue
|
||||
out.append(line)
|
||||
if changed:
|
||||
path.write_text("\n".join(out) + ("\n" if text.endswith("\n") else ""))
|
||||
return changed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
total = 0
|
||||
for p in sys.argv[1:]:
|
||||
n = convert_file(Path(p))
|
||||
print(f"{p}: {n} lines converted")
|
||||
total += n
|
||||
print(f"total: {total}")
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""C8 repatriation helper — move impl from praisonai-code to praisonai wrapper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
CODE = REPO / "src/praisonai-code/praisonai_code"
|
||||
WRAP = REPO / "src/praisonai/praisonai"
|
||||
|
||||
|
||||
def repatriate_file(code_rel: str, wrap_rel: str | None = None) -> None:
|
||||
"""Copy code file to wrapper and delete code copy."""
|
||||
wrap_rel = wrap_rel or code_rel
|
||||
src = CODE / code_rel
|
||||
dst = WRAP / wrap_rel
|
||||
if not src.exists():
|
||||
print(f"SKIP missing: {src}", file=sys.stderr)
|
||||
return
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
src.unlink()
|
||||
print(f"repatriated {code_rel} -> {wrap_rel}")
|
||||
|
||||
|
||||
def repatriate_command(name: str) -> None:
|
||||
repatriate_file(f"cli/commands/{name}.py")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("items", nargs="+", help="command names or code-relative paths")
|
||||
parser.add_argument("--feature", action="store_true", help="path under cli/features/")
|
||||
args = parser.parse_args()
|
||||
for item in args.items:
|
||||
if args.feature:
|
||||
repatriate_file(f"cli/features/{item}")
|
||||
elif "/" in item:
|
||||
repatriate_file(item)
|
||||
else:
|
||||
repatriate_command(item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# C7 import-direction gate for praisonai-code (standalone hot path).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${1:-src/praisonai-code/praisonai_code}"
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# Module-level hot path: praisonai wrapper only (not praisonaiagents / praisonai_code).
|
||||
HOT_PATH_RE='^from praisonai([[:space:]]|\.|$)|^import praisonai([[:space:]]|\.|$)'
|
||||
# Line-level wrapper imports (exclude praisonaiagents, praisonai_code, praisonai-tools).
|
||||
# Count indented bare ``import praisonai`` as well (C8.1 regex fix).
|
||||
ANY_WRAPPER_RE='(^[[:space:]]*from praisonai([[:space:]]|\.)|^[[:space:]]*import praisonai($|\.))'
|
||||
ALLOWLIST="scripts/c7_wrapper_import_allowlist.txt"
|
||||
|
||||
echo "== C7 hot-path module-level gate =="
|
||||
for f in \
|
||||
"$ROOT/cli/main.py" \
|
||||
"$ROOT/cli/app.py" \
|
||||
"$ROOT/cli/commands/run.py" \
|
||||
"$ROOT/cli/commands/chat.py" \
|
||||
"$ROOT/cli/commands/code.py"
|
||||
do
|
||||
if grep -E "$HOT_PATH_RE" "$f" 2>/dev/null; then
|
||||
echo "FAIL: module-level wrapper import in $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "hot-path module-level gate ok"
|
||||
|
||||
echo "== C7 strict gate (run/chat/code/app — no wrapper imports) =="
|
||||
for f in \
|
||||
"$ROOT/cli/app.py" \
|
||||
"$ROOT/cli/commands/run.py" \
|
||||
"$ROOT/cli/commands/chat.py" \
|
||||
"$ROOT/cli/commands/code.py"
|
||||
do
|
||||
if grep -E "$ANY_WRAPPER_RE" "$f" 2>/dev/null; then
|
||||
echo "FAIL: wrapper import in $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "strict hot-path gate ok"
|
||||
|
||||
echo "== C7 regression baseline (total wrapper import lines) =="
|
||||
BASELINE="${C8_WRAPPER_IMPORT_BASELINE:-${C7_WRAPPER_IMPORT_BASELINE:-50}}"
|
||||
# Count wrapper import lines with a portable tool. Prefer ripgrep when present
|
||||
# (fast, respects the same regex) and fall back to POSIX grep -r otherwise, so
|
||||
# the gate does not exit 127 on runners without rg. The `|| true` keeps a
|
||||
# no-match (grep/rg exit 1) from aborting the script under `set -o pipefail`.
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
COUNT="$( { rg -c "$ANY_WRAPPER_RE" "$ROOT" --glob '*.py' 2>/dev/null || true; } | awk -F: '{s+=$2} END {print s+0}')"
|
||||
else
|
||||
COUNT="$( { grep -rEc --include='*.py' "$ANY_WRAPPER_RE" "$ROOT" 2>/dev/null || true; } | awk -F: '{s+=$2} END {print s+0}')"
|
||||
fi
|
||||
echo "wrapper import lines: $COUNT (baseline $BASELINE)"
|
||||
if [ "$COUNT" -gt "$BASELINE" ]; then
|
||||
echo "FAIL: wrapper import count regressed above baseline ($COUNT > $BASELINE)"
|
||||
exit 1
|
||||
fi
|
||||
echo "regression baseline ok"
|
||||
|
||||
echo "== C7 allowlist enforcement =="
|
||||
if [ -f "$ALLOWLIST" ]; then
|
||||
# Files (relative to $ROOT) that carry wrapper imports.
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
OFFENDERS="$( { rg -l "$ANY_WRAPPER_RE" "$ROOT" --glob '*.py' 2>/dev/null || true; } )"
|
||||
else
|
||||
OFFENDERS="$( { grep -rlE --include='*.py' "$ANY_WRAPPER_RE" "$ROOT" 2>/dev/null || true; } )"
|
||||
fi
|
||||
# Strip the reasons/comments and blank/comment lines from the allowlist.
|
||||
ALLOWED="$(sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$ALLOWLIST" | sed -E 's/[[:space:]]+$//')"
|
||||
FAIL=0
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
rel="${f#"$ROOT/"}"
|
||||
if ! printf '%s\n' "$ALLOWED" | grep -Fxq "$rel"; then
|
||||
echo "FAIL: non-allowlisted wrapper import in $rel (add to $ALLOWLIST after review)"
|
||||
FAIL=1
|
||||
fi
|
||||
done <<< "$OFFENDERS"
|
||||
if [ "$FAIL" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "allowlist enforcement ok"
|
||||
else
|
||||
echo "allowlist file missing: $ALLOWLIST (skipping enforcement)"
|
||||
fi
|
||||
@@ -539,7 +539,7 @@ Repeat Phases 12 → 13 → 14 until the mergeability gate in Phase 13 is fully
|
||||
|
||||
### When the PR has merge conflicts
|
||||
|
||||
If `mergeStateStatus=DIRTY`, the **Auto Merge Conflict → Claude** workflow (`.github/workflows/merge-conflict-claude.yml`) posts `@claude` automatically on push to `main` and on PR sync. You can still comment manually:
|
||||
If `mergeStateStatus=DIRTY`, ask Claude to rebase rather than resolving conflicts yourself on non-trivial logic:
|
||||
|
||||
```bash
|
||||
gh pr comment $PR --repo $REPO --body "@claude this PR has merge conflicts with \`main\` (PR #<X> was merged, touching the same regions). Please rebase onto latest \`main\`, re-run \`pytest <paths>\`, and force-push."
|
||||
@@ -571,17 +571,6 @@ gh pr diff $PR --repo $REPO | less
|
||||
|
||||
### 15b. Merge
|
||||
|
||||
**Primary:** Trigger or wait for **Claude PR merge gate** (`.github/workflows/claude-merge-gate.yml`):
|
||||
|
||||
```bash
|
||||
gh workflow run claude-merge-gate.yml --repo $REPO -f pr_number=$PR
|
||||
gh pr view $PR --repo $REPO --json state,mergedAt,labels
|
||||
```
|
||||
|
||||
The gate merges when `MERGE_GATE_VERDICT: APPROVE`, CI is green, and the PR lacks `no-auto-merge`. Do not merge while `claude-conflict-pending` or during active `@claude` review/fix.
|
||||
|
||||
**Manual fallback** (opt-out or gate unavailable):
|
||||
|
||||
```bash
|
||||
gh pr merge $PR --repo $REPO --squash --delete-branch
|
||||
# --squash preserves linear main; --delete-branch keeps the repo clean
|
||||
|
||||
@@ -755,20 +755,6 @@ Developer Push ─────────────────────
|
||||
|
||||
**Result**: `https://praison.ai/install.sh` is automatically updated **only when the source files change**!
|
||||
|
||||
### Auto-deploy when install scripts change (PraisonAI → praison.ai)
|
||||
|
||||
Pushing `install.sh` or `install.ps1` to PraisonAI **`main`** triggers [`.github/workflows/deploy-install-scripts.yml`](/Users/praison/praisonai-package/.github/workflows/deploy-install-scripts.yml), which dispatches the existing praison.ai **`CI`** workflow (`workflow_dispatch` on `master`). No manual website push or `make upgrade` is required.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
pushMain["PraisonAI push main\ninstall.sh|install.ps1"] --> deployWF["deploy-install-scripts.yml"]
|
||||
deployWF --> dispatch["gh workflow run CI"]
|
||||
dispatch --> siteCI["praison.ai azure-pipeline.yaml"]
|
||||
siteCI --> live["https://praison.ai/install.sh"]
|
||||
```
|
||||
|
||||
Manual re-deploy: `gh workflow run "Deploy install scripts to praison.ai" --repo MervinPraison/PraisonAI`
|
||||
|
||||
### How install.sh is Served
|
||||
|
||||
The `web/install.sh` file is copied into the Docker image at `/var/www/html/web/install.sh` and served by nginx:
|
||||
@@ -811,20 +797,20 @@ git push origin master
|
||||
|
||||
**Task 2: Update install.sh and deploy**
|
||||
```bash
|
||||
# 1. Update install.sh / install.ps1
|
||||
# 1. Update install.sh
|
||||
vim /Users/praison/praisonai-package/src/praisonai/scripts/install.sh
|
||||
|
||||
# 2. Test install.sh
|
||||
bash /Users/praison/praisonai-package/src/praisonai/scripts/test-install-smoke.sh
|
||||
bash /Users/praison/praisonai-package/src/praisonai/scripts/install.sh --dry-run
|
||||
|
||||
# 3. Commit and push to PraisonAI main — auto-triggers praison.ai deploy
|
||||
git add src/praisonai/scripts/install.sh src/praisonai/scripts/install.ps1
|
||||
# 3. Commit and push
|
||||
git add /Users/praison/praisonai-package/src/praisonai/scripts/install.sh
|
||||
git commit -m "fix: update install script"
|
||||
git push origin main
|
||||
# deploy-install-scripts.yml → praison.ai CI → https://praison.ai/install.sh
|
||||
|
||||
# Manual re-deploy (optional):
|
||||
gh workflow run "Deploy install scripts to praison.ai" --repo MervinPraison/PraisonAI
|
||||
# 4. Deploy website (if needed)
|
||||
cd /Users/praison/Sites/localhost/praisonai
|
||||
make upgrade
|
||||
```
|
||||
|
||||
**Task 3: Rollback deployment**
|
||||
|
||||
@@ -137,15 +137,6 @@ Final rule: conclude only when evidence shows missing = 0.
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
PUBLISH WORKFLOW (reference)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GitHub Actions (recommended):
|
||||
Manual: Actions → PyPI Release → Run workflow on main (bump defaults to patch)
|
||||
Nightly: `.github/workflows/nightly-release-gate.yml` runs at 00:00 UTC (midnight) when:
|
||||
- changes exist in src/praisonai or src/praisonai-agents since last v* tag
|
||||
- Core Tests succeeded on current main HEAD
|
||||
→ dispatches pypi-release.yml with bump=patch, source=nightly
|
||||
→ still requires pypi environment approval before publish
|
||||
Ad-hoc minor/major: manual PyPI Release only (explicit bump selection)
|
||||
|
||||
Step 1 — Publish praisonaiagents (Core SDK):
|
||||
cd /Users/praison/praisonai-package/src/praisonai-agents
|
||||
praisonai publish pypi
|
||||
@@ -170,4 +161,4 @@ Step 3 — Publish praisonai-tools (External, if needed):
|
||||
# Bump version in pyproject.toml, then:
|
||||
python3.13 -m build && uv run twine upload dist/*
|
||||
|
||||
PraisonAI PRs: default merge via **Claude PR merge gate** (`.github/workflows/claude-merge-gate.yml`) when validation passes. Add label `no-auto-merge` to opt out. Manual `gh pr merge` only as fallback when the gate cannot run.
|
||||
PraisonAI PRs: NEVER merge automatically — user merges manually.
|
||||
@@ -187,7 +187,7 @@ gh pr create \
|
||||
|
||||
**Commit prefixes:** `fix:`, `feat:`, `security:`, `refactor:`, `docs:`, `test:`
|
||||
|
||||
> **Merge:** Default is the **Claude PR merge gate** (`.github/workflows/claude-merge-gate.yml`). Do not merge manually unless the PR has `no-auto-merge` or you are explicitly told to use `gh pr merge`.
|
||||
> **Do NOT merge automatically** — user merges manually unless explicitly told otherwise.
|
||||
|
||||
### 5b. Verify PR state
|
||||
|
||||
|
||||
@@ -129,9 +129,7 @@ python -m pytest tests/ -q \
|
||||
|
||||
## Step 6 — Approve or Request Changes
|
||||
|
||||
**Primary (automated):** The **Claude PR merge gate** (`.github/workflows/claude-merge-gate.yml`) assesses readiness and merges when `MERGE_GATE_VERDICT: APPROVE`, CI is green, and the PR lacks `no-auto-merge`. It never posts `@claude` and does not use `gh pr review` (avoids re-triggering `claude.yml`).
|
||||
|
||||
**Manual fallback — if the PR is good and gate is skipped or opted out:**
|
||||
**If the PR is good:**
|
||||
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && \
|
||||
@@ -149,14 +147,6 @@ gh pr review <NUMBER> --request-changes --body "Requesting changes: <specific fe
|
||||
|
||||
## Step 7 — Merge
|
||||
|
||||
**Primary:** Wait for merge gate or verify `mergedAt` after a gate run:
|
||||
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && gh pr view <NUMBER> --json state,mergedAt,labels
|
||||
```
|
||||
|
||||
**Manual fallback** (label `no-auto-merge` or gate disabled):
|
||||
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && \
|
||||
gh pr merge <NUMBER> --merge --subject "<PR title>"
|
||||
@@ -267,21 +257,6 @@ find $(python -c "import site; print(site.getsitepackages()[0])") -name "<suspic
|
||||
|
||||
---
|
||||
|
||||
## Fork PRs — contributor checklist and Claude push
|
||||
|
||||
For PRs from external forks (`head.repo != MervinPraison/PraisonAI`), ask the contributor:
|
||||
|
||||
1. **Allow edits from maintainers** on the PR (right sidebar).
|
||||
2. **Keep fork and branch** until the PR is merged or closed.
|
||||
|
||||
Optional: private fork → add maintainer as collaborator; fork with Actions → only enable “Allow edits and access to secrets by maintainers” if they accept the risk.
|
||||
|
||||
When `maintainerCanModify` is true, `claude.yml` pushes via `GH_TOKEN` to the fork remote and `merge-conflict-claude.yml` may auto-trigger `@claude` for `DIRTY` PRs (after FINAL architecture review).
|
||||
|
||||
Manual unblock: `gh pr checkout <N> && git fetch origin main && git rebase origin/main && git push --force-with-lease`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference — Common Commands
|
||||
|
||||
| Action | Command |
|
||||
|
||||
@@ -50,7 +50,7 @@ The workflow uses the agent configuration at `.github/praisonai-reviewer.yaml` a
|
||||
- Copilot NEVER triggers before CodeRabbit/Qodo complete.
|
||||
- Claude NEVER triggers before Copilot completes.
|
||||
- Duplicate `@copilot` comments are prevented by checking existing comments.
|
||||
- Default merge: **Claude PR merge gate** (`.github/workflows/claude-merge-gate.yml`) after review chain + CI green. Opt out with label `no-auto-merge`. Manual merge only as fallback.
|
||||
- PraisonAI PRs: NEVER merge automatically — user merges manually.
|
||||
|
||||
## Security & Credentials
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ gh api repos/MervinPraison/PraisonAI/security-advisories/<GHSA_ID> --jq '{ghsa:
|
||||
3. For each advisory, validate the vulnerability in source code:
|
||||
- Read the file(s) mentioned in the advisory description
|
||||
- Confirm the vulnerability exists (or is already fixed)
|
||||
- Classify using triage labels (see **Managing ongoing audits** below)
|
||||
- Classify: `real` | `already-fixed` | `not-reproducible`
|
||||
- Reference the official repository for latest source: https://github.com/MervinPraison/PraisonAI
|
||||
|
||||
## PHASE 2 — VALIDATE & FIX
|
||||
@@ -99,29 +99,9 @@ pip index versions praisonaiagents | head -1
|
||||
pip index versions praisonai | head -1
|
||||
```
|
||||
|
||||
13. Publish **praisonai-platform** (only when the advisory targets platform code):
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package/src/praisonai-platform
|
||||
praisonai publish pypi
|
||||
# or: uv lock && uv build && uv publish --token $PYPI_TOKEN
|
||||
```
|
||||
Platform releases are **independent** of `praisonai` / `praisonaiagents` version numbers. A platform-only fix (e.g. `0.1.8`) does **not** require bumping the main SDK.
|
||||
## PHASE 4 — UPDATE ADVISORIES (only after packages are on PyPI)
|
||||
|
||||
14. Publish **praisonai (npm / TypeScript)** (only when the advisory targets `src/praisonai-ts`):
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package/src/praisonai-ts
|
||||
npm version patch && npm publish
|
||||
```
|
||||
|
||||
15. Verify platform / npm when applicable:
|
||||
```bash
|
||||
pip index versions praisonai-platform | head -1
|
||||
npm view praisonai version
|
||||
```
|
||||
|
||||
## PHASE 4 — UPDATE ADVISORIES (only after packages are on PyPI/npm)
|
||||
|
||||
16. For each advisory, reopen to draft state and set patched versions + credits:
|
||||
13. For each advisory, reopen to draft state and set patched versions + credits:
|
||||
```bash
|
||||
gh api repos/MervinPraison/PraisonAI/security-advisories/<GHSA_ID> \
|
||||
--method PATCH \
|
||||
@@ -139,86 +119,34 @@ gh api repos/MervinPraison/PraisonAI/security-advisories/<GHSA_ID> \
|
||||
}
|
||||
EOF
|
||||
```
|
||||
- `<PACKAGE_NAME>`: `praisonaiagents` (core SDK), `praisonai` (Python wrapper), `praisonai-platform` (platform layer), or `praisonai` (npm ecosystem for TypeScript)
|
||||
- `<PACKAGE_NAME>`: `praisonaiagents` for core SDK, `praisonai` for wrapper
|
||||
- `<LAST_VULNERABLE_VERSION>`: version before the fix
|
||||
- `<FIRST_PATCHED_VERSION>`: the version just published with the fix
|
||||
- Always include credits for the reporter
|
||||
|
||||
17. Publish each advisory:
|
||||
14. Publish each advisory:
|
||||
```bash
|
||||
gh api repos/MervinPraison/PraisonAI/security-advisories/<GHSA_ID> \
|
||||
--method PATCH --field state=published
|
||||
```
|
||||
|
||||
18. Request CVE for each published advisory:
|
||||
15. Request CVE for each published advisory:
|
||||
```bash
|
||||
gh api repos/MervinPraison/PraisonAI/security-advisories/<GHSA_ID>/cve --method POST
|
||||
```
|
||||
GitHub reviews and assigns CVE IDs within 1-3 business days.
|
||||
|
||||
19. Verify final state:
|
||||
16. Verify final state:
|
||||
```bash
|
||||
gh api repos/MervinPraison/PraisonAI/security-advisories --jq '.[] | "\(.ghsa_id) \(.state) \(.cve_id // "pending") \(.severity) \([.credits[].login] | join(",") ) \(.summary | .[0:50])"'
|
||||
```
|
||||
|
||||
## MANAGING ONGOING AUDITS
|
||||
|
||||
Continuous security reports are **expected** for an agent framework. Treat them as a product signal, not a crisis. Use a consistent process so triage stays manageable.
|
||||
|
||||
### Triage labels
|
||||
|
||||
After validating each advisory in source code, assign **one** label:
|
||||
|
||||
| Label | Meaning | Action |
|
||||
|---|---|---|
|
||||
| `real` | Reproducible, valid security issue | Fix → publish → advisory |
|
||||
| `already-fixed` | Fixed in `main` but advisory still open | Set patched version to current release; publish advisory |
|
||||
| `duplicate` | Same root cause as another GHSA | Close or merge; link to primary advisory |
|
||||
| `not-reproducible` | Cannot reproduce with steps provided | Request more detail; hold in triage |
|
||||
| `theoretical` | Requires unlikely preconditions or no practical impact | Document reasoning; close as wontfix or low priority |
|
||||
| `documented-opt-out` | Behaviour is intentional with documented escape hatch | Close as wontfix; point to docs/env var |
|
||||
| `out-of-scope` | Wrong package, fork, or deployment the project does not ship | Close with explanation |
|
||||
|
||||
### Triage load
|
||||
|
||||
- Not every report is valid — filter with the labels above before writing code.
|
||||
- Batch related GHSAs (same file, same pattern) into one fix where possible.
|
||||
- Prefer regression tests per GHSA so fixes are not re-opened by the next audit round.
|
||||
|
||||
### Version churn and release boundaries
|
||||
|
||||
| Package | Version line | Notes |
|
||||
|---|---|---|
|
||||
| `praisonaiagents` + `praisonai` | Aligned patch (e.g. `1.6.62` / `4.6.62`) | Main SDK + wrapper; bump together via `bump_and_release.py` |
|
||||
| `praisonai-platform` | Independent semver (e.g. `0.1.8`) | Platform-only fixes; users need **not** upgrade the main SDK |
|
||||
| `praisonai` (npm) | Independent semver (e.g. `1.7.2`) | TypeScript SDK; publish from `src/praisonai-ts` |
|
||||
|
||||
Always set the advisory `patched_versions` to the **package that actually contains the fix**. Do not imply users must upgrade all packages when only one changed.
|
||||
|
||||
### Researcher incentives
|
||||
|
||||
- Some reports are bounty-driven or low-quality — still acknowledge and triage promptly.
|
||||
- Credit valid reporters in the advisory even for small fixes.
|
||||
- Close invalid reports with a clear label and short rationale (no exploit discussion in public issues).
|
||||
- Never fix “for the sake of it” — see Phase 2 validate checks.
|
||||
|
||||
### When to close as wontfix
|
||||
|
||||
Close (or publish advisory with no code change only if already mitigated) when:
|
||||
|
||||
- Documented opt-out exists and risk is accepted (e.g. localhost-only dev modes with explicit env vars).
|
||||
- Issue is theoretical with no realistic attack path in supported deployments.
|
||||
- Fix would remove existing features or materially bloat the SDK — escalate instead.
|
||||
- Duplicate of an existing published advisory.
|
||||
|
||||
## REFERENCE — Package-to-file mapping
|
||||
|
||||
| Package | Ecosystem | Canonical Path | Key files |
|
||||
|---|---|---|---|
|
||||
| praisonaiagents | pip | src/praisonai-agents/praisonaiagents/ | tools/, mcp/, memory/, agent/, sandbox/ |
|
||||
| praisonai | pip | src/praisonai/praisonai/ | cli/, capabilities/, mcp_server/, sandbox/, recipe/ |
|
||||
| praisonai-platform | pip | src/praisonai-platform/praisonai_platform/ | api/routes/, services/, auth |
|
||||
| praisonai | npm | src/praisonai-ts/src/ | agent/, mcp/, tools/, code-mode/ |
|
||||
| Package | Canonical Path | Key files |
|
||||
|---|---|---|
|
||||
| praisonaiagents | src/praisonai-agents/praisonaiagents/ | tools/python_tools.py, mcp/, memory/, agent/ |
|
||||
| praisonai | src/praisonai/praisonai/ | ui/sql_alchemy.py, capabilities/, mcp_server/, cli/ |
|
||||
|
||||
## RESOURCES & DOCUMENTATION
|
||||
|
||||
@@ -232,10 +160,8 @@ Close (or publish advisory with no code change only if already mitigated) when:
|
||||
- NEVER expose exploit details in public issues or PRs
|
||||
- ALWAYS credit the reporter in the advisory
|
||||
- ALWAYS set patched_versions before publishing advisory
|
||||
- ALWAYS publish to PyPI/npm BEFORE publishing advisories (so Dependabot can point users to a fix)
|
||||
- Python SDK: `praisonai publish pypi` from `src/praisonai-agents`, then `bump_and_release.py` for the wrapper
|
||||
- Platform-only: `praisonai publish pypi` from `src/praisonai-platform` — no SDK bump required
|
||||
- npm: `npm publish` from `src/praisonai-ts` when the TypeScript SDK is affected
|
||||
- ALWAYS publish to PyPI BEFORE publishing advisories (so Dependabot can point users to a fix) - to publish `praisonai publish pypi` from praisonai-agents folder, after that
|
||||
`cd /Users/praison/praisonai-package/src/praisonai && python scripts/bump_and_release.py 4.5.125 --agents 1.5.125 --wait`. 4.5.125 is praisonai version number and 1.5.125 is praisonaiagents version number. Last 2 numbers will be same 4/1.xx.xxx
|
||||
- ALWAYS request CVE AFTER publishing advisory
|
||||
- Use generic commit messages: "refactor: harden X" not "fix: SQL injection in Y"
|
||||
- Fix on main, push directly — no feature branches for security patches
|
||||
@@ -246,6 +172,7 @@ Close (or publish advisory with no code change only if already mitigated) when:
|
||||
|
||||
## REPORTING SECURITY ISSUES
|
||||
|
||||
If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](../../../../SECURITY.md) at the monorepo root for scope, supported packages, and the private advisory link.
|
||||
|
||||
Maintainers and agents: use [.cursor/skills/security-audit/SKILL.md](../../.cursor/skills/security-audit/SKILL.md) for Cursor skill entry point.
|
||||
If you discover a security vulnerability, please report it responsibly:
|
||||
1. Open a draft security advisory at https://github.com/MervinPraison/PraisonAI/security/advisories/new
|
||||
2. Include detailed reproduction steps and impact assessment
|
||||
3. Allow time for the maintainers to address the issue before public disclosure
|
||||
+11
-140
@@ -42,11 +42,7 @@ Each feature runs 3 ways: **CLI, YAML, Python**.
|
||||
│ │ └── pyproject.toml # Package config
|
||||
│ │
|
||||
│ ├── praisonai/ # Wrapper (praisonai)
|
||||
│ │ ├── praisonai/ # Bot/channel CLI, integrations, heavy impls
|
||||
│ │ └── pyproject.toml
|
||||
│ │
|
||||
│ ├── praisonai-code/ # Agentic terminal CLI (praisonai_code)
|
||||
│ │ ├── praisonai_code/ # run, chat, code, runtime, cli_backends
|
||||
│ │ ├── praisonai/ # CLI, integrations, heavy impls
|
||||
│ │ └── pyproject.toml
|
||||
│ │
|
||||
│ └── praisonai-ts/ # TypeScript SDK
|
||||
@@ -84,10 +80,7 @@ Each feature runs 3 ways: **CLI, YAML, Python**.
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ praisonai (Wrapper) │
|
||||
│ Bot/channel CLI • Gateway • Integrations • Heavy impls │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ praisonai-code (Terminal agent) │
|
||||
│ run • chat • code • doctor • runtime • cli_backends • Typer │
|
||||
│ CLI • Integrations • Heavy Implementations • Optional Deps │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ praisonaiagents (Core SDK) │
|
||||
│ Protocols • Hooks • Adapters • Base Classes • Decorators │
|
||||
@@ -97,123 +90,6 @@ Each feature runs 3 ways: **CLI, YAML, Python**.
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 `praisonai-code` — Agentic Terminal CLI Split (C5/C6)
|
||||
|
||||
The **agentic terminal CLI** (`run`, `chat`, `code`, warm runtime, CLI backends, Typer app) lives in the separate **`praisonai-code`** PyPI package (`praisonai_code`). The **`praisonai`** wrapper keeps bot/channel orchestration, gateway, and heavy integrations. **`pip install praisonai`** still installs everything; old import paths are preserved via shims.
|
||||
|
||||
| Product | Location | Stays out of |
|
||||
|---------|----------|--------------|
|
||||
| Agentic terminal CLI | `src/praisonai-code/praisonai_code/` | Must **not** depend on `praisonai` (PyPI cycle) |
|
||||
| Bot/channel runtime | `src/praisonai/praisonai/` | Gateway, BotOS, onboarding, dashboard |
|
||||
| Core SDK | `src/praisonai-agents/praisonaiagents/` | Heavy CLI / integrations |
|
||||
|
||||
**Moved into `praisonai_code` (~304 modules):**
|
||||
|
||||
| Step | Content |
|
||||
|------|---------|
|
||||
| C1 | `runtime/`, `cli_backends/` |
|
||||
| C2 | `cli/interactive/`, `execution/`, `ui/`, `output/`, `state/` |
|
||||
| C3 | `cli/commands/*` (80 Typer command modules) |
|
||||
| C4 | `cli/features/*` (158 feature handlers; bot features excepted) |
|
||||
| C5 | `cli/main.py`, `app.py`, `configuration/`, `session/`, `utils/`, schema modules |
|
||||
|
||||
**Intentionally retained in the wrapper (not shims):**
|
||||
|
||||
- **Commands:** `gateway`, `bot`, `pairing`, `identity`, `onboard`, `kanban`, `dashboard`, `claw`
|
||||
- **Features:** `gateway`, `bots_cli`, `onboard`, `approval`, `serve`
|
||||
- **Domains:** `praisonai.gateway.*`, `praisonai.bots.*`, integrations, endpoints
|
||||
|
||||
**Backward-compat shim patterns** (old `praisonai.*` paths must keep working):
|
||||
|
||||
| Pattern | Used for | Module identity |
|
||||
|---------|----------|-----------------|
|
||||
| `sys.modules[__name__] = _impl` | `cli/main`, `cli/app`, each moved `cli/commands/*`, schema modules | Same object as `praisonai_code.*` |
|
||||
| `alias_package()` + `_AliasFinder` | `cli/output`, `interactive`, `execution`, `ui`, `state`, `session`, `configuration`, `utils` | Same submodule objects |
|
||||
| Package `__getattr__` + submodule alias | `runtime/`, `cli_backends/` | Submodules identical; package object differs |
|
||||
| `features/__init__.py` `__path__` extension | Extracted features; five bot features stay local | Handler imports via `__getattr__` |
|
||||
|
||||
**Typer lazy-load:** `praisonai_code.cli.app._WRAPPER_COMMANDS` lists bot/channel commands that resolve via **`praisonai.cli.commands.*`** (wrapper), not `praisonai_code.cli.commands.*`.
|
||||
|
||||
**Monorepo bootstrap:** `praisonai._bootstrap.ensure_praisonai_code()` adds sibling `src/praisonai-code` to `sys.path` so `PYTHONPATH=src/praisonai-agents:src/praisonai` works without an explicit code entry.
|
||||
|
||||
**Install order (CI and local dev):** `praisonai-agents` → `praisonai-code` → `praisonai` (see `.github/actions/install-monorepo-packages`).
|
||||
|
||||
**Invocation paths that must remain valid:**
|
||||
|
||||
```bash
|
||||
praisonai … # console script → praisonai.__main__
|
||||
python -m praisonai …
|
||||
python -m praisonai.cli.main …
|
||||
python -m praisonai.runtime …
|
||||
from praisonai.cli.main import PraisonAI
|
||||
from praisonai.cli.commands.run import …
|
||||
unittest.mock.patch("praisonai.cli.commands.run.X") # same module object
|
||||
```
|
||||
|
||||
**Regression gate:** `src/praisonai/tests/unit/test_c5_backward_compat.py` (also run in optimised CI smoke). Standalone `pip install praisonai-code` requires declared deps (e.g. `toml` for config resolver).
|
||||
|
||||
**Key files:**
|
||||
|
||||
| What | Where |
|
||||
|------|-------|
|
||||
| Typer app + lazy commands | `praisonai_code/cli/app.py` |
|
||||
| Shim helper | `praisonai/cli/_shim.py` |
|
||||
| Bootstrap | `praisonai/_bootstrap.py` |
|
||||
| CLI main shim | `praisonai/cli/main.py` |
|
||||
| Runtime / backends shims | `praisonai/runtime/`, `praisonai/cli_backends/` |
|
||||
|
||||
**Do not** recreate `src/praisonai/praisonai_code/` — it shadows the real package.
|
||||
|
||||
**PyPI publish order:** `praisonaiagents` → `praisonai-code` → `praisonai` (see `src/praisonai/scripts/bump_and_release.py` and `.github/workflows/pypi-release.yml`). Gateway/bots changes release via wrapper only; agentic CLI changes also bump `praisonai-code`.
|
||||
|
||||
#### C6 integration gate (#2519)
|
||||
|
||||
C6 is the **sign-off step** after C0–C5 file moves: no further moves, only regression verification and release wiring.
|
||||
|
||||
| Check | Command / artefact |
|
||||
|-------|-------------------|
|
||||
| Smoke `--help` | `praisonai`, `praisonai run/chat/code/daemon/attach/gateway/bot/doctor --help` |
|
||||
| Compat regression | `pytest src/praisonai/tests/unit/test_c5_backward_compat.py` |
|
||||
| CLI shard | `pytest src/praisonai/tests/unit/cli/` |
|
||||
| Warm runtime | `pytest src/praisonai/tests/unit/test_warm_runtime.py` |
|
||||
| Doctor shard | `pytest src/praisonai/tests/unit/doctor/` |
|
||||
| Import perf | `pytest src/praisonai/tests/unit/test_performance_benchmarks.py` (< 500ms) |
|
||||
| Real LLM smoke | `RUN_REAL_KEY_TESTS=1 PRAISONAI_ALLOW_NETWORK=1 PRAISONAI_TEST_PROVIDERS=all pytest src/praisonai/tests/integration/test_real_key_smoke.py` |
|
||||
| Verification log | `src/praisonai/tests/C6_VERIFICATION.md` |
|
||||
|
||||
**Command split (wrapper vs code):**
|
||||
|
||||
| Install target | Agentic CLI (`run`, `chat`, `code`, …) | Bot/gateway (`bot`, `gateway`, `pairing`, …) |
|
||||
|----------------|----------------------------------------|---------------------------------------------|
|
||||
| `pip install praisonai` | Yes (via `praisonai-code` dep + shims) | Yes |
|
||||
| `pip install praisonai-code` only | Yes — agentic hot path (`run`, `chat`, `code`, warm runtime) without wrapper import | No — wrapper-only commands hidden when `praisonai` absent |
|
||||
|
||||
**C7 (hot path complete):** Core agentic CLI imports no longer require the `praisonai`
|
||||
wrapper at module level. Remaining lazy wrapper imports (~211, regression-gated) are optional commands
|
||||
and features (train, capabilities, bots, framework adapters) — use
|
||||
`praisonai_code._wrapper_bridge` or `pip install praisonai`. See
|
||||
`src/praisonai/tests/C7_VERIFICATION.md`. CI gate: `scripts/check_c7_imports.sh`.
|
||||
|
||||
### 2.4 Package ownership (C7.1)
|
||||
|
||||
| Tier | Package | Owns |
|
||||
|------|---------|------|
|
||||
| 1 | `praisonaiagents` | Agent, tools, memory, hooks, `frameworks/` protocols |
|
||||
| 2 | `praisonai-code` | `run`/`chat`/`code`, Typer, runtime, llm, tool resolution |
|
||||
| 3 | `praisonai` | Gateway, bots, `framework_adapters/`, capabilities, train, serve orchestration |
|
||||
|
||||
**Cross-tier rule:** `praisonai-code` must not declare a PyPI dependency on `praisonai`. Use
|
||||
[`praisonai_code._wrapper_bridge`](../praisonai-code/praisonai_code/_wrapper_bridge.py) for lazy wrapper access.
|
||||
|
||||
**framework_adapters:** protocols in SDK; registry + CrewAI/AutoGen/PraisonAI bodies stay in
|
||||
[`praisonai/framework_adapters/`](../praisonai/praisonai/framework_adapters/). Do not move to `praisonai-code`.
|
||||
|
||||
**Typer `_WRAPPER_COMMANDS`:** only for commands whose implementation lives in the wrapper
|
||||
(`bot`, `gateway`, `pairing`, `identity`, `onboard`, `kanban`, `dashboard`, `claw`). Commands
|
||||
implemented in `praisonai_code` (`train`, `serve`) stay local; bridge internal wrapper imports.
|
||||
|
||||
Full boundary doc: [`src/praisonai/tests/C7.1_BOUNDARIES.md`](../praisonai/tests/C7.1_BOUNDARIES.md).
|
||||
|
||||
---
|
||||
|
||||
## 3. Core SDK Architecture (praisonaiagents)
|
||||
@@ -302,14 +178,15 @@ mongodb = ["pymongo>=4.6.3", "motor>=3.4.0"]
|
||||
### 4.1 Protocol-Driven Core (MUST)
|
||||
|
||||
```
|
||||
Core SDK (praisonaiagents) praisonai-code (terminal CLI) Wrapper (praisonai)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━
|
||||
✅ Protocols ✅ run/chat/code/doctor/setup ✅ Bot/gateway CLI
|
||||
✅ Hooks ✅ runtime, cli_backends ✅ Integrations
|
||||
✅ Adapters ✅ Typer app, moved commands ✅ Heavy implementations
|
||||
✅ Base classes ✅ Depends on agents only ✅ Optional deps
|
||||
✅ Decorators ❌ Must not import praisonai ✅ UI, DB adapters
|
||||
❌ Heavy implementations ❌ Bot/channel commands ❌ Core logic
|
||||
Core SDK (praisonaiagents) Wrapper (praisonai)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━
|
||||
✅ Protocols ✅ Heavy implementations
|
||||
✅ Hooks ✅ CLI commands
|
||||
✅ Adapters ✅ External integrations
|
||||
✅ Base classes ✅ Optional deps
|
||||
✅ Decorators ✅ UI components
|
||||
✅ Dataclasses ✅ Database adapters
|
||||
❌ Heavy implementations ❌ Core logic
|
||||
```
|
||||
|
||||
### 4.2 No Performance Impact (MUST)
|
||||
@@ -862,12 +739,6 @@ from praisonaiagents import Workflow, Route, Parallel, Loop
|
||||
| Policy engine | `praisonaiagents/policy/engine.py` |
|
||||
| Scheduler module | `praisonaiagents/scheduler/` |
|
||||
| Schedule tools | `praisonaiagents/tools/schedule_tools.py` |
|
||||
| Agentic CLI (Typer) | `praisonai_code/cli/app.py` |
|
||||
| CLI main / run | `praisonai_code/cli/main.py` |
|
||||
| Warm runtime | `praisonai_code/runtime/` |
|
||||
| CLI backends | `praisonai_code/cli_backends/` |
|
||||
| Back-compat shims | `praisonai/cli/_shim.py`, `praisonai/cli/main.py` |
|
||||
| C5 regression tests | `praisonai/tests/unit/test_c5_backward_compat.py` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Documentation Parity Tracker (Python)
|
||||
|
||||
> **Categories:** 66 | **Documented:** 66 | **Parity:** 100.0%
|
||||
> **Categories:** 60 | **Documented:** 57 | **Parity:** 95.0%
|
||||
|
||||
This report compares **Python SDK feature categories** against **Python documentation** (docs/concepts, docs/features, etc.).
|
||||
|
||||
@@ -8,87 +8,108 @@ This report compares **Python SDK feature categories** against **Python document
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Feature Categories | 66 |
|
||||
| **Documented Categories** | **66** |
|
||||
| **Undocumented Categories** | **0** |
|
||||
| **Parity** | **100.0%** |
|
||||
| Feature Categories | 60 |
|
||||
| **Documented Categories** | **57** |
|
||||
| **Undocumented Categories** | **3** |
|
||||
| **Parity** | **95.0%** |
|
||||
|
||||
## Documented Categories
|
||||
|
||||
| Category | Features | Docs | Lines |
|
||||
|----------|----------|------|-------|
|
||||
| ✅ AGUI | 1 | 2 | 420 |
|
||||
| ✅ Agent | 21 | 43 | 12014 |
|
||||
| ✅ Agent-to-Agent (A2A) | 1 | 7 | 2951 |
|
||||
| ✅ Agent-to-User (A2U) | 1 | 3 | 585 |
|
||||
| ✅ Approval | 1 | 5 | 2106 |
|
||||
| ✅ Audio | 2 | 10 | 658 |
|
||||
| ✅ Auto Generation | 5 | 7 | 2046 |
|
||||
| ✅ Autonomy | 3 | 6 | 2163 |
|
||||
| ✅ Bots | 7 | 24 | 8599 |
|
||||
| ✅ Budget | 1 | 1 | 287 |
|
||||
| ✅ CLI | 5 | 111 | 28360 |
|
||||
| ✅ Chunking | 2 | 2 | 372 |
|
||||
| ✅ Agent | 19 | 30 | 8367 |
|
||||
| ✅ Agent-to-Agent (A2A) | 1 | 3 | 625 |
|
||||
| ✅ Approval | 1 | 2 | 889 |
|
||||
| ✅ Audio | 2 | 10 | 649 |
|
||||
| ✅ Auto Generation | 3 | 6 | 1386 |
|
||||
| ✅ Autonomy | 3 | 4 | 1146 |
|
||||
| ✅ Bots | 5 | 2 | 573 |
|
||||
| ✅ Chunking | 2 | 2 | 285 |
|
||||
| ✅ Citations | 2 | 1 | 202 |
|
||||
| ✅ Code Execution | 2 | 13 | 3752 |
|
||||
| ✅ Conditions | 1 | 3 | 1124 |
|
||||
| ✅ Configuration | 3 | 7 | 2299 |
|
||||
| ✅ Context Management | 16 | 33 | 11478 |
|
||||
| ✅ Database | 1 | 40 | 5744 |
|
||||
| ✅ Deep Research | 8 | 2 | 497 |
|
||||
| ✅ Display | 6 | 3 | 648 |
|
||||
| ✅ Embeddings | 6 | 23 | 1990 |
|
||||
| ✅ Evaluation | 1 | 6 | 2511 |
|
||||
| ✅ Events | 1 | 2 | 735 |
|
||||
| ✅ Execution | 3 | 4 | 1455 |
|
||||
| ✅ Failover | 2 | 1 | 400 |
|
||||
| ✅ Files | 2 | 6 | 1845 |
|
||||
| ✅ Flow | 1 | 3 | 777 |
|
||||
| ✅ Gateway | 7 | 36 | 13110 |
|
||||
| ✅ Guardrails | 4 | 4 | 1642 |
|
||||
| ✅ Handoffs | 11 | 6 | 2654 |
|
||||
| ✅ Hooks | 2 | 9 | 3564 |
|
||||
| ✅ Image | 1 | 11 | 1113 |
|
||||
| ✅ Knowledge | 4 | 12 | 3658 |
|
||||
| ✅ LLM | 3 | 12 | 4292 |
|
||||
| ✅ Loops | 4 | 3 | 797 |
|
||||
| ✅ MCP | 1 | 52 | 11155 |
|
||||
| ✅ Memory | 6 | 17 | 6141 |
|
||||
| ✅ OCR | 2 | 1 | 237 |
|
||||
| ✅ Observability | 2 | 23 | 2768 |
|
||||
| ✅ Optimizer | 1 | 2 | 750 |
|
||||
| ✅ Output | 3 | 5 | 1102 |
|
||||
| ✅ Parallel Execution | 3 | 2 | 392 |
|
||||
| ✅ Planning | 6 | 6 | 1418 |
|
||||
| ✅ Plugins | 8 | 3 | 994 |
|
||||
| ✅ Prompts | 2 | 7 | 1201 |
|
||||
| ✅ Providers | 1 | 54 | 7047 |
|
||||
| ✅ Query | 1 | 2 | 623 |
|
||||
| ✅ RAG | 5 | 15 | 2891 |
|
||||
| ✅ Code Execution | 3 | 13 | 3395 |
|
||||
| ✅ Conditions | 1 | 3 | 1086 |
|
||||
| ✅ Configuration | 2 | 7 | 1753 |
|
||||
| ✅ Context Management | 13 | 29 | 7576 |
|
||||
| ✅ Database | 1 | 31 | 3037 |
|
||||
| ✅ Display | 6 | 2 | 1175 |
|
||||
| ✅ Embeddings | 6 | 23 | 1966 |
|
||||
| ✅ Evaluation | 1 | 6 | 2538 |
|
||||
| ✅ Events | 1 | 2 | 699 |
|
||||
| ✅ Execution | 3 | 2 | 515 |
|
||||
| ✅ Files | 2 | 5 | 863 |
|
||||
| ✅ Flow | 1 | 1 | 284 |
|
||||
| ✅ Gateway | 6 | 1 | 256 |
|
||||
| ✅ Guardrails | 4 | 4 | 2279 |
|
||||
| ✅ Handoffs | 10 | 4 | 1674 |
|
||||
| ✅ Hooks | 2 | 6 | 1606 |
|
||||
| ✅ Image | 1 | 11 | 1096 |
|
||||
| ✅ Knowledge | 3 | 12 | 3262 |
|
||||
| ✅ LLM | 2 | 2 | 1049 |
|
||||
| ✅ Loops | 4 | 1 | 250 |
|
||||
| ✅ MCP | 2 | 47 | 9754 |
|
||||
| ✅ Memory | 6 | 12 | 4474 |
|
||||
| ✅ Output | 3 | 4 | 868 |
|
||||
| ✅ Parallel Execution | 2 | 2 | 347 |
|
||||
| ✅ Planning | 6 | 5 | 1506 |
|
||||
| ✅ Plugins | 8 | 3 | 1494 |
|
||||
| ✅ Prompts | 2 | 4 | 796 |
|
||||
| ✅ Providers | 2 | 50 | 5396 |
|
||||
| ✅ Query | 1 | 2 | 616 |
|
||||
| ✅ RAG | 5 | 14 | 2910 |
|
||||
| ✅ Realtime | 2 | 5 | 584 |
|
||||
| ✅ Reflection | 3 | 3 | 665 |
|
||||
| ✅ Retrieval | 2 | 5 | 915 |
|
||||
| ✅ Routing | 1 | 2 | 390 |
|
||||
| ✅ Sandbox | 5 | 6 | 2464 |
|
||||
| ✅ Security | 1 | 3 | 2775 |
|
||||
| ✅ Sessions | 4 | 12 | 4292 |
|
||||
| ✅ Skills | 6 | 13 | 4856 |
|
||||
| ✅ Tasks | 2 | 6 | 2715 |
|
||||
| ✅ Telemetry | 1 | 2 | 576 |
|
||||
| ✅ Templates | 1 | 8 | 1657 |
|
||||
| ✅ Tools | 12 | 125 | 31358 |
|
||||
| ✅ Reflection | 3 | 2 | 469 |
|
||||
| ✅ Retrieval | 2 | 5 | 941 |
|
||||
| ✅ Routing | 1 | 2 | 597 |
|
||||
| ✅ Sandbox | 5 | 3 | 1002 |
|
||||
| ✅ Security | 1 | 1 | 875 |
|
||||
| ✅ Sessions | 2 | 8 | 2128 |
|
||||
| ✅ Skills | 5 | 5 | 2002 |
|
||||
| ✅ Tasks | 2 | 5 | 2590 |
|
||||
| ✅ Telemetry | 1 | 2 | 541 |
|
||||
| ✅ Templates | 1 | 7 | 1411 |
|
||||
| ✅ Tools | 4 | 97 | 17827 |
|
||||
| ✅ Tracing | 3 | 2 | 139 |
|
||||
| ✅ Vector Store | 1 | 12 | 1139 |
|
||||
| ✅ Video | 2 | 6 | 510 |
|
||||
| ✅ Video | 2 | 5 | 460 |
|
||||
| ✅ Vision | 2 | 1 | 283 |
|
||||
| ✅ Web | 3 | 8 | 1910 |
|
||||
| ✅ Workflows | 5 | 16 | 6764 |
|
||||
| ✅ Web | 4 | 6 | 1267 |
|
||||
| ✅ Workflows | 4 | 9 | 3579 |
|
||||
|
||||
## Stub Documentation (Need Content)
|
||||
|
||||
These categories have documentation files but < 50 lines (stubs):
|
||||
|
||||
| Category | Features | Docs | Lines |
|
||||
|----------|----------|------|-------|
|
||||
| ⚠️ OCR | 2 | 1 | 49 |
|
||||
|
||||
## Undocumented Categories (Need Documentation)
|
||||
|
||||
| Category | Features |
|
||||
|----------|----------|
|
||||
| ❌ Failover | 2 |
|
||||
| ❌ Optimizer | 1 |
|
||||
|
||||
## Documentation Without Features
|
||||
|
||||
These docs exist but don't match any implemented feature category:
|
||||
|
||||
- ℹ️ Documents (1 docs, 787 lines)
|
||||
- ℹ️ AI SDK (1 docs, 112 lines)
|
||||
- ℹ️ Agent-to-User (A2U) (3 docs, 770 lines)
|
||||
- ℹ️ CLI (91 docs, 21386 lines)
|
||||
- ℹ️ Callbacks (1 docs, 985 lines)
|
||||
- ℹ️ Chat (3 docs, 546 lines)
|
||||
- ℹ️ Deep Research (2 docs, 437 lines)
|
||||
- ℹ️ Deployment (29 docs, 5931 lines)
|
||||
- ℹ️ Jobs (1 docs, 358 lines)
|
||||
- ℹ️ Middleware (1 docs, 60 lines)
|
||||
- ℹ️ Observability (21 docs, 1580 lines)
|
||||
- ℹ️ Process (1 docs, 506 lines)
|
||||
- ℹ️ Scheduler (2 docs, 858 lines)
|
||||
- ℹ️ Storage (4 docs, 722 lines)
|
||||
- ℹ️ Streaming (3 docs, 485 lines)
|
||||
- ℹ️ Token Management (3 docs, 1020 lines)
|
||||
- ℹ️ Vector Store (11 docs, 733 lines)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,70 +1,35 @@
|
||||
# PraisonAI Agents - Benchmark Results
|
||||
|
||||
**Generated:** 2026-06-17
|
||||
**Methodology:** 10 warmup runs + 1000 measured iterations (`performance_benchmark.py` config)
|
||||
**Config:** `Agent(name=..., instructions=..., model="gpt-4o-mini", tools=[get_weather], output="silent")`
|
||||
**Generated:** 2025-12-18 14:40:22
|
||||
**Iterations:** 100
|
||||
**Test:** Agent instantiation (without tools)
|
||||
|
||||
## Agent Instantiation Time
|
||||
## Results
|
||||
|
||||
| Framework | Avg Time (μs) | Min (μs) | Median (μs) | Relative |
|
||||
|-----------|---------------|----------|-------------|----------|
|
||||
| OpenAI Agents SDK | 5.05 | — | — | 1.00x (fastest) |
|
||||
| Agno | 5.27 | — | — | 1.04x |
|
||||
| **PraisonAI** | **13.62** | **12.58** | **12.96** | **2.69x** |
|
||||
| PraisonAI (LiteLLM) | 14.49 | 13.37 | 13.71 | 2.86x |
|
||||
|
||||
### PraisonAI detail (1000 iter)
|
||||
|
||||
| Variant | Avg (μs) | Min (μs) | Median (μs) | Std (μs) |
|
||||
|---------|----------|----------|-------------|----------|
|
||||
| With tools (`gpt-4o-mini`) | 13.62 | 12.58 | 12.96 | 2.73 |
|
||||
| With tools (`openai/gpt-4o-mini`) | 14.49 | 13.37 | 13.71 | 4.14 |
|
||||
| Minimal, no tools | 13.40 | — | — | — |
|
||||
|
||||
## Historical baseline
|
||||
|
||||
| Source | Config | Avg (μs) | Notes |
|
||||
|--------|--------|----------|-------|
|
||||
| `performance_benchmark.py` (Dec 2025) | tools + 1000 iter | **3.20** | Pre-init-bloat baseline |
|
||||
| `simple_benchmark.py` (Dec 2025) | no tools, 100 iter | **3.77** | Saved in prior `BENCHMARK_RESULTS.md` |
|
||||
| Current (Jun 2026) | tools + 1000 iter | **13.62** | After lazy-init optimisations (was ~31 μs pre-fix) |
|
||||
| Framework | Avg Time (μs) | Relative |
|
||||
|-----------|---------------|----------|
|
||||
| **PraisonAI** | **3.77** | **1.00x (fastest)** |
|
||||
| OpenAI Agents SDK | 5.26 | 1.39x |
|
||||
| Agno | 5.64 | 1.49x |
|
||||
| PraisonAI (LiteLLM) | 7.56 | 2.00x |
|
||||
| PydanticAI | 226.94 | 60.16x |
|
||||
| LangGraph | 4,558.71 | 1,209x |
|
||||
| CrewAI | 15,607.92 | 4,138x |
|
||||
|
||||
## Package Versions
|
||||
|
||||
| Package | Version |
|
||||
|---------|---------|
|
||||
| PraisonAI | 1.6.60 |
|
||||
| Agno | 2.4.2 |
|
||||
| OpenAI Agents SDK | 0.7.0 |
|
||||
|---------|--------|
|
||||
| PraisonAI | 0.1.5 |
|
||||
| Agno | 2.3.14 |
|
||||
| PydanticAI | 1.35.0 |
|
||||
| OpenAI Agents SDK | 0.6.3 |
|
||||
| LangGraph | 1.0.5 |
|
||||
| CrewAI | 1.6.1 |
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
PraisonAI-only (fast; avoids CrewAI/LangGraph import hang):
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
PYTHONWARNINGS='ignore::DeprecationWarning' python -c "
|
||||
import time, statistics
|
||||
from typing import Literal
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def get_weather(city: Literal['nyc', 'sf']):
|
||||
return 'cloudy' if city == 'nyc' else 'sunny'
|
||||
|
||||
def bench(fn, n=1000, w=10):
|
||||
for _ in range(w): fn()
|
||||
times = []
|
||||
for _ in range(n):
|
||||
s = time.perf_counter(); fn(); times.append(time.perf_counter()-s)
|
||||
print(f'avg={statistics.mean(times)*1e6:.2f} μs')
|
||||
|
||||
bench(lambda: Agent(name='Test', instructions='Hi', model='gpt-4o-mini', tools=[get_weather], output='silent'))
|
||||
"
|
||||
```
|
||||
|
||||
Full multi-framework benchmark:
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
PYTHONWARNINGS='ignore::DeprecationWarning' python benchmarks/performance_benchmark.py --save
|
||||
python benchmarks/simple_benchmark.py
|
||||
```
|
||||
@@ -1,62 +0,0 @@
|
||||
# Agent Instantiation — Root Cause Analysis
|
||||
|
||||
**Date:** 2026-06-17
|
||||
**Version:** praisonaiagents 1.6.58 (dev)
|
||||
**Methodology:** 10 warmup + 1000 measured iterations, `time.perf_counter()`
|
||||
|
||||
## Summary
|
||||
|
||||
Instantiation regressed from **~3.2 μs** (Dec 2025) to **~31 μs** (~10×). After fixes, steady-state is **~16 μs** (~2× historical). PraisonAI is competitive with Agno (~5 μs) on warmed benchmarks using `model=`.
|
||||
|
||||
## Before vs After Fixes
|
||||
|
||||
| Config | Historical | Pre-fix | Post-fix |
|
||||
|--------|------------|---------|----------|
|
||||
| Minimal (`name`, `output="silent"`) | 3.20 μs | 31.36 μs | **16.35 μs** |
|
||||
| `model="gpt-4o-mini"` | 3.77 μs | 32.19 μs | **16.86 μs** |
|
||||
| `llm="gpt-4o-mini"` (deprecated) | 3.77 μs | 46.48 μs | **~18 μs** (warn once) |
|
||||
| LiteLLM `openai/gpt-4o-mini` cold | 7.31 μs | **3.7 s** | **53.63 μs** |
|
||||
| LiteLLM warmed instantiate | — | 60 μs | **19.83 μs** |
|
||||
| Import `Agent` | — | 20.8 ms | **29.6 ms** |
|
||||
|
||||
## Root Causes (validated)
|
||||
|
||||
| ID | Cause | Status | Fix applied |
|
||||
|----|-------|--------|-------------|
|
||||
| H1 | Config loader cold start (+9 ms first init) | CONFIRMED | Fast path when no config defaults (`_defaults_has_any_values`) |
|
||||
| H2 | LoopGuard on every init (~2 μs) | CONFIRMED | Lazy `_ensure_loop_guard()` on first tool call |
|
||||
| H3 | Approval presets | REJECTED | No change needed |
|
||||
| H4 | `llm=` deprecation every init (+16 μs) | CONFIRMED | Warn once per process in `warn_deprecated_param` |
|
||||
| H5 | Init bloat / config resolution (~25 μs) | CONFIRMED | Partially addressed via loader fast path |
|
||||
| H6 | LiteLLM eager `LLM()` import (+3.7 s) | CONFIRMED | Deferred `_llm_init_params` + lazy `llm_instance` property |
|
||||
| H7 | `ExecutionConfig` deprecation every init | CONFIRMED | Fixed stack walk; internal SDK path cached |
|
||||
|
||||
## cProfile — Post-fix (500 warmed `model=` inits)
|
||||
|
||||
| Function | Time (ms) | Per call |
|
||||
|----------|-----------|----------|
|
||||
| `Agent.__init__` | 8 | ~16 μs |
|
||||
| `apply_config_defaults` | 1 | ~2 μs |
|
||||
| `ExecutionConfig.__post_init__` | 1 | ~2 μs (first call only walks stack) |
|
||||
| `_warnings.warn` | **0** | removed from hot path |
|
||||
|
||||
## Fixes implemented
|
||||
|
||||
1. **`praisonaiagents/config/feature_configs.py`** — Fix `ExecutionConfig.__post_init__` deprecation guard; cache internal SDK detection.
|
||||
2. **`praisonaiagents/utils/deprecation.py`** — Warn once per process for deprecated params.
|
||||
3. **`praisonaiagents/config/loader.py`** — Skip `apply_config_defaults` lookups when config has no defaults.
|
||||
4. **`praisonaiagents/agent/agent.py`** — Lazy LoopGuard, lazy LLM instance, deferred LiteLLM params.
|
||||
5. **`benchmarks/simple_benchmark.py`** — Use `model=`, add 10-iteration warmup.
|
||||
6. **`benchmarks/performance_benchmark.py`** — Use `model=` instead of deprecated `llm=`.
|
||||
|
||||
## Remaining gap (~16 μs vs ~3 μs)
|
||||
|
||||
The residual ~13 μs is primarily **`Agent.__init__` size** (~1200 lines of consolidated config resolution added since v0.1.5). Further optimisation requires a dedicated slim fast-path for `Agent(name=..., output="silent")` without full config object churn.
|
||||
|
||||
## How to reproduce
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
PYTHONWARNINGS='ignore::DeprecationWarning' python benchmarks/simple_benchmark.py
|
||||
PYTHONWARNINGS='ignore::DeprecationWarning' python benchmarks/performance_benchmark.py
|
||||
```
|
||||
@@ -120,8 +120,8 @@ def run_benchmark(iterations: int = 100) -> List[ParamResult]:
|
||||
('backstory', 'I am an AI assistant.', 'backstory="..."'),
|
||||
|
||||
# LLM configuration
|
||||
('model', 'gpt-4o-mini', 'model="gpt-4o-mini"'),
|
||||
('model', 'openai/gpt-4o-mini', 'model="openai/gpt-4o-mini" (LiteLLM)'),
|
||||
('llm', 'gpt-4o-mini', 'llm="gpt-4o-mini"'),
|
||||
('llm', 'openai/gpt-4o-mini', 'llm="openai/gpt-4o-mini" (LiteLLM)'),
|
||||
|
||||
# Tools
|
||||
('tools', [sample_tool], 'tools=[sample_tool]'),
|
||||
@@ -298,7 +298,7 @@ def save_results(results: List[ParamResult], filename: str = 'PARAM_IMPACT_RESUL
|
||||
f.write('### Best Practices\n\n')
|
||||
f.write('1. Use `output="silent"` for production workloads\n')
|
||||
f.write('2. Only enable `context=True` when needed for long conversations\n')
|
||||
f.write('3. Use `model="gpt-4o-mini"` for fastest instantiation\n')
|
||||
f.write('3. Use `llm="gpt-4o-mini"` (OpenAI SDK) for fastest instantiation\n')
|
||||
f.write('4. Avoid `memory=True` unless persistent memory is required\n')
|
||||
|
||||
f.write('\n## How to Reproduce\n\n')
|
||||
|
||||
@@ -185,7 +185,7 @@ def instantiate_praisonai_agent():
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
model="gpt-4o-mini",
|
||||
llm="gpt-4o-mini",
|
||||
tools=tools,
|
||||
output="silent"
|
||||
)
|
||||
@@ -196,7 +196,7 @@ def instantiate_praisonai_litellm_agent():
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
model="openai/gpt-4o-mini",
|
||||
llm="openai/gpt-4o-mini",
|
||||
tools=tools,
|
||||
output="silent"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,6 @@ import argparse
|
||||
|
||||
|
||||
ITERATIONS = 100
|
||||
WARMUP = 10
|
||||
|
||||
|
||||
def sample_tool(city: Literal['nyc', 'sf']):
|
||||
@@ -24,10 +23,8 @@ def sample_tool(city: Literal['nyc', 'sf']):
|
||||
return 'sunny'
|
||||
|
||||
|
||||
def measure_instantiation(create_fn, iterations=ITERATIONS, warmup=WARMUP):
|
||||
def measure_instantiation(create_fn, iterations=ITERATIONS):
|
||||
"""Measure average instantiation time in microseconds."""
|
||||
for _ in range(warmup):
|
||||
create_fn()
|
||||
times = []
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
@@ -51,11 +48,11 @@ def run_benchmark():
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
|
||||
results['PraisonAI'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', model='gpt-4o-mini', output="silent")
|
||||
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', output="silent")
|
||||
)
|
||||
|
||||
results['PraisonAI (LiteLLM)'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', model='openai/gpt-4o-mini', output="silent")
|
||||
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', output="silent")
|
||||
)
|
||||
|
||||
# Other frameworks for comparison
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example of using LLMConfig with Agent for model fallback configuration."""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config import LLMConfig
|
||||
|
||||
# Example 1: Pass LLMConfig via llm parameter
|
||||
def example_llm_param():
|
||||
"""Using LLMConfig via the llm parameter."""
|
||||
config = LLMConfig(
|
||||
model="gpt-4o",
|
||||
fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
|
||||
base_url="https://api.example.com",
|
||||
api_key="test-key"
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant",
|
||||
llm=config # Pass LLMConfig object directly
|
||||
)
|
||||
|
||||
print(f"Primary model: {agent.llm}")
|
||||
print(f"Fallback models: {agent.fallback_models}")
|
||||
print(f"Base URL: {agent.base_url}")
|
||||
print(f"API Key: {agent.api_key}")
|
||||
|
||||
# Example 2: Pass LLMConfig via model parameter
|
||||
def example_model_param():
|
||||
"""Using LLMConfig via the model parameter (preferred)."""
|
||||
config = LLMConfig(
|
||||
model="gpt-4o",
|
||||
fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
|
||||
base_url="https://api.example.com",
|
||||
api_key="test-key"
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant",
|
||||
model=config # Pass LLMConfig via model parameter
|
||||
)
|
||||
|
||||
print(f"Primary model: {agent.llm}")
|
||||
print(f"Fallback models: {agent.fallback_models}")
|
||||
print(f"Base URL: {agent.base_url}")
|
||||
print(f"API Key: {agent.api_key}")
|
||||
|
||||
# Example 3: Traditional usage still works
|
||||
def example_traditional():
|
||||
"""Traditional usage without LLMConfig (no fallback support)."""
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant",
|
||||
model="gpt-4o",
|
||||
base_url="https://api.example.com",
|
||||
api_key="test-key"
|
||||
)
|
||||
|
||||
print(f"Primary model: {agent.llm}")
|
||||
print(f"Fallback models: {agent.fallback_models}") # Will be empty
|
||||
print(f"Base URL: {agent.base_url}")
|
||||
print(f"API Key: {agent.api_key}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Example 1: LLMConfig via llm parameter ===")
|
||||
example_llm_param()
|
||||
print()
|
||||
|
||||
print("=== Example 2: LLMConfig via model parameter ===")
|
||||
example_model_param()
|
||||
print()
|
||||
|
||||
print("=== Example 3: Traditional usage (no fallback) ===")
|
||||
example_traditional()
|
||||
@@ -114,9 +114,6 @@ def _get_lazy_cache():
|
||||
# ============================================================================
|
||||
|
||||
_LAZY_IMPORTS = {
|
||||
# Version information (lazy loaded to avoid file I/O on import)
|
||||
'__version__': ('praisonaiagents._version', '__version__'),
|
||||
|
||||
# Tools (moved from eager imports for lazy loading)
|
||||
'Tools': ('praisonaiagents.tools.tools', 'Tools'),
|
||||
'BaseTool': ('praisonaiagents.tools.base', 'BaseTool'),
|
||||
@@ -178,12 +175,6 @@ _LAZY_IMPORTS = {
|
||||
'loop': ('praisonaiagents.workflows', 'loop'),
|
||||
'repeat': ('praisonaiagents.workflows', 'repeat'),
|
||||
'when': ('praisonaiagents.workflows', 'when'),
|
||||
'if_': ('praisonaiagents.workflows', 'if_'),
|
||||
'include': ('praisonaiagents.workflows', 'include'),
|
||||
'Include': ('praisonaiagents.workflows', 'Include'),
|
||||
'MAX_NESTING_DEPTH': ('praisonaiagents.workflows', 'MAX_NESTING_DEPTH'),
|
||||
'YAMLWorkflowParser': ('praisonaiagents.workflows', 'YAMLWorkflowParser'),
|
||||
'WorkflowHooksConfig': ('praisonaiagents.workflows', 'WorkflowHooksConfig'),
|
||||
|
||||
# Conditions (Protocol-driven condition evaluation)
|
||||
'ConditionProtocol': ('praisonaiagents.conditions.protocols', 'ConditionProtocol'),
|
||||
@@ -236,15 +227,8 @@ _LAZY_IMPORTS = {
|
||||
'CliBackendResult': ('praisonaiagents.cli_backend.protocols', 'CliBackendResult'),
|
||||
'CliBackendDelta': ('praisonaiagents.cli_backend.protocols', 'CliBackendDelta'),
|
||||
|
||||
# Runtime Selection (Auto harness selection and registry)
|
||||
'AgentRuntimeProtocol': ('praisonaiagents.runtime.protocols', 'AgentRuntimeProtocol'),
|
||||
'resolve_runtime': ('praisonaiagents.runtime.registry', 'resolve_runtime'),
|
||||
'register_runtime': ('praisonaiagents.runtime.registry', 'register_runtime'),
|
||||
'list_runtimes': ('praisonaiagents.runtime.registry', 'list_runtimes'),
|
||||
|
||||
# Agent classes
|
||||
'Agent': ('praisonaiagents.agent.agent', 'Agent'),
|
||||
'RetryBackoffConfig': ('praisonaiagents.agent.retry_utils', 'RetryBackoffConfig'),
|
||||
'BudgetExceededError': ('praisonaiagents.errors', 'BudgetExceededError'),
|
||||
|
||||
# Error hierarchy - structured exception handling
|
||||
@@ -314,12 +298,6 @@ _LAZY_IMPORTS = {
|
||||
# Knowledge
|
||||
'Knowledge': ('praisonaiagents.knowledge.knowledge', 'Knowledge'),
|
||||
'Chunking': ('praisonaiagents.knowledge.chunking', 'Chunking'),
|
||||
'KnowledgeStoreProtocol': ('praisonaiagents.knowledge.protocols', 'KnowledgeStoreProtocol'),
|
||||
'ScopeRequiredError': ('praisonaiagents.knowledge.protocols', 'ScopeRequiredError'),
|
||||
'BackendNotAvailableError': ('praisonaiagents.knowledge.protocols', 'BackendNotAvailableError'),
|
||||
'FileTracker': ('praisonaiagents.knowledge.indexing', 'FileTracker'),
|
||||
'IndexResult': ('praisonaiagents.knowledge.indexing', 'IndexResult'),
|
||||
'CorpusStats': ('praisonaiagents.knowledge.indexing', 'CorpusStats'),
|
||||
|
||||
# FastContext
|
||||
'FastContext': ('praisonaiagents.context.fast', 'FastContext'),
|
||||
@@ -343,24 +321,9 @@ _LAZY_IMPORTS = {
|
||||
'SkillProperties': ('praisonaiagents.skills', 'SkillProperties'),
|
||||
'SkillMetadata': ('praisonaiagents.skills', 'SkillMetadata'),
|
||||
'SkillLoader': ('praisonaiagents.skills', 'SkillLoader'),
|
||||
'validate': ('praisonaiagents.skills', 'validate'),
|
||||
'validate_metadata': ('praisonaiagents.skills', 'validate_metadata'),
|
||||
'discover_skills': ('praisonaiagents.skills', 'discover_skills'),
|
||||
'load_skill': ('praisonaiagents.skills', 'load_skill'),
|
||||
'EnforcementLevel': ('praisonaiagents.skills', 'EnforcementLevel'),
|
||||
'SkillState': ('praisonaiagents.skills', 'SkillState'),
|
||||
|
||||
# Memory
|
||||
'Memory': ('praisonaiagents.memory.memory', 'Memory'),
|
||||
'AutoMemory': ('praisonaiagents.memory', 'AutoMemory'),
|
||||
'ChromaMemory': ('praisonaiagents.memory.adapters.factories', 'create_chroma_memory_adapter'),
|
||||
'register_memory_adapter': ('praisonaiagents.memory.adapters', 'register_memory_adapter'),
|
||||
'register_memory_factory': ('praisonaiagents.memory.adapters', 'register_memory_factory'),
|
||||
'get_memory_adapter': ('praisonaiagents.memory.adapters', 'get_memory_adapter'),
|
||||
'list_memory_adapters': ('praisonaiagents.memory.adapters', 'list_memory_adapters'),
|
||||
'add_memory_adapter': ('praisonaiagents.memory.adapters.registry', 'add_memory_adapter'),
|
||||
'add_memory_factory': ('praisonaiagents.memory.adapters.registry', 'add_memory_factory'),
|
||||
'has_memory_adapter': ('praisonaiagents.memory.adapters', 'has_memory_adapter'),
|
||||
|
||||
# Planning
|
||||
'Plan': ('praisonaiagents.planning', 'Plan'),
|
||||
@@ -510,7 +473,7 @@ _LAZY_IMPORTS = {
|
||||
'GatewayClientProtocol': ('praisonaiagents.gateway.protocols', 'GatewayClientProtocol'),
|
||||
'GatewayEvent': ('praisonaiagents.gateway.protocols', 'GatewayEvent'),
|
||||
'GatewayMessage': ('praisonaiagents.gateway.protocols', 'GatewayMessage'),
|
||||
'GatewayEventType': ('praisonaiagents.gateway.protocols', 'EventType'),
|
||||
'EventType': ('praisonaiagents.gateway.protocols', 'EventType'),
|
||||
'GatewayConfig': ('praisonaiagents.gateway.config', 'GatewayConfig'),
|
||||
'SessionConfig': ('praisonaiagents.gateway.config', 'SessionConfig'),
|
||||
|
||||
@@ -534,10 +497,6 @@ _LAZY_IMPORTS = {
|
||||
|
||||
# Managed backend protocol (implementation + config in praisonai wrapper)
|
||||
'ManagedBackendProtocol': ('praisonaiagents.agent.protocols', 'ManagedBackendProtocol'),
|
||||
|
||||
# Framework adapter protocol (implementations in praisonai wrapper / praisonai-frameworks)
|
||||
'FrameworkAdapterProtocol': ('praisonaiagents.frameworks.protocols', 'FrameworkAdapterProtocol'),
|
||||
'BaseFrameworkAdapter': ('praisonaiagents.frameworks.base', 'BaseFrameworkAdapter'),
|
||||
|
||||
# Managed agent events (provider-agnostic)
|
||||
'ManagedEvent': ('praisonaiagents.managed.events', 'ManagedEvent'),
|
||||
@@ -816,12 +775,8 @@ def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
|
||||
# ============================================================================
|
||||
|
||||
__all__ = [
|
||||
# Version information
|
||||
'__version__',
|
||||
|
||||
# Core classes - the essentials
|
||||
'Agent',
|
||||
'RetryBackoffConfig',
|
||||
'AgentTeam', # Primary class for multi-agent coordination (v1.0+)
|
||||
'AgentManager', # Silent alias for AgentTeam
|
||||
'Agents', # Deprecated alias for AgentTeam (emits warning)
|
||||
|
||||
File renamed without changes.
@@ -111,3 +111,32 @@ def _get_plugins_list() -> list:
|
||||
|
||||
PLUGINS_ENABLED = _get_plugins_enabled()
|
||||
PLUGINS_LIST = _get_plugins_list()
|
||||
|
||||
|
||||
def missing_dependency_error(
|
||||
package_name: str,
|
||||
extra_name: Optional[str] = None,
|
||||
feature_name: Optional[str] = None
|
||||
) -> ImportError:
|
||||
"""
|
||||
Create a standardized ImportError for missing optional dependencies.
|
||||
|
||||
Args:
|
||||
package_name: The name of the missing package (e.g., 'litellm')
|
||||
extra_name: The pip extra to install (e.g., 'llm' for [llm])
|
||||
feature_name: Human-readable feature name (e.g., 'LLM support')
|
||||
|
||||
Returns:
|
||||
ImportError with helpful install instructions
|
||||
"""
|
||||
if extra_name:
|
||||
install_cmd = f"pip install 'praisonaiagents[{extra_name}]'"
|
||||
else:
|
||||
install_cmd = f"pip install {package_name}"
|
||||
|
||||
feature_desc = f" for {feature_name}" if feature_name else ""
|
||||
|
||||
return ImportError(
|
||||
f"{package_name} is required{feature_desc} but not installed. "
|
||||
f"Please install with: {install_cmd}"
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Resolver helper functions for consolidated parameters.
|
||||
|
||||
These helpers wrap the canonical resolver for specific parameter types,
|
||||
handling special cases like callable validators for guardrails.
|
||||
"""
|
||||
|
||||
from typing import Any, Type
|
||||
|
||||
from .config.param_resolver import resolve, ArrayMode
|
||||
from .config.presets import GUARDRAIL_PRESETS
|
||||
|
||||
|
||||
def resolve_guardrails(value: Any, config_class: Type) -> Any:
|
||||
"""
|
||||
Resolve guardrails parameter.
|
||||
|
||||
Handles special case of callable validators which have highest precedence.
|
||||
|
||||
Args:
|
||||
value: The guardrails parameter value
|
||||
config_class: GuardrailConfig class
|
||||
|
||||
Returns:
|
||||
Resolved config, callable, or string
|
||||
"""
|
||||
# Handle None
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Handle False (disabled)
|
||||
if value is False:
|
||||
return None
|
||||
|
||||
# Handle callable (highest precedence after None check)
|
||||
# This includes functions, lambdas, and callable objects
|
||||
if callable(value) and not isinstance(value, type):
|
||||
return value
|
||||
|
||||
# Use canonical resolver for everything else
|
||||
return resolve(
|
||||
value=value,
|
||||
param_name="guardrails",
|
||||
config_class=config_class,
|
||||
presets=GUARDRAIL_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
string_mode="llm_prompt",
|
||||
default=None,
|
||||
)
|
||||
@@ -1,51 +0,0 @@
|
||||
"""
|
||||
Version utilities for praisonaiagents package.
|
||||
|
||||
This module provides version information by reading from pyproject.toml
|
||||
to avoid duplication and ensure the single source of truth.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
"""
|
||||
Get the version string from pyproject.toml.
|
||||
|
||||
Returns:
|
||||
str: Version string (e.g., "1.6.52")
|
||||
|
||||
Raises:
|
||||
RuntimeError: If version cannot be found or parsed
|
||||
"""
|
||||
try:
|
||||
# Get the package root (praisonaiagents directory)
|
||||
package_root = Path(__file__).parent
|
||||
# Go up to src/praisonai-agents directory
|
||||
pyproject_path = package_root.parent / "pyproject.toml"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
raise RuntimeError(f"pyproject.toml not found at {pyproject_path}")
|
||||
|
||||
content = pyproject_path.read_text()
|
||||
|
||||
# Look for version = "X.Y.Z" pattern
|
||||
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
||||
if not match:
|
||||
raise RuntimeError("Version not found in pyproject.toml")
|
||||
|
||||
return match.group(1)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to "unknown" to avoid breaking imports
|
||||
import warnings
|
||||
warnings.warn(
|
||||
f"Failed to read version from pyproject.toml: {e}. Using 'unknown'.",
|
||||
RuntimeWarning
|
||||
)
|
||||
return "unknown"
|
||||
|
||||
|
||||
# Cache the version to avoid reading the file multiple times
|
||||
__version__ = get_version()
|
||||
@@ -1,71 +0,0 @@
|
||||
"""
|
||||
Shared lazy-loaders for display utilities used across the Agent class and mixins.
|
||||
|
||||
These helpers defer importing rich and the heavy ``..main`` display module until
|
||||
first use, which keeps import time low for silent mode and avoids circular
|
||||
imports during package initialisation. Results are cached in module globals and
|
||||
protected by per-resource locks using double-checked locking, so each import is
|
||||
resolved at most once across threads. Independent resources use separate locks so
|
||||
that a slow first import of one does not stall initialisation of the others.
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
# Lazy-loaded modules (populated on first use, each protected by its own lock)
|
||||
_console_lock = threading.Lock()
|
||||
_live_lock = threading.Lock()
|
||||
_main_module_lock = threading.Lock()
|
||||
_rich_console = None
|
||||
_rich_live = None
|
||||
_main_module = None
|
||||
|
||||
|
||||
def _get_console():
|
||||
"""Lazy load rich.console.Console (thread-safe)."""
|
||||
global _rich_console
|
||||
if _rich_console is None:
|
||||
with _console_lock:
|
||||
if _rich_console is None:
|
||||
from rich.console import Console
|
||||
_rich_console = Console
|
||||
return _rich_console
|
||||
|
||||
|
||||
def _get_live():
|
||||
"""Lazy load rich.live.Live (thread-safe)."""
|
||||
global _rich_live
|
||||
if _rich_live is None:
|
||||
with _live_lock:
|
||||
if _rich_live is None:
|
||||
from rich.live import Live
|
||||
_rich_live = Live
|
||||
return _rich_live
|
||||
|
||||
|
||||
def _get_display_functions():
|
||||
"""Lazy load display functions from main module (thread-safe)."""
|
||||
global _main_module
|
||||
if _main_module is None:
|
||||
with _main_module_lock:
|
||||
if _main_module is None:
|
||||
from ..main import (
|
||||
display_error,
|
||||
display_instruction,
|
||||
display_interaction,
|
||||
display_generating,
|
||||
display_self_reflection,
|
||||
ReflectionOutput,
|
||||
adisplay_instruction,
|
||||
execute_sync_callback
|
||||
)
|
||||
_main_module = {
|
||||
'display_error': display_error,
|
||||
'display_instruction': display_instruction,
|
||||
'display_interaction': display_interaction,
|
||||
'display_generating': display_generating,
|
||||
'display_self_reflection': display_self_reflection,
|
||||
'ReflectionOutput': ReflectionOutput,
|
||||
'adisplay_instruction': adisplay_instruction,
|
||||
'execute_sync_callback': execute_sync_callback,
|
||||
}
|
||||
return _main_module
|
||||
File diff suppressed because it is too large.
Load diff
@@ -6,15 +6,113 @@ Extracted from agent.py for better modularity and maintainability.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Any, Dict, List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatHandlerMixin:
|
||||
"""Mixin reserved for chat coordination on the Agent class.
|
||||
|
||||
This is a documented placeholder retained for the public mixin surface.
|
||||
Concrete chat logic (``chat``/``achat``, history management, and response
|
||||
processing) lives in ``ChatMixin``/``SessionManagerMixin``.
|
||||
"""Mixin providing chat handling methods for the Agent class.
|
||||
|
||||
This mixin focuses on high-level chat coordination and response processing,
|
||||
while the actual LLM interaction is handled by ChatMixin.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _format_chat_response(self, response: str, metadata: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""Format chat response with any necessary post-processing.
|
||||
|
||||
Args:
|
||||
response: Raw response from the LLM
|
||||
metadata: Optional metadata about the response
|
||||
|
||||
Returns:
|
||||
Formatted response string
|
||||
"""
|
||||
if not response:
|
||||
return ""
|
||||
|
||||
# Basic response formatting - can be extended with additional logic
|
||||
formatted = response.strip()
|
||||
|
||||
# Add any metadata-based formatting if needed
|
||||
if metadata and metadata.get('add_timestamp'):
|
||||
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
formatted = f"[{timestamp}] {formatted}"
|
||||
|
||||
return formatted
|
||||
|
||||
def _validate_chat_input(self, prompt: str) -> bool:
|
||||
"""Validate chat input before processing.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt to validate
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
if not prompt or not isinstance(prompt, str):
|
||||
logger.warning("Invalid chat input: prompt must be a non-empty string")
|
||||
return False
|
||||
|
||||
if len(prompt.strip()) == 0:
|
||||
logger.warning("Invalid chat input: prompt cannot be empty or whitespace only")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _prepare_chat_context(self, prompt: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Prepare context for chat processing.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
**kwargs: Additional context parameters
|
||||
|
||||
Returns:
|
||||
Context dictionary for chat processing
|
||||
"""
|
||||
context = {
|
||||
'prompt': prompt,
|
||||
'timestamp': time.time(),
|
||||
'agent_name': getattr(self, 'name', None) or 'Agent',
|
||||
}
|
||||
|
||||
# Add any additional context from kwargs
|
||||
context.update(kwargs)
|
||||
|
||||
return context
|
||||
|
||||
def _handle_chat_error(self, error: Exception, prompt: str) -> str:
|
||||
"""Handle chat processing errors gracefully.
|
||||
|
||||
Args:
|
||||
error: The exception that occurred
|
||||
prompt: The original prompt that caused the error
|
||||
|
||||
Returns:
|
||||
Error response message
|
||||
"""
|
||||
logger.error(f"Chat error occurred: {error}", exc_info=True)
|
||||
|
||||
# Return a user-friendly error message without exposing internal details.
|
||||
# Include a timestamp reference so users can correlate with server logs.
|
||||
error_ref = time.strftime('%Y%m%d%H%M%S')
|
||||
return f"I encountered an error while processing your request (ref: {error_ref}). Please try again."
|
||||
|
||||
def _log_chat_interaction(self, prompt: str, response: str, duration: Optional[float] = None):
|
||||
"""Log chat interaction for debugging and analytics.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
response: Generated response
|
||||
duration: Processing duration in seconds
|
||||
"""
|
||||
agent_name = getattr(self, 'name', None) or 'Agent'
|
||||
duration_str = f" (took {duration:.2f}s)" if duration else ""
|
||||
|
||||
logger.debug(f"[{agent_name}] Chat interaction{duration_str}")
|
||||
logger.debug(f"[{agent_name}] Prompt: {prompt[:100]}{'...' if len(prompt) > 100 else ''}")
|
||||
logger.debug(f"[{agent_name}] Response: {response[:100]}{'...' if len(response) > 100 else ''}")
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 1212 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user