Compare commits

..
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison 25e7af3c8f fix: force UTF-8 env for child processes in suite runner
Force PYTHONIOENCODING=utf-8 (instead of setdefault) so an inherited
non-UTF-8 value cannot conflict with the parent's UTF-8 output decoder
and corrupt captured reports (per Greptile review). Also set PYTHONUTF8=1
to enable UTF-8 Mode, covering child-side file I/O on Windows (per Gemini
review).

Co-authored-by: Mervin Praison <[email protected]>
2026-07-21 09:31:10 +00:00
1226 changed files with 14113 additions and 101015 deletions

No files matched your search

@@ -1,5 +1,5 @@
name: Install PraisonAI monorepo packages
description: Install praisonai-agents, praisonai-code, praisonai-bot, praisonai-train, praisonai-browser, praisonai-mcp, praisonai-sandbox, praisonai-deploy, and praisonai wrapper from local paths (mirrors PyPI nine-package install order).
description: Install praisonai-agents, praisonai-code, praisonai-bot, praisonai-train, praisonai-browser, praisonai-mcp, praisonai-sandbox, and praisonai wrapper from local paths (mirrors PyPI eight-package install order).
inputs:
repo-root:
@@ -40,7 +40,6 @@ runs:
BROWSER_PKG="${BASE}src/praisonai-browser"
MCP_PKG="${BASE}src/praisonai-mcp"
SANDBOX_PKG="${BASE}src/praisonai-sandbox"
DEPLOY_PKG="${BASE}src/praisonai-deploy"
WRAPPER_PKG="${BASE}src/praisonai"
normalise_extras() {
@@ -70,8 +69,6 @@ runs:
uv pip install --system -e .
cd "$SANDBOX_PKG"
uv pip install --system -e .
cd "$DEPLOY_PKG"
uv pip install --system -e .
cd "$WRAPPER_PKG"
uv pip install --system "${WRAPPER_SUFFIX}"
else
@@ -89,8 +86,6 @@ runs:
pip install -e .
cd "$SANDBOX_PKG"
pip install -e .
cd "$DEPLOY_PKG"
pip install -e .
cd "$WRAPPER_PKG"
pip install -e "${WRAPPER_SUFFIX}"
fi
-56
View File
@@ -40,62 +40,6 @@ assert('claude final reply detected', mg.isClaudeFinalReplyComment(withClaudeRep
assert('cancelled detect-and-trigger does not block', mg.OPTIONAL_CANCELLED_CHECKS.has('detect-and-trigger'));
const coreGreenRuns = [
{ name: 'test-core', status: 'completed', conclusion: 'success' },
{ name: 'test-core (cli)', status: 'completed', conclusion: 'success' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'test-windows', status: 'completed', conclusion: 'cancelled' },
];
assert('core green allows cancelled smoke', mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
coreGreenRuns
));
assert('core green allows cancelled test-windows', mg.isAcceptableCheckConclusion(
{ name: 'test-windows', status: 'completed', conclusion: 'cancelled' },
coreGreenRuns
));
assert('cancelled smoke blocks when core missing', !mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
[{ name: 'smoke', status: 'completed', conclusion: 'cancelled' }]
));
assert('cancelled smoke blocks when core failed', !mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
[
{ name: 'test-core', status: 'completed', conclusion: 'failure' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
]
));
assert('bestRunsByName prefers success over cancelled', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', conclusion: 'cancelled' },
{ name: 'smoke', conclusion: 'success' },
]);
return best.length === 1 && best[0].conclusion === 'success';
})());
assert('bestRunsByName prefers pending over completed cancelled', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'smoke', status: 'in_progress', conclusion: null },
]);
return best.length === 1 && best[0].status === 'in_progress';
})());
assert('bestRunsByName keeps pending regardless of order', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', status: 'in_progress', conclusion: null },
{ name: 'smoke', status: 'completed', conclusion: 'success' },
]);
return best.length === 1 && best[0].status === 'in_progress';
})());
assert('allChecksGreenOnSha style: pending re-run blocks despite core green', (() => {
const runs = mg.bestRunsByName([
{ name: 'test-core', status: 'completed', conclusion: 'success' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'smoke', status: 'queued', conclusion: null },
]);
const smoke = runs.find((r) => r.name === 'smoke');
return smoke.status !== 'completed';
})());
// Stale-FINAL recovery guards (PR #2560 push loop)
const nowMs = Date.now();
const iso = (ms) => new Date(ms).toISOString();
+5 -79
View File
@@ -44,8 +44,6 @@ const BLOCK_LABELS = new Set([
const MERGE_READY_LABEL = 'pipeline/merge-ready';
/** Superseded concurrency runs; must not block merge when real tests passed. */
const OPTIONAL_CANCELLED_CHECKS = new Set(['detect-and-trigger']);
/** Cancelled smoke/windows after timeout are non-blocking when core shards passed on HEAD. */
const OPTIONAL_CANCELLED_WHEN_CORE_GREEN = new Set(['smoke', 'test-windows']);
const BOT_REVIEWER_PATTERNS = [
'coderabbit',
'qodo',
@@ -327,70 +325,8 @@ function listFailedChecksOnSha(runs) {
});
}
function isCoreTestRun(run) {
const name = run?.name || '';
return name === 'test-core' || name.startsWith('test-core ') || name === 'test-core-collect';
}
function coreTestsGreenOnRuns(runs) {
const coreRuns = (runs || []).filter(isCoreTestRun);
if (coreRuns.length === 0) return false;
return coreRuns.every(
(run) =>
run.status === 'completed' &&
['success', 'neutral', 'skipped'].includes(run.conclusion)
);
}
function checkConclusionRank(conclusion) {
if (conclusion === 'success') return 4;
if (conclusion === 'neutral') return 3;
if (conclusion === 'skipped') return 2;
if (conclusion === 'cancelled') return 1;
return 0;
}
function isPendingRun(run) {
return !!(run && run.status && run.status !== 'completed');
}
function bestRunsByName(runs) {
const byName = new Map();
for (const run of runs || []) {
if (!run) continue;
const existing = byName.get(run.name);
if (!existing) {
byName.set(run.name, run);
continue;
}
const runPending = isPendingRun(run);
const existingPending = isPendingRun(existing);
if (existingPending) continue;
if (
runPending ||
checkConclusionRank(run.conclusion) > checkConclusionRank(existing.conclusion)
) {
byName.set(run.name, run);
}
}
return [...byName.values()];
}
function isAcceptableCheckConclusion(run, runs) {
if (['success', 'neutral', 'skipped'].includes(run.conclusion)) return true;
if (run.conclusion === 'cancelled' && OPTIONAL_CANCELLED_CHECKS.has(run.name)) return true;
if (
run.conclusion === 'cancelled' &&
OPTIONAL_CANCELLED_WHEN_CORE_GREEN.has(run.name) &&
coreTestsGreenOnRuns(runs)
) {
return true;
}
return false;
}
async function allChecksGreenOnSha(github, owner, repo, sha, core) {
const runs = bestRunsByName(await listChecksOnSha(github, owner, repo, sha));
const runs = await listChecksOnSha(github, owner, repo, sha);
if (runs.length === 0) {
core?.info?.(`No check runs on ${sha.slice(0, 7)} — allowing (e.g. docs-only PR)`);
return true;
@@ -400,17 +336,13 @@ async function allChecksGreenOnSha(github, owner, repo, sha, core) {
core?.info?.(`Check pending: ${run.name} (${run.status})`);
return false;
}
if (!isAcceptableCheckConclusion(run, runs)) {
const ok =
['success', 'neutral', 'skipped'].includes(run.conclusion) ||
(run.conclusion === 'cancelled' && OPTIONAL_CANCELLED_CHECKS.has(run.name));
if (!ok) {
core?.info?.(`Check failed: ${run.name} (${run.conclusion})`);
return false;
}
if (
run.conclusion === 'cancelled' &&
OPTIONAL_CANCELLED_WHEN_CORE_GREEN.has(run.name) &&
coreTestsGreenOnRuns(runs)
) {
core?.info?.(`Ignoring cancelled ${run.name} — test-core green on HEAD`);
}
}
return true;
}
@@ -923,12 +855,6 @@ module.exports = {
finalClaudeCompletedOnSha,
getMergeState,
OPTIONAL_CANCELLED_CHECKS,
OPTIONAL_CANCELLED_WHEN_CORE_GREEN,
isCoreTestRun,
coreTestsGreenOnRuns,
isPendingRun,
bestRunsByName,
isAcceptableCheckConclusion,
listChecksOnSha,
listFailedChecksOnSha,
allChecksGreenOnSha,
+22 -87
View File
@@ -11,112 +11,47 @@ assert.ok(versions.currentCode);
assert.ok(versions.currentWrapper);
assert.ok(versions.targetCode);
assert.ok(rg.PACKAGE_PATHS.includes('src/praisonai-code'));
// Every released package must make the gate's path-change check eligible.
for (const p of [
'src/praisonai', 'src/praisonai-agents', 'src/praisonai-code',
'src/praisonai-bot', 'src/praisonai-train', 'src/praisonai-browser',
'src/praisonai-mcp', 'src/praisonai-sandbox', 'src/praisonai-deploy',
]) {
assert.ok(rg.PACKAGE_PATHS.includes(p), `PACKAGE_PATHS missing ${p}`);
}
const noonUtc = new Date('2026-07-08T12:00:00Z');
const dayStart = rg.utcDayStart(noonUtc);
assert.strictEqual(dayStart.toISOString(), '2026-07-08T00:00:00.000Z');
function mockGithub({ runs = [], releases = [] } = {}) {
return {
rest: {
actions: {
listWorkflowRuns: async () => ({ data: { workflow_runs: runs } }),
},
repos: {
listReleases: async () => ({ data: releases }),
},
},
};
}
(async () => {
assert.strictEqual(await rg.pypiVersionExists('praisonaiagents', '0.0.0'), false);
assert.strictEqual(rg.PATCH_RELEASE_INTERVAL_DAYS, 3);
// Dedupe is release-based: a v* release inside the window blocks…
const recentRelease = mockGithub({
releases: [{ tag_name: 'v4.6.159', published_at: '2026-07-08T09:00:00Z' }],
});
const mockGithub = {
rest: {
actions: {
listWorkflowRuns: async () => ({
data: {
workflow_runs: [
{
conclusion: 'success',
created_at: '2026-07-08T09:00:00Z',
status: 'completed',
},
],
},
}),
},
},
};
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(recentRelease, 'o', 'r', noonUtc),
await rg.hasSuccessfulReleaseToday(mockGithub, 'o', 'r', noonUtc),
true
);
const result = await rg.evaluateReleasePreflight(
recentRelease, 'o', 'r',
mockGithub, 'o', 'r',
{ headSha: 'abc', isCiTrigger: false, bump: 'patch', now: noonUtc },
null
);
assert.ok(result.reasons.some((r) => r.includes('every 3 days')));
// …an old release does not…
const oldRelease = mockGithub({
releases: [{ tag_name: 'v4.6.150', published_at: '2026-07-04T09:00:00Z' }],
});
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(oldRelease, 'o', 'r', noonUtc),
false
);
// …and a dry-run "success" (workflow run, no release) does not consume the
// window: only real releases count.
const dryRunOnly = mockGithub({
runs: [{ conclusion: 'success', created_at: '2026-07-08T09:00:00Z', status: 'completed' }],
releases: [],
});
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(dryRunOnly, 'o', 'r', noonUtc),
false
);
// Active-run blocking: fresh waiting run blocks…
const freshWaiting = mockGithub({
runs: [{ status: 'waiting', conclusion: null, created_at: '2026-07-08T11:00:00Z', html_url: 'x' }],
});
assert.strictEqual(
await rg.hasActiveReleaseRun(freshWaiting, 'o', 'r', noonUtc, null),
true
);
// …a waiting run stuck longer than STALE_WAITING_HOURS no longer blocks…
const staleWaiting = mockGithub({
runs: [{ status: 'waiting', conclusion: null, created_at: '2026-07-08T01:00:00Z', html_url: 'x' }],
});
const warnings = [];
const fakeCore = { warning: (m) => warnings.push(m), info: () => {} };
assert.strictEqual(rg.STALE_WAITING_HOURS, 6);
assert.strictEqual(
await rg.hasActiveReleaseRun(staleWaiting, 'o', 'r', noonUtc, fakeCore),
false
);
assert.ok(warnings.length === 1 && warnings[0].includes('approve or cancel'));
// …but an in_progress run always blocks regardless of age.
const oldInProgress = mockGithub({
runs: [{ status: 'in_progress', conclusion: null, created_at: '2026-07-07T01:00:00Z', html_url: 'x' }],
});
assert.strictEqual(
await rg.hasActiveReleaseRun(oldInProgress, 'o', 'r', noonUtc, null),
true
);
assert.ok(result.reasons.some((r) => r.includes('already released today')));
console.log('ok: bumpPatch');
console.log('ok: readVersionsFromTree');
console.log('ok: PACKAGE_PATHS covers all 9 released packages');
console.log('ok: pypiVersionExists missing version');
console.log('ok: utcDayStart');
console.log('ok: release-based dedupe (recent blocks, old does not)');
console.log('ok: dry-run success does not consume the release window');
console.log('ok: 3-day dedupe blocks second release');
console.log('ok: fresh waiting run blocks; stale waiting run ignored with warning');
console.log('ok: in_progress always blocks');
console.log('ok: hasSuccessfulReleaseToday');
console.log('ok: daily dedupe blocks second release');
})();
+20 -105
View File
@@ -4,31 +4,10 @@
const https = require('https');
/** Every directory whose changes should make a release eligible — all nine
* published packages (mirrors the skip_* inputs in pypi-release.yml). */
const PACKAGE_PATHS = [
'src/praisonai',
'src/praisonai-agents',
'src/praisonai-code',
'src/praisonai-bot',
'src/praisonai-train',
'src/praisonai-browser',
'src/praisonai-mcp',
'src/praisonai-sandbox',
'src/praisonai-deploy',
];
/** Minimum days between successful patch auto-releases. */
const PATCH_RELEASE_INTERVAL_DAYS = 3;
const PACKAGE_PATHS = ['src/praisonai', 'src/praisonai-agents', 'src/praisonai-code'];
const ACTIVE_RELEASE_STATUSES = new Set([
'queued', 'in_progress', 'waiting', 'pending', 'requested',
]);
/** A run stuck in `waiting` (environment approval) longer than this no longer
* blocks the gate. GitHub only auto-fails unapproved deployments after 30
* days, so without a cutoff one forgotten manual dispatch stalls all
* auto-releases for up to a month. The run is warned about, never cancelled —
* if later approved, the pypi-release concurrency group serializes it and its
* pypi_exists checks no-op anything already published. */
const STALE_WAITING_HOURS = 6;
function bumpPatch(version) {
const parts = version.split('.');
@@ -84,65 +63,36 @@ function pypiVersionExists(packageName, version) {
});
}
async function hasActiveReleaseRun(github, owner, repo, now = new Date(), core = null) {
async function hasActiveReleaseRun(github, owner, repo) {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'pypi-release.yml',
per_page: 20,
});
const staleCutoff = now.getTime() - STALE_WAITING_HOURS * 60 * 60 * 1000;
return runs.data.workflow_runs.some((r) => {
if (!ACTIVE_RELEASE_STATUSES.has(r.status) || r.conclusion) return false;
if (r.status === 'waiting' && new Date(r.created_at).getTime() < staleCutoff) {
if (core) {
core.warning(
`Ignoring release run waiting >${STALE_WAITING_HOURS}h for environment approval: `
+ `${r.html_url} — approve or cancel it. It no longer blocks auto-releases.`
);
}
return false;
}
return true;
});
return runs.data.workflow_runs.some(
(r) => ACTIVE_RELEASE_STATUSES.has(r.status) && !r.conclusion
);
}
function utcDayStart(now = new Date()) {
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
}
function releaseIntervalStart(now = new Date(), intervalDays = PATCH_RELEASE_INTERVAL_DAYS) {
return new Date(now.getTime() - intervalDays * 24 * 60 * 60 * 1000);
}
async function hasSuccessfulReleaseWithinDays(
github,
owner,
repo,
now = new Date(),
intervalDays = PATCH_RELEASE_INTERVAL_DAYS,
) {
const windowStart = releaseIntervalStart(now, intervalDays);
// Dedupe on GitHub releases (v* tags) rather than run conclusions: a
// dry_run=true dispatch concludes 'success' without publishing anything and
// must not consume the release window. Only a real full release creates a
// v* GitHub release (bump_and_release.py's `gh release create`).
const releases = await github.rest.repos.listReleases({
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
const dayStart = utcDayStart(now);
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
per_page: 10,
workflow_id: 'pypi-release.yml',
status: 'completed',
per_page: 30,
});
return releases.data.some(
(r) => r.tag_name && r.tag_name.startsWith('v')
&& new Date(r.published_at || r.created_at) >= windowStart
return runs.data.workflow_runs.some(
(r) => r.conclusion === 'success' && new Date(r.created_at) >= dayStart
);
}
/** @deprecated Use hasSuccessfulReleaseWithinDays — kept for selftests. */
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
return hasSuccessfulReleaseWithinDays(github, owner, repo, now, 1);
}
async function lastGreenCoreTestsSha(github, owner, repo) {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
@@ -179,18 +129,15 @@ async function evaluateReleasePreflight(github, owner, repo, options, core) {
return out;
}
const referenceTime = options.now instanceof Date ? options.now : new Date();
if (await hasActiveReleaseRun(github, owner, repo, referenceTime, core)) {
if (await hasActiveReleaseRun(github, owner, repo)) {
reasons.push('PyPI Release already in progress or awaiting approval');
return out;
}
if (await hasSuccessfulReleaseWithinDays(github, owner, repo, referenceTime)) {
reasons.push(
`already released within last ${PATCH_RELEASE_INTERVAL_DAYS} days; `
+ `max one patch release every ${PATCH_RELEASE_INTERVAL_DAYS} days`
);
const referenceTime = options.now instanceof Date ? options.now : new Date();
if (await hasSuccessfulReleaseToday(github, owner, repo, referenceTime)) {
const day = referenceTime.toISOString().slice(0, 10);
reasons.push(`already released today (UTC ${day}); max one patch release per day`);
return out;
}
@@ -260,35 +207,8 @@ async function evaluateReleasePreflight(github, owner, repo, options, core) {
out.headSha = evalSha;
const greenSha = await lastGreenCoreTestsSha(github, owner, repo);
if (greenSha !== evalSha) {
// Core Tests has path filters and honors [skip ci] (the release
// safety-net commit uses it), so the tip of main may legitimately have
// no Core Tests run of its own — e.g. a docs-only commit, or the
// "chore(release): … [skip ci]" commit right after a release. Accept a
// green ancestor when nothing under the released package paths changed
// after it; otherwise the cron path stalls until the next src/** push.
let greenAncestorOk = false;
if (greenSha && /^[0-9a-f]{40}$/.test(greenSha)) {
try {
execSync(`git merge-base --is-ancestor ${greenSha} ${evalSha}`);
const delta = execSync(
`git diff --name-only ${greenSha} ${evalSha} -- ${PACKAGE_PATHS.join(' ')}`,
{ encoding: 'utf8' }
).trim();
greenAncestorOk = delta === '';
} catch {
greenAncestorOk = false;
}
}
if (!greenAncestorOk) {
reasons.push(`CI not green on HEAD (last green: ${greenSha ? greenSha.slice(0, 7) : 'none'})`);
return out;
}
if (core) {
core.info(
`HEAD ${evalSha.slice(0, 7)} has no Core Tests run; accepting green ancestor `
+ `${greenSha.slice(0, 7)} (no package-path changes since).`
);
}
reasons.push(`CI not green on HEAD (last green: ${greenSha ? greenSha.slice(0, 7) : 'none'})`);
return out;
}
}
@@ -302,11 +222,6 @@ module.exports = {
bumpPatch,
readVersionsFromTree,
pypiVersionExists,
PATCH_RELEASE_INTERVAL_DAYS,
STALE_WAITING_HOURS,
releaseIntervalStart,
hasActiveReleaseRun,
hasSuccessfulReleaseWithinDays,
hasSuccessfulReleaseToday,
utcDayStart,
evaluateReleasePreflight,
+5 -8
View File
@@ -49,13 +49,13 @@ jobs:
// --- Label triage ---
const labels = [];
// Kind labels (bug before "how to" — bug reports often include "## How to fix")
// Kind labels
if (content.includes('feature request') || content.includes('[feature')) {
labels.push('enhancement');
} else if (!title.includes('[question]') && (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback'))) {
labels.push('bug');
} else if (title.includes('[question]') || content.includes('how do i') || (content.includes('how to') && !content.includes('how to fix'))) {
} else if (title.includes('[question]') || content.includes('how do i') || content.includes('how to')) {
labels.push('question');
} else if (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback')) {
labels.push('bug');
} else if (content.includes('question')) {
labels.push('question');
}
@@ -108,10 +108,7 @@ jobs:
// --- Auto-trigger Claude for owner issues and actionable external bug/enhancement reports ---
const titleLower = (issue.title || '').toLowerCase();
const hasActionableTitle = /^(\[[^\]]+\]|bug:|fix[:(]|feat:|ux:|enhancement:)/i.test(titleLower);
const autoClaude = isOwner || (hasActionableTitle && (
labels.includes('bug') ||
(labels.includes('enhancement') && !labels.includes('question'))
));
const autoClaude = isOwner || (hasActionableTitle && (labels.includes('bug') || labels.includes('enhancement')) && !labels.includes('question'));
if (autoClaude) {
await github.rest.issues.addLabels({
issue_number: issue.number,
+4 -8
View File
@@ -1,12 +1,12 @@
name: Nightly Release Gate
# Preflight before PyPI Release (patch bump).
# Triggers: Core Tests success on main (workflow_run) or cron every 3 days at 00:00 UTC.
# Dedupe: max one successful patch release every 3 days (see release-gate.js).
# Triggers: Core Tests success on main (workflow_run) or nightly cron at 00:00 UTC.
# Dedupe: max one successful patch release per UTC day (see release-gate.js).
on:
schedule:
- cron: '0 0 */3 * *'
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
dry_run:
@@ -41,7 +41,7 @@ jobs:
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.event == 'push' &&
!startsWith(github.event.workflow_run.head_commit.message, 'Release v') &&
!startsWith(github.event.workflow_run.head_commit.message, 'chore(release):')
!startsWith(github.event.workflow_run.head_commit.message, 'Bump praisonai')
)
runs-on: ubuntu-latest
env:
@@ -69,10 +69,6 @@ jobs:
- name: Fetch main and tags
run: git fetch --tags origin main
# Guards the gate logic itself — no workflow ran these selftests before.
- name: Selftest release-gate script
run: node .github/scripts/release-gate-selftest.js
- name: Preflight — path changes, CI SHA, dedupe
id: preflight
uses: actions/github-script@v7
+170 -427
View File
@@ -61,29 +61,6 @@ on:
required: false
default: false
type: boolean
skip_deploy:
description: 'Skip praisonai-deploy publish (recovery if already on PyPI)'
required: false
default: false
type: boolean
# NOTE: GitHub caps workflow_dispatch at 25 top-level inputs; this
# workflow has 23. Adding a 10th package (skip_* + *_version) hits 25.
only:
description: 'Release ONLY this package (forces skip on all others); all = no restriction'
required: false
default: all
type: choice
options:
- all
- agents
- code
- bot
- train
- browser
- mcp
- sandbox
- deploy
- wrapper
agents_version:
description: 'Override agents version (empty = auto bump from pyproject.toml)'
required: false
@@ -124,11 +101,6 @@ on:
required: false
default: ''
type: string
deploy_version:
description: 'Override praisonai-deploy version (empty = auto bump from pyproject.toml)'
required: false
default: ''
type: string
source:
description: 'Trigger source (audit trail)'
required: false
@@ -162,35 +134,15 @@ jobs:
'pypi-auto' || 'pypi'
}}
env:
# PyPI API token auth: username __token__, password = pypi-… token (see pypi.org/help/#invalid-auth)
UV_PUBLISH_USERNAME: __token__
UV_PUBLISH_PASSWORD: ${{ secrets.PYPI_TOKEN || secrets.PYPI_API_TOKEN }}
# GH_TOKEN is NOT set here: it is a GitHub App installation token minted in
# "Generate GitHub App token" and exported via $GITHUB_ENV, so releases are
# authored by praisonai-triage-agent[bot] rather than MervinPraison.
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN || secrets.PYPI_API_TOKEN }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
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 }}
# Scope to this repo only. Without it the token is minted for every
# repository the owner has; this job never leaves PraisonAI (checkout,
# push to origin, and gh release create all target it).
repositories: ${{ github.event.repository.name }}
- name: Checkout main
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: main
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
# Do not bake the token into .git/config: an http.extraheader would
# outrank the remote URL set in "Configure git" and pin this first
# (1h) token, defeating the refresh before the wrapper release.
persist-credentials: false
token: ${{ secrets.GH_TOKEN }}
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
@@ -201,19 +153,10 @@ jobs:
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
- name: Configure git
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
git config user.name "praisonai-triage-agent[bot]"
git config user.email "272766704+praisonai-triage-agent[bot]@users.noreply.github.com"
# persist-credentials:false left the remote unauthenticated; push auth
# comes from the app token embedded here (masked in logs, and the
# token dies with the runner).
git remote set-url origin "https://x-access-token:${APP_TOKEN}@github.com/${REPO}.git"
echo "GH_TOKEN=${APP_TOKEN}" >> "$GITHUB_ENV"
GH_TOKEN="${APP_TOKEN}" gh auth setup-git
git config user.name "MervinPraison"
git config user.email "[email protected]"
gh auth setup-git
- name: Release summary
run: |
@@ -223,36 +166,6 @@ jobs:
echo "Dry run: ${{ inputs.dry_run }}"
echo "Environment: ${{ (inputs.source == 'ci' || inputs.source == 'nightly') && inputs.bump == 'patch' && 'pypi-auto' || 'pypi' }}"
# Single source of truth for which packages this run releases. `only`
# inverts into the skip flags here; everything downstream (version
# computation, PyPI detection, publish conditions) consumes these
# outputs, so a dedicated one-package release is one input instead of
# eight skip flags.
- name: Resolve effective skips
id: flags
env:
ONLY: ${{ inputs.only }}
SKIP_AGENTS: ${{ inputs.skip_agents }}
SKIP_CODE: ${{ inputs.skip_code }}
SKIP_BOT: ${{ inputs.skip_bot }}
SKIP_TRAIN: ${{ inputs.skip_train }}
SKIP_BROWSER: ${{ inputs.skip_browser }}
SKIP_MCP: ${{ inputs.skip_mcp }}
SKIP_SANDBOX: ${{ inputs.skip_sandbox }}
SKIP_DEPLOY: ${{ inputs.skip_deploy }}
SKIP_WRAPPER: ${{ inputs.skip_wrapper }}
run: |
set -euo pipefail
for pkg in agents code bot train browser mcp sandbox deploy wrapper; do
var="SKIP_$(echo "$pkg" | tr '[:lower:]' '[:upper:]')"
skip="${!var}"
if [ -n "${ONLY}" ] && [ "${ONLY}" != "all" ] && [ "${ONLY}" != "$pkg" ]; then
skip=true
fi
echo "skip_${pkg}=${skip}" >> "$GITHUB_OUTPUT"
echo "skip_${pkg}=${skip}"
done
- name: Compute release versions
id: versions
env:
@@ -265,15 +178,13 @@ jobs:
BROWSER_OVERRIDE: ${{ inputs.browser_version }}
MCP_OVERRIDE: ${{ inputs.mcp_version }}
SANDBOX_OVERRIDE: ${{ inputs.sandbox_version }}
DEPLOY_OVERRIDE: ${{ inputs.deploy_version }}
SKIP_AGENTS: ${{ steps.flags.outputs.skip_agents }}
SKIP_CODE: ${{ steps.flags.outputs.skip_code }}
SKIP_BOT: ${{ steps.flags.outputs.skip_bot }}
SKIP_TRAIN: ${{ steps.flags.outputs.skip_train }}
SKIP_BROWSER: ${{ steps.flags.outputs.skip_browser }}
SKIP_MCP: ${{ steps.flags.outputs.skip_mcp }}
SKIP_SANDBOX: ${{ steps.flags.outputs.skip_sandbox }}
SKIP_DEPLOY: ${{ steps.flags.outputs.skip_deploy }}
SKIP_AGENTS: ${{ inputs.skip_agents }}
SKIP_CODE: ${{ inputs.skip_code }}
SKIP_BOT: ${{ inputs.skip_bot }}
SKIP_TRAIN: ${{ inputs.skip_train }}
SKIP_BROWSER: ${{ inputs.skip_browser }}
SKIP_MCP: ${{ inputs.skip_mcp }}
SKIP_SANDBOX: ${{ inputs.skip_sandbox }}
run: |
python <<'PY'
import os
@@ -310,7 +221,6 @@ jobs:
browser_override = os.environ.get("BROWSER_OVERRIDE", "").strip()
mcp_override = os.environ.get("MCP_OVERRIDE", "").strip()
sandbox_override = os.environ.get("SANDBOX_OVERRIDE", "").strip()
deploy_override = os.environ.get("DEPLOY_OVERRIDE", "").strip()
validate_version("agents_version", agents_override)
validate_version("wrapper_version", wrapper_override)
validate_version("code_version", code_override)
@@ -319,7 +229,6 @@ jobs:
validate_version("browser_version", browser_override)
validate_version("mcp_version", mcp_override)
validate_version("sandbox_version", sandbox_override)
validate_version("deploy_version", deploy_override)
agents_pyproject = root / "src/praisonai-agents/pyproject.toml"
agents_content = agents_pyproject.read_text()
@@ -377,14 +286,6 @@ jobs:
sys.exit(1)
current_sandbox = sandbox_match.group(1)
deploy_pyproject = root / "src/praisonai-deploy/pyproject.toml"
deploy_content = deploy_pyproject.read_text()
deploy_match = re.search(r'^version\s*=\s*"([^"]+)"', deploy_content, re.MULTILINE)
if not deploy_match:
print("Could not read praisonai-deploy version from pyproject.toml", file=sys.stderr)
sys.exit(1)
current_deploy = deploy_match.group(1)
wrapper_version_py = root / "src/praisonai/praisonai/version.py"
wrapper_content = wrapper_version_py.read_text()
wrapper_match = re.search(r'__version__ = "([^"]+)"', wrapper_content)
@@ -401,7 +302,6 @@ jobs:
("browser", current_browser),
("mcp", current_mcp),
("sandbox", current_sandbox),
("deploy", current_deploy),
("wrapper", current_wrapper),
):
if not SEMVER.match(value):
@@ -415,7 +315,6 @@ jobs:
skip_browser = os.environ.get("SKIP_BROWSER", "false").lower() == "true"
skip_mcp = os.environ.get("SKIP_MCP", "false").lower() == "true"
skip_sandbox = os.environ.get("SKIP_SANDBOX", "false").lower() == "true"
skip_deploy = os.environ.get("SKIP_DEPLOY", "false").lower() == "true"
if agents_override:
agents_version = agents_override
@@ -466,13 +365,6 @@ jobs:
else:
sandbox_version = bump_version(current_sandbox)
if deploy_override:
deploy_version = deploy_override
elif skip_deploy:
deploy_version = current_deploy
else:
deploy_version = bump_version(current_deploy)
if wrapper_override:
wrapper_version = wrapper_override
else:
@@ -487,7 +379,6 @@ jobs:
fh.write(f"browser_version={browser_version}\n")
fh.write(f"mcp_version={mcp_version}\n")
fh.write(f"sandbox_version={sandbox_version}\n")
fh.write(f"deploy_version={deploy_version}\n")
fh.write(f"wrapper_version={wrapper_version}\n")
fh.write(f"current_agents={current_agents}\n")
fh.write(f"current_code={current_code}\n")
@@ -496,7 +387,6 @@ jobs:
fh.write(f"current_browser={current_browser}\n")
fh.write(f"current_mcp={current_mcp}\n")
fh.write(f"current_sandbox={current_sandbox}\n")
fh.write(f"current_deploy={current_deploy}\n")
fh.write(f"current_wrapper={current_wrapper}\n")
print(f"Agents: {current_agents} -> {agents_version}")
@@ -506,7 +396,6 @@ jobs:
print(f"Browser: {current_browser} -> {browser_version}")
print(f"MCP: {current_mcp} -> {mcp_version}")
print(f"Sandbox: {current_sandbox} -> {sandbox_version}")
print(f"Deploy: {current_deploy} -> {deploy_version}")
print(f"Wrapper: {current_wrapper} -> {wrapper_version}")
PY
@@ -521,17 +410,15 @@ jobs:
BROWSER_VERSION: ${{ steps.versions.outputs.browser_version }}
MCP_VERSION: ${{ steps.versions.outputs.mcp_version }}
SANDBOX_VERSION: ${{ steps.versions.outputs.sandbox_version }}
DEPLOY_VERSION: ${{ steps.versions.outputs.deploy_version }}
WRAPPER_VERSION: ${{ steps.versions.outputs.wrapper_version }}
INPUT_SKIP_AGENTS: ${{ steps.flags.outputs.skip_agents }}
INPUT_SKIP_CODE: ${{ steps.flags.outputs.skip_code }}
INPUT_SKIP_BOT: ${{ steps.flags.outputs.skip_bot }}
INPUT_SKIP_TRAIN: ${{ steps.flags.outputs.skip_train }}
INPUT_SKIP_BROWSER: ${{ steps.flags.outputs.skip_browser }}
INPUT_SKIP_MCP: ${{ steps.flags.outputs.skip_mcp }}
INPUT_SKIP_SANDBOX: ${{ steps.flags.outputs.skip_sandbox }}
INPUT_SKIP_DEPLOY: ${{ steps.flags.outputs.skip_deploy }}
INPUT_SKIP_WRAPPER: ${{ steps.flags.outputs.skip_wrapper }}
INPUT_SKIP_AGENTS: ${{ inputs.skip_agents }}
INPUT_SKIP_CODE: ${{ inputs.skip_code }}
INPUT_SKIP_BOT: ${{ inputs.skip_bot }}
INPUT_SKIP_TRAIN: ${{ inputs.skip_train }}
INPUT_SKIP_BROWSER: ${{ inputs.skip_browser }}
INPUT_SKIP_MCP: ${{ inputs.skip_mcp }}
INPUT_SKIP_SANDBOX: ${{ inputs.skip_sandbox }}
INPUT_SKIP_WRAPPER: ${{ inputs.skip_wrapper }}
run: |
set -euo pipefail
skip_agents="${INPUT_SKIP_AGENTS}"
@@ -541,53 +428,30 @@ jobs:
skip_browser="${INPUT_SKIP_BROWSER}"
skip_mcp="${INPUT_SKIP_MCP}"
skip_sandbox="${INPUT_SKIP_SANDBOX}"
skip_deploy="${INPUT_SKIP_DEPLOY}"
skip_wrapper="${INPUT_SKIP_WRAPPER}"
# Returns 0 if published, 1 if definitively absent (404). Any other
# HTTP status (PyPI outage, 5xx, rate limit) FAILS the run: guessing
# "not published" would lead to a duplicate-upload failure midway
# through the release, which is strictly worse than stopping here
# before anything has been published.
pypi_published() { # $1=package $2=version
local code attempt
for attempt in 1 2 3; do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 \
"https://pypi.org/pypi/$1/$2/json" || echo "000")
case "$code" in
200) return 0 ;;
404) return 1 ;;
*) echo "PyPI check $1==$2 returned HTTP ${code} (attempt ${attempt}/3)"; sleep 5 ;;
esac
done
echo "❌ Cannot determine PyPI state for $1==$2; refusing to guess." >&2
exit 2
}
if [ "$skip_agents" != "true" ]; then
pypi_published praisonaiagents "${AGENTS_VERSION}" && skip_agents=true || true
curl -fsSL "https://pypi.org/pypi/praisonaiagents/${AGENTS_VERSION}/json" >/dev/null && skip_agents=true || true
fi
if [ "$skip_code" != "true" ]; then
pypi_published praisonai-code "${CODE_VERSION}" && skip_code=true || true
curl -fsSL "https://pypi.org/pypi/praisonai-code/${CODE_VERSION}/json" >/dev/null && skip_code=true || true
fi
if [ "$skip_bot" != "true" ]; then
pypi_published praisonai-bot "${BOT_VERSION}" && skip_bot=true || true
curl -fsSL "https://pypi.org/pypi/praisonai-bot/${BOT_VERSION}/json" >/dev/null && skip_bot=true || true
fi
if [ "$skip_train" != "true" ]; then
pypi_published praisonai-train "${TRAIN_VERSION}" && skip_train=true || true
curl -fsSL "https://pypi.org/pypi/praisonai-train/${TRAIN_VERSION}/json" >/dev/null && skip_train=true || true
fi
if [ "$skip_browser" != "true" ]; then
pypi_published praisonai-browser "${BROWSER_VERSION}" && skip_browser=true || true
curl -fsSL "https://pypi.org/pypi/praisonai-browser/${BROWSER_VERSION}/json" >/dev/null && skip_browser=true || true
fi
if [ "$skip_mcp" != "true" ]; then
pypi_published praisonai-mcp "${MCP_VERSION}" && skip_mcp=true || true
curl -fsSL "https://pypi.org/pypi/praisonai-mcp/${MCP_VERSION}/json" >/dev/null && skip_mcp=true || true
fi
if [ "$skip_sandbox" != "true" ]; then
pypi_published praisonai-sandbox "${SANDBOX_VERSION}" && skip_sandbox=true || true
fi
if [ "$skip_deploy" != "true" ]; then
pypi_published praisonai-deploy "${DEPLOY_VERSION}" && skip_deploy=true || true
curl -fsSL "https://pypi.org/pypi/praisonai-sandbox/${SANDBOX_VERSION}/json" >/dev/null && skip_sandbox=true || true
fi
if [ "$skip_wrapper" != "true" ]; then
pypi_published praisonai "${WRAPPER_VERSION}" && skip_wrapper=true || true
curl -fsSL "https://pypi.org/pypi/praisonai/${WRAPPER_VERSION}/json" >/dev/null && skip_wrapper=true || true
fi
echo "skip_agents=${skip_agents}" >> "$GITHUB_OUTPUT"
echo "skip_code=${skip_code}" >> "$GITHUB_OUTPUT"
@@ -596,9 +460,8 @@ jobs:
echo "skip_browser=${skip_browser}" >> "$GITHUB_OUTPUT"
echo "skip_mcp=${skip_mcp}" >> "$GITHUB_OUTPUT"
echo "skip_sandbox=${skip_sandbox}" >> "$GITHUB_OUTPUT"
echo "skip_deploy=${skip_deploy}" >> "$GITHUB_OUTPUT"
echo "skip_wrapper=${skip_wrapper}" >> "$GITHUB_OUTPUT"
if [ "$skip_agents" = "true" ] && [ "$skip_code" = "true" ] && [ "$skip_bot" = "true" ] && [ "$skip_train" = "true" ] && [ "$skip_browser" = "true" ] && [ "$skip_mcp" = "true" ] && [ "$skip_sandbox" = "true" ] && [ "$skip_deploy" = "true" ] && [ "$skip_wrapper" = "true" ]; then
if [ "$skip_agents" = "true" ] && [ "$skip_code" = "true" ] && [ "$skip_bot" = "true" ] && [ "$skip_train" = "true" ] && [ "$skip_browser" = "true" ] && [ "$skip_mcp" = "true" ] && [ "$skip_sandbox" = "true" ] && [ "$skip_wrapper" = "true" ]; then
echo "All target versions already on PyPI — nothing to publish."
fi
@@ -612,7 +475,6 @@ jobs:
steps.pypi_exists.outputs.skip_browser == 'true' &&
steps.pypi_exists.outputs.skip_mcp == 'true' &&
steps.pypi_exists.outputs.skip_sandbox == 'true' &&
steps.pypi_exists.outputs.skip_deploy == 'true' &&
steps.pypi_exists.outputs.skip_wrapper == 'true'
run: echo "Idempotent skip — target versions already on PyPI."
@@ -620,43 +482,34 @@ jobs:
if: inputs.dry_run
run: |
echo "Dry run — no publish, commit, or push."
echo "Only: ${{ inputs.only }}"
echo "Agents version: ${{ steps.versions.outputs.agents_version }}"
echo "Code version: ${{ steps.versions.outputs.code_version }}"
echo "Bot version: ${{ steps.versions.outputs.bot_version }}"
echo "Train version: ${{ steps.versions.outputs.train_version }}"
echo "Browser version: ${{ steps.versions.outputs.browser_version }}"
echo "MCP version: ${{ steps.versions.outputs.mcp_version }}"
echo "Sandbox version: ${{ steps.versions.outputs.sandbox_version }}"
echo "Deploy version: ${{ steps.versions.outputs.deploy_version }}"
echo "Wrapper version: ${{ steps.versions.outputs.wrapper_version }}"
echo ""
if [ "${{ steps.flags.outputs.skip_agents }}" != "true" ]; then
if [ "${{ inputs.skip_agents }}" != "true" ]; then
echo "Would publish praisonaiagents ${{ steps.versions.outputs.agents_version }}"
fi
if [ "${{ steps.flags.outputs.skip_code }}" != "true" ]; then
if [ "${{ inputs.skip_code }}" != "true" ]; then
echo "Would publish praisonai-code ${{ steps.versions.outputs.code_version }}"
fi
if [ "${{ steps.flags.outputs.skip_bot }}" != "true" ]; then
if [ "${{ inputs.skip_bot }}" != "true" ]; then
echo "Would publish praisonai-bot ${{ steps.versions.outputs.bot_version }}"
fi
if [ "${{ steps.flags.outputs.skip_train }}" != "true" ]; then
if [ "${{ inputs.skip_train }}" != "true" ]; then
echo "Would publish praisonai-train ${{ steps.versions.outputs.train_version }}"
fi
if [ "${{ steps.flags.outputs.skip_browser }}" != "true" ]; then
if [ "${{ inputs.skip_browser }}" != "true" ]; then
echo "Would publish praisonai-browser ${{ steps.versions.outputs.browser_version }}"
fi
if [ "${{ steps.flags.outputs.skip_mcp }}" != "true" ]; then
if [ "${{ inputs.skip_mcp }}" != "true" ]; then
echo "Would publish praisonai-mcp ${{ steps.versions.outputs.mcp_version }}"
fi
if [ "${{ steps.flags.outputs.skip_sandbox }}" != "true" ]; then
echo "Would publish praisonai-sandbox ${{ steps.versions.outputs.sandbox_version }}"
fi
if [ "${{ steps.flags.outputs.skip_deploy }}" != "true" ]; then
echo "Would publish praisonai-deploy ${{ steps.versions.outputs.deploy_version }}"
fi
if [ "${{ steps.flags.outputs.skip_wrapper }}" != "true" ]; then
echo "Would run bump_and_release.py ${{ steps.versions.outputs.wrapper_version }} --agents ${{ steps.versions.outputs.agents_version }} --code-pin ${{ steps.versions.outputs.code_version }} --bot-pin ${{ steps.versions.outputs.bot_version }} --train-pin ${{ steps.versions.outputs.train_version }} --browser-pin ${{ steps.versions.outputs.browser_version }} --mcp-pin ${{ steps.versions.outputs.mcp_version }} --sandbox-pin ${{ steps.versions.outputs.sandbox_version }} --deploy-pin ${{ steps.versions.outputs.deploy_version }}"
if [ "${{ inputs.skip_wrapper }}" != "true" ]; then
echo "Would run bump_and_release.py ${{ steps.versions.outputs.wrapper_version }} --agents ${{ steps.versions.outputs.agents_version }} --code-pin ${{ steps.versions.outputs.code_version }} --bot-pin ${{ steps.versions.outputs.bot_version }} --train-pin ${{ steps.versions.outputs.train_version }} --browser-pin ${{ steps.versions.outputs.browser_version }} --mcp-pin ${{ steps.versions.outputs.mcp_version }}"
echo "Would uv publish praisonai ${{ steps.versions.outputs.wrapper_version }}"
fi
@@ -684,9 +537,7 @@ jobs:
rm -rf dist
uv lock
uv build
# --check-url makes re-uploads idempotent: files already on PyPI are
# skipped instead of failing the publish (partial-upload recovery).
uv publish --check-url https://pypi.org/simple/
uv publish
- name: Wait for praisonaiagents on PyPI
if: >-
@@ -715,6 +566,19 @@ jobs:
sleep "$INTERVAL"
done
- name: Commit agents version bump
if: >-
inputs.dry_run != true &&
inputs.skip_agents != true &&
steps.pypi_exists.outputs.skip_agents != 'true'
run: |
set -euo pipefail
git add src/praisonai-agents/pyproject.toml src/praisonai-agents/uv.lock
git diff --cached --quiet && { echo "No agents files to commit"; exit 0; }
git commit -m "Bump praisonaiagents to ${{ steps.versions.outputs.agents_version }}"
git pull --rebase origin main
git push origin main
- name: Publish praisonai-code
if: >-
inputs.dry_run != true &&
@@ -743,9 +607,7 @@ jobs:
rm -rf dist
uv lock
uv build
# --check-url makes re-uploads idempotent: files already on PyPI are
# skipped instead of failing the publish (partial-upload recovery).
uv publish --check-url https://pypi.org/simple/
uv publish
- name: Wait for praisonai-code on PyPI
if: >-
@@ -774,6 +636,19 @@ jobs:
sleep "$INTERVAL"
done
- name: Commit praisonai-code version bump
if: >-
inputs.dry_run != true &&
inputs.skip_code != true &&
steps.pypi_exists.outputs.skip_code != 'true'
run: |
set -euo pipefail
git add src/praisonai-code/pyproject.toml src/praisonai-code/uv.lock src/praisonai-code/praisonai_code/__init__.py
git diff --cached --quiet && { echo "No code files to commit"; exit 0; }
git commit -m "Bump praisonai-code to ${{ steps.versions.outputs.code_version }}"
git pull --rebase origin main
git push origin main
- name: Publish praisonai-bot
if: >-
inputs.dry_run != true &&
@@ -802,9 +677,7 @@ jobs:
rm -rf dist
uv lock
uv build
# --check-url makes re-uploads idempotent: files already on PyPI are
# skipped instead of failing the publish (partial-upload recovery).
uv publish --check-url https://pypi.org/simple/
uv publish
- name: Wait for praisonai-bot on PyPI
if: >-
@@ -833,6 +706,19 @@ jobs:
sleep "$INTERVAL"
done
- name: Commit praisonai-bot version bump
if: >-
inputs.dry_run != true &&
inputs.skip_bot != true &&
steps.pypi_exists.outputs.skip_bot != 'true'
run: |
set -euo pipefail
git add src/praisonai-bot/pyproject.toml src/praisonai-bot/uv.lock src/praisonai-bot/praisonai_bot/_version.py
git diff --cached --quiet && { echo "No bot files to commit"; exit 0; }
git commit -m "Bump praisonai-bot to ${{ steps.versions.outputs.bot_version }}"
git pull --rebase origin main
git push origin main
- name: Publish praisonai-train
if: >-
inputs.dry_run != true &&
@@ -861,9 +747,7 @@ jobs:
rm -rf dist
uv lock
uv build
# --check-url makes re-uploads idempotent: files already on PyPI are
# skipped instead of failing the publish (partial-upload recovery).
uv publish --check-url https://pypi.org/simple/
uv publish
- name: Wait for praisonai-train on PyPI
if: >-
@@ -892,6 +776,19 @@ jobs:
sleep "$INTERVAL"
done
- name: Commit praisonai-train version bump
if: >-
inputs.dry_run != true &&
inputs.skip_train != true &&
steps.pypi_exists.outputs.skip_train != 'true'
run: |
set -euo pipefail
git add src/praisonai-train/pyproject.toml src/praisonai-train/uv.lock src/praisonai-train/praisonai_train/_version.py
git diff --cached --quiet && { echo "No train files to commit"; exit 0; }
git commit -m "Bump praisonai-train to ${{ steps.versions.outputs.train_version }}"
git pull --rebase origin main
git push origin main
- name: Publish praisonai-browser
if: >-
inputs.dry_run != true &&
@@ -920,9 +817,7 @@ jobs:
rm -rf dist
uv lock
uv build
# --check-url makes re-uploads idempotent: files already on PyPI are
# skipped instead of failing the publish (partial-upload recovery).
uv publish --check-url https://pypi.org/simple/
uv publish
- name: Wait for praisonai-browser on PyPI
if: >-
@@ -951,6 +846,19 @@ jobs:
sleep "$INTERVAL"
done
- name: Commit praisonai-browser version bump
if: >-
inputs.dry_run != true &&
inputs.skip_browser != true &&
steps.pypi_exists.outputs.skip_browser != 'true'
run: |
set -euo pipefail
git add src/praisonai-browser/pyproject.toml src/praisonai-browser/uv.lock src/praisonai-browser/praisonai_browser/_version.py
git diff --cached --quiet && { echo "No browser files to commit"; exit 0; }
git commit -m "Bump praisonai-browser to ${{ steps.versions.outputs.browser_version }}"
git pull --rebase origin main
git push origin main
- name: Publish praisonai-mcp
if: >-
inputs.dry_run != true &&
@@ -979,9 +887,7 @@ jobs:
rm -rf dist
uv lock
uv build
# --check-url makes re-uploads idempotent: files already on PyPI are
# skipped instead of failing the publish (partial-upload recovery).
uv publish --check-url https://pypi.org/simple/
uv publish
- name: Wait for praisonai-mcp on PyPI
if: >-
@@ -1010,6 +916,19 @@ jobs:
sleep "$INTERVAL"
done
- name: Commit praisonai-mcp version bump
if: >-
inputs.dry_run != true &&
inputs.skip_mcp != true &&
steps.pypi_exists.outputs.skip_mcp != 'true'
run: |
set -euo pipefail
git add src/praisonai-mcp/pyproject.toml src/praisonai-mcp/uv.lock src/praisonai-mcp/praisonai_mcp/_version.py
git diff --cached --quiet && { echo "No mcp files to commit"; exit 0; }
git commit -m "Bump praisonai-mcp to ${{ steps.versions.outputs.mcp_version }}"
git pull --rebase origin main
git push origin main
- name: Publish praisonai-sandbox
if: >-
inputs.dry_run != true &&
@@ -1038,9 +957,7 @@ jobs:
rm -rf dist
uv lock
uv build
# --check-url makes re-uploads idempotent: files already on PyPI are
# skipped instead of failing the publish (partial-upload recovery).
uv publish --check-url https://pypi.org/simple/
uv publish
- name: Wait for praisonai-sandbox on PyPI
if: >-
@@ -1069,101 +986,20 @@ jobs:
sleep "$INTERVAL"
done
- name: Publish praisonai-deploy
- name: Commit praisonai-sandbox version bump
if: >-
inputs.dry_run != true &&
inputs.skip_deploy != true &&
steps.pypi_exists.outputs.skip_deploy != 'true'
working-directory: src/praisonai-deploy
env:
NEW_VERSION: ${{ steps.versions.outputs.deploy_version }}
CURRENT: ${{ steps.versions.outputs.current_deploy }}
inputs.skip_sandbox != true &&
steps.pypi_exists.outputs.skip_sandbox != 'true'
run: |
set -euo pipefail
python <<'PY'
import os
import re
from pathlib import Path
new = os.environ["NEW_VERSION"]
current = os.environ["CURRENT"]
pyproject = Path("pyproject.toml")
content = pyproject.read_text()
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
version_py = Path("praisonai_deploy/_version.py")
version_content = version_py.read_text()
version_py.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new}"', version_content, count=1))
PY
rm -rf dist
uv lock
uv build
# --check-url makes re-uploads idempotent: files already on PyPI are
# skipped instead of failing the publish (partial-upload recovery).
uv publish --check-url https://pypi.org/simple/
- name: Wait for praisonai-deploy on PyPI
if: >-
inputs.dry_run != true &&
inputs.skip_deploy != true &&
steps.pypi_exists.outputs.skip_deploy != 'true'
run: |
set -euo pipefail
PACKAGE="praisonai-deploy"
VERSION="${{ steps.versions.outputs.deploy_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
# All per-package version bumps are still uncommitted at this point: the
# wrapper release below stages every one of them (bump_and_release.py's
# release_files list) into a SINGLE "Release v…" commit.
# App tokens expire after 1h and this job can exceed that (9 packages ×
# up to a 600s PyPI wait). Re-mint so the final commit/tag/push/release
# never runs on an expired token.
- name: Refresh GitHub App token
id: app-token-final
if: ${{ !cancelled() && inputs.dry_run != true }}
# A failed re-mint must not fail the release: the original token may
# still be valid, and the next step only swaps tokens on success.
continue-on-error: true
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 }}
repositories: ${{ github.event.repository.name }}
# Guarded on the refresh actually succeeding: if the re-mint failed
# (API blip), the ORIGINAL token — still wired into the remote URL and
# GH_TOKEN — may well be valid, and overwriting it with an empty string
# would turn a recoverable situation into a guaranteed auth failure.
- name: Use refreshed GitHub App token
if: ${{ !cancelled() && inputs.dry_run != true && steps.app-token-final.outcome == 'success' }}
env:
APP_TOKEN: ${{ steps.app-token-final.outputs.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
git remote set-url origin "https://x-access-token:${APP_TOKEN}@github.com/${REPO}.git"
echo "GH_TOKEN=${APP_TOKEN}" >> "$GITHUB_ENV"
git add src/praisonai-sandbox/pyproject.toml src/praisonai-sandbox/uv.lock src/praisonai-sandbox/praisonai_sandbox/_version.py
git diff --cached --quiet && { echo "No sandbox files to commit"; exit 0; }
git commit -m "Bump praisonai-sandbox to ${{ steps.versions.outputs.sandbox_version }}"
git pull --rebase origin main
git push origin main
- name: Bump and release wrapper
id: wrapper
if: >-
inputs.dry_run != true &&
inputs.skip_wrapper != true &&
@@ -1178,8 +1014,7 @@ jobs:
--browser-pin "${{ steps.versions.outputs.browser_version }}" \
--mcp-pin "${{ steps.versions.outputs.mcp_version }}" \
--sandbox-pin "${{ steps.versions.outputs.sandbox_version }}" \
--deploy-pin "${{ steps.versions.outputs.deploy_version }}" \
--wait --max-wait 600 --no-add-all --force
--wait --max-wait 600 --no-add-all
- name: Publish praisonai wrapper to PyPI
if: >-
@@ -1187,7 +1022,7 @@ jobs:
inputs.skip_wrapper != true &&
steps.pypi_exists.outputs.skip_wrapper != 'true'
working-directory: src/praisonai
run: uv publish --check-url https://pypi.org/simple/
run: uv publish
- name: Wait for praisonai on PyPI
if: >-
@@ -1216,139 +1051,47 @@ jobs:
sleep "$INTERVAL"
done
# Actually probes PyPI — previously this step only echoed checkmarks
# from the input flags, so it "verified" publishes that never happened.
- name: Verify PyPI versions
if: inputs.dry_run != true
run: |
set -euo pipefail
verify() { # $1=pypi-name $2=version $3=effective-skip
if [ "$3" = "true" ]; then
echo "⏭️ $1 skipped"
return 0
fi
if curl -fsSL --max-time 30 "https://pypi.org/pypi/$1/$2/json" >/dev/null; then
echo "✅ $1==$2"
else
echo "❌ $1==$2 NOT found on PyPI"
exit 1
fi
}
verify praisonaiagents "${{ steps.versions.outputs.agents_version }}" "${{ steps.flags.outputs.skip_agents }}"
verify praisonai-code "${{ steps.versions.outputs.code_version }}" "${{ steps.flags.outputs.skip_code }}"
verify praisonai-bot "${{ steps.versions.outputs.bot_version }}" "${{ steps.flags.outputs.skip_bot }}"
verify praisonai-train "${{ steps.versions.outputs.train_version }}" "${{ steps.flags.outputs.skip_train }}"
verify praisonai-browser "${{ steps.versions.outputs.browser_version }}" "${{ steps.flags.outputs.skip_browser }}"
verify praisonai-mcp "${{ steps.versions.outputs.mcp_version }}" "${{ steps.flags.outputs.skip_mcp }}"
verify praisonai-sandbox "${{ steps.versions.outputs.sandbox_version }}" "${{ steps.flags.outputs.skip_sandbox }}"
verify praisonai-deploy "${{ steps.versions.outputs.deploy_version }}" "${{ steps.flags.outputs.skip_deploy }}"
verify praisonai "${{ steps.versions.outputs.wrapper_version }}" "${{ steps.flags.outputs.skip_wrapper }}"
AGENTS="${{ steps.versions.outputs.agents_version }}"
CODE="${{ steps.versions.outputs.code_version }}"
BOT="${{ steps.versions.outputs.bot_version }}"
TRAIN="${{ steps.versions.outputs.train_version }}"
BROWSER="${{ steps.versions.outputs.browser_version }}"
MCP="${{ steps.versions.outputs.mcp_version }}"
SANDBOX="${{ steps.versions.outputs.sandbox_version }}"
WRAPPER="${{ steps.versions.outputs.wrapper_version }}"
# Safety net. On a clean run the wrapper's "Release v…" commit already
# carries every bump and this is a no-op. It only fires when the single
# commit could not happen: a package publish failed midway (those versions
# are already live on PyPI, so the bumps must not be lost), skip_wrapper
# was set, or the wrapper itself failed.
- name: Persist any uncommitted version bumps
if: ${{ !cancelled() && inputs.dry_run != true }}
run: |
set -euo pipefail
# If the wrapper release did not complete, drop any uncommitted
# wrapper-side rewrites (version.py, Dockerfiles, pins): committing
# them would put a wrapper version on main that never reached PyPI,
# and the next auto-bump would skip past it forever. Committed
# changes are untouched (checkout only reverts the working tree),
# and if the wrapper was skipped these files were never modified.
if [ "${{ steps.wrapper.outcome }}" != "success" ]; then
git checkout -- \
src/praisonai/praisonai/version.py \
src/praisonai/pyproject.toml \
src/praisonai/uv.lock \
src/praisonai/README.md \
src/praisonai/praisonai.rb \
src/praisonai-deploy/praisonai_deploy/docker.py \
docker/ 2>/dev/null || true
if [ "${{ inputs.skip_agents }}" != "true" ]; then
echo "✅ praisonaiagents==${AGENTS}"
fi
# -u = tracked modifications only; uv build output under dist/ is
# untracked and stays out of the commit.
git add -u
git diff --cached --quiet && { echo "Nothing left to commit"; exit 0; }
# Name the packages actually staged, so a single-package release
# (every other skip_* set) reads "bump praisonai-mcp" rather than a
# generic message. Falls back when nothing under src/ is staged.
PKGS="$(git diff --cached --name-only \
| sed -n 's#^src/\([^/]*\)/.*#\1#p' | sort -u | tr '\n' ' ' | sed 's/ *$//')"
if [ -n "$PKGS" ]; then
MSG="chore(release): bump ${PKGS} [skip ci]"
else
MSG="chore(release): persist version bumps [skip ci]"
fi
echo "Commit message: ${MSG}"
git commit -m "$MSG"
# This step fires exactly when the release went sideways — i.e. when
# main is most likely to have moved — so the rebase+push retries.
# Losing this commit deadlocks future auto-releases: the packages
# are on PyPI at N+1 while the repo says N, so every later run
# detects "already published" and skips both publish and bump.
for attempt in 1 2 3; do
git fetch origin main
if ! git rebase origin/main; then
git rebase --abort || true
# During rebase, "theirs" is the commit being replayed — the
# version bumps — so they win any conflict.
if ! git rebase -X theirs origin/main; then
git rebase --abort || true
echo "Rebase attempt ${attempt}/3 failed"
sleep 5
continue
fi
fi
if git push origin main; then
echo "Pushed version bumps (attempt ${attempt}/3)"
exit 0
fi
echo "Push attempt ${attempt}/3 rejected; retrying..."
sleep 5
done
echo "❌ Could not push version bumps after 3 attempts."
echo " Repo is now BEHIND PyPI; commit the version files on main manually"
echo " or future auto-releases will no-op ('already published')."
exit 1
# Dedicated releases (skip_wrapper) create no "Release v…" tag — the
# wrapper drives that — so tag each published package individually for
# provenance. Full releases keep their single v… tag unchanged.
- name: Tag dedicated package releases
if: >-
${{ !cancelled() && inputs.dry_run != true &&
steps.flags.outputs.skip_wrapper == 'true' &&
steps.versions.outcome == 'success' }}
run: |
set -euo pipefail
tag_pkg() { # $1=effective-skip $2=pypi-name $3=version
if [ "$1" = "true" ]; then
return 0
fi
local tag="$2-v$3"
# Only tag what is confirmed on PyPI — a package whose publish
# failed must not get a tag claiming it shipped.
if ! curl -fsSL --max-time 30 "https://pypi.org/pypi/$2/$3/json" >/dev/null; then
echo "⏭️ $2==$3 not on PyPI; not tagging"
return 0
fi
if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
echo "tag ${tag} already exists"
return 0
fi
git tag "${tag}"
git push origin "refs/tags/${tag}"
echo "🏷️ ${tag}"
}
tag_pkg "${{ steps.flags.outputs.skip_agents }}" praisonaiagents "${{ steps.versions.outputs.agents_version }}"
tag_pkg "${{ steps.flags.outputs.skip_code }}" praisonai-code "${{ steps.versions.outputs.code_version }}"
tag_pkg "${{ steps.flags.outputs.skip_bot }}" praisonai-bot "${{ steps.versions.outputs.bot_version }}"
tag_pkg "${{ steps.flags.outputs.skip_train }}" praisonai-train "${{ steps.versions.outputs.train_version }}"
tag_pkg "${{ steps.flags.outputs.skip_browser }}" praisonai-browser "${{ steps.versions.outputs.browser_version }}"
tag_pkg "${{ steps.flags.outputs.skip_mcp }}" praisonai-mcp "${{ steps.versions.outputs.mcp_version }}"
tag_pkg "${{ steps.flags.outputs.skip_sandbox }}" praisonai-sandbox "${{ steps.versions.outputs.sandbox_version }}"
tag_pkg "${{ steps.flags.outputs.skip_deploy }}" praisonai-deploy "${{ steps.versions.outputs.deploy_version }}"
if [ "${{ inputs.skip_code }}" != "true" ]; then
echo "✅ praisonai-code==${CODE}"
fi
if [ "${{ inputs.skip_bot }}" != "true" ]; then
echo "✅ praisonai-bot==${BOT}"
fi
if [ "${{ inputs.skip_train }}" != "true" ]; then
echo "✅ praisonai-train==${TRAIN}"
fi
if [ "${{ inputs.skip_browser }}" != "true" ]; then
echo "✅ praisonai-browser==${BROWSER}"
fi
if [ "${{ inputs.skip_mcp }}" != "true" ]; then
echo "✅ praisonai-mcp==${MCP}"
fi
if [ "${{ inputs.skip_sandbox }}" != "true" ]; then
echo "✅ praisonai-sandbox==${SANDBOX}"
fi
if [ "${{ inputs.skip_wrapper }}" != "true" ]; then
echo "✅ praisonai==${WRAPPER}"
fi
@@ -1,47 +0,0 @@
name: Terminal-Bench Smoke
# Manual (and optional weekly) smoke run of the `praisonai code` assistant on a
# small verified Terminal-Bench 2.1 subset via the Harbor harness.
# Never runs on PRs — it costs money (LLM calls) and is inherently flaky.
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
permissions:
contents: read
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Harbor + PraisonAI
run: |
python -m pip install --upgrade pip
pip install harbor
pip install -e src/praisonai-agents -e src/praisonai
- name: Run Terminal-Bench 2.1 smoke subset
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PYTHONPATH: .
run: |
harbor run -c examples/terminal_bench/job_code_smoke.yaml || true
- name: Upload Harbor results
if: always()
uses: actions/upload-artifact@v4
with:
name: harbor-results
path: |
runs/
**/results.json
if-no-files-found: warn
+4 -38
View File
@@ -5,9 +5,6 @@ on:
branches: [ main, develop ]
paths:
- 'src/**'
- 'src/praisonai-deploy/infra/**'
- 'src/praisonai-bot/infra/**'
- 'scripts/check_helm_charts.sh'
- 'pyproject.toml'
- '.github/workflows/test-core.yml'
- '.github/actions/install-monorepo-packages/**'
@@ -15,9 +12,6 @@ on:
branches: [ main, develop ]
paths:
- 'src/**'
- 'src/praisonai-deploy/infra/**'
- 'src/praisonai-bot/infra/**'
- 'scripts/check_helm_charts.sh'
- 'pyproject.toml'
- '.github/workflows/test-core.yml'
- '.github/actions/install-monorepo-packages/**'
@@ -120,17 +114,11 @@ jobs:
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: deploy
workdir: src/praisonai-deploy
paths: >-
tests/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: subdirs
paths: >-
tests/unit/scheduler/
tests/unit/integrations/
tests/unit/deploy/
tests/unit/mcp/
tests/unit/knowledge/
tests/unit/llm/
@@ -195,9 +183,6 @@ jobs:
if [ "$WORKDIR" = "src/praisonai-sandbox" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c13_sandbox_imports.sh"
fi
if [ "$WORKDIR" = "src/praisonai-deploy" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c14_deploy_imports.sh"
fi
if [ "$WORKDIR" = "src/praisonai-mcp" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c12_mcp_imports.sh"
fi
@@ -283,27 +268,9 @@ jobs:
--ignore=tests/integration/test_tracker_complex_tasks.py \
--ignore=tests/integration/test_serve_integration.py
helm-charts:
name: helm-charts
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: v3.14.4
- name: Lint and template Helm charts
shell: bash
run: |
set -euo pipefail
bash "$GITHUB_WORKSPACE/scripts/check_helm_charts.sh"
test-core:
name: test-core
needs: [test-core-collect, test-core-unit, test-core-integration, helm-charts]
needs: [test-core-collect, test-core-unit, test-core-integration]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
@@ -315,9 +282,8 @@ jobs:
collect='${{ needs.test-core-collect.result }}'
unit='${{ needs.test-core-unit.result }}'
integration='${{ needs.test-core-integration.result }}'
helm='${{ needs.helm-charts.result }}'
echo "collect=$collect unit=$unit integration=$integration helm=$helm"
if [ "$collect" != "success" ] || [ "$unit" != "success" ] || [ "$integration" != "success" ] || [ "$helm" != "success" ]; then
echo "collect=$collect unit=$unit integration=$integration"
if [ "$collect" != "success" ] || [ "$unit" != "success" ] || [ "$integration" != "success" ]; then
echo "Core Tests failed"
exit 1
fi
+2 -22
View File
@@ -42,7 +42,7 @@ jobs:
# ============================================
smoke:
runs-on: ubuntu-latest
timeout-minutes: 12
timeout-minutes: 8
steps:
- uses: actions/checkout@v4
with:
@@ -86,7 +86,6 @@ jobs:
tests/unit/test_c11_browser_backward_compat.py \
tests/unit/test_c12_mcp_backward_compat.py \
tests/unit/test_c13_sandbox_backward_compat.py \
tests/unit/test_c14_deploy_backward_compat.py \
-q --tb=line --timeout=30 --maxfail=5
python -m pytest tests/unit/cli/ \
../praisonai-bot/tests/unit/cli/test_onboard_command.py \
@@ -270,26 +269,10 @@ jobs:
bash "$GITHUB_WORKSPACE/scripts/check_c13_sandbox_imports.sh"
python -m pytest src/praisonai-sandbox/tests -q -m "not network"
- name: Standalone praisonai-deploy smoke (C14)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-deploy-standalone
/tmp/praisonai-deploy-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-deploy
/tmp/praisonai-deploy-standalone/bin/python -c "
from praisonai_deploy._version import __version__ as pkg_version
import praisonai_deploy
assert praisonai_deploy.__version__ == pkg_version
assert 'main' not in __import__('sys').modules
print('deploy standalone ok', praisonai_deploy.__version__)
"
/tmp/praisonai-deploy-standalone/bin/praisonai-deploy --help
bash "$GITHUB_WORKSPACE/scripts/check_c14_deploy_imports.sh"
python -m pytest src/praisonai-deploy/tests -q -m "not network"
- name: Audit hybrid module imports (C8 full stack)
run: |
set -euo pipefail
pip install -q -e src/praisonai-agents -e src/praisonai-code -e src/praisonai-bot -e src/praisonai-train -e src/praisonai-browser -e src/praisonai-mcp -e src/praisonai-sandbox -e src/praisonai-deploy -e src/praisonai
pip install -q -e src/praisonai-agents -e src/praisonai-code -e src/praisonai-bot -e src/praisonai-train -e src/praisonai-browser -e src/praisonai-mcp -e src/praisonai-sandbox -e src/praisonai
python scripts/audit_hybrid_modules.py
- name: Smoke Test Summary
@@ -799,9 +782,6 @@ jobs:
tests/unit/test_decorator_simple.py
tests/unit/test_db_adapter_tracing_init.py
../praisonai-bot/tests/unit/test_bot_wiring.py
../praisonai-bot/tests/unit/bots/test_enable_shell.py
../praisonai-bot/tests/unit/gateway/test_preflight.py
../praisonai-bot/tests/unit/gateway/test_gateway_doctor.py
../praisonai-bot/tests/unit/test_bot_history.py
../praisonai-bot/tests/unit/test_bot_fixes.py
tests/unit/test_auto_lazy_loading.py
+1 -1
View File
@@ -20,7 +20,7 @@ on:
jobs:
test-windows:
runs-on: windows-latest
timeout-minutes: 10
timeout-minutes: 5
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL_NAME: gpt-4o-mini
-12
View File
@@ -12,7 +12,6 @@ assets
.DS_Store
.pytest_cache
praisonAI.egg-info
src/*/build/
flagged
test.yaml
db
@@ -25,22 +24,11 @@ output
!src/praisonai-agents/praisonaiagents/output/
!src/praisonai-code/praisonai_code/cli/output/
!src/praisonai-bot/praisonai_bot/cli/output/
!src/praisonai-ts/src/db/
!src/praisonai-ts/src/cli/output/
.files
threads.db
threads.db-journal
# Example run artifacts (generated at runtime, must not be committed)
examples/**/knowledge.db
examples/**/knowledge.db-journal
examples/python/managed-agents/managed_ids.json
examples/python/save_output/blog_post.md
examples/python/save_output/generated/
examples/python/token-metrics/token_metrics_export.json
examples/python/usecases/market_research_results.json
.chainlit
!praisonai/ui/config/.chainlit
+2 -2
View File
@@ -7,9 +7,9 @@ You are working on the PraisonAI project.
- Be concise and helpful in responses
- Test implementation thoroughly
- Ensure backward compatibility with existing APIs
- Follow protocol-driven design across the nine packages: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, deployment in `praisonai-deploy/`, integrations/serve/dashboard in the `praisonai/` wrapper
- Follow protocol-driven design across the eight packages: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, integrations/serve/dashboard in the `praisonai/` wrapper
- Preserve old `praisonai.*` import paths via shims when moving code between packages (see §2.3 in `src/praisonai-agents/AGENTS.md`; shim helpers in `src/praisonai/praisonai/cli/_shim.py`)
- Package boundaries and dependency rules: `ARCHITECTURE.md` §2 (Tier 2 packages must never PyPI-depend on the wrapper; cross-tier access goes through lazy `_*_bridge` modules)
- Boundary manifests: `src/praisonai/tests/PRAISONAI_BOT_MANIFEST.md` (C9), `src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md` (C10), `src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md` (C11), `src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md` (C12), `src/praisonai/tests/PRAISONAI_SANDBOX_MANIFEST.md` (C13), `src/praisonai/tests/PRAISONAI_DEPLOY_MANIFEST.md` (C14)
- Boundary manifests: `src/praisonai/tests/PRAISONAI_BOT_MANIFEST.md` (C9), `src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md` (C10), `src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md` (C11), `src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md` (C12), `src/praisonai/tests/PRAISONAI_SANDBOX_MANIFEST.md` (C13)
- When reviewing a PR or an issue, evaluate whether the change addresses a framework concern or a user goal, and design its surface (params, naming, defaults) accordingly
- The aim of this package is to stay **lightweight and powerful**. Do a critical review at each stage — when triaging an issue, when planning a fix, and when reviewing/implementing a PR. Reject scope creep for the sake of adding features: if a capability already exists (e.g. via existing Agent params like `instructions`/`backstory`/`tools`/`hooks`/`memory`), prefer it over a new API surface. A change must genuinely strengthen the SDK (simpler, more robust, more user-friendly) — do not add knobs, params, modules, or exports that have no live consumer or that merely duplicate existing behaviour.
+10 -29
View File
@@ -1,6 +1,6 @@
# PraisonAI Architecture
> **Last updated:** 2026-07-31 (C14 — nine-package tiered model)
> **Last updated:** 2026-07-17 (C13 — eight-package tiered model)
>
> Strategic architecture document for PraisonAI — a multi-agent AI framework.
> Covers **Python tiered package model (C7.1 + C9 + C10)**, system design, runtime
@@ -11,7 +11,7 @@
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13 + C14)](#2-python-tiered-package-model-c71--c9--c10--c11--c12--c13--c14)
2. [Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13)](#2-python-tiered-package-model-c71--c9--c10--c11--c12--c13)
3. [System Overview](#3-system-overview)
4. [Layered Architecture](#4-layered-architecture)
5. [Core Data Contracts](#5-core-data-contracts)
@@ -51,18 +51,17 @@ Orchestration + Observability** core to unlock adoption and trust:
---
## 2. Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13 + C14)
## 2. Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13)
**Release:** v4.6.110+ · `praisonaiagents` · `praisonai-code` · `praisonai-bot` · `praisonai-train` · `praisonai-browser` · `praisonai-mcp` · `praisonai-sandbox` · `praisonai-deploy` · `praisonai`
**Release:** v4.6.110+ · `praisonaiagents` · `praisonai-code` · `praisonai-bot` · `praisonai-train` · `praisonai-browser` · `praisonai-mcp` · `praisonai-sandbox` · `praisonai`
The Python monorepo publishes nine packages in three tiers with strict dependency
The Python monorepo publishes eight packages in three tiers with strict dependency
direction. C7 delivered a standalone agentic hot path; C7.1 formalised
code/wrapper ownership; C9 extracted bots, gateway, and channel CLI into
`praisonai-bot`; C10 extracted LLM fine-tuning and agent training into
`praisonai-train`; C11 extracted browser automation into `praisonai-browser`;
C12 extracted the heavy MCP host into `praisonai-mcp`.
C13 extracted sandbox backends into `praisonai-sandbox`.
C14 extracted deployment (API, Docker, cloud) into `praisonai-deploy`.
```mermaid
flowchart TB
@@ -70,14 +69,13 @@ flowchart TB
Agents["praisonaiagents<br/>Agent, tools, memory, hooks, protocols"]
end
subgraph tier2 [Tier 2 — Terminal + Bot + Train + Browser + MCP + Sandbox + Deploy]
subgraph tier2 [Tier 2 — Terminal + Bot + Train + Browser + MCP + Sandbox]
Code["praisonai-code<br/>run, chat, code, Typer, runtime, LLM"]
Bot["praisonai-bot<br/>bots, gateway, channel CLI, OS daemon"]
Train["praisonai-train<br/>LLM fine-tuning, agent training"]
Browser["praisonai-browser<br/>extension bridge, CDP, Playwright"]
MCP["praisonai-mcp<br/>MCP server host, auth, capability adapters"]
Sandbox["praisonai-sandbox<br/>Docker, E2B, Modal, Sandlock backends"]
Deploy["praisonai-deploy<br/>API, Docker, AWS/Azure/GCP deploy"]
end
subgraph tier3 [Tier 3 — Wrapper]
@@ -90,53 +88,36 @@ flowchart TB
Agents --> Browser
Agents --> MCP
Agents --> Sandbox
Agents --> Deploy
Agents --> Wrapper
Code -.->|"lazy _bot_bridge"| Bot
Code -.->|"lazy _train_bridge"| Train
Code -.->|"lazy _browser_bridge"| Browser
Code -.->|"lazy _mcp_bridge"| MCP
Code -.->|"lazy _sandbox_bridge"| Sandbox
Code -.->|"lazy _deploy_bridge"| Deploy
Code -.->|"lazy _wrapper_bridge"| Wrapper
Bot -.->|"lazy _code_bridge / _wrapper_bridge"| Code
Bot -.->|"lazy _wrapper_bridge"| Wrapper
Train -.->|"lazy _code_bridge / _wrapper_bridge"| Code
```
**PyPI publish order:** `praisonaiagents` → `praisonai-code` + `praisonai-bot` + `praisonai-train` + `praisonai-browser` + `praisonai-mcp` + `praisonai-sandbox` + `praisonai-deploy` → `praisonai`
**PyPI publish order:** `praisonaiagents` → `praisonai-code` + `praisonai-bot` + `praisonai-train` + `praisonai-browser` + `praisonai-mcp` + `praisonai-sandbox` → `praisonai`
**Backward compatibility:** `praisonai.bots`, `praisonai.gateway`, `praisonai.train`,
`praisonai.browser`, `praisonai.mcp_server`, `praisonai.sandbox`, `praisonai.deploy`, and related CLI paths remain as
`praisonai.browser`, `praisonai.mcp_server`, `praisonai.sandbox`, and related CLI paths remain as
`alias_package` shims to `praisonai_bot.*` / `praisonai_train.*` /
`praisonai_browser.*` / `praisonai_mcp.*` / `praisonai_sandbox.*` / `praisonai_deploy.*`.
`praisonai_browser.*` / `praisonai_mcp.*` / `praisonai_sandbox.*`.
| Tier | Package | Owns | Must not depend on |
|------|---------|------|-------------------|
| 1 | `src/praisonai-agents/` | Agent, tools, memory, hooks, `frameworks/` protocols, sandbox protocols | `praisonai`, `praisonai-code`, `praisonai-bot`, `praisonai-train`, `praisonai-browser`, `praisonai-mcp`, `praisonai-sandbox`, `praisonai-deploy` |
| 1 | `src/praisonai-agents/` | Agent, tools, memory, hooks, `frameworks/` protocols, sandbox protocols | `praisonai`, `praisonai-code`, `praisonai-bot`, `praisonai-train`, `praisonai-browser`, `praisonai-mcp`, `praisonai-sandbox` |
| 2a | `src/praisonai-code/` | `run`/`chat`/`code`, Typer, runtime, LLM, tool resolution | **`praisonai` as a PyPI dependency** (optional lazy imports via `_wrapper_bridge` only) |
| 2b | `src/praisonai-bot/` | Bots, gateway, channel CLI, OS daemon, gateway scheduler tick | **`praisonai` as a PyPI dependency** (optional lazy `_wrapper_bridge` for jobs/UI) |
| 2c | `src/praisonai-train/` | LLM fine-tuning (Unsloth), agent training, `train` CLI, conda env setup | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for legacy dispatch) |
| 2d | `src/praisonai-browser/` | Extension bridge, CDP/hybrid automation, `browser` CLI | **`praisonai` as a PyPI dependency** (none required; depends on `praisonaiagents` only) |
| 2e | `src/praisonai-mcp/` | MCP server host, auth, transports, `mcp` CLI | **`praisonai` as a PyPI dependency** (lazy `_wrapper_bridge` for full capability registry) |
| 2f | `src/praisonai-sandbox/` | Sandbox backends (Docker, E2B, Modal, Sandlock, SSH), `SandboxRegistry` | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for `PluginRegistry`) |
| 2g | `src/praisonai-deploy/` | Deploy API/Docker/cloud, `deploy` CLI, scheduler integration | **`praisonai` as a PyPI dependency** (lazy `_plugin_registry` via `_code_bridge`) |
| 3 | `src/praisonai/` | `framework_adapters/`, serve, dashboard, async jobs API | — |
### Repo infra (not PyPI)
Deployment packaging that stays in the git checkout, not in any tier-2 wheel:
| Path | Runtime owner | Orchestration |
|------|---------------|---------------|
| `src/praisonai-bot/infra/helm/praisonai-gateway/` | `praisonai-bot` (gateway) | C14 `deploy helm` wrapper |
| `src/praisonai-deploy/infra/helm/praisonai-agents-api/` | Generated API from C14 | C14 cross-link |
| `src/praisonai-deploy/infra/compose/agents-stack/` | Docker Compose | `praisonai deploy compose up/down` |
| `src/praisonai-deploy/infra/starters/` | Starter scaffolds | `praisonai deploy create --template` |
| `docker/` | Mixed (wrapper/bot dev stacks) | Not C14 — see bot manifest for `docker/bots/` |
Same boundary as C14: Helm/K8s manifests are **repo infra**, not `pip install praisonai-deploy` content.
**Config kernel:** Phase 0 `praisonai/common/` was skipped; shared config lives in
`praisonai_code/cli/configuration/` and is reached by the bot tier via lazy
`_code_bridge` (see `src/praisonai/tests/CONFIG_KERNEL.md`).
+20 -188
View File
@@ -41,6 +41,10 @@ curl -fsSL https://praison.ai/install.sh | bash
<img src=".github/images/dashboard.png" alt="PraisonAI Dashboard" width="800" />
</p>
<p align="center">
<img src=".github/images/agentflow.gif" alt="PraisonAI AgentFlow" width="800" />
</p>
```
██████╗ ██████╗ █████╗ ██╗███████╗ ██████╗ ███╗ ██╗ █████╗ ██╗
██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██╔═══██╗████╗ ██║ ██╔══██╗██║
@@ -52,6 +56,12 @@ curl -fsSL https://praison.ai/install.sh | bash
pip install praisonai
```
<p align="center">
<img src=".github/images/latest_ai_news_and_crawl_each_url_to_find_info.gif" alt="PraisonAI command execution" width="800" />
</p>
\* `export TAVILY_API_KEY=xxxxx`
<div align="center">
<a href="https://docs.praison.ai">
<p align="center">
@@ -96,170 +106,6 @@ agent.start("Analyze the top 3 tech trends of 2026 and format as a markdown tabl
---
## 🧬 The Five-Layer Agent Stack
Most frameworks hand you one or two layers and leave the rest as homework. PraisonAI covers **all five** — plus the outer layer that decides *where* your agent actually runs.
Each layer wraps the one inside it. When an agent misbehaves, the layer tells you where to look.
```
┌─────────────────────────────────────────────────────────────────┐
│ ⬡ MANAGED AGENTS — Where does it actually run? │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 5 · GRAPH — Who runs when, and who checks whom? │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 4 · LOOP — When do we stop? │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 3 · HARNESS — Can it act, and be checked? │ │ │ │
│ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ 2 · CONTEXT — Is the right thing in the window? │ │ │ │ │
│ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ 1 · PROMPT — Did I say it clearly? │ │ │ │ │ │
│ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │
│ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
| Layer | The question it answers | PraisonAI |
|:--|:--|:--|
| **1 · Prompt** | Did I say it clearly? | `instructions=`, `role`/`goal`/`backstory`, `output=`, `templates=` |
| **2 · Context** | Is the right thing in the window? | `memory=`, `knowledge=`, `context=`, handoff `ContextPolicy` |
| **3 · Harness** | Can it act, and be checked? | `tools=`, `MCP()`, `guardrails=`, `approval=`, `hooks=`, `sandbox=` |
| **4 · Loop** | When do we stop? | `execution=ExecutionConfig(...)`, `reflection=`, `autonomy=`, doom-loop detection |
| **5 · Graph** | Who runs when, and who checks whom? | `AgentFlow`, `route()`, `parallel()`, `loop()`, `repeat()` |
| **⬡ Managed** | *Where does it actually run?* | `backend=ManagedAgent(compute="e2b")` — remote sandbox, or a fully hosted loop |
### Layer 1 · Prompt — *Did I say it clearly?*
Role, instructions, examples, output format.
```python
from praisonaiagents import Agent
agent = Agent(
role="Senior Data Analyst",
goal="Turn raw numbers into decisions",
output="verbose", # markdown-formatted output
)
agent.start("Summarise Q3 revenue trends")
```
### Layer 2 · Context — *Is the right thing in the window?*
Write, select, compress, isolate — the four context operations, one parameter each.
```python
from praisonaiagents import Agent
agent = Agent(
instructions="You are a support engineer.",
memory={"user_id": "u-42"}, # write — persists across runs (needs a user_id)
knowledge=["docs/"], # select — retrieves only what's relevant
context="summarize", # compress — auto-compacts before the limit
)
```
> **Isolate** is `handoffs=[specialist]` — a sub-agent inherits the last few messages and the intersection of your tools, not your whole transcript. [📖 Handoffs](https://docs.praison.ai/docs/concepts/handoffs)
### Layer 3 · Harness — *Can it act, and be checked?*
*Agent = Model + Harness.* Tool dispatch, plus the guides that steer before acting and the sensors that observe after.
```python
from praisonaiagents import Agent, MCP, tool
@tool
def deploy(env: str) -> str:
"""Deploy the current build to an environment."""
return f"Deployed to {env}"
agent = Agent(
name="ReleaseEngineer",
instructions="You are a release engineer.",
tools=[deploy, MCP("npx -y @modelcontextprotocol/server-filesystem /tmp")],
approval=True, # guide — human gate before risky tools run
)
agent.start("Deploy to staging, then list the files you can read")
```
### Layer 4 · Loop — *When do we stop?*
Hard iteration caps, budget ceilings, no-progress detection and completion checks — every brake is explicit.
```python
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
instructions="Fix the failing tests.",
execution=ExecutionConfig(max_iter=30, max_budget=0.50, on_budget_exceeded="stop"),
reflection=True, # completion check — the agent grades its own answer
autonomy=True, # required to drive the loop with run_autonomous()
)
result = agent.run_autonomous("Refactor the auth module", max_iterations=5)
print(result.completion_reason)
# goal | no_tool_calls | max_iterations | timeout | doom_loop | needs_help | error
# (with on_budget_exceeded="stop", hitting the cap raises BudgetExceededError,
# surfaced here as completion_reason="error")
```
> **Doom-loop detection is on by default.** Repeated identical tool calls and A→B→A→B oscillation get caught — while a poller whose output keeps changing does not. [📖 Doom Loop Detection](https://docs.praison.ai/docs/features/doom-loop-detection)
### Layer 5 · Graph — *Who runs when, and who checks whom?*
Topology as a versionable artifact: prompt chaining, routing, parallelisation, orchestrator-worker.
```python
from praisonaiagents import AgentFlow
from praisonaiagents.workflows import route, parallel, repeat
flow = AgentFlow(steps=[
classifier,
route({"bug": [bug_agent], "feature": [feature_agent], "default": [triage]}),
parallel([reviewer, tester]), # fan out, join automatically
repeat(editor, until=lambda ctx: "approved" in ctx.previous_result.lower(),
max_iterations=3), # evaluator–optimizer
])
flow.run("Ticket #123: login fails on Safari")
```
> The same graph is expressible in YAML with no Python at all. [📖 AgentFlow](https://docs.praison.ai/docs/concepts/agentflow)
### ⬡ Outside the stack: Managed Agents — *Where does it actually run?*
The harness is commoditising; **where** the agent executes is the next multiplier. Rather than burning your laptop's CPU, hand an agent a short-lived cloud sandbox — repo, tools and tests run there.
```bash
pip install praisonai
```
```python
from praisonai import Agent, ManagedAgent, LocalManagedConfig
# A. Tools run in a remote sandbox; the agent loop stays local
sandboxed = ManagedAgent(
provider="local", compute="e2b", # or modal | daytona | flyio | docker | tenki
config=LocalManagedConfig(model="gpt-4o-mini", name="RemoteTools"),
)
agent = Agent(name="builder", backend=sandboxed)
# B. The entire agent loop runs in the cloud (needs ANTHROPIC_API_KEY;
# with no key set, ManagedAgent() falls back to a local loop)
agent = Agent(name="teacher", backend=ManagedAgent())
agent.start("Write a Python script that prints the first 10 primes, then run it")
```
Sandboxes shut themselves down when idle (`auto_shutdown`, `idle_timeout_s`), and a post-setup snapshot is reused so the next run skips the image pull and dependency install. Commit a `.praisonai/environment.yaml` and the environment travels with the repo.
> 📖 [20 runnable examples](examples/python/managed-agents/) · manage sessions with `praisonai managed sessions list <agent-id>` or `praisonai managed sessions resume <session-id> "<prompt>"`
<sub>Stack framing adapted from [The Five-Layer Agent Stack](https://mer.vin/2026/07/five-layer-agent-stack-match-bug-to-right-layer/) and [Agent Harnesses vs Orbs](https://mer.vin/2026/08/agent-harnesses-vs-orbs-why-remote-sandboxes-beat-local-agent-loops/).</sub>
---
## 🌌 The PraisonAI Ecosystem
Start simple with the core SDK, or expand to full visual builders and dashboards when you're ready.
@@ -354,9 +200,9 @@ Powered by 100+ LLMs (OpenAI, Anthropic, Gemini & local models).
| 🧠 | **Planning Mode** — plan → execute → reason | `planning=True` |
| 🔍 | **Deep Research** — multi-step autonomous research | [Docs](https://docs.praison.ai/docs/agents/deep-research) |
| 🤖 | **External Agents** — orchestrate Claude Code, Gemini CLI, Codex | [Docs](https://docs.praison.ai/docs/code/external-agents) |
| 🔄 | **Agent Handoffs** — seamless conversation passing | `handoffs=[other_agent]` |
| 🔄 | **Agent Handoffs** — seamless conversation passing | `handoff=True` |
| 🛡️ | **Guardrails** — input/output validation | [Docs](https://docs.praison.ai/docs/concepts/guardrails) |
| | **Web Search + Fetch** — native browsing | `web=True` |
| | **Web Search + Fetch** — native browsing | `web_search=True` |
| 🪞 | **Self Reflection** — agent reviews its own output | [Docs](https://docs.praison.ai/docs/concepts/reflection) |
| 🔀 | **Workflow Patterns** — route, parallel, loop, repeat | [Docs](https://docs.praison.ai/docs/concepts/agentflow) |
| 🧠 | **Memory (zero deps)** — works out of the box | `memory=True` |
@@ -366,9 +212,9 @@ Powered by 100+ LLMs (OpenAI, Anthropic, Gemini & local models).
| | Feature | How |
|--|---------|-----|
| 💡 | **Prompt Caching** — reduce latency + cost | `caching=True` |
| 💡 | **Prompt Caching** — reduce latency + cost | `prompt_caching=True` |
| 💾 | **Sessions + Auto-Save** — persistent state across restarts | `auto_save="my-project"` |
| 💭 | **Thinking Budgets** — control reasoning depth | `agent.thinking_budget = 1024` |
| 💭 | **Thinking Budgets** — control reasoning depth | `thinking_budget=1024` |
| 📚 | **RAG + Quality-Based RAG** — auto quality scoring retrieval | [Docs](https://docs.praison.ai/docs/concepts/rag) |
| 📊 | **Model Router** — auto-routes to cheapest capable model | [Docs](https://docs.praison.ai/docs/features/model-router) |
| 🧊 | **Shadow Git Checkpoints** — auto-rollback on failure | [Docs](https://docs.praison.ai/docs/features/checkpoints) |
@@ -495,10 +341,8 @@ from praisonaiagents import Agent, db
agent = Agent(
name="Assistant",
memory={
"db": db(database_url="postgresql://localhost/mydb"),
"session_id": "my-session",
},
db=db(database_url="postgresql://localhost/mydb"),
session_id="my-session"
)
agent.chat("Hello!") # Auto-persists messages, runs, traces
```
@@ -944,16 +788,6 @@ PraisonAI is built for speed, with agent instantiation in around 14μs. This red
---
<p align="center">
<img src=".github/images/agentflow.gif" alt="PraisonAI AgentFlow" width="800" />
</p>
<p align="center">
<img src=".github/images/latest_ai_news_and_crawl_each_url_to_find_info.gif" alt="PraisonAI command execution" width="800" />
</p>
\* `export TAVILY_API_KEY=xxxxx`
## 🔍 Langfuse Tracing
```bash
@@ -1057,10 +891,8 @@ from praisonaiagents import Agent, db
agent = Agent(
name="Assistant",
memory={
"db": db(database_url="postgresql://localhost/mydb"),
"session_id": "my-session",
},
db=db(database_url="postgresql://localhost/mydb"),
session_id="my-session"
)
```
@@ -1076,8 +908,8 @@ from praisonaiagents import Agent
agent = Agent(
name="Assistant",
# Enables file-based memory (no extra deps!)
memory={"user_id": "user123"},
memory=True, # Enables file-based memory (no extra deps!)
user_id="user123"
)
```
+22 -20
View File
@@ -25,7 +25,7 @@ Methods:
Types:
```python
from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PraisonAIAgents, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
```
Methods:
@@ -36,9 +36,13 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">analyze_prompt</a>(prompt: str) -> set</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>() -> Optional[bool]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value: Optional[bool]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>() -> Optional[bool]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value: Optional[bool]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_history</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_history</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>() -> Optional[bool]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value: Optional[bool]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clone_for_channel</a>() -> 'Agent'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">close</a>() -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>() -> Optional[Any]</code>
@@ -79,7 +83,6 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous_async</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str = '', threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False, goal: Optional[str] = None, goal_criteria: Optional[Any] = None, judge_model: Optional[str] = None) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until_async</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">set_snapshot_root</a>(project_path: str) -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', action: str = 'add', **kwargs: Any) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>() -> Optional[Any]</code>
@@ -176,15 +179,14 @@ Methods:
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">get_schema</a>() -> Dict[str, Any]</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">run</a>(**kwargs) -> Any</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">safe_run</a>(**kwargs) -> ToolResult</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_model_output</a>(result: Any) -> Optional[Any]</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate</a>() -> bool</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_class</a>() -> bool</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_schema_roundtrip</a>() -> bool</code>
* <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 FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">to_model_output</a>(result: Any) -> Optional[Any]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">clear</a>() -> None</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_plugins</a>() -> int</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_single_file_plugins</a>() -> int</code>
@@ -230,6 +232,13 @@ Methods:
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">repeat</a>(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None, max_iterations: int = 10) -> Repeat</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">route</a>(routes: Dict[str, List], default: Optional[List] = None) -> Route</code>
# DB
Types:
```python
from praisonaiagents import db
```
# Memory
Types:
@@ -250,12 +259,9 @@ Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memory</a>(memory_id: str, memory_type: Optional[str] = None) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_short_term</a>(memory_id: str) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">finalize_task_output</a>(content: str, agent_name: str, quality_score: float, threshold: float = 0.7, metrics: Dict[str, Any] = None, task_id: str = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">forget</a>(**kwargs) -> int</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_all_memories</a>() -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_learn_context</a>() -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">learn</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">recall</a>(query: str, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">remember</a>(content: str, **kwargs) -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_all</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_entity_only</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_long_term</a>()</code>
@@ -263,8 +269,8 @@ Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_user_memory</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search</a>(query: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_entity</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, metadata_filter: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, metadata_filter: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_user_memory</a>(user_id: str, query: str, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_with_quality</a>(query: str, min_quality: float = 0.0, memory_type: Literal['short', 'long'] = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_entity</a>(name: str, type_: str, desc: str, relations: str)</code>
@@ -510,7 +516,6 @@ Methods:
* <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">reload</a>() -> Dict[str, List[str]]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">remove_file</a>(name: str, file_path: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">remove_skill_file</a>(name: str, file_path: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">resolve</a>(selectors: List[str]) -> List[str]</code>
@@ -570,7 +575,6 @@ Methods:
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_prompts</a>() -> List[dict]</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_resources</a>() -> List[dict]</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_tools</a>() -> List[Callable]</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">list_active_server_names</a>() -> set</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">shutdown</a>()</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">to_openai_tool</a>()</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">with_tool_prefix</a>(prefix: str) -> 'MCP'</code>
@@ -734,7 +738,7 @@ Methods:
Types:
```python
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, RetryBackoffConfig, RunOutcome, __version__, aembedding, aembeddings, embedding, embeddings, get_dimensions
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, RetryBackoffConfig, __version__, aembedding, aembeddings, embedding, embeddings, get_dimensions
```
Methods:
@@ -768,7 +772,6 @@ Methods:
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aspawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart_for_each</a>(inputs, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">await_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
@@ -799,21 +802,16 @@ Methods:
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> bool</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">spawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start_for_each</a>(inputs, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">stream_emitter</a>()</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">wait_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval</a>(request: ApprovalRequest) -> ApprovalDecision</code>
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval_sync</a>(request: ApprovalRequest) -> ApprovalDecision</code>
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">completed</a>(output: Optional[str] = None) -> 'RunOutcome'</code>
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">from_exception</a>(exc: BaseException, output: Optional[str] = None) -> 'RunOutcome'</code>
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">succeeded</a>() -> bool</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/dimensions.py">get_dimensions</a>(model_name: str) -> int</code>
@@ -822,7 +820,7 @@ Methods:
Types:
```python
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, DB, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, __version__, arun, run
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, CloudProvider, DB, Deploy, DeployConfig, DeployType, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, __version__, arun, run
```
# CLI
@@ -949,6 +947,10 @@ Methods:
* <code title="cli">praisonai schedule suggestion-dismiss <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule suggestion-propose <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule suggestions <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai standardise check <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise fix <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise init <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise report <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai 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>
-10
View File
@@ -1,10 +0,0 @@
# Deployment infra has moved
Deployment assets are no longer at the monorepo root.
| Asset | New location |
|-------|----------------|
| Gateway Helm chart | [`src/praisonai-bot/infra/helm/praisonai-gateway/`](../src/praisonai-bot/infra/helm/praisonai-gateway/) |
| Agents API Helm, Compose, Starters | [`src/praisonai-deploy/infra/`](../src/praisonai-deploy/infra/) |
See [`src/praisonai-deploy/infra/README.md`](../src/praisonai-deploy/infra/README.md).
+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.162" \
"praisonai>=4.6.155" \
"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.162" \
"praisonai>=4.6.155" \
"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.162" \
"praisonai>=4.6.155" \
"praisonai[ui]" \
"praisonai[crewai]"
View File
Whitespace-only changes.
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
HTTP Approval Example
=====================
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Slack Approval Example
======================
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Completions Capability Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Embeddings Capability Example
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Images Capability Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Moderations Capability Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Energy Industry Template
========================
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Transportation Industry Template
================================
+1 -4
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
CI Integration Example - Using Doctor in CI/CD pipelines
@@ -20,9 +19,7 @@ def run_doctor_ci():
[sys.executable, "-m", "praisonai", "doctor", "ci"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=120,
timeout=120
)
# Parse JSON output
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Agent-centric Embedding Example
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
PraisonAI Endpoints Example
@@ -36,12 +36,6 @@ if __name__ == "__main__":
print(f"Custom agent max_rpm: {agent.max_rpm}")
print(f"Custom agent max_execution_time: {agent.max_execution_time}")
print(f"Custom agent max_retry_limit: {agent.max_retry_limit}")
# max_rpm auto-creates a live RateLimiter (not just a stored number)
assert agent_rate_limited.max_rpm == 10
assert agent_rate_limited._rate_limiter is not None
assert agent_rate_limited._rate_limiter.requests_per_minute == 10
print(f"Rate-limited agent limiter: {agent_rate_limited._rate_limiter}")
# Test custom agent
print("\n--- Custom Execution ---")
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
Fixed example demonstrating proper guardrail usage with PraisonAI Agents.
+6 -13
View File
@@ -149,28 +149,21 @@ def main():
print(f"\nOriginal document length: {len(content)} characters")
chunks = [{"text": content, "metadata": {"source": doc_path}}]
target_tokens = 2000
# Compress with different ratios
for ratio in [0.3, 0.5, 0.7]:
compressor = ContextCompressor(verbose=False)
result = compressor.compress(
chunks,
query="security verification code",
target_tokens=target_tokens,
compression_ratio=ratio,
compressor = ContextCompressor(
max_tokens=2000,
target_ratio=ratio,
)
result = compressor.compress([content], query="security verification code")
print(f"\nCompression ratio {ratio}:")
print(f" Original tokens: {result.original_tokens}")
print(f" Compressed tokens: {result.compressed_tokens}")
print(f" Actual ratio: {result.compression_ratio:.2f}")
print(f" Actual ratio: {result.compressed_tokens / max(result.original_tokens, 1):.2f}")
# Check if secret code is preserved
compressed_text = " ".join(
c.get("text", "") for c in result.chunks
) if result.chunks else ""
compressed_text = " ".join(result.chunks) if result.chunks else ""
if SECRET_CODE in compressed_text:
print(f" ✅ Secret code preserved in compressed output")
else:
@@ -65,6 +65,7 @@ def main():
name="Policy_Expert",
instructions="You are a policy expert.",
knowledge=[temp_dir],
agent_id="policy_agent_v1", # Knowledge scoped to this agent
)
response = shared_agent.chat("What are the expense report rules?")
@@ -80,6 +81,7 @@ def main():
instructions="You are a personal HR assistant.",
knowledge=[temp_dir],
memory={"user_id": "user_bob"},
agent_id="hr_bot_v2",
)
response = combined_agent.chat("What training is required?")
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + ClickHouse Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + MongoDB Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + MySQL Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + PostgreSQL Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + Redis Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + SQLite Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
Custom Tools MCP Server Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
HTTP Stream MCP Server Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
MCP Client Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""Multi-Agent Shared Session Demo"""
from praisonaiagents import Agent
from praisonai.persistence import create_conversation_store
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Langfuse Integration Example (Updated for TraceSinkProtocol)
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""MLflow Local Observability Demo (no API key needed)"""
import mlflow
import time
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
ClickHouse — Persistence Example
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Minimal Agent with Database Persistence
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
MongoDB StateStore — Full Persistence Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
MySQL ConversationStore — Full Persistence Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
PostgreSQL ConversationStore — Full Persistence Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
PostgreSQL with Runs and Traces
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Redis StateStore — Full Persistence Example
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Session Resume Example
+2 -4
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Redis State Store - Agent-First Example
@@ -14,8 +13,7 @@ Expected Output:
Agent responds with state persisted to Redis
"""
from praisonaiagents import Agent
from praisonaiagents.db import db
from praisonaiagents import Agent, db
print("=== Redis State Store (Agent-First) ===")
@@ -28,7 +26,7 @@ my_db = db(
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
memory=my_db,
db=my_db,
session_id="redis-state-example"
)
@@ -61,7 +61,7 @@ def demonstrate_collision_problem():
_reset_default_registry()
# Agent 1 registers "custom" provider
register_llm_provider("custom", Agent1Provider, override=True)
register_llm_provider("custom", Agent1Provider)
print("Agent 1 registered 'custom' provider")
# Agent 2 tries to register same name - ERROR!
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
PraisonAI A2A Server Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Agent-Centric Autonomy Example.
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
AutoAgents Workflow Patterns Example
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
Context Agent Example - Basic Usage
+1 -3
View File
@@ -1,5 +1,3 @@
# praisonai: skip=true
# Uses removed praisonaiagents.tools exports; pending example refresh
from praisonaiagents import Agent, Tools
from praisonaiagents.tools import read_csv, read_excel, write_csv, write_excel, filter_data, get_summary, group_by, pivot_table
import os
@@ -9,4 +7,4 @@ agent.start(f"""
Read the data from the csv file {os.path.join(os.path.dirname(__file__), "tesla-stock-price.csv")}
Analyse the data and give me the insights
read_csv to read the file
""")
""")
+1 -3
View File
@@ -1,7 +1,5 @@
# praisonai: skip=true
# Uses removed praisonaiagents.tools exports; pending example refresh
from praisonaiagents import Agent, Tools
from praisonaiagents.tools import get_stock_price, get_stock_info, get_historical_data
agent = Agent(instructions="You are a Research Agent", tools=[get_stock_price, get_stock_info, get_historical_data])
agent.start("Understand current stock price and historical data of Apple and Google. Tell me if I can invest in them")
agent.start("Understand current stock price and historical data of Apple and Google. Tell me if I can invest in them")
@@ -1,111 +0,0 @@
"""
Human Review Between Sequential AgentTeam Tasks
================================================
Demonstrates a human-in-the-loop review gate between sequential tasks using the
existing ``on_task_complete`` hook — no new API required.
IMPORTANT — two different ``on_task_complete`` hooks exist, with DIFFERENT
signatures. Do not confuse them:
AgentTeam(hooks=MultiAgentHooksConfig(on_task_complete=fn))
-> fn(task, task_output) # team-level; gets the Task and its output
Task(on_task_complete=fn)
-> fn(task_output) # 1 param: just the TaskOutput
-> fn(task_output, metadata) # 2+ params: TaskOutput + metadata dict
# NOTE: NOT (task, task_output)
This example uses the TEAM-level hook so we get ``(task, task_output)`` and can
filter which tasks require review by ``task.name``.
Approve / reject / edit:
- approve -> return, workflow continues
- reject -> raise; see "stopping the workflow" note below
- edit -> mutate ``task_output.raw``; downstream tasks that read this task via
``context=[...]`` pick up the edited text
Stopping the workflow on reject
-------------------------------
The TEAM-level hook logs and swallows exceptions, so a bare ``raise`` inside it
does NOT stop the sequential run today. To hard-stop on reject, use the PER-TASK
callback path with the 1-argument signature and ``fail_on_callback_error=True``
(see ``review_output`` / ``t1_strict`` at the bottom of this file).
"""
from praisonaiagents import Agent, Task, AgentTeam
from praisonaiagents.config.feature_configs import MultiAgentHooksConfig
# Only these task names trigger a human review gate.
REVIEW_TASKS = {"research"}
# --- Team-level review gate: signature is (task, task_output) ---------------
def review_gate(task, task_output):
if task.name not in REVIEW_TASKS:
return
print(f"\n--- Review: {task.name} ---\n{task_output.raw}\n")
choice = input("Approve (y), reject (n), edit (e)? ").strip().lower()
if choice == "n":
# NOTE: team-level hook swallows this; see per-task strict path below
# for a version that actually halts the workflow.
raise RuntimeError(f"Human rejected task '{task.name}'")
if choice == "e":
# task.result is the same object as task_output, so mutating .raw here
# propagates to downstream tasks that use context=[this_task].
task_output.raw = input("Edited output: ").strip()
researcher = Agent(name="Researcher", role="Researcher", goal="Research", backstory="Expert")
writer = Agent(name="Writer", role="Writer", goal="Write", backstory="Writer")
t1 = Task(
name="research",
description="List 5 facts about {{topic}}", # {{...}} is the substitution syntax
expected_output="5 bullets",
agent=researcher,
)
t2 = Task(
name="summary",
description="Write 3-sentence summary",
expected_output="3 sentences",
agent=writer,
context=[t1], # reads the (possibly edited) output of t1
)
team = AgentTeam(
agents=[researcher, writer],
tasks=[t1, t2],
process="sequential",
variables={"topic": "REST APIs"}, # substituted into {{topic}} in task descriptions
hooks=MultiAgentHooksConfig(on_task_complete=review_gate),
)
# --- Alternative: per-task strict gate that HALTS on reject -----------------
# Per-task Task.on_task_complete uses the 1-argument signature (task_output).
# With fail_on_callback_error=True a raised exception re-raises and stops the run.
def review_output(task_output):
print(f"\n--- Review output ---\n{task_output.raw}\n")
if input("Approve? (y/n) ").strip().lower() == "n":
raise RuntimeError("Human rejected output")
t1_strict = Task(
name="research",
description="List 5 facts about {{topic}}",
expected_output="5 bullets",
agent=researcher,
variables={"topic": "REST APIs"},
on_task_complete=review_output, # signature: (task_output)
fail_on_callback_error=True, # required so reject halts the workflow
)
if __name__ == "__main__":
# NOTE: template values come from AgentTeam(variables=...), NOT start(inputs=...).
# start() forwards **kwargs but does not consume an ``inputs`` argument.
team.start()
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Math Agent Example
@@ -135,7 +135,7 @@ def example_auto_agents_multi_provider():
from praisonaiagents.agents import AutoAgents
# Create AutoAgents that will automatically assign appropriate models
auto_agents = AutoAgents(
auto_agents = AutoAgentTeam(
instructions="Create a market research report on electric vehicles. Include data analysis, competitor analysis, and future projections.",
max_agents=3,
llm="gpt-4o-mini", # Default model for agent generation
+1 -3
View File
@@ -1,5 +1,3 @@
# praisonai: skip=true
# Uses removed praisonaiagents.tools exports; pending example refresh
from praisonaiagents import Agent, Task, AgentTeam
from praisonaiagents.tools import wiki_search, wiki_summary, wiki_page, wiki_random, wiki_language
@@ -15,4 +13,4 @@ agent.start(
"First search the history of AI"
"Read the page of the history of AI"
"Get the summary of the page"
)
)
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import Agent, MCP
qa_agent = Agent(
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import Agent, AgentTeam, Tools
research_agent = Agent(name="Research", instructions="You are a research agent to search internet about AI 2024", tools=[Tools.internet_search])
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import Agent, AgentTeam, Tools
research_agent = Agent(name="Research", instructions="You are a research agent to search internet about AI 2024", tools=[Tools.internet_search])
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Secondary Market Research FastAPI Application
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import Agent, MCP
search_agent = Agent(
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import Agent, AgentTeam
from duckduckgo_search import DDGS
+1 -11
View File
@@ -1,15 +1,5 @@
import threading
import time
from praisonaiagents import Agent
if __name__ == "__main__":
agent = Agent(name="TweetAgent", instructions="Create a Tweet based on the topic provided")
server = threading.Thread(
target=lambda: agent.launch(port=8080, protocol="mcp", path="/mcp"),
daemon=True,
)
server.start()
time.sleep(2)
print("MCP server started on http://127.0.0.1:8080/mcp")
agent.launch(port=8080, protocol="mcp", path="/")
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import AudioAgent
# Text to Speech
-3
View File
@@ -1,5 +1,3 @@
# praisonai: skip=true
# Requires local audio.mp3 fixture
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="groq/whisper-large-v3") # 10x faster
@@ -7,4 +5,3 @@ text = agent.transcribe("audio.mp3", language="en")
print(text)
# Models: openai/whisper-1, groq/whisper-large-v3, deepgram/nova-2
-3
View File
@@ -1,5 +1,3 @@
# praisonai: skip=true
# Requires local audio.mp3 fixture
# Speech-to-Text with Groq (fastest)
# Requires: export GROQ_API_KEY=your-key
@@ -8,4 +6,3 @@ from praisonaiagents import AudioAgent
agent = AudioAgent(llm="groq/whisper-large-v3")
text = agent.listen("audio.mp3") # Replace with your audio file
print(text)
-3
View File
@@ -1,8 +1,5 @@
# praisonai: skip=true
# Requires local audio.mp3 fixture
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="deepgram/nova-2")
text = agent.listen("audio.mp3")
print(text)
-3
View File
@@ -1,8 +1,5 @@
# praisonai: skip=true
# Requires local audio.mp3 fixture
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="groq/whisper-large-v3")
text = agent.listen("audio.mp3")
print(text)
-3
View File
@@ -1,8 +1,5 @@
# praisonai: skip=true
# Requires local audio.mp3 fixture
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="openai/whisper-1")
text = agent.listen("audio.mp3")
print(text)
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="openai/tts-1-hd")
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
# Text-to-Speech with OpenAI
# Requires: export OPENAI_API_KEY=your-key
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="elevenlabs/eleven_multilingual_v2")
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="gemini/gemini-2.5-flash-preview-tts")
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="openai/tts-1")
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Bot Commands Example — Built-in /status, /new, /help commands.
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Bot Gateway Example — Run multiple bots from one gateway server.
+2 -2
View File
@@ -102,7 +102,7 @@ async def demo_with_run_control():
from praisonai.bots._run_control import SessionRunControl
from praisonai.bots._session import BotSessionManager
from praisonai.bots._commands import handle_stop_command_async
from praisonai.bots._commands import handle_stop_command
# Session manager with run control
run_control = SessionRunControl(
@@ -136,7 +136,7 @@ async def demo_with_run_control():
print("\n3. User decides to stop current task:")
print(" User: '/stop'")
stop_response = await handle_stop_command_async(user_id, run_control)
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)
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Standalone gateway example (praisonai-bot first).
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""HTTP Approval — serve a local web dashboard for tool approvals."""
from praisonaiagents import Agent
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Basic Camera Integration Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Continuous Camera Monitoring Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Multi-Agent Camera Analysis Example
@@ -46,6 +46,6 @@ custom_cmd = SlashCommand(
alt_names=["mc"]
)
handler.register_command(custom_cmd)
handler.register(custom_cmd)
result = handler.execute("/mycommand arg1 arg2")
print(f"\n=== Custom Command ===\n{result.get('message', '')}")
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Claude Code CLI Integration Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Codex CLI Integration Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Cursor CLI Integration Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
External Agents Integration Example
Loaded 100 of 1226 files, more files were not shown because too many files have changed in this diff. Show more