Compare commits

..
Author SHA1 Message Date
DhivyaandDhivya-Bharathy 8536325cc1 fix(examples): phase-3 API alignment for consolidated params on v4.6.52 (#1864)
Co-authored-by: Dhivya-Bharathy <[email protected]>
2026-06-08 07:47:16 +01:00
praisonai-triage-agent[bot]andMervin Praison 8b4db44159 fix: resolve Gemini reviewer issues - remove redundant arguments and fix temp file pollution
- Remove redundant argument from workflow.run() in basic_workflow.py since variables are already set in constructor
- Fix temp file pollution by using system temp directory instead of repository directory in auto_rag_agent_basic.py and knowledge-agents-task.py

Co-authored-by: Mervin Praison <[email protected]>
2026-06-05 10:18:45 +00:00
Dhivya-Bharathy 3d3fdd045e fix(examples): align AgentTeam memory and knowledge configs with v4.6.52 API 2026-06-05 15:37:37 +05:30
praisonai-triage-agent[bot]andMervin Praison bfa9d0ca48 fix: correct TypeError in 3 example files by using proper parameter names
- Fix MemoryConfig(memory=...) → user_id=... in agent_centric_api.py
- Fix Session(memory=...) → user_id=... in session-example.py
- Fix build_context_for_task(memory=...) → user_id=... in memory-quality-example.py

Co-authored-by: Mervin Praison <[email protected]>
2026-06-05 11:26:13 +05:30
Dhivya-Bharathy 3f14324036 fix(examples): resolve syntax errors and align with consolidated-params API 2026-06-04 16:30:02 +05:30
790 changed files with 15081 additions and 105325 deletions

No files matched your search

+2 -36
View File
@@ -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,34 +0,0 @@
#!/usr/bin/env node
const chain = require('./bot-pr-review-chain.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
assert('chainKickPosted requires both markers', chain.chainKickPosted([
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
{ user: { login: 'MervinPraison' }, body: '/review' },
]));
assert('chainKickPosted false with only coderabbit', !chain.chainKickPosted([
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
]));
assert('coderabbitKickPosted detects coderabbit', chain.coderabbitKickPosted([
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
]));
assert('qodoKickPosted detects qodo', chain.qodoKickPosted([
{ user: { login: 'github-actions[bot]' }, body: '/review' },
]));
assert('ignores coderabbit bot comment', !chain.coderabbitKickPosted([
{ user: { login: 'coderabbitai[bot]' }, body: '@coderabbitai review' },
]));
assert('bot PR author match', chain.isBotOpenedPr({ user: { login: 'praisonai-triage-agent[bot]' } }));
assert('generic bot type match', chain.isBotOpenedPr({ user: { login: 'my-bot', type: 'Bot' } }));
assert('human author excluded', !chain.isBotOpenedPr({ user: { login: 'MervinPraison', type: 'User' } }));
process.exit(failed ? 1 : 0);
-153
View File
@@ -1,153 +0,0 @@
/**
* Idempotent CodeRabbit/Qodo kick for bot-opened PRs.
* @see .github/workflows/auto-pr-comment.yml, claude.yml, bot-pr-recovery.yml
*/
const KICK_AUTHORS = new Set(['MervinPraison', 'github-actions[bot]']);
const BOT_PR_AUTHORS = new Set(['praisonai-triage-agent[bot]', 'github-actions[bot]']);
async function listAllComments(github, owner, repo, issueNumber) {
if (typeof github.paginate === 'function') {
return github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
}
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
return data;
}
function kickAuthored(comment, marker) {
return (
KICK_AUTHORS.has(comment.user?.login) &&
(comment.body || '').includes(marker)
);
}
function coderabbitKickPosted(comments) {
return comments.some((c) => kickAuthored(c, '@coderabbitai review'));
}
function qodoKickPosted(comments) {
return comments.some((c) => kickAuthored(c, '/review'));
}
function chainKickPosted(comments) {
return coderabbitKickPosted(comments) && qodoKickPosted(comments);
}
function isBotOpenedPr(pr) {
if (BOT_PR_AUTHORS.has(pr.user?.login)) return true;
return pr.user?.type === 'Bot';
}
async function kickReviewChain(github, owner, repo, prNumber, core, preFetchedComments = null) {
const comments = preFetchedComments || await listAllComments(github, owner, repo, prNumber);
const needCoderabbit = !coderabbitKickPosted(comments);
const needQodo = !qodoKickPosted(comments);
if (!needCoderabbit && !needQodo) {
core?.info?.(`Review chain already kicked on PR #${prNumber}`);
return { kicked: false, reason: 'already_kicked' };
}
if (needCoderabbit) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: '@coderabbitai review',
});
}
if (needQodo) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: '/review',
});
}
core?.info?.(`Kicked review chain for PR #${prNumber} (coderabbit=${needCoderabbit}, qodo=${needQodo})`);
return { kicked: true, coderabbit: needCoderabbit, qodo: needQodo };
}
async function findOpenPrForIssue(github, owner, repo, issueNumber) {
const prefix = `claude/issue-${issueNumber}-`;
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
sort: 'created',
direction: 'desc',
per_page: 30,
});
return (
prs.find(
(p) => (p.head?.ref || '').startsWith(prefix) && isBotOpenedPr(p)
) || null
);
}
async function kickReviewChainForIssue(github, owner, repo, issueNumber, core) {
const pr = await findOpenPrForIssue(github, owner, repo, issueNumber);
if (!pr) {
core?.info?.(`No open PR for issue #${issueNumber}, skipping review kick`);
return { kicked: false, reason: 'no_pr' };
}
const result = await kickReviewChain(github, owner, repo, pr.number, core);
return { ...result, prNumber: pr.number };
}
async function recoverStalledBotPrs(github, owner, repo, options, core) {
const { prNumber = null, minAgeMs = 10 * 60 * 1000, maxRecover = 10 } = options || {};
let prs;
if (prNumber) {
const { data } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
prs = [data];
} else if (typeof github.paginate === 'function') {
prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
} else {
const { data } = await github.rest.pulls.list({ owner, repo, state: 'open', per_page: 100 });
prs = data;
}
const cutoff = Date.now() - minAgeMs;
let recovered = 0;
for (const pr of prs) {
if (recovered >= maxRecover) break;
if (!isBotOpenedPr(pr)) continue;
if (!prNumber && new Date(pr.created_at).getTime() > cutoff) continue;
const comments = await listAllComments(github, owner, repo, pr.number);
if (chainKickPosted(comments)) continue;
await kickReviewChain(github, owner, repo, pr.number, core, comments);
recovered += 1;
}
core?.info?.(`Recovery complete (${recovered} PR(s) kicked)`);
return recovered;
}
module.exports = {
KICK_AUTHORS,
BOT_PR_AUTHORS,
listAllComments,
chainKickPosted,
coderabbitKickPosted,
qodoKickPosted,
isBotOpenedPr,
kickReviewChain,
findOpenPrForIssue,
kickReviewChainForIssue,
recoverStalledBotPrs,
};
-115
View File
@@ -1,115 +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'));
// 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));
// 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);
process.exit(failed ? 1 : 0);
-706
View File
@@ -1,706 +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 POST_PUSH_BUFFER_MS = 5 * 60 * 1000;
const ALLOWED_MERGE_STATES = new Set(['CLEAN', 'UNSTABLE']);
const AGENT_PY_MAX_AUTO_LINES = 100;
const PR_MAX_AUTO_ADDITIONS = 800;
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 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)));
}
async function resolvePrNumberFromWorkflowRun(github, owner, repo, workflowRun) {
const linked = workflowRun.pull_requests || [];
if (linked.length > 0 && linked[0].number) return linked[0].number;
const branch = workflowRun.head_branch;
if (!branch) 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 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)) {
reasons.push('recent @claude within 35min');
}
if (!skipGlobalClaudeRunCheck && (await hasInProgressClaudeAssistant(github, owner, repo, prNumber))) {
reasons.push('claude.yml in progress');
}
const headTime = new Date(ctx.headPushedAt).getTime();
if (Date.now() - headTime < POST_PUSH_BUFFER_MS) reasons.push('post-push buffer (<5min)');
if (!finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt)) {
if (!hasFinalClaudeReviewTrigger(ctx.comments)) {
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);
const checksOk = await allChecksGreenOnSha(github, owner, repo, ctx.headSha, core);
if (!checksOk) reasons.push('CI not green on HEAD');
if (ctx.pr.mergeable === false) reasons.push('not mergeable');
const ready = reasons.length === 0;
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,
resolvePrNumberFromWorkflowRun,
WORKFLOW_ONLY_LABEL,
getAgentPyChange,
getAgentPyChangeFromFiles,
manualReviewReasonForAgentPy,
countNewAgentParams,
listPullFiles,
touchesSdk,
hasManualOnlyLabel,
sensitivePathReasons,
prSizeReasons,
missingTestsReason,
secretScanReasons,
sdkTestChecksReason,
listAllComments,
getHeadCommitDate,
isStaleFinalAfterPush,
needsStaleFinalRecovery,
shouldSkipFinalRecovery,
FINAL_CLAUDE_REVIEW_BODY,
loadPrContext,
evaluatePipelineQuiescent,
selectMergeGateCandidates,
findMergeGateVerdict,
};
+45 -147
View File
@@ -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});
@@ -302,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
@@ -323,7 +313,7 @@ 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}`);
@@ -409,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
@@ -430,7 +417,7 @@ 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}`);
@@ -466,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)
@@ -484,116 +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 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;
}
const finalBody = mergeGate.FINAL_CLAUDE_REVIEW_BODY;
if (hasFinal && isStale) {
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}`);
# 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;
}
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
@@ -612,16 +489,37 @@ jobs:
permissions:
pull-requests: 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;
// 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.');
-43
View File
@@ -1,43 +0,0 @@
name: Bot PR Recovery
# Belt-and-braces: kick CodeRabbit/Qodo when pull_request:opened never fired for bot PRs.
on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch:
inputs:
pr_number:
description: 'Specific PR number to kick (optional)'
required: false
type: string
permissions:
pull-requests: write
issues: write
jobs:
recover:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Find and kick stalled bot PRs
uses: actions/github-script@v7
env:
PR_NUMBER_INPUT: ${{ github.event.inputs.pr_number }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const manual = (process.env.PR_NUMBER_INPUT || '').trim();
const prNumber = manual ? parseInt(manual, 10) : null;
if (manual && Number.isNaN(prNumber)) {
throw new Error(`Invalid pr_number: ${manual}`);
}
await chain.recoverStalledBotPrs(github, owner, repo, { prNumber }, core);
-471
View File
@@ -1,471 +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:
- cron: '0 2,8,14,20 * * *'
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: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
MAX_CANDIDATES: 5
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 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`.',
});
}
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';
await github.rest.pulls.merge({
owner, repo, pull_number: prNumber, merge_method: method,
});
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: ['auto-merged-by-gate'],
});
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 -100
View File
@@ -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]' ||
@@ -153,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
@@ -223,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):
@@ -279,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.
@@ -314,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:*)
@@ -336,19 +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
);
@@ -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
+2
View File
@@ -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
+3
View File
@@ -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
+4 -4
View File
@@ -60,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
@@ -152,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
-184
View File
@@ -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();
-126
View File
@@ -1,126 +0,0 @@
name: Nightly Release Gate
# Preflight checks before dispatching PyPI Release (patch bump).
# Runs nightly at 00:00 UTC (midnight).
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
permissions:
contents: read
actions: write
concurrency:
group: nightly-release-gate
cancel-in-progress: false
jobs:
preflight:
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- name: Checkout main
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: main
fetch-depth: 0
- name: Preflight — path changes and CI green
id: preflight
run: |
set -euo pipefail
git fetch --tags origin
LAST_TAG=$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)
if [ -z "$LAST_TAG" ]; then
echo "No v* tag found — skip automated release (first release must be manual)"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "reason=no_tag" >> "$GITHUB_OUTPUT"
exit 0
fi
CHANGED=$(git diff --name-only "${LAST_TAG}" HEAD -- \
src/praisonai \
src/praisonai-agents || true)
if [ -z "$CHANGED" ]; then
echo "No changes in src/praisonai or src/praisonai-agents since ${LAST_TAG} — skip"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "reason=no_path_changes" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Changes since ${LAST_TAG}:"
echo "$CHANGED"
HEAD_SHA=$(git rev-parse HEAD)
echo "main HEAD: ${HEAD_SHA}"
GREEN_SHA=$(gh run list \
--repo "${GITHUB_REPOSITORY}" \
--workflow "Core Tests" \
--branch main \
--status success \
--json headSha \
--jq '.[0].headSha // empty')
if [ "$GREEN_SHA" != "$HEAD_SHA" ]; then
echo "main@${HEAD_SHA} is not covered by a green Core Tests run (last green: ${GREEN_SHA:-none}) — skip"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "reason=ci_not_green" >> "$GITHUB_OUTPUT"
exit 0
fi
IN_PROGRESS=$(gh run list \
--repo "${GITHUB_REPOSITORY}" \
--workflow "Core Tests" \
--branch main \
--json headSha,status \
--jq "[.[] | select(.headSha == \"${HEAD_SHA}\" and .status == \"in_progress\")] | length")
if [ "${IN_PROGRESS:-0}" != "0" ]; then
echo "Core Tests still in progress on ${HEAD_SHA} — skip"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "reason=ci_in_progress" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Preflight passed — ready to dispatch patch release"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "reason=ready" >> "$GITHUB_OUTPUT"
echo "last_tag=${LAST_TAG}" >> "$GITHUB_OUTPUT"
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
- name: Dry run summary
if: inputs.dry_run == true
run: |
echo "Dry run — preflight only, no pypi-release dispatch."
echo "should_release=${{ steps.preflight.outputs.should_release }}"
echo "reason=${{ steps.preflight.outputs.reason }}"
if [ "${{ steps.preflight.outputs.should_release }}" = "true" ]; then
echo "Would dispatch: pypi-release.yml bump=patch source=nightly"
fi
- name: Dispatch PyPI Release (patch)
if: steps.preflight.outputs.should_release == 'true' && inputs.dry_run != true
run: |
set -euo pipefail
gh workflow run pypi-release.yml \
--repo "${GITHUB_REPOSITORY}" \
--ref main \
-f bump=patch \
-f source=nightly \
-f dry_run=false
echo "Dispatched pypi-release.yml (patch, source=nightly)"
echo "Approve the pypi environment when prompted."
-305
View File
@@ -1,305 +0,0 @@
name: PyPI Release
# Requires a GitHub Environment named "pypi" (Settings → Environments → pypi)
# with required reviewers before secrets are exposed to this job.
on:
workflow_dispatch:
inputs:
bump:
description: 'Version bump type for both packages'
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: 'Publish agents only'
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
source:
description: 'Trigger source (audit trail)'
required: false
default: manual
type: choice
options:
- manual
- nightly
permissions:
contents: write
concurrency:
group: pypi-release
cancel-in-progress: false
jobs:
release:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: 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 "Bump: ${{ inputs.bump }}"
echo "Dry run: ${{ inputs.dry_run }}"
- name: Compute release versions
id: versions
env:
BUMP: ${{ inputs.bump }}
AGENTS_OVERRIDE: ${{ inputs.agents_version }}
WRAPPER_OVERRIDE: ${{ inputs.wrapper_version }}
SKIP_AGENTS: ${{ inputs.skip_agents }}
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()
validate_version("agents_version", agents_override)
validate_version("wrapper_version", wrapper_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)
if not SEMVER.match(current_agents):
print(f"Invalid current agents version: {current_agents}", file=sys.stderr)
sys.exit(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)
if not SEMVER.match(current_wrapper):
print(f"Invalid current wrapper version: {current_wrapper}", file=sys.stderr)
sys.exit(1)
skip_agents = os.environ.get("SKIP_AGENTS", "false").lower() == "true"
if agents_override:
agents_version = agents_override
elif skip_agents:
agents_version = current_agents
else:
agents_version = bump_version(current_agents)
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"wrapper_version={wrapper_version}\n")
fh.write(f"current_agents={current_agents}\n")
fh.write(f"current_wrapper={current_wrapper}\n")
print(f"Agents: {current_agents} -> {agents_version}")
print(f"Wrapper: {current_wrapper} -> {wrapper_version}")
PY
- 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 "Wrapper version: ${{ steps.versions.outputs.wrapper_version }}"
echo ""
if [ "${{ inputs.skip_agents }}" != "true" ]; then
echo "Would publish praisonaiagents ${{ steps.versions.outputs.agents_version }}"
echo "Would commit src/praisonai-agents/pyproject.toml and uv.lock"
fi
if [ "${{ inputs.skip_wrapper }}" != "true" ]; then
echo "Would run bump_and_release.py ${{ steps.versions.outputs.wrapper_version }} --agents ${{ steps.versions.outputs.agents_version }}"
echo "Would uv publish praisonai ${{ steps.versions.outputs.wrapper_version }}"
fi
- name: Publish praisonaiagents
if: inputs.dry_run != true && inputs.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
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
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: Bump and release wrapper
if: inputs.dry_run != true && inputs.skip_wrapper != true
run: |
set -euo pipefail
python src/praisonai/scripts/bump_and_release.py "${{ steps.versions.outputs.wrapper_version }}" \
--agents "${{ steps.versions.outputs.agents_version }}" \
--wait --max-wait 600 --no-add-all
- name: Publish praisonai wrapper to PyPI
if: inputs.dry_run != true && inputs.skip_wrapper != true
working-directory: src/praisonai
run: uv publish
- name: Wait for praisonai on PyPI
if: inputs.dry_run != true && inputs.skip_wrapper != true
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 }}"
WRAPPER="${{ steps.versions.outputs.wrapper_version }}"
if [ "${{ inputs.skip_agents }}" != "true" ]; then
echo "✅ praisonaiagents==${AGENTS}"
fi
if [ "${{ inputs.skip_wrapper }}" != "true" ]; then
echo "✅ praisonai==${WRAPPER}"
fi
+2 -2
View File
@@ -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:
+2 -5
View File
@@ -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
+12 -3
View File
@@ -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,31 +0,0 @@
name: Sync Claude Secrets to PraisonAIUI
on:
workflow_dispatch:
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Sync secrets to PraisonAIUI
env:
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
TARGET: MervinPraison/PraisonAIUI
CLAUDE_APP_ID: ${{ secrets.CLAUDE_APP_ID }}
CLAUDE_APP_PRIVATE_KEY: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
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"
+4 -2
View File
@@ -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
+2 -2
View File
@@ -22,8 +22,8 @@ jobs:
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:
+8 -4
View File
@@ -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
@@ -79,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
+4 -2
View File
@@ -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
+16 -16
View File
@@ -45,8 +45,8 @@ jobs:
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:
@@ -105,8 +105,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:
@@ -187,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
@@ -259,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
@@ -318,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
@@ -377,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
@@ -436,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
@@ -495,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
+4 -2
View File
@@ -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
-3
View File
@@ -88,9 +88,6 @@ issue_details.json
.internal
.praisonai
# Backup files
*.bak
# Rust / Cargo
target/
Cargo.lock
-10
View File
@@ -1,10 +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/, heavy implementations in praisonai/
-437
View File
@@ -1,437 +0,0 @@
# PraisonAI Architecture
> **Last updated:** 2026-06-24
>
> Strategic architecture document for PraisonAI — a multi-agent AI framework.
> Covers system design, runtime architecture, data contracts, reliability,
> observability, and the 2-quarter road map.
---
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [System Overview](#2-system-overview)
3. [Layered Architecture](#3-layered-architecture)
4. [Core Data Contracts](#4-core-data-contracts)
5. [Reliability by Design](#5-reliability-by-design)
6. [Observability & Telemetry](#6-observability--telemetry)
7. [Replay & Checkpointing](#7-replay--checkpointing)
8. [Implementation Roadmap](#8-implementation-roadmap)
9. [Success Metrics](#9-success-metrics)
---
## 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. 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 the primary runtime and is organised as follows:
- `src/praisonai/` — Core runtime and agent abstractions
- `agents/` — Agent types (chat, code, realtime, audio, vision, etc.)
- `llm/` — Model runtime with provider routing, rate limiting, failover
- `tools/` — Tool runtime with sandbox, approval, retry
- `memory/` — Memory runtime (in-memory, SQLite, MongoDB, Mem0 adapters)
- `knowledge/` — Knowledge management (indexing, retrieval, chunking)
- `workflow/` — Workflow engine (YAML/SDK-based orchestration)
- `telemetry/` — Observability, monitoring, token tracking
- `reliability/` — Error classification, circuit breaker, failover
- `skill/` — Plugin/skill system
- `loop_detection/` — Doom loop detection and prevention
- `mcp/` — MCP protocol support
- `a2a/`, `a2ui/` — Agent-to-agent and agent-to-UI protocols
### Multi-SDK Layout
- **Python SDK** — Primary runtime (`src/praisonai/`)
- **TypeScript SDK** — JS/TS runtime (`ts-sdk/`)
- **Rust SDK** — High-performance Rust runtime (`rust-sdk/`)
- **CLI** — Command-line interface (`cli/`)
- **UI** — Web UI (`ui/`)
---
## 3. 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.
---
## 4. 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
```
---
## 5. 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
```
---
## 6. 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.
---
## 7. 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
---
## 8. Implementation Roadmap
### 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
```
---
## 9. Success Metrics
### KPI Targets
| KPI | Target | Primary Workstream |
|-----|--------|-------------------|
| Time-to-first-successful run | 30-40% reduction | Golden-path CLI + Doctor Auto-Fix |
| Cross-platform issue rate | 50% reduction | Reliability Core + adapter layer |
| Incident triage time | 40% reduction | Trace + Replay + failure classifier |
| CI confidence | Flaky test rate <2% | Deterministic tests + fixture isolation |
### Governance
- **Architecture Review Council** — Reviews runtime-contract changes
- **Release Quality Gate** — Platform matrix + deterministic test thresholds
- **Monthly telemetry review** — DX, reliability, adoption trends
---
*This document is a living reference. Update as the architecture evolves.*
-14
View File
@@ -1,14 +0,0 @@
# Contributing to PraisonAI
Thank you for your interest! Here's how to get started:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request
## Guidelines
- Follow existing code style
- Add tests for new features
- Update documentation as needed
+2 -2
View File
@@ -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
View File
@@ -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`
+19 -107
View File
@@ -15,7 +15,6 @@ Methods:
* <code title="class GuardrailResult">GuardrailResult.<a href="./src/praisonai-agents/praisonaiagents/guardrails/guardrail_result.py">from_tuple</a>(result: Tuple[bool, Any]) -> 'GuardrailResult'</code>
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_dict</a>(data: Dict[str, Any]) -> 'HandoffConfig'</code>
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class HandoffResult">HandoffResult.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_outcome</a>(outcome: AgentRunOutcome, target_agent: Optional[str] = None, source_agent: Optional[str] = None, handoff_depth: int = 0) -> 'HandoffResult'</code>
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">json</a>() -> Optional[str]</code>
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">to_dict</a>() -> dict</code>
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_dict</a>() -> Dict[str, Any]</code>
@@ -30,7 +29,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>
@@ -60,9 +58,6 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to_async</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">is_closed</a>() -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">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>
@@ -72,8 +67,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>
@@ -82,7 +75,7 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until_async</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', action: str = 'add', **kwargs: Any) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs: Any) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>(value: Optional[Any]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>() -> Optional[int]</code>
@@ -182,7 +175,6 @@ Methods:
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_schema_roundtrip</a>() -> bool</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">__call__</a>(*args, **kwargs) -> Any</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">check_availability</a>() -> tuple[bool, str]</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">get_schema</a>() -> Dict[str, Any]</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">injected_params</a>() -> Dict[str, Any]</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">run</a>(**kwargs) -> Any</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">clear</a>() -> None</code>
@@ -190,18 +182,16 @@ Methods:
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_single_file_plugins</a>() -> int</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_all</a>() -> Dict[str, Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool_definitions</a>() -> List[Dict[str, Any]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_trust_level</a>(name: str) -> Optional[str]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_available_tools</a>(context: Optional[Dict[str, Any]] = None, ttl_seconds: Optional[float] = None) -> List[Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_available_tools</a>(context: Optional[Dict[str, Any]] = None) -> List[Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_base_tools</a>() -> List[BaseTool]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools</a>() -> List[str]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools_with_allowed_filter</a>(context: Optional[Dict[str, Any]] = None) -> List[str]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False, trust_level: Optional[str] = None, dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None) -> None</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False) -> None</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">unregister</a>(name: str) -> bool</code>
* <code title="class Tools">Tools.<a href="./src/praisonai-agents/praisonaiagents/tools/tools.py">internet_search</a>(*args, **kwargs)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_registry</a>() -> ToolRegistry</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, trust_level: Optional[str] = None, dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None) -> None</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None) -> None</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">tool</a>(func: Optional[Callable] = None) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_tool</a>(tool: Any) -> bool</code>
@@ -246,7 +236,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>
@@ -364,7 +353,7 @@ Methods:
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_tool_function</a>(source_agent: 'Agent') -> Callable</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_description</a>() -> str</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_name</a>() -> str</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None, tool_policy_mode: Optional[Literal['intersect', 'passthrough']] = None, blocked_tools: Optional[List[str]] = None) -> Handoff</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None) -> Handoff</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">compress_history</a>(data: HandoffInputData) -> HandoffInputData</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">keep_last_n_messages</a>(n: int) -> Callable[[HandoffInputData], HandoffInputData]</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_all_tools</a>(data: HandoffInputData) -> HandoffInputData</code>
@@ -483,16 +472,11 @@ Methods:
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate</a>(skill: LoadedSkill) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate_by_name</a>(name: str) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill</a>(skill_path: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">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">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">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>
@@ -501,24 +485,15 @@ Methods:
* <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">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>
@@ -536,12 +511,10 @@ Methods:
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_memory</a>(text: str, memory_type: str = 'long', **metadata) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">chat</a>(message: str, **kwargs) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">clear_memory</a>(memory_type: str = 'all') -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">close</a>() -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">create_agent</a>(*args, **kwargs) -> 'Agent'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_context</a>(query: str, max_items: int = 3) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_state</a>(key: str, default: Any = None) -> Any</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">increment_state</a>(key: str, increment: int = 1, default: int = 0) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">is_expired</a>() -> bool</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">knowledge</a>() -> 'Knowledge'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">memory</a>() -> 'Memory'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">restore_state</a>() -> Dict[str, Any]</code>
@@ -550,7 +523,6 @@ Methods:
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_memory</a>(query: str, memory_type: str = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">send_message</a>(message: str, **kwargs) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">set_state</a>(key: str, value: Any) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">time_to_expiry</a>() -> Optional[float]</code>
# MCP
@@ -633,7 +605,7 @@ Methods:
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">register_snapshot_callback</a>(callback: Callable[[SnapshotHookData], None]) -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">reset</a>() -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">set_tool_budget</a>(tool_name: str, max_tokens: int, protected: bool = False) -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str, tool_call_id: str = None, run_id: str = None) -> str</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str) -> str</code>
# UI
@@ -654,7 +626,6 @@ Methods:
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/autonomy.py">effective_track_changes</a>() -> bool</code>
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/autonomy.py">from_dict</a>(data: Dict[str, Any]) -> 'AutonomyConfig'</code>
* <code title="class CachingConfig">CachingConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">from_dict</a>(data: Dict[str, Any]) -> 'ExecutionConfig'</code>
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class HooksConfig">HooksConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
@@ -722,7 +693,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:
@@ -747,16 +718,12 @@ Methods:
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>(value: bool)</code>
* <code title="class AgentOSProtocol">AgentOSProtocol.<a href="./src/praisonai-agents/praisonaiagents/app/protocols.py">get_app</a>() -> Any</code>
* <code title="class AgentOSProtocol">AgentOSProtocol.<a href="./src/praisonai-agents/praisonaiagents/app/protocols.py">serve</a>(host: Optional[str] = None, port: Optional[int] = None, reload: bool = False, **kwargs: Any) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aannounce_completion</a>(agent_id: str, task_id: str, result: Any, success: bool = True, error: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">announce_completion</a>(agent_id: str, task_id: str, result: Any, success: bool = True, error: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aspawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">await_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">close</a>() -> None</code>
@@ -771,7 +738,6 @@ Methods:
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_spawned_agents</a>() -> List[SpawnedSubAgent]</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
@@ -788,12 +754,10 @@ Methods:
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">spawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">wait_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval</a>(request: ApprovalRequest) -> ApprovalDecision</code>
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval_sync</a>(request: ApprovalRequest) -> ApprovalDecision</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
@@ -813,18 +777,11 @@ Methods:
* <code title="cli">praisonai <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai acp acp-main <a href="./src/praisonai/praisonai/cli/commands/acp.py">--help</a></code>
* <code title="cli">praisonai agent list <a href="./src/praisonai/praisonai/cli/commands/agent.py">--help</a></code>
* <code title="cli">praisonai agent show <a href="./src/praisonai/praisonai/cli/commands/agent.py">--help</a></code>
* <code title="cli">praisonai agents create <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai agents info <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai agents list <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai app app <a href="./src/praisonai/praisonai/cli/commands/app.py">--help</a></code>
* <code title="cli">praisonai attach attach-main <a href="./src/praisonai/praisonai/cli/commands/attach.py">--help</a></code>
* <code title="cli">praisonai audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.py">--help</a></code>
* <code title="cli">praisonai auth list <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
* <code title="cli">praisonai auth login <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
* <code title="cli">praisonai auth logout <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
* <code title="cli">praisonai auth status <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
* <code title="cli">praisonai batch batch-run <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
* <code title="cli">praisonai batch list <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
* <code title="cli">praisonai batch report <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
@@ -862,16 +819,9 @@ Methods:
* <code title="cli">praisonai call call-main <a href="./src/praisonai/praisonai/cli/commands/call.py">--help</a></code>
* <code title="cli">praisonai chat <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai chat chat-main <a href="./src/praisonai/praisonai/cli/commands/chat.py">--help</a></code>
* <code title="cli">praisonai checkpoint delete <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai checkpoint diff <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai checkpoint list <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai checkpoint restore <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai checkpoint save <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai claw claw <a href="./src/praisonai/praisonai/cli/commands/claw.py">--help</a></code>
* <code title="cli">praisonai code <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai code code-main <a href="./src/praisonai/praisonai/cli/commands/code.py">--help</a></code>
* <code title="cli">praisonai command list <a href="./src/praisonai/praisonai/cli/commands/command.py">--help</a></code>
* <code title="cli">praisonai command show <a href="./src/praisonai/praisonai/cli/commands/command.py">--help</a></code>
* <code title="cli">praisonai commit commit-main <a href="./src/praisonai/praisonai/cli/commands/commit.py">--help</a></code>
* <code title="cli">praisonai completion bash <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
* <code title="cli">praisonai completion completion-callback <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
@@ -884,9 +834,6 @@ Methods:
* <code title="cli">praisonai config path <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config reset <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config set <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config show <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config sources <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config validate <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai context add <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context clear <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context compact <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
@@ -896,10 +843,6 @@ Methods:
* <code title="cli">praisonai context show <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context stats <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context tail <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai daemon daemon-main <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
* <code title="cli">praisonai daemon start <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
* <code title="cli">praisonai daemon status <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
* <code title="cli">praisonai daemon stop <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
* <code title="cli">praisonai dashboard unified <a href="./src/praisonai/praisonai/cli/commands/dashboard.py">--help</a></code>
* <code title="cli">praisonai debug acp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
* <code title="cli">praisonai debug debug-callback <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
@@ -921,18 +864,17 @@ Methods:
* <code title="cli">praisonai docs serve <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs stats <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai doctor cleanup <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor docker <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor 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 llm-providers <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor mcp <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor memory-store <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor metadata-store <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor network <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor packaging <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor performance <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor runtime <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor selftest <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor skills <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor 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>
@@ -960,9 +902,6 @@ Methods:
* <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>
@@ -972,11 +911,6 @@ Methods:
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai hooks list <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai hooks remove <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai identity import <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
* <code title="cli">praisonai identity link <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
* <code title="cli">praisonai identity list <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
* <code title="cli">praisonai identity unlink <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
* <code title="cli">praisonai init init <a href="./src/praisonai/praisonai/cli/commands/init.py">--help</a></code>
* <code title="cli">praisonai kanban block <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban boards <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban comment <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
@@ -1041,9 +975,6 @@ Methods:
* <code title="cli">praisonai memory search <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory show <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory status <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai models describe <a href="./src/praisonai/praisonai/cli/commands/models.py">--help</a></code>
* <code title="cli">praisonai models list <a href="./src/praisonai/praisonai/cli/commands/models.py">--help</a></code>
* <code title="cli">praisonai models validate <a href="./src/praisonai/praisonai/cli/commands/models.py">--help</a></code>
* <code title="cli">praisonai n8n export <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
* <code title="cli">praisonai n8n import <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
* <code title="cli">praisonai n8n list <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
@@ -1061,14 +992,6 @@ Methods:
* <code title="cli">praisonai pairing list <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
* <code title="cli">praisonai pairing revoke <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
* <code title="cli">praisonai paths show <a href="./src/praisonai/praisonai/cli/commands/paths.py">--help</a></code>
* <code title="cli">praisonai permissions allow <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions ask <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions deny <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions export <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions import-rules <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions list <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions remove <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions reset <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai plugins create <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins disable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins discover <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
@@ -1131,8 +1054,6 @@ Methods:
* <code title="cli">praisonai sandbox sandbox-callback <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox status <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai schedule add <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule blueprint <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule blueprint-list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule delete <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
@@ -1142,10 +1063,6 @@ Methods:
* <code title="cli">praisonai schedule start <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule stats <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule stop <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule suggestion-accept <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule suggestion-dismiss <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule suggestion-propose <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule suggestions <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai 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>
@@ -1154,7 +1071,6 @@ Methods:
* <code title="cli">praisonai serve gateway <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve lsp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve mcp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve openai <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve rag <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve recipe <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve registry <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
@@ -1181,7 +1097,6 @@ Methods:
* <code title="cli">praisonai skills eligible <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills info <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills install <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills learn <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills list <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills search <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills validate <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
@@ -1223,9 +1138,6 @@ Methods:
* <code title="cli">praisonai up logs <a href="./src/praisonai/praisonai/cli/commands/up.py">--help</a></code>
* <code title="cli">praisonai up status <a href="./src/praisonai/praisonai/cli/commands/up.py">--help</a></code>
* <code title="cli">praisonai up up-start <a href="./src/praisonai/praisonai/cli/commands/up.py">--help</a></code>
* <code title="cli">praisonai validate check <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
* <code title="cli">praisonai validate schema <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
* <code title="cli">praisonai validate validate <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
* <code title="cli">praisonai version check <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
* <code title="cli">praisonai version show <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
* <code title="cli">praisonai version version-callback <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
@@ -1377,8 +1289,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
+1 -1
View File
@@ -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.80" \
"praisonai>=4.6.52" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -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.80" \
"praisonai>=4.6.52" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -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.80" \
"praisonai>=4.6.52" \
"praisonai[ui]" \
"praisonai[crewai]"
+1 -6
View File
@@ -12,13 +12,8 @@ from praisonaiagents import Agent
from praisonaiagents.approval import AgentApproval
from praisonaiagents.tools.shell_tools import execute_command
reviewer_agent = Agent(
name="CommandReviewer",
instructions="Only approve read-only commands like 'ls' or 'cat'. Deny destructive commands.",
)
reviewer = AgentApproval(
approver_agent=reviewer_agent,
instructions="Only approve read-only commands like 'ls' or 'cat'. Deny destructive commands.",
)
agent = Agent(
+3 -3
View File
@@ -5,12 +5,12 @@ Demonstrates web search with consolidated params.
Presets: duckduckgo, tavily, google, bing, serper, search_only, fetch_only
"""
from praisonaiagents import Agent, WebConfig
from praisonaiagents import Agent
# Basic: Enable web search with preset (search only — fetch needs Claude)
# Basic: Enable web search with preset
agent = Agent(
instructions="You are a research assistant with web access.",
web="search_only", # Compatible across models without web_fetch
web="duckduckgo", # Presets: duckduckgo, tavily, google, bing, serper
)
if __name__ == "__main__":
@@ -30,5 +30,5 @@ if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
result = workflow.run("AI agents")
result = workflow.run()
print(result)
@@ -1,216 +0,0 @@
# Industry Templates for PraisonAI
This directory contains industry-specific agent templates based on the SRAO Framework, providing pre-built workflows for rapid deployment of agent workforces across different industries.
## Overview
These templates demonstrate how PraisonAI can be used to create specialized agent teams for various industries, with **70% code reuse** across different sectors through shared patterns and abstractions.
## Available Templates
### 1. Manufacturing (`manufacturing_template.py`)
**Key Agents:**
- **ParseOrder**: Extracts and structures order information from various formats
- **CheckInventory**: Validates material availability and supply chain status
- **OptimizeSchedule**: Optimizes production scheduling based on constraints
- **DefectDetect**: Quality control and defect detection
**Features:**
- Order processing with multiple input formats
- Real-time inventory management
- Production optimization algorithms
- Quality inspection with defect detection
- Fallback strategies for each critical step
### 2. Energy (`energy_template.py`)
**Key Agents:**
- **SCADAReader**: Processes SCADA telemetry data from wind turbines
- **VibrationAnalyzer**: Analyzes vibration patterns for predictive maintenance
- **PowerForecaster**: Predicts power generation based on weather and history
- **MaintenanceScheduler**: Schedules predictive maintenance
**Features:**
- Real-time wind farm monitoring
- Vibration-based fault detection
- Power generation forecasting
- Grid integration and load balancing
- Predictive maintenance scheduling
### 3. Healthcare (`healthcare_template.py`)
**Key Agents:**
- **VitalSignsCapture**: Processes patient vital signs from monitoring devices
- **EMRRetrieval**: Retrieves electronic medical records with HIPAA compliance
- **TriageRecommendation**: Provides ESI-based triage recommendations
- **ResourceAllocator**: Manages hospital resource allocation
**Features:**
- Emergency triage workflow
- HIPAA-compliant data handling
- Patient safety protocols
- Resource optimization
- Cross-department coordination
### 4. Agriculture (`agriculture_template.py`)
**Key Agents:**
- **MultispectralAnalyzer**: Analyzes multispectral imagery for crop health
- **DiseaseIdentifier**: Identifies crop diseases and pests using AI
- **SprayRecommender**: Recommends targeted spraying strategies
- **YieldPredictor**: Predicts crop yield based on conditions
**Features:**
- Precision agriculture with drone/satellite imagery
- Pest and disease early warning system
- Optimized chemical application
- Yield forecasting
- Sustainable farming patterns
### 5. Transportation (`transportation_template.py`)
**Key Agents:**
- **LiDARFusion**: Processes LiDAR point cloud data for infrastructure
- **DisplacementCalculator**: Calculates structural displacement
- **HeatmapGenerator**: Generates safety visualization heatmaps
- **MaintenancePlanner**: Plans infrastructure maintenance
**Features:**
- Tunnel/bridge safety monitoring
- LiDAR-based structural analysis
- Real-time safety assessment
- Traffic flow optimization
- Predictive maintenance scheduling
## Key Features Across All Templates
### 1. I/O Schemas
Each template includes Pydantic models for structured data handling:
- Input validation
- Type safety
- Clear data contracts between agents
### 2. SLA Requirements
Every agent has defined Service Level Agreements:
- Processing time requirements
- Response time guarantees
- Performance benchmarks
### 3. Fallback Strategies
Robust error handling with fallback mechanisms:
- Graceful degradation
- Manual intervention queues
- Conservative estimates when systems fail
### 4. Cross-Industry Patterns (70% Reuse)
Reusable base patterns that work across industries:
- Data parsing agents
- Resource availability checkers
- Optimization engines
- Inspection/quality control agents
## Usage Example
```python
from manufacturing_template import (
order_parser,
inventory_checker,
schedule_optimizer,
quality_inspector,
manufacturing_workflow
)
# Process a manufacturing order
order_text = "Customer ABC needs 100 units of Product-XYZ by March 15th, high priority"
result = manufacturing_workflow(order_text)
print(result)
```
## Adapting Templates for Your Industry
### Using Base Patterns
Each template includes reusable pattern classes that can be adapted:
```python
from manufacturing_template import IndustryAgentPattern
# Create a custom data parser for any domain
custom_parser = IndustryAgentPattern.create_data_parser(
name="CustomParser",
domain="retail",
sla_seconds=20
)
# Create a resource checker for any resource type
resource_checker = IndustryAgentPattern.create_resource_checker(
name="StaffAvailability",
resource_type="customer_service_staff",
sla_seconds=3
)
```
### Creating Custom Workflows
Combine agents from different templates or create new ones:
```python
from praisonaiagents import Agent, tool
# Define custom tools
@tool
def custom_analysis(data: str) -> Dict:
"""Your custom analysis logic"""
return {"result": "analyzed"}
# Create custom agent
custom_agent = Agent(
name="CustomAnalyzer",
instructions="Your specific instructions",
tools=[custom_analysis]
)
```
## Integration with PraisonAI Core
These templates are built on top of PraisonAI's core Agent framework:
```python
from praisonaiagents import Agent, tool, AgentTeam, Task
# Combine multiple industry agents into a team
team = AgentTeam(
agents=[order_parser, inventory_checker, schedule_optimizer],
tasks=[
Task(name="parse", agent=order_parser),
Task(name="check", agent=inventory_checker),
Task(name="optimize", agent=schedule_optimizer)
]
)
```
## Best Practices
1. **Start with Templates**: Use these as starting points and customize for your specific needs
2. **Maintain SLAs**: Define clear performance requirements for each agent
3. **Implement Fallbacks**: Always have fallback strategies for critical operations
4. **Use Type Safety**: Leverage Pydantic models for data validation
5. **Monitor Performance**: Track agent performance against SLAs
6. **Iterate and Improve**: Continuously refine based on real-world usage
## Contributing
To add a new industry template:
1. Follow the existing template structure
2. Include all key components:
- I/O Schemas (Pydantic models)
- Agent definitions with SLAs
- Tools for agent capabilities
- Complete workflow function
- Fallback strategies
- Cross-industry patterns
3. Add comprehensive documentation
4. Include usage examples
## License
These templates are part of the PraisonAI project and follow the same licensing terms.
## References
Based on the [SRAO Framework](https://github.com/beixuan577/SRAO-Framework) (MIT License)
@@ -1,38 +0,0 @@
"""
Industry Templates for PraisonAI
================================
Pre-built agent templates for rapid deployment across different industries.
Based on SRAO Framework with 70% cross-industry code reuse.
Available templates:
- Manufacturing: Order processing, inventory, scheduling, quality control
- Energy: Wind farm monitoring, predictive maintenance, power forecasting
- Healthcare: Emergency triage, EMR retrieval, resource allocation
- Agriculture: Precision farming, pest detection, yield prediction
- Transportation: Infrastructure monitoring, safety assessment, maintenance
Example usage:
from Industry_Templates.manufacturing_template import manufacturing_workflow
result = manufacturing_workflow("Customer order text")
"""
__version__ = "1.0.0"
__author__ = "PraisonAI Team"
# Import the modules so they're accessible via the package
from . import manufacturing_template
from . import energy_template
from . import healthcare_template
from . import agriculture_template
from . import transportation_template
# Export commonly used items for convenience
__all__ = [
"manufacturing_template",
"energy_template",
"healthcare_template",
"agriculture_template",
"transportation_template"
]
@@ -1,551 +0,0 @@
"""
Agriculture Industry Template
=============================
Precision agriculture and pest management workflow
Based on SRAO Framework with IoT sensor integration
Key agents:
- MultispectralAnalyzer: Analyzes multispectral imagery
- DiseaseIdentifier: Identifies crop diseases and pests
- SprayRecommender: Recommends targeted spraying strategies
- YieldPredictor: Predicts crop yield based on conditions
"""
from praisonaiagents import Agent, tool
from typing import Dict, List, Any, Tuple, Optional
from pydantic import BaseModel
from datetime import datetime, timedelta
from enum import Enum
# Crop health levels
class CropHealthLevel(str, Enum):
EXCELLENT = "excellent"
GOOD = "good"
FAIR = "fair"
POOR = "poor"
CRITICAL = "critical"
# Pest/Disease severity
class SeverityLevel(str, Enum):
NONE = "none"
LOW = "low"
MODERATE = "moderate"
HIGH = "high"
SEVERE = "severe"
# I/O Schemas
class MultispectralData(BaseModel):
"""Multispectral imagery analysis data"""
field_id: str
capture_time: str
ndvi_index: float # Normalized Difference Vegetation Index
ndre_index: float # Normalized Difference Red Edge
ccci_index: float # Canopy Chlorophyll Content Index
moisture_level: float # Soil moisture percentage
temperature: float # Celsius
affected_area: float # Hectares
gps_coordinates: Tuple[float, float]
class PestDiseaseReport(BaseModel):
"""Pest and disease identification report"""
report_id: str
field_id: str
detection_time: str
pest_type: Optional[str]
disease_type: Optional[str]
severity: SeverityLevel
affected_crops: List[str]
spread_rate: float # % per day
environmental_factors: Dict[str, Any]
confidence_score: float
class SprayRecommendation(BaseModel):
"""Targeted spraying recommendation"""
recommendation_id: str
field_id: str
treatment_type: str # pesticide, fungicide, herbicide, fertilizer
product_name: str
application_rate: float # L/hectare
target_zones: List[Dict[str, Any]] # GPS zones
optimal_time: str
weather_window: Dict[str, Any]
cost_estimate: float
environmental_impact: str # low, medium, high
class YieldForecast(BaseModel):
"""Crop yield prediction"""
forecast_id: str
field_id: str
crop_type: str
predicted_yield: float # tons/hectare
confidence_interval: Tuple[float, float]
harvest_window: Tuple[str, str]
quality_grade: str # A, B, C
market_price_estimate: float
risk_factors: List[str]
# Agriculture-specific tools
@tool
def analyze_multispectral_imagery(field_id: str, image_data: Dict) -> Dict:
"""Analyze multispectral drone/satellite imagery for crop health"""
# Simulate multispectral analysis
ndvi = 0.75 # Healthy vegetation typically 0.6-0.9
# Determine health based on NDVI
if ndvi > 0.8:
health = CropHealthLevel.EXCELLENT
affected_area = 0.0
elif ndvi > 0.6:
health = CropHealthLevel.GOOD
affected_area = 2.5
elif ndvi > 0.4:
health = CropHealthLevel.FAIR
affected_area = 5.0
else:
health = CropHealthLevel.POOR
affected_area = 10.0
return {
"field_id": field_id,
"capture_time": datetime.now().isoformat(),
"ndvi_index": ndvi,
"ndre_index": 0.68,
"ccci_index": 0.72,
"moisture_level": 35.0,
"temperature": 24.5,
"affected_area": affected_area,
"gps_coordinates": (40.7128, -74.0060),
"health_level": health.value
}
@tool
def identify_pest_disease(spectral_data: Dict, historical_data: List) -> Dict:
"""Identify pests and diseases using AI image recognition"""
# Simulate pest/disease detection
ndvi = spectral_data.get("ndvi_index", 0.75)
affected_area = spectral_data.get("affected_area", 0)
if affected_area > 8.0:
pest_type = "aphids"
disease_type = "powdery_mildew"
severity = SeverityLevel.HIGH
spread_rate = 2.5
elif affected_area > 4.0:
pest_type = "whiteflies"
disease_type = "leaf_spot"
severity = SeverityLevel.MODERATE
spread_rate = 1.5
elif affected_area > 0:
pest_type = None
disease_type = "early_blight"
severity = SeverityLevel.LOW
spread_rate = 0.5
else:
pest_type = None
disease_type = None
severity = SeverityLevel.NONE
spread_rate = 0.0
return {
"report_id": f"PDR-{datetime.now().strftime('%Y%m%d%H%M')}",
"field_id": spectral_data["field_id"],
"detection_time": datetime.now().isoformat(),
"pest_type": pest_type,
"disease_type": disease_type,
"severity": severity.value,
"affected_crops": ["wheat", "corn"],
"spread_rate": spread_rate,
"environmental_factors": {
"humidity": 65,
"temperature": spectral_data.get("temperature", 25),
"rainfall_last_week": 15 # mm
},
"confidence_score": 0.85
}
@tool
def calculate_spray_recommendation(pest_report: Dict, field_data: Dict, weather: Dict) -> Dict:
"""Calculate optimal spraying strategy for pest/disease control"""
severity = pest_report.get("severity", "none")
if severity == "high" or severity == "severe":
treatment = "pesticide"
product = "Imidacloprid"
rate = 0.5
impact = "medium"
elif severity == "moderate":
treatment = "fungicide"
product = "Azoxystrobin"
rate = 0.3
impact = "low"
elif severity == "low":
treatment = "organic_treatment"
product = "Neem Oil"
rate = 0.2
impact = "minimal"
else:
treatment = "fertilizer"
product = "NPK 20-20-20"
rate = 0.4
impact = "positive"
# Calculate target zones based on affected area
affected_area = field_data.get("affected_area", 0)
if affected_area > 0:
target_zones = [
{
"zone_id": "A1",
"gps_boundary": [(40.71, -74.00), (40.72, -74.01)],
"priority": "high"
}
]
else:
target_zones = []
return {
"recommendation_id": f"SPR-{datetime.now().strftime('%Y%m%d%H%M')}",
"field_id": field_data["field_id"],
"treatment_type": treatment,
"product_name": product,
"application_rate": rate,
"affected_area": affected_area,
"target_zones": target_zones,
"optimal_time": (datetime.now() + timedelta(days=1)).isoformat(),
"weather_window": {
"wind_speed": weather.get("wind_speed", 5),
"precipitation": weather.get("precipitation", 0),
"suitable": True
},
"cost_estimate": rate * affected_area * 50, # $/hectare
"environmental_impact": impact
}
@tool
def predict_crop_yield(field_id: str, crop_type: str, current_health: Dict, weather_forecast: Dict) -> Dict:
"""Predict crop yield based on current conditions and forecasts"""
# Simulate yield prediction
health_level = current_health.get("health_level", "good")
base_yield = {
"wheat": 3.5,
"corn": 9.5,
"soybeans": 2.8,
"rice": 6.0
}.get(crop_type, 5.0)
# Adjust based on health
health_multiplier = {
"excellent": 1.2,
"good": 1.0,
"fair": 0.8,
"poor": 0.6,
"critical": 0.4
}.get(health_level, 1.0)
predicted_yield = base_yield * health_multiplier
# Quality grade based on conditions
if health_level in ["excellent", "good"]:
quality = "A"
price_multiplier = 1.2
elif health_level == "fair":
quality = "B"
price_multiplier = 1.0
else:
quality = "C"
price_multiplier = 0.8
return {
"forecast_id": f"YLD-{datetime.now().strftime('%Y%m%d%H%M')}",
"field_id": field_id,
"crop_type": crop_type,
"predicted_yield": predicted_yield,
"confidence_interval": (predicted_yield * 0.9, predicted_yield * 1.1),
"harvest_window": (
(datetime.now() + timedelta(days=60)).isoformat(),
(datetime.now() + timedelta(days=75)).isoformat()
),
"quality_grade": quality,
"market_price_estimate": predicted_yield * 250 * price_multiplier, # $/ton
"risk_factors": ["weather_variability", "pest_pressure", "market_volatility"]
}
# Agriculture agent definitions
multispectral_agent = Agent(
name="MultispectralAnalyzer",
instructions="""You are a remote sensing specialist for precision agriculture.
Analyze multispectral imagery from drones and satellites.
Calculate vegetation indices (NDVI, NDRE, CCCI) to assess crop health.
Identify stress zones and anomalies in field conditions.
SLA: Process imagery within 2 minutes per field.""",
tools=[analyze_multispectral_imagery]
)
disease_agent = Agent(
name="DiseaseIdentifier",
instructions="""You are a plant pathology expert with AI vision capabilities.
Identify crop diseases, pests, and nutrient deficiencies.
Assess severity levels and predict spread patterns.
Consider environmental factors affecting disease development.
SLA: Complete identification within 30 seconds.""",
tools=[identify_pest_disease]
)
spray_agent = Agent(
name="SprayRecommender",
instructions="""You are a precision application specialist.
Recommend targeted spraying strategies for pest and disease control.
Optimize chemical usage to minimize environmental impact.
Consider weather windows and application regulations.
SLA: Generate recommendations within 1 minute.""",
tools=[calculate_spray_recommendation]
)
yield_agent = Agent(
name="YieldPredictor",
instructions="""You are a crop yield forecasting specialist.
Predict harvest yields based on current conditions and historical data.
Assess crop quality grades and market value estimates.
Identify risk factors that could impact final yield.
SLA: Generate forecast within 45 seconds.""",
tools=[predict_crop_yield]
)
# Precision agriculture workflow
def precision_agriculture_workflow(field_ids: List[str], crop_type: str, weather_data: Dict):
"""
Complete precision agriculture workflow from monitoring to treatment
Includes early warning system and sustainable farming practices
"""
workflow_results = {
"timestamp": datetime.now().isoformat(),
"fields_monitored": len(field_ids),
"alerts": [],
"treatments_needed": [],
"yield_forecasts": [],
"sustainability_score": 0.0
}
for field_id in field_ids:
try:
# Step 1: Multispectral analysis
spectral_analysis = multispectral_agent.start(
f"Analyze multispectral imagery for field {field_id}"
)
# Step 2: Disease and pest detection
pest_report = disease_agent.start(
f"Identify pests/diseases from spectral data: {spectral_analysis}"
)
# Check for alerts
if pest_report.get("severity") in ["high", "severe"]:
workflow_results["alerts"].append({
"field_id": field_id,
"type": "pest_disease_alert",
"severity": pest_report["severity"],
"pest": pest_report.get("pest_type"),
"disease": pest_report.get("disease_type"),
"action": "immediate_treatment_required"
})
# Step 3: Generate spray recommendation
spray_recommendation = spray_agent.start(
f"Calculate spray strategy for pest report: {pest_report}, "
f"field: {spectral_analysis}, weather: {weather_data}"
)
workflow_results["treatments_needed"].append(spray_recommendation)
# Step 4: Yield prediction
yield_forecast = yield_agent.start(
f"Predict yield for field {field_id}, crop {crop_type}, "
f"health: {spectral_analysis}, weather: {weather_data}"
)
workflow_results["yield_forecasts"].append(yield_forecast)
except Exception as e:
# Fallback: Schedule manual inspection
workflow_results["alerts"].append({
"field_id": field_id,
"type": "monitoring_failure",
"error": str(e),
"action": "schedule_manual_inspection"
})
# Calculate sustainability score
total_chemical = sum(
t.get("application_rate", 0) * t.get("affected_area", 0)
for t in workflow_results["treatments_needed"]
)
workflow_results["sustainability_score"] = max(0, 100 - (total_chemical * 10))
return workflow_results
# Sustainable farming patterns
class SustainableFarmingPatterns:
"""
Patterns for sustainable and regenerative agriculture
"""
@staticmethod
def integrated_pest_management(pest_level: str) -> Dict:
"""IPM strategy based on pest pressure"""
strategies = {
"low": {
"method": "biological_control",
"actions": ["release_beneficial_insects", "plant_trap_crops"],
"chemical_use": 0
},
"moderate": {
"method": "targeted_intervention",
"actions": ["spot_treatment", "pheromone_traps", "minimal_spray"],
"chemical_use": 30
},
"high": {
"method": "integrated_approach",
"actions": ["systematic_spray", "crop_rotation", "resistant_varieties"],
"chemical_use": 70
}
}
return strategies.get(pest_level, strategies["moderate"])
@staticmethod
def precision_irrigation(moisture_level: float, crop_stage: str) -> Dict:
"""Calculate optimal irrigation based on soil moisture and crop stage"""
# Water requirements by growth stage (mm/day)
stage_requirements = {
"germination": 3.0,
"vegetative": 5.0,
"flowering": 7.0,
"grain_filling": 6.0,
"maturation": 2.0
}
required = stage_requirements.get(crop_stage, 5.0)
deficit = max(0, 40 - moisture_level) # Target 40% moisture
return {
"irrigation_needed": deficit > 10,
"amount_mm": deficit * 2,
"method": "drip" if deficit < 20 else "sprinkler",
"schedule": "night_irrigation" if deficit > 15 else "morning_irrigation",
"water_saved": (100 - deficit) / 100 * 1000 # Liters per hectare
}
@staticmethod
def crop_rotation_optimizer(current_crop: str, soil_data: Dict) -> str:
"""Recommend next crop for rotation based on soil health"""
rotations = {
"wheat": ["soybeans", "canola"], # Nitrogen fixation
"corn": ["soybeans", "alfalfa"], # Break disease cycle
"soybeans": ["wheat", "corn"], # Utilize fixed nitrogen
"rice": ["legumes", "vegetables"], # Soil structure improvement
}
return rotations.get(current_crop, ["cover_crops"])[0]
# IoT sensor integration patterns
class IoTSensorPatterns:
"""
Patterns for integrating IoT sensors in smart farming
"""
@staticmethod
def create_sensor_network_agent(sensor_types: List[str]) -> Agent:
"""Create agent for managing IoT sensor networks"""
return Agent(
name="SensorNetworkManager",
instructions=f"""Manage IoT sensor network with {', '.join(sensor_types)}.
Collect real-time data from field sensors.
Detect sensor anomalies and maintenance needs.
Aggregate data for precision agriculture decisions.
SLA: Data collection every 15 minutes."""
)
@staticmethod
def process_sensor_data(sensor_readings: List[Dict]) -> Dict:
"""Process and aggregate IoT sensor data"""
aggregated = {
"timestamp": datetime.now().isoformat(),
"sensor_count": len(sensor_readings),
"averages": {},
"alerts": []
}
# Calculate averages
for reading in sensor_readings:
for key, value in reading.items():
if isinstance(value, (int, float)):
if key not in aggregated["averages"]:
aggregated["averages"][key] = []
aggregated["averages"][key].append(value)
# Convert to means
for key in aggregated["averages"]:
values = aggregated["averages"][key]
aggregated["averages"][key] = sum(values) / len(values)
# Check for anomalies
if key == "soil_ph" and (aggregated["averages"][key] < 6.0 or aggregated["averages"][key] > 7.5):
aggregated["alerts"].append({
"type": "soil_ph_anomaly",
"value": aggregated["averages"][key],
"action": "adjust_soil_treatment"
})
return aggregated
# Example usage
if __name__ == "__main__":
# Monitor multiple fields
fields = ["FIELD-001", "FIELD-002", "FIELD-003"]
weather = {
"temperature": 25,
"humidity": 60,
"wind_speed": 8,
"precipitation": 0,
"forecast": "partly_cloudy"
}
result = precision_agriculture_workflow(fields, "wheat", weather)
print("Agriculture workflow result:", result)
# Sustainable farming example
ipm_strategy = SustainableFarmingPatterns.integrated_pest_management("moderate")
print("IPM Strategy:", ipm_strategy)
irrigation_plan = SustainableFarmingPatterns.precision_irrigation(25.0, "flowering")
print("Irrigation Plan:", irrigation_plan)
next_crop = SustainableFarmingPatterns.crop_rotation_optimizer("wheat", {})
print("Recommended next crop:", next_crop)
# IoT integration example
sensor_agent = IoTSensorPatterns.create_sensor_network_agent(
["soil_moisture", "temperature", "humidity", "light", "ph"]
)
sensor_data = [
{"sensor_id": "S001", "soil_moisture": 35, "temperature": 24, "soil_ph": 6.8},
{"sensor_id": "S002", "soil_moisture": 32, "temperature": 25, "soil_ph": 7.2},
{"sensor_id": "S003", "soil_moisture": 38, "temperature": 23, "soil_ph": 5.8}
]
processed_data = IoTSensorPatterns.process_sensor_data(sensor_data)
print("Processed sensor data:", processed_data)
@@ -1,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%}")
@@ -80,7 +80,7 @@ Be precise and quote exact values when asked about codes or numbers.""",
print(f"\nAnswer: {response}\n")
# Verify the answer contains the unique code
if "ZEBRA-71" in str(response or "").upper():
if "ZEBRA-71" in response.upper():
print("✅ VERIFIED: Agent correctly retrieved the unique code from context!\n")
else:
print("❌ WARNING: Agent did not find the code - check retrieval\n")
@@ -104,7 +104,7 @@ Be precise and quote exact values when asked about codes or numbers.""",
response = agent.chat("What is the security incident code?")
print(f"\nAnswer: {response}\n")
if "HAWK-88" in str(response or "").upper():
if "HAWK-88" in response.upper():
print("✅ VERIFIED: Agent correctly retrieved the security code!\n")
else:
print("❌ WARNING: Agent did not find the code - check retrieval\n")
+3 -13
View File
@@ -19,16 +19,6 @@ from praisonaiagents import Agent, AgentTeam
# Initialize observability
obs.init()
def set_span_attr(span, key, value):
"""Set span attribute safely across observability implementations."""
if hasattr(span, "set_attribute"):
span.set_attribute(key, value)
return
attrs = getattr(span, "attributes", None)
if attrs is not None:
attrs[key] = value
# Create agents
researcher = Agent(
name="Researcher",
@@ -47,13 +37,13 @@ with obs.trace("content-creation", metadata={"workflow": "research-write"}):
# Research phase
with obs.span("research-phase", kind=SpanKind.AGENT) as research_span:
set_span_attr(research_span, "agent", "Researcher")
research_span.attributes["agent"] = "Researcher"
research_result = researcher.chat("Research the benefits of AI in healthcare")
# Writing phase
with obs.span("writing-phase", kind=SpanKind.AGENT) as write_span:
set_span_attr(write_span, "agent", "Writer")
set_span_attr(write_span, "input_length", len(str(research_result)))
write_span.attributes["agent"] = "Writer"
write_span.attributes["input_length"] = len(str(research_result))
final_content = writer.chat(f"Write a blog post based on: {research_result}")
print("Final Content:")
+5 -8
View File
@@ -13,8 +13,7 @@ Expected output:
- Session can be resumed later with same session_id
"""
from praisonaiagents import Agent
from praisonaiagents.db import db
from praisonaiagents import Agent, db
# Create database connection (PostgreSQL + Redis)
my_db = db(
@@ -26,12 +25,10 @@ my_db = db(
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant. Be concise.",
memory={
"db": my_db,
# session_id is optional - defaults to per-hour ID (YYYYMMDDHH-hash)
# Set explicitly for session continuity across runs:
"session_id": "my-persistent-session",
},
db=my_db,
# session_id is optional - defaults to per-hour ID (YYYYMMDDHH-hash)
# Set explicitly for session continuity across runs:
session_id="my-persistent-session"
)
# Chat - messages are automatically persisted
+3 -6
View File
@@ -12,8 +12,7 @@ Expected output:
- Export shows full session data
"""
from praisonaiagents import Agent
from praisonaiagents.db import db
from praisonaiagents import Agent, db
def get_weather(city: str) -> str:
"""Get weather for a city."""
@@ -30,10 +29,8 @@ agent = Agent(
name="WeatherBot",
instructions="You help with weather. Use the get_weather tool when asked.",
tools=[get_weather],
memory={
"db": my_db,
"session_id": "weather-session-001",
},
db=my_db,
session_id="weather-session-001"
)
# First chat - triggers run tracking
+3 -6
View File
@@ -13,8 +13,7 @@ Expected output:
Agent responds with state persisted to Redis
"""
from praisonaiagents import Agent
from praisonaiagents.db import db
from praisonaiagents import Agent, db
print("=== Redis State Store (Agent-First) ===")
@@ -27,10 +26,8 @@ my_db = db(
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
memory={
"db": my_db,
"session_id": "redis-state-session",
},
db=my_db,
session_id="redis-state-session"
)
# Chat - state is automatically managed via Redis
+3 -6
View File
@@ -11,8 +11,7 @@ Expected output:
- Second run: Resumes with history
"""
from praisonaiagents import Agent
from praisonaiagents.db import db
from praisonaiagents import Agent, db
# Create database
my_db = db(
@@ -26,10 +25,8 @@ SESSION_ID = "resume-demo-session"
agent = Agent(
name="MemoryBot",
instructions="You remember our conversation. Be helpful and concise.",
memory={
"db": my_db,
"session_id": SESSION_ID,
},
db=my_db,
session_id=SESSION_ID
)
# Check if this is a new or resumed session
+3 -6
View File
@@ -26,8 +26,7 @@ Expected Output:
Messages persisted: 2
"""
from praisonaiagents import Agent
from praisonaiagents.db import db
from praisonaiagents import Agent, db
# Create database adapter (simplified import)
db_instance = db(
@@ -38,10 +37,8 @@ db_instance = db(
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant. Keep responses brief.",
memory={
"db": db_instance,
"session_id": "demo-session-001", # Same session_id = resume conversation
},
db=db_instance,
session_id="demo-session-001", # Same session_id = resume conversation
output="silent"
)
+3 -6
View File
@@ -11,8 +11,7 @@ Expected output:
- Data persisted to local SQLite file
"""
from praisonaiagents import Agent
from praisonaiagents.db import db
from praisonaiagents import Agent, db
import tempfile
import os
@@ -26,10 +25,8 @@ print(f"Database: {db_path}")
agent = Agent(
name="LocalBot",
instructions="You are a helpful assistant.",
memory={
"db": my_db,
"session_id": "local-session",
},
db=my_db,
session_id="local-session"
)
# Chat
+2 -7
View File
@@ -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]}...")
@@ -60,10 +60,9 @@ agents_system = AgentTeam(
process="sequential", output="verbose"
)
if __name__ == "__main__":
print("Starting hierarchical agent generation...")
result = agents_system.start()
print("Starting hierarchical agent generation...")
result = agents_system.start()
print(f"\nHierarchical Result: {str(result)[:200]}...")
print("\n✅ AutoAgents hierarchical generation complete!")
print("Demonstrated coordinated hierarchical workflow between manager and specialist agents.")
print(f"\nHierarchical Result: {result[:200]}...")
print("\n✅ AutoAgents hierarchical generation complete!")
print("Demonstrated coordinated hierarchical workflow between manager and specialist agents.")
+12 -16
View File
@@ -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__":
+16 -19
View File
@@ -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__":
+2 -3
View File
@@ -1,5 +1,4 @@
from praisonaiagents import Agent
if __name__ == "__main__":
agent = Agent(name="TweetAgent", instructions="Create a Tweet based on the topic provided")
agent.launch(port=8080, protocol="mcp", path="/")
agent = Agent(name="TweetAgent", instructions="Create a Tweet based on the topic provided")
agent.launch(port=8080, protocol="mcp")
@@ -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)")
-238
View File
@@ -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())
+2 -11
View File
@@ -90,18 +90,9 @@ def main():
if result:
print("\n🔍 Analysis Results:")
task_results = result.get("task_results") if isinstance(result, dict) else result
if isinstance(task_results, dict):
iterable = task_results.items()
elif isinstance(task_results, list):
iterable = enumerate(task_results)
else:
iterable = [("result", task_results)]
for task_id, task_result in iterable:
for task_id, task_result in result["task_results"].items():
print(f"\nTask {task_id}:")
raw_value = getattr(task_result, "raw", None)
print(raw_value if raw_value is not None else str(task_result))
print(task_result.raw)
else:
print("❌ Failed to capture and analyze camera feed")
+2 -11
View File
@@ -148,19 +148,10 @@ def main():
"scene_analysis": "🌍 SCENE ANALYSIS"
}
task_results = result.get("task_results") if isinstance(result, dict) else result
if isinstance(task_results, dict):
iterable = task_results.items()
elif isinstance(task_results, list):
iterable = enumerate(task_results)
else:
iterable = [("result", task_results)]
for task_id, task_result in iterable:
for task_id, task_result in result["task_results"].items():
print(f"\n{agent_names.get(task_id, f'Task {task_id}')}:")
print("-" * 40)
raw_value = getattr(task_result, "raw", None)
print(raw_value if raw_value is not None else str(task_result))
print(task_result.raw)
print("\n" + "="*60)
print("✅ Multi-agent analysis complete!")
@@ -34,5 +34,5 @@ print(f"\nChanged to: {handler.get_mode()}")
print("File edits now auto-approved, shell commands still require approval")
# Get statistics
stats = manager.get_stats()
stats = handler.get_stats()
print(f"\nStats: {stats}")
+1 -1
View File
@@ -31,4 +31,4 @@ print(f"Total cost: ${summary['total_cost']:.4f}")
print(f"Avg cost/request: ${summary['avg_cost_per_request']:.4f}")
# Display formatted (uses Rich if available)
handler.print_summary()
handler.display_summary()
@@ -41,9 +41,9 @@ def my_custom_handler(args, context):
custom_cmd = SlashCommand(
name="mycommand",
description="My custom command",
action=my_custom_handler,
kind=CommandKind.CUSTOM,
alt_names=["mc"]
handler=my_custom_handler,
kind=CommandKind.ACTION,
aliases=["mc"]
)
handler.register(custom_cmd)
@@ -1,9 +1,8 @@
from pathlib import Path
from praisonaiagents import Agent, Task, AgentTeam
import logging
import tempfile
import os
import sys
import logging
from praisonaiagents import Agent, Task, AgentTeam
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -20,9 +19,11 @@ config = {
}
}
_sample_path = Path(__file__).with_name("sample_knowledge.txt")
if not _sample_path.exists():
_sample_path.write_text("Mervin Praison is the creator of PraisonAI.\n", encoding="utf-8")
# Create sample file in system temp directory
_sample_path = os.path.join(tempfile.gettempdir(), "sample_knowledge.txt")
if not os.path.exists(_sample_path):
with open(_sample_path, "w", encoding="utf-8") as f:
f.write("Mervin Praison is the creator of PraisonAI.\n")
# Create an agent with knowledge capabilities
knowledge_agent = Agent(
@@ -30,7 +31,7 @@ knowledge_agent = Agent(
role="Information Specialist",
goal="Store and retrieve knowledge efficiently",
backstory="Expert in managing and utilizing stored knowledge",
knowledge={**config, "sources": [str(_sample_path)]},
knowledge={**config, "sources": [_sample_path]},
)
# Define a task for the agent
@@ -19,5 +19,4 @@ agents = AgentTeam(
execution={"max_iter": 30},
)
if __name__ == "__main__":
agents.start()
agents.start()
@@ -5,8 +5,7 @@ This example demonstrates self-reflection capabilities using PraisonAI's
built-in reflection features for iterative improvement and quality optimization.
"""
from praisonaiagents import Agent, Task
from praisonaiagents.agents import AgentTeam
from praisonaiagents import Agent, Task, AgentTeam
from praisonaiagents.tools import internet_search
print("=== Self-Reflection Optimization Patterns Example ===\n")
@@ -34,16 +33,9 @@ reflection_task = Task(
)
# Run with self-reflection optimization
if __name__ == "__main__":
print("Starting self-reflection optimization demonstration...")
agents = AgentTeam(
agents=[reflection_agent],
tasks=[reflection_task],
process="sequential",
output="verbose",
)
result = agents.start()
print("Starting self-reflection optimization demonstration...")
result = reflection_agent.execute_task(reflection_task)
print(f"\nSelf-Reflection Result: {str(result)[:200]}...")
print("\n✅ Self-reflection optimization complete!")
print("Agent demonstrated iterative improvement through built-in self-reflection capabilities.")
print(f"\nSelf-Reflection Result: {result[:200]}...")
print("\n✅ Self-reflection optimization complete!")
print("Agent demonstrated iterative improvement through built-in self-reflection capabilities.")
+1 -2
View File
@@ -136,5 +136,4 @@ agents = AgentTeam(
output="minimal",
)
if __name__ == "__main__":
agents.start()
agents.start()
-201
View File
@@ -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())
+6 -16
View File
@@ -49,20 +49,10 @@ agents = AgentTeam(
process="sequential"
)
if __name__ == "__main__":
# Run all tasks
result = agents.start()
# Run all tasks
result = agents.start()
# Print results
task_results = result.get("task_results") if isinstance(result, dict) else result
if isinstance(task_results, dict):
iterable = task_results.items()
elif isinstance(task_results, list):
iterable = enumerate(task_results)
else:
iterable = [("result", task_results)]
for task_id, task_result in iterable:
print(f"\nTask {task_id} Result:")
raw_value = getattr(task_result, "raw", None)
print(raw_value if raw_value is not None else str(task_result))
# Print results
for task_id, task_result in result["task_results"].items():
print(f"\nTask {task_id} Result:")
print(task_result.raw)
+11 -38
View File
@@ -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
@@ -14,33 +14,30 @@ agent = Agent(name="env-mgmt", backend=managed)
# First call creates agent + environment + session
agent.start("Say hello briefly")
if hasattr(managed, "_get_client"):
# Access the underlying Anthropic client for environment management
client = managed._get_client()
env_id = managed.environment_id
print(f"Environment ID: {env_id}")
# Access the underlying Anthropic client for environment management
client = managed._get_client()
env_id = managed.environment_id
print(f"Environment ID: {env_id}")
# List all environments
environments = client.beta.environments.list()
print(f"\nTotal environments: {len(environments.data)}")
for env in environments.data[:5]:
print(f" {env.id} | {env.name} | {getattr(env, 'status', 'active')}")
# List all environments
environments = client.beta.environments.list()
print(f"\nTotal environments: {len(environments.data)}")
for env in environments.data[:5]:
print(f" {env.id} | {env.name} | {getattr(env, 'status', 'active')}")
# Retrieve a specific environment
env = client.beta.environments.retrieve(env_id)
print(f"\nRetrieved: {env.id} | {env.name}")
# Retrieve a specific environment
env = client.beta.environments.retrieve(env_id)
print(f"\nRetrieved: {env.id} | {env.name}")
# Archive (read-only, existing sessions continue)
archived = client.beta.environments.archive(env_id)
print(f"Archived: {archived.id} | status: {getattr(archived, 'status', 'archived')}")
# Archive (read-only, existing sessions continue)
archived = client.beta.environments.archive(env_id)
print(f"Archived: {archived.id} | status: {getattr(archived, 'status', 'archived')}")
# Delete (only if no sessions reference it — may fail if sessions exist)
try:
client.beta.environments.delete(env_id)
print(f"Deleted: {env_id}")
except Exception as e:
print(f"Delete skipped (expected if sessions exist): {e}")
else:
print("Environment management demo skipped: backend does not expose _get_client().")
# Delete (only if no sessions reference it — may fail if sessions exist)
try:
client.beta.environments.delete(env_id)
print(f"Deleted: {env_id}")
except Exception as e:
print(f"Delete skipped (expected if sessions exist): {e}")
print("\nEnvironment management complete.")
+7 -6
View File
@@ -15,6 +15,13 @@ Protocol: MCP 2025-11-25
from praisonaiagents import Agent, MCP
# Agent with resumability support (automatic)
agent = Agent(
name="Resumable Assistant",
instructions="You are a helpful assistant with resumable connections.",
tools=MCP("https://api.example.com/mcp")
)
# Resumability is automatic - the MCP client:
# 1. Tracks event IDs from SSE streams (id: field)
# 2. Stores last event ID per stream
@@ -22,12 +29,6 @@ from praisonaiagents import Agent, MCP
# 4. Respects retry delay from server (retry: field)
if __name__ == "__main__":
agent = Agent(
name="Resumable Assistant",
instructions="You are a helpful assistant with resumable connections.",
tools=MCP("https://api.example.com/mcp"),
)
print("MCP Resumability Example")
print("=" * 40)
print()
@@ -15,6 +15,13 @@ Protocol: MCP 2025-11-25
from praisonaiagents import Agent, MCP
# Basic agent with automatic session management
agent = Agent(
name="Session Assistant",
instructions="You are a helpful assistant with session support.",
tools=MCP("https://api.example.com/mcp")
)
# Session management is automatic - the MCP client:
# 1. Receives Mcp-Session-Id from server during initialization
# 2. Includes session ID in all subsequent requests
@@ -22,12 +29,6 @@ from praisonaiagents import Agent, MCP
# 4. Can terminate session explicitly when done
if __name__ == "__main__":
agent = Agent(
name="Session Assistant",
instructions="You are a helpful assistant with session support.",
tools=MCP("https://api.example.com/mcp"),
)
print("MCP Session Management Example")
print("=" * 40)
print()
+27 -24
View File
@@ -16,31 +16,34 @@ Protocol: MCP 2025-11-25
from praisonaiagents import Agent, MCP
# Basic Streamable HTTP connection
agent = Agent(
name="HTTP Stream Assistant",
instructions="You are a helpful assistant connected via Streamable HTTP.",
tools=MCP("https://api.example.com/mcp")
)
# With authentication headers
agent_auth = Agent(
name="Authenticated Assistant",
instructions="You are a helpful assistant with authenticated connection.",
tools=MCP(
"https://api.example.com/mcp",
headers={"Authorization": "Bearer your-token"}
)
)
# With timeout and debug
agent_debug = Agent(
name="Debug Assistant",
tools=MCP(
"https://api.example.com/mcp",
timeout=120,
debug=True
)
)
if __name__ == "__main__":
agent = Agent(
name="HTTP Stream Assistant",
instructions="You are a helpful assistant connected via Streamable HTTP.",
tools=MCP("https://api.example.com/mcp"),
)
agent_auth = Agent(
name="Authenticated Assistant",
instructions="You are a helpful assistant with authenticated connection.",
tools=MCP(
"https://api.example.com/mcp",
headers={"Authorization": "Bearer your-token"},
),
)
agent_debug = Agent(
name="Debug Assistant",
tools=MCP(
"https://api.example.com/mcp",
timeout=120,
debug=True,
),
)
print("Streamable HTTP MCP Transport Example")
print("=" * 40)
print("This example requires a Streamable HTTP MCP server running.")
+29 -26
View File
@@ -16,33 +16,36 @@ Protocol: SEP-1288 (WebSocket Transport)
from praisonaiagents import Agent, MCP
# Basic WebSocket connection
agent = Agent(
name="WebSocket Assistant",
instructions="You are a helpful assistant connected via WebSocket.",
tools=MCP("ws://localhost:8080/mcp")
)
# Secure WebSocket with authentication
agent_secure = Agent(
name="Secure WebSocket Assistant",
instructions="You are a helpful assistant with secure WebSocket connection.",
tools=MCP(
"wss://api.example.com/mcp",
auth_token="Bearer your-secret-token",
timeout=60
)
)
# WebSocket with custom options
agent_custom = Agent(
name="Custom WebSocket Assistant",
tools=MCP(
"wss://api.example.com/mcp",
auth_token="your-token",
timeout=120,
debug=True
)
)
if __name__ == "__main__":
agent = Agent(
name="WebSocket Assistant",
instructions="You are a helpful assistant connected via WebSocket.",
tools=MCP("ws://localhost:8080/mcp"),
)
agent_secure = Agent(
name="Secure WebSocket Assistant",
instructions="You are a helpful assistant with secure WebSocket connection.",
tools=MCP(
"wss://api.example.com/mcp",
auth_token="Bearer your-secret-token",
timeout=60,
),
)
agent_custom = Agent(
name="Custom WebSocket Assistant",
tools=MCP(
"wss://api.example.com/mcp",
auth_token="your-token",
timeout=120,
debug=True,
),
)
# Note: Requires a running WebSocket MCP server
# Example server: https://github.com/modelcontextprotocol/servers
-173
View File
@@ -1,173 +0,0 @@
#!/usr/bin/env python3
"""
Message Steering Example
Demonstrates real-time message steering for agents during execution.
Shows how to send guidance messages to agents while they're running tasks.
Usage:
# Python API
python message_steering_example.py
# CLI with steering enabled
praisonai --message-steering "Write a detailed report about AI trends"
# YAML (add message_steering: true to agent config)
"""
import asyncio
import threading
import time
from praisonaiagents import Agent
def basic_message_steering():
"""Basic message steering example - enable steering and send guidance."""
print("🎯 Basic Message Steering Example")
print("=" * 50)
# Create agent with message steering enabled
agent = Agent(
name="researcher",
instructions="You are a helpful research assistant",
message_steering=True, # Enable real-time message steering
llm="gpt-4o-mini"
)
print("✅ Agent created with message steering enabled")
print(f"📊 Steering status: {agent.get_steering_status()}")
# Send some guidance
msg_id_1 = agent.steer("Focus on recent developments from 2024")
msg_id_2 = agent.steer("Keep response under 200 words", priority=10)
print(f"📨 Sent steering messages: {msg_id_1}, {msg_id_2}")
print(f"📊 Steering status: {agent.get_steering_status()}")
# Execute task - steering will be automatically injected
result = agent.start("Summarize the latest AI trends")
print("\n📝 Result:")
print(result)
def threaded_message_steering():
"""Advanced example - send steering messages while agent is running."""
print("\n🚀 Threaded Message Steering Example")
print("=" * 50)
agent = Agent(
name="writer",
instructions="You are a content writer",
message_steering=True,
llm="gpt-4o-mini"
)
result_holder = [None]
def run_agent():
"""Run agent in background thread."""
result = agent.start("Write a comprehensive article about quantum computing")
result_holder[0] = result
# Start agent in background
thread = threading.Thread(target=run_agent)
thread.start()
# Send steering messages while it's running
time.sleep(0.5) # Let execution start
agent.steer("Make it accessible to beginners")
time.sleep(1.0)
agent.steer("Include practical applications", priority=15)
time.sleep(1.5)
agent.steer("Keep it under 300 words total", priority=20)
# Wait for completion
thread.join(timeout=60)
if result_holder[0]:
print("\n📝 Final Result:")
print(result_holder[0])
else:
print("⚠️ Agent execution timed out")
async def async_message_steering():
"""Async example showing message steering with async execution."""
print("\n⚡ Async Message Steering Example")
print("=" * 50)
agent = Agent(
name="analyst",
instructions="You are a data analyst",
message_steering=True,
llm="gpt-4o-mini"
)
# Start async task
async def run_analysis():
return await agent.astart("Analyze the impact of AI on job markets")
# Create task
task = asyncio.create_task(run_analysis())
# Send steering while running
await asyncio.sleep(0.3)
agent.steer("Focus on positive opportunities")
await asyncio.sleep(0.7)
agent.steer("Include specific examples", priority=12)
# Wait for result
result = await task
print("\n📝 Analysis Result:")
print(result)
def cli_yaml_examples():
"""Show CLI and YAML usage examples."""
print("\n💻 CLI & YAML Examples")
print("=" * 50)
print("🔧 CLI Usage:")
print(" praisonai --message-steering 'Your task here'")
print(" # Then in another terminal:")
print(" # agent.steer('Additional guidance')")
print("\n📄 YAML Configuration:")
yaml_example = """
# agents.yaml
framework: praisonai
roles:
researcher:
role: Research Assistant
goal: Conduct thorough research
backstory: Expert researcher with steering support
message_steering: true # Enable steering for this agent
tools:
- internet_search
- write_file
tasks:
research_task:
description: Research AI trends and write report
expected_output: Comprehensive research report
"""
print(yaml_example.strip())
if __name__ == "__main__":
# Run examples
basic_message_steering()
threaded_message_steering()
# Run async example
asyncio.run(async_message_steering())
# Show CLI/YAML usage
cli_yaml_examples()
print("\n✨ Message steering examples completed!")
print("📚 See docs for more advanced patterns and priority levels")
@@ -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)
+2 -2
View File
@@ -12,6 +12,7 @@ Environment:
"""
import os
import tempfile
from praisonaiagents import Agent, AutoRagAgent
# Ensure API key is set
@@ -20,8 +21,7 @@ if not os.environ.get("OPENAI_API_KEY"):
exit(1)
# Create a sample document for testing
from pathlib import Path
sample_doc = str(Path(__file__).with_name("sample_doc.txt"))
sample_doc = os.path.join(tempfile.gettempdir(), "sample_doc.txt")
with open(sample_doc, "w", encoding="utf-8") as f:
f.write("""
PraisonAI Framework Documentation
@@ -4,7 +4,7 @@ Example 3: Save Agent Output Using Workflow output_file
Workflow step output is automatically saved with variable substitution.
"""
from praisonaiagents import Agent, AgentFlow, Task
from praisonaiagents import AgentFlow
# Define workflow with output_file
workflow_config = {
@@ -33,30 +33,9 @@ workflow_config = {
]
}
# Build agent and workflow from config dict
writer_cfg = workflow_config["agents"]["writer"]
writer = Agent(
name="writer",
role=writer_cfg["role"],
goal=writer_cfg["goal"],
llm=writer_cfg["llm"],
)
step_cfg = workflow_config["steps"][0]
workflow = AgentFlow(
name=workflow_config["metadata"]["name"],
variables=workflow_config["variables"],
steps=[
Task(
name="write_tutorial",
description=step_cfg["action"],
expected_output=step_cfg["expected_output"],
output_file=step_cfg["output_file"],
agent=writer,
)
],
)
result = workflow.start()
# Run workflow
workflow = AgentFlow(config=workflow_config)
result = workflow.run()
print("✅ Workflow completed!")
print("Output saved to: generated/tutorial.md")
@@ -69,10 +69,9 @@ agents_system = AgentTeam(
memory=True, output="verbose"
)
if __name__ == "__main__":
print("Starting state management demonstration...")
result = agents_system.start()
print("Starting state management demonstration...")
result = agents_system.start()
print(f"\nState Management Result: {str(result)[:200]}...")
print("\n✅ State management patterns complete!")
print("Agent demonstrated persistent state tracking across multiple tasks.")
print(f"\nState Management Result: {result[:200]}...")
print("\n✅ State management patterns complete!")
print("Agent demonstrated persistent state tracking across multiple tasks.")
@@ -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,55 +1,284 @@
"""
Production Telemetry Example
Demonstrates compatible telemetry setup with `enable_telemetry()` and
`TelemetryCollector` patterns.
This example demonstrates comprehensive telemetry and monitoring setup for
production AI agent systems, including metrics collection, performance tracking,
and observability.
Features demonstrated:
- OpenTelemetry integration for distributed tracing
- Custom metrics collection and monitoring
- Performance tracking and analytics
- Error tracking and alerting
- Usage analytics and reporting
"""
from contextlib import nullcontext
from praisonaiagents import Agent, Task, AgentTeam
from praisonaiagents.tools import duckduckgo
from praisonaiagents.telemetry import enable_telemetry, TelemetryCollector
from praisonaiagents.telemetry import enable_telemetry
import time
import random
from datetime import datetime
# Initialize telemetry for production monitoring
print("="*80)
print("PRODUCTION TELEMETRY SETUP")
print("="*80)
def main() -> None:
print("=" * 80)
print("PRODUCTION TELEMETRY SETUP")
print("=" * 80)
# Configure telemetry (in production, you'd use actual telemetry backends)
telemetry = enable_telemetry(
backend="opentelemetry",
config={
"service_name": "praisonai_production_agents",
"service_version": "1.0.0",
"environment": "production",
"export_console": True, # For demo purposes
"export_file": True, # Export to file for analysis
"sample_rate": 1.0, # 100% sampling for demo
"attributes": {
"deployment": "cloud",
"region": "us-east-1",
"cluster": "main"
}
}
)
# New API: no args
enable_telemetry()
collector = TelemetryCollector()
# Create agents for production workflow with telemetry
customer_service_agent = Agent(
name="CustomerServiceAgent",
role="Customer Service Representative",
goal="Provide excellent customer service with fast response times",
backstory="You are a customer service representative focused on resolving customer issues quickly and effectively.",
tools=[duckduckgo],
instructions="Provide helpful, accurate customer service responses. Research solutions when needed."
)
customer_service_agent = Agent(
name="CustomerServiceAgent",
role="Customer Service Representative",
goal="Provide excellent customer service with fast response times",
backstory="Customer service expert focused on quick issue resolution.",
tools=[duckduckgo],
instructions="Provide accurate customer-service responses.",
)
technical_support_agent = Agent(
name="TechnicalSupportAgent",
role="Technical Support Specialist",
goal="Resolve technical issues with detailed solutions",
backstory="You are a technical support specialist who provides detailed technical solutions and troubleshooting.",
instructions="Provide detailed technical solutions with step-by-step instructions."
)
requests = [
"Customer asking about monthly bill charges",
"Customer experiencing connectivity issues",
quality_assurance_agent = Agent(
name="QualityAssuranceAgent",
role="Quality Assurance Specialist",
goal="Ensure all customer interactions meet quality standards",
backstory="You review and assess the quality of customer service interactions to ensure high standards.",
instructions="Review customer service interactions and provide quality scores with detailed feedback."
)
# Production workflow simulation with telemetry tracking
def simulate_customer_requests():
"""Simulate various customer requests for telemetry demonstration"""
customer_requests = [
{
"type": "billing_inquiry",
"complexity": "low",
"description": "Customer asking about their monthly bill charges",
"expected_duration": 30
},
{
"type": "technical_issue",
"complexity": "high",
"description": "Customer experiencing connectivity issues with the service",
"expected_duration": 120
},
{
"type": "account_setup",
"complexity": "medium",
"description": "New customer needs help setting up their account",
"expected_duration": 60
},
{
"type": "feature_question",
"complexity": "low",
"description": "Customer asking about product features and capabilities",
"expected_duration": 45
}
]
return customer_requests
for idx, description in enumerate(requests, start=1):
trace_ctx = getattr(collector, "trace", None)
trace_cm = trace_ctx(f"customer_request_{idx}") if callable(trace_ctx) else nullcontext()
with trace_cm:
task = Task(
name=f"customer_request_{idx}",
description=description,
expected_output="Professional customer service response with solution",
agent=customer_service_agent,
)
agents = AgentTeam(agents=[customer_service_agent], tasks=[task], output="minimal")
result = agents.start()
print(f"✅ Request {idx} completed, response length={len(str(result))}")
# Execute production workflow with comprehensive telemetry
print("Starting production workflow with telemetry tracking...")
print("\nTelemetry demonstration completed.")
customer_requests = simulate_customer_requests()
for i, request in enumerate(customer_requests):
print(f"\n{'='*60}")
print(f"PROCESSING CUSTOMER REQUEST {i+1}: {request['type'].upper()}")
print(f"Complexity: {request['complexity']} | Expected Duration: {request['expected_duration']}s")
print(f"{'='*60}")
# Start telemetry trace for this customer interaction
with telemetry.trace_operation(f"customer_request_{request['type']}") as trace:
# Add custom attributes to the trace
trace.set_attributes({
"request.type": request['type'],
"request.complexity": request['complexity'],
"request.expected_duration": request['expected_duration'],
"customer.id": f"customer_{random.randint(1000, 9999)}",
"agent.assigned": "CustomerServiceAgent"
})
start_time = time.time()
# Create customer service task with telemetry
customer_task = Task(
name=f"customer_request_{i+1}",
description=request['description'],
expected_output="Professional customer service response with solution",
agent=customer_service_agent
)
# Execute with telemetry tracking
try:
# Customer service response
with telemetry.trace_agent_execution("CustomerServiceAgent") as agent_trace:
agents = AgentTeam(
agents=[customer_service_agent],
tasks=[customer_task],
output="minimal" # Reduce verbosity for telemetry demo
)
cs_result = agents.start()
agent_trace.set_attributes({
"agent.response_length": len(str(cs_result)),
"agent.tools_used": len(customer_service_agent.tools) if customer_service_agent.tools else 0
})
# Technical support (if needed for high complexity)
if request['complexity'] == 'high':
with telemetry.trace_agent_execution("TechnicalSupportAgent") as tech_trace:
tech_task = Task(
name=f"technical_support_{i+1}",
description=f"Provide detailed technical support for: {request['description']}",
expected_output="Detailed technical solution with troubleshooting steps",
agent=technical_support_agent,
context=[customer_task]
)
tech_agents = AgentTeam(
agents=[technical_support_agent],
tasks=[tech_task], output="minimal"
)
tech_result = tech_agents.start()
tech_trace.set_attributes({
"agent.response_length": len(str(tech_result)),
"agent.escalation": True
})
# Quality assurance check
with telemetry.trace_agent_execution("QualityAssuranceAgent") as qa_trace:
qa_task = Task(
name=f"quality_check_{i+1}",
description=f"Review the quality of customer service provided for: {request['description']}",
expected_output="Quality assessment score and feedback",
agent=quality_assurance_agent
)
qa_agents = AgentTeam(
agents=[quality_assurance_agent],
tasks=[qa_task], output="minimal"
)
qa_result = qa_agents.start()
# Extract quality score (simulated)
quality_score = random.uniform(0.8, 1.0) # Simulated score
qa_trace.set_attributes({
"qa.score": quality_score,
"qa.passed": quality_score >= 0.85
})
# Calculate performance metrics
end_time = time.time()
duration = end_time - start_time
# Record custom metrics
telemetry.record_metric("request_duration", duration, {
"request_type": request['type'],
"complexity": request['complexity']
})
telemetry.record_metric("request_success", 1, {
"request_type": request['type']
})
# Set final trace attributes
trace.set_attributes({
"request.duration_seconds": duration,
"request.status": "completed",
"request.quality_score": quality_score,
"request.escalated": request['complexity'] == 'high'
})
print(f"✅ Request completed successfully in {duration:.2f}s (Quality: {quality_score:.2f})")
except Exception as e:
# Record error metrics
telemetry.record_metric("request_error", 1, {
"request_type": request['type'],
"error_type": type(e).__name__
})
trace.set_attributes({
"request.status": "error",
"request.error": str(e)
})
print(f"❌ Request failed: {str(e)}")
# Add some delay between requests
time.sleep(2)
if __name__ == "__main__":
main()
# Generate telemetry summary
print(f"\n{'='*80}")
print("TELEMETRY SUMMARY AND ANALYTICS")
print(f"{'='*80}")
# Simulated telemetry analytics (in production, this would come from your telemetry backend)
print("📊 Performance Metrics:")
print("- Total Requests Processed: 4")
print("- Average Response Time: 3.2s")
print("- Success Rate: 100%")
print("- Quality Score Average: 0.91")
print("- High Complexity Requests: 25%")
print("\n📈 Usage Analytics:")
print("- Most Common Request Type: technical_issue")
print("- Peak Performance Window: All requests within SLA")
print("- Agent Utilization: CustomerService (100%), TechnicalSupport (25%), QA (100%)")
print("\n🔍 Observability Features Demonstrated:")
print("- Distributed tracing across agent interactions")
print("- Custom metrics for business KPIs")
print("- Performance monitoring and SLA tracking")
print("- Error tracking and alerting")
print("- Quality assurance metrics")
print("- User journey tracking")
print("- Resource utilization monitoring")
print("\n⚙️ Production Monitoring Setup:")
print("- OpenTelemetry integration for standardized observability")
print("- Custom metrics for business-specific KPIs")
print("- Trace correlation across multi-agent workflows")
print("- Performance benchmarking against expected durations")
print("- Quality scoring and automated validation")
print("- Error categorization and alerting")
print(f"\n{'='*80}")
print("TELEMETRY DEMONSTRATION COMPLETED")
print(f"{'='*80}")
print("This example demonstrated comprehensive production telemetry including:")
print("- Request tracing and performance monitoring")
print("- Custom business metrics collection")
print("- Quality assurance tracking")
print("- Error handling and alerting")
print("- Multi-agent workflow observability")
print("- Production-ready monitoring patterns")
-115
View File
@@ -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()
@@ -5,17 +5,11 @@ from dotenv import load_dotenv
load_dotenv()
serper_api_key = os.getenv("SERPER_API_KEY")
if serper_api_key is not None:
os.environ["SERPER_API_KEY"] = serper_api_key
os.environ['SERPER_API_KEY'] = os.getenv('SERPER_API_KEY')
search = GoogleSerperAPIWrapper()
if __name__ == "__main__":
if not serper_api_key:
print("SERPER_API_KEY is not set. Skipping Google Serper example.")
else:
search = GoogleSerperAPIWrapper()
data_agent = Agent(instructions="Suggest me top 5 most visited websites for Dosa Recipe", tools=[search])
editor_agent = Agent(instructions="List out the websites with their url and a short description")
agents = AgentTeam(agents=[data_agent, editor_agent])
agents.start()
data_agent = Agent(instructions="Suggest me top 5 most visited websites for Dosa Recipe", tools=[search])
editor_agent = Agent(instructions="List out the websites with their url and a short description")
agents = AgentTeam(agents=[data_agent, editor_agent])
agents.start()
-227
View File
@@ -1,227 +0,0 @@
"""
Named Toolsets Example
This example demonstrates how to use named toolsets to organize and reuse
groups of tools across agents and workflows.
Toolsets provide:
- Organized tool collections (web, research, development, etc.)
- Progressive disclosure (safe → research → development)
- Composable design (toolsets can include other toolsets)
- YAML configuration support
"""
import os
from praisonaiagents import Agent, Task, AgentTeam
from praisonaiagents.toolsets import (
list_toolsets,
resolve_toolset,
register_toolset
)
# Set up environment (replace with your actual API key)
os.environ.setdefault("OPENAI_API_KEY", "your-openai-api-key-here")
def demo_basic_toolsets():
"""Demonstrate basic toolset functionality."""
print("=== Basic Toolsets Demo ===")
# List available toolsets
available_toolsets = list_toolsets()
print(f"Available toolsets: {available_toolsets}")
# Show what tools are in each prebuilt toolset
for toolset_name in ["safe", "web", "research"]:
tools = resolve_toolset(toolset_name)
print(f"\n{toolset_name.upper()} toolset contains: {tools}")
def demo_agent_with_toolsets():
"""Demonstrate agents using toolsets."""
print("\n=== Agent with Toolsets Demo ===")
# Create a research agent with research toolset
researcher = Agent(
name="researcher",
role="Research Specialist",
goal="Research topics thoroughly using web tools",
instructions="You are a research specialist with access to web search and file tools.",
toolsets=["research"], # includes web tools + file tools
llm="gpt-4o-mini"
)
print(f"Research agent has {len(researcher.tools)} tools available")
print(f"Tools: {[getattr(t, '__name__', str(t)) for t in researcher.tools]}")
# Create a safe agent with minimal tools
safe_agent = Agent(
name="safe_assistant",
role="Safe Assistant",
goal="Help users safely without risky operations",
instructions="You are a helpful assistant with access to only safe, read-only tools.",
toolsets=["safe"],
llm="gpt-4o-mini"
)
print(f"\nSafe agent has {len(safe_agent.tools)} tools available")
print(f"Safe tools: {[getattr(t, '__name__', str(t)) for t in safe_agent.tools]}")
def demo_custom_toolsets():
"""Demonstrate creating and using custom toolsets."""
print("\n=== Custom Toolsets Demo ===")
# Register a custom toolset for data analysis
register_toolset(
"data_analysis",
tools=["read_file", "write_file"],
includes=["safe"], # Include safe tools too
description="Tools for data analysis workflows"
)
# Create an agent with the custom toolset
analyst = Agent(
name="data_analyst",
role="Data Analyst",
goal="Analyze data files and generate reports",
instructions="You specialize in data analysis. Read files, process data, and write reports.",
toolsets=["data_analysis"],
llm="gpt-4o-mini"
)
print(f"Data analyst has {len(analyst.tools)} tools available")
print(f"Analysis tools: {[getattr(t, '__name__', str(t)) for t in analyst.tools]}")
def demo_mixed_tools_and_toolsets():
"""Demonstrate mixing explicit tools with toolsets."""
print("\n=== Mixed Tools and Toolsets Demo ===")
# Create agent with both explicit tools and toolsets
hybrid_agent = Agent(
name="hybrid_assistant",
role="Hybrid Assistant",
goal="Assist with both research and custom tasks",
instructions="You can research topics and perform custom analysis tasks.",
tools=["write_file"], # Explicit tool
toolsets=["research"], # Toolset with web + file tools
llm="gpt-4o-mini"
)
print(f"Hybrid agent has {len(hybrid_agent.tools)} tools available")
tool_names = [getattr(t, '__name__', str(t)) for t in hybrid_agent.tools]
print(f"Hybrid tools: {tool_names}")
# Note: write_file might appear twice (once explicit, once from toolset)
# The agent handles deduplication automatically
def demo_multi_agent_with_toolsets():
"""Demonstrate multi-agent workflows with different toolsets."""
print("\n=== Multi-Agent with Toolsets Demo ===")
# Create agents with different toolsets for different roles
researcher = Agent(
name="researcher",
role="Research Specialist",
goal="Research topics and gather information",
toolsets=["research"],
llm="gpt-4o-mini"
)
writer = Agent(
name="writer",
role="Content Writer",
goal="Write content based on research",
toolsets=["safe"], # Safe tools for writing
llm="gpt-4o-mini"
)
# Create tasks for each agent
research_task = Task(
name="research_task",
description="Research the benefits of renewable energy",
expected_output="A summary of key benefits of renewable energy",
agent=researcher
)
writing_task = Task(
name="writing_task",
description="Write a blog post about renewable energy based on the research",
expected_output="A well-written blog post about renewable energy benefits",
agent=writer
)
# Create team with different toolsets per agent
team = AgentTeam(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process="sequential"
)
print(f"Team created with {len(team.agents)} agents using different toolsets")
print(f"Research agent tools: {len(researcher.tools)}")
print(f"Writer agent tools: {len(writer.tools)}")
def demo_yaml_config():
"""Show how toolsets work in YAML configuration."""
print("\n=== YAML Configuration Example ===")
yaml_config = """
# agents.yaml - Example using toolsets
agents:
researcher:
role: Research Specialist
goal: Research topics thoroughly
toolsets:
- research # Includes web search, file tools
llm: gpt-4o-mini
analyst:
role: Data Analyst
goal: Analyze research data
tools:
- write_file # Explicit tool
toolsets:
- safe # Safe toolset
llm: gpt-4o-mini
tasks:
research_task:
description: Research renewable energy trends
agent: researcher
expected_output: Research summary
analysis_task:
description: Analyze the research data
agent: analyst
expected_output: Analysis report
"""
print("Example YAML configuration with toolsets:")
print(yaml_config)
print("Use: praisonai run 'Research renewable energy' --toolset research")
print("Or: praisonai chat --toolset web,files")
if __name__ == "__main__":
print("🛠️ PraisonAI Named Toolsets Example")
print("=" * 50)
# Run all demos
demo_basic_toolsets()
demo_agent_with_toolsets()
demo_custom_toolsets()
demo_mixed_tools_and_toolsets()
demo_multi_agent_with_toolsets()
demo_yaml_config()
print("\n✅ Toolsets example completed!")
print("\n💡 Key Benefits:")
print(" • Organized tool collections")
print(" • Progressive disclosure (safe → research → development)")
print(" • Reusable across agents and workflows")
print(" • YAML configuration support")
print(" • Composable design with includes")
@@ -2,7 +2,6 @@ from praisonaiagents import Agent, Task, AgentTeam
from pydantic import BaseModel
from typing import List, Dict
from gitingest import ingest
import sys
class CodeMetrics(BaseModel):
category: str
@@ -61,10 +60,7 @@ def analyze_code(code_source: str) -> CodeAnalysisReport:
Analyze code from directory path or GitHub URL
"""
# Ingest code content
try:
summary, tree, content = ingest(code_source)
except Exception as exc:
raise RuntimeError(f"Failed to ingest code source: {exc}") from exc
summary, tree, content = ingest(code_source)
# Concatenate context into structured format
context_text = f"""
@@ -90,22 +86,10 @@ def analyze_code(code_source: str) -> CodeAnalysisReport:
tasks=[code_analysis_task]
)
result = agents.start(context_text)
if result is None:
raise RuntimeError("No analysis result returned by AgentTeam.start()")
return result
def main() -> None:
# Example usage
code_source = "https://github.com/openai/openai-python/tree/main/src/openai/cli/_api/chat" # GitHub URL or local directory
try:
result = analyze_code(code_source)
except RuntimeError as exc:
print(f"Code analysis failed: {exc}")
sys.exit(1)
print(result)
return agents.start(context_text)
if __name__ == "__main__":
main()
# Example usage
code_source = "https://github.com/openai/openai-python/tree/main/src/openai/cli/_api/chat" # GitHub URL or local directory
result = analyze_code(code_source)
print(result)
@@ -77,44 +77,25 @@ agents = AgentTeam(
process="sequential", output="verbose"
)
def _extract_pydantic_result(result_obj):
"""Extract pydantic payload from AgentTeam.start() output shapes."""
if hasattr(result_obj, "pydantic"):
return result_obj.pydantic
if isinstance(result_obj, dict):
task_results = result_obj.get("task_results")
if isinstance(task_results, dict):
values = list(task_results.values())
if values and hasattr(values[-1], "pydantic"):
return values[-1].pydantic
if isinstance(task_results, list) and task_results and hasattr(task_results[-1], "pydantic"):
return task_results[-1].pydantic
return None
# Start the analysis
result = agents.start()
pydantic_result = _extract_pydantic_result(result)
if pydantic_result is None:
raise ValueError("Could not extract structured CVAnalysisReport from workflow result")
# Access the structured results
print("\nCV Analysis Results:")
print(f"\nOverall Score: {pydantic_result.overall_score}/100")
print(f"\nOverall Score: {result.pydantic.overall_score}/100")
print("\nKey Strengths:")
for strength in pydantic_result.key_strengths:
for strength in result.pydantic.key_strengths:
print(f"- {strength}")
print("\nSkill Analysis:")
print("\nTechnical Skills:")
for skill in pydantic_result.skill_analysis.technical_skills:
for skill in result.pydantic.skill_analysis.technical_skills:
print(f"- {skill}")
print("\nExperience Highlights:")
for achievement in pydantic_result.experience_analysis.key_achievements:
for achievement in result.pydantic.experience_analysis.key_achievements:
print(f"- {achievement}")
print("\nRecommendations:")
for recommendation in pydantic_result.recommendations:
for recommendation in result.pydantic.recommendations:
print(f"- {recommendation}")
@@ -100,8 +100,6 @@ def evaluate_project(video_path: str) -> ProjectEvaluation:
evaluation_data = response
print(f"Debug - Parsed evaluation_data: {evaluation_data}")
if not evaluation_data or not isinstance(evaluation_data, dict):
raise ValueError("No structured evaluation data returned by agent")
# Create and return ProjectEvaluation instance
return ProjectEvaluation(
+3 -9
View File
@@ -1,11 +1,5 @@
from praisonaiagents import VideoAgent
import os
if __name__ == "__main__":
if not os.getenv("AZURE_API_BASE"):
print("AZURE_API_BASE is not configured. Skipping Azure video example.")
else:
agent = VideoAgent(llm="azure/sora-2")
video = agent.generate("Mountain landscape")
print(video.id)
agent = VideoAgent(llm="azure/sora-2")
video = agent.generate("Mountain landscape")
print(video.id)
+1 -1
View File
@@ -5,7 +5,7 @@ Demonstrates using Agent instances directly as workflow steps.
Each agent processes the input and passes output to the next agent.
"""
from praisonaiagents import Agent, AgentFlow
from praisonaiagents import Agent, Workflow
# Create agents with specific roles
researcher = Agent(
+2 -5
View File
@@ -6,7 +6,6 @@ with Agents for workflow monitoring and logging.
"""
from praisonaiagents import Agent, Task, AgentTeam
from praisonaiagents.config.feature_configs import MultiAgentHooksConfig
# Create an agent
researcher = Agent(
@@ -48,10 +47,8 @@ if __name__ == "__main__":
agents=[researcher],
tasks=[task1, task2],
process="workflow",
hooks=MultiAgentHooksConfig(
on_task_start=on_start,
on_task_complete=on_complete,
),
on_task_start=on_start,
on_task_complete=on_complete,
variables={"global_var": "shared_value"} # Global variables
)
@@ -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']}")
@@ -54,13 +54,11 @@ workflow = AgentFlow(
analyzer, # Always runs
Task(
name="tech_review",
description="Review technical aspects when technical keywords are detected.",
agent=tech_reviewer,
should_run=is_technical # Only runs for technical content
),
Task(
name="creative_enhance",
description="Enhance narrative quality when creative keywords are detected.",
agent=creative_enhancer,
should_run=needs_creative # Only runs for creative content
),
@@ -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']}")
+15 -8
View File
@@ -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")
Loaded 100 of 790 files, more files were not shown because too many files have changed in this diff. Show more