mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 01:08:39 -10:00
Compare commits
215
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bb52b9f07 | ||
|
|
a1c56fd2ba | ||
|
|
a207997441 | ||
|
|
fdbf260ad0 | ||
|
|
d6337fc462 | ||
|
|
30d250e76c | ||
|
|
6af30d2d07 | ||
|
|
b1ce2fb117 | ||
|
|
d4b9d37a81 | ||
|
|
51720c9236 | ||
|
|
18949bfc9c | ||
|
|
a983d4921a | ||
|
|
a89fff5b58 | ||
|
|
5c2fc72c97 | ||
|
|
16fe986176 | ||
|
|
c6d2fdaaec | ||
|
|
f9a88fc843 | ||
|
|
b4dbb52703 | ||
|
|
f95c07a8c8 | ||
|
|
7363190f77 | ||
|
|
ce6a7b0eeb | ||
|
|
9b26b35f1e | ||
|
|
aaf55f85da | ||
|
|
ee9876f5f9 | ||
|
|
287db4847b | ||
|
|
bdf0cdc3b8 | ||
|
|
b15c8a6611 | ||
|
|
192c21b59d | ||
|
|
bd8ed31e00 | ||
|
|
7a8e11a2e2 | ||
|
|
ed9d7c2985 | ||
|
|
fa45810bab | ||
|
|
e2df0c7fd7 | ||
|
|
3c769d7fc5 | ||
|
|
0060da8ac4 | ||
|
|
378f5a23f1 | ||
|
|
77198f514f | ||
|
|
392ac32ce5 | ||
|
|
b7b877c5f1 | ||
|
|
3c8531ec4f | ||
|
|
a5d8138a9a | ||
|
|
56644ba7c1 | ||
|
|
8af852f79e | ||
|
|
e76e3130c0 | ||
|
|
5f39d667ee | ||
|
|
b456e6b20f | ||
|
|
affeb9e537 | ||
|
|
5f2470b102 | ||
|
|
829e883e28 | ||
|
|
52636c2347 | ||
|
|
3230ea708a | ||
|
|
f689889f7a | ||
|
|
e3ce707547 | ||
|
|
978115e02d | ||
|
|
f758467c1e | ||
|
|
3aa9cbc2bd | ||
|
|
345e47cab3 | ||
|
|
d83427f9b2 | ||
|
|
1aa317ce6c | ||
|
|
0115d51a2d | ||
|
|
f5f5bd3f52 | ||
|
|
88cf0c2915 | ||
|
|
4f09886b51 | ||
|
|
8ce5658d8b | ||
|
|
b7c8ab036b | ||
|
|
e120bef8d3 | ||
|
|
a743a3a507 | ||
|
|
6da229e0e2 | ||
|
|
6377b84864 | ||
|
|
ac8e4e4bbd | ||
|
|
618d2142f3 | ||
|
|
9fa3e4803e | ||
|
|
5b0154bdcd | ||
|
|
46f9e18828 | ||
|
|
320eb74d2c | ||
|
|
ee06c22f8a | ||
|
|
8d1dfa1333 | ||
|
|
7a9e53718c | ||
|
|
5f44246030 | ||
|
|
183b623800 | ||
|
|
b7d21a91d2 | ||
|
|
d2091a7712 | ||
|
|
688d770607 | ||
|
|
63ae440fcd | ||
|
|
d87915e3de | ||
|
|
863ade10c4 | ||
|
|
22378ffb42 | ||
|
|
9bcf9c6a00 | ||
|
|
7814844961 | ||
|
|
c31e335770 | ||
|
|
0698acbf00 | ||
|
|
1620b49f36 | ||
|
|
a08752e528 | ||
|
|
644721accb | ||
|
|
9d5db0ccca | ||
|
|
f2b62978a2 | ||
|
|
6cb97e26c0 | ||
|
|
ef63c233c0 | ||
|
|
a4f8d2050f | ||
|
|
708b2de56f | ||
|
|
66c1c47efd | ||
|
|
c175691991 | ||
|
|
2ece9d59db | ||
|
|
b025d199fd | ||
|
|
6d7f1630b1 | ||
|
|
d0eb52156a | ||
|
|
751de46794 | ||
|
|
8311acff9f | ||
|
|
3015ce4387 | ||
|
|
3a222a1cb8 | ||
|
|
a6e4c26a24 | ||
|
|
e56456a3e1 | ||
|
|
a160fa6de5 | ||
|
|
17aca46f91 | ||
|
|
8b82df0065 | ||
|
|
1e84466e46 | ||
|
|
846568c7a5 | ||
|
|
2c2678d192 | ||
|
|
9fbddf4105 | ||
|
|
2a855c4700 | ||
|
|
d51073710a | ||
|
|
6b4b59fbe7 | ||
|
|
709a038785 | ||
|
|
64f7fdeb84 | ||
|
|
f834d42448 | ||
|
|
705fae31f8 | ||
|
|
b4270173d4 | ||
|
|
6256744348 | ||
|
|
15d597da37 | ||
|
|
04d5e07e1b | ||
|
|
754386022c | ||
|
|
1e27a73633 | ||
|
|
2adfe7e832 | ||
|
|
1ad58ca029 | ||
|
|
2f9677abb2 | ||
|
|
9991e00f52 | ||
|
|
86aeaa4d89 | ||
|
|
a66db1836a | ||
|
|
e90d922318 | ||
|
|
43b9552f9b | ||
|
|
d5c77c244b | ||
|
|
bcb6957dac | ||
|
|
fe78664261 | ||
|
|
d9a7c1a01e | ||
|
|
6a122c3671 | ||
|
|
1f1cfa974b | ||
|
|
34d60ae8fa | ||
|
|
095653d78a | ||
|
|
7f6a483e62 | ||
|
|
c6883def69 | ||
|
|
d3c4a2afad | ||
|
|
034138ebc9 | ||
|
|
70b2df135b | ||
|
|
3532600887 | ||
|
|
13ba1ed954 | ||
|
|
4a1fe173e8 | ||
|
|
147bcce448 | ||
|
|
b5e8052809 | ||
|
|
8a93bc5096 | ||
|
|
f65b83614c | ||
|
|
25000d9b26 | ||
|
|
98cfdcc9ad | ||
|
|
6f2df7db2a | ||
|
|
495d4604fe | ||
|
|
a4386796c9 | ||
|
|
e1f9ae5b6b | ||
|
|
aa5315b90e | ||
|
|
b9d3ede110 | ||
|
|
441b428cc9 | ||
|
|
bcae07cf58 | ||
|
|
bf28787705 | ||
|
|
5a8f4aedf8 | ||
|
|
459a12a656 | ||
|
|
d9224adc70 | ||
|
|
82f1d8881b | ||
|
|
ca07ebf813 | ||
|
|
2b2443ff63 | ||
|
|
c8938c6aeb | ||
|
|
fc072d37a3 | ||
|
|
ccfc66092b | ||
|
|
03e5f0b11a | ||
|
|
6b051dcafb | ||
|
|
ade64e2b9c | ||
|
|
3791c6ea64 | ||
|
|
ef8e1fa6b6 | ||
|
|
aa273b7c36 | ||
|
|
98cf202178 | ||
|
|
f49d5daf15 | ||
|
|
011f54fbc5 | ||
|
|
89eefb87fe | ||
|
|
a14db7f39a | ||
|
|
ee126c60b4 | ||
|
|
4f8cfb783f | ||
|
|
00872ee314 | ||
|
|
4ee593cabb | ||
|
|
4ed1412820 | ||
|
|
f8031a4349 | ||
|
|
b0a2aa35d4 | ||
|
|
74273677a8 | ||
|
|
7c9899d0da | ||
|
|
5bff3e51bc | ||
|
|
f55cc2c4ee | ||
|
|
0f9425010f | ||
|
|
222d4f68eb | ||
|
|
aad319c8d5 | ||
|
|
d51191d073 | ||
|
|
8cb0e52a95 | ||
|
|
744ed799af | ||
|
|
c88bd1eeea | ||
|
|
7890723aa6 | ||
|
|
8567f44a4c | ||
|
|
6d26ff96a9 | ||
|
|
2dbc1bcbdf | ||
|
|
197f8d73ca | ||
|
|
ce97667156 |
No files matched your search
@@ -151,24 +151,40 @@ jobs:
|
||||
id: check_fork
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const isPR = context.eventName === 'pull_request' || (context.payload.issue && context.payload.issue.pull_request);
|
||||
if (isPR) {
|
||||
const prNumber = context.payload.pull_request ? context.payload.pull_request.number : context.payload.issue.number;
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
core.setOutput('pr_branch', pr.data.head.ref);
|
||||
|
||||
if (pr.data.head.repo && pr.data.head.repo.full_name !== context.repo.full_name) {
|
||||
core.setOutput('is_fork', 'true');
|
||||
core.setOutput('clone_url', pr.data.head.repo.clone_url);
|
||||
core.setOutput('branch', pr.data.head.ref);
|
||||
return;
|
||||
}
|
||||
const isPR = context.eventName === 'pull_request'
|
||||
|| context.eventName === 'pull_request_review'
|
||||
|| context.eventName === 'pull_request_review_comment'
|
||||
|| (context.payload.issue && context.payload.issue.pull_request);
|
||||
|
||||
if (!isPR) {
|
||||
core.setOutput('is_pr', 'false');
|
||||
core.setOutput('is_fork', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const prNumber = context.payload.pull_request
|
||||
? context.payload.pull_request.number
|
||||
: context.payload.issue.number;
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
core.setOutput('is_pr', 'true');
|
||||
core.setOutput('pr_number', String(prNumber));
|
||||
core.setOutput('pr_branch', pr.data.head.ref);
|
||||
core.setOutput('head_repo', pr.data.head.repo.full_name);
|
||||
core.setOutput('merge_state', (pr.data.mergeable_state || pr.data.merge_state_status || '').toUpperCase());
|
||||
|
||||
const baseRepo = `${context.repo.owner}/${context.repo.repo}`;
|
||||
if (pr.data.head.repo && pr.data.head.repo.full_name !== baseRepo) {
|
||||
core.setOutput('is_fork', 'true');
|
||||
core.setOutput('clone_url', pr.data.head.repo.clone_url);
|
||||
core.setOutput('branch', pr.data.head.ref);
|
||||
return;
|
||||
}
|
||||
core.setOutput('is_fork', 'false');
|
||||
|
||||
@@ -180,17 +196,19 @@ jobs:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Fetch PR branch and Setup Remote
|
||||
if: github.event.issue.pull_request
|
||||
if: steps.check_fork.outputs.is_pr == 'true'
|
||||
env:
|
||||
PR_BRANCH: ${{ steps.check_fork.outputs.pr_branch }}
|
||||
PR_NUMBER: ${{ steps.check_fork.outputs.pr_number }}
|
||||
IS_FORK: ${{ steps.check_fork.outputs.is_fork }}
|
||||
run: |
|
||||
# Fetch PR head from base repo and put it into local branch
|
||||
git fetch origin pull/${{ github.event.issue.number }}/head:"${PR_BRANCH}"
|
||||
|
||||
git fetch origin "pull/${PR_NUMBER}/head:${PR_BRANCH}"
|
||||
git checkout "${PR_BRANCH}"
|
||||
git fetch origin main
|
||||
|
||||
# If it's a fork, make origin point to local to trick claude-code-action's `git fetch origin <branch>`
|
||||
if [ "${IS_FORK}" = "true" ]; then
|
||||
git remote set-url origin file://$(pwd)
|
||||
git remote set-url origin "file://$(pwd)"
|
||||
fi
|
||||
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
@@ -200,11 +218,13 @@ jobs:
|
||||
allowed_bots: 'praisonai-triage-agent[bot]'
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
model: opus
|
||||
trigger_phrase: "@claude"
|
||||
label_trigger: "claude"
|
||||
|
||||
direct_prompt: |
|
||||
${{ steps.check_fork.outputs.is_fork == 'true' && 'CRITICAL: THIS IS A PULL REQUEST FROM A FORK. YOU DO NOT HAVE PUSH PERMISSIONS TO THIS REPOSITORY. ONLY READ FILES, VALIDATE CODE, AND PROVIDE YOUR COMMENTS/FEEDBACK DIRECTLY IN THIS PR VIA COMMENTS. DO NOT ATTEMPT TO PUSH OR CREATE PULL REQUESTS.\n\n' || 'NOTE: The branch is under MervinPraison/PraisonAI (not a fork). You are able to make modifications and push directly to this branch.\n\n' }}
|
||||
${{ steps.check_fork.outputs.is_fork == 'true' && 'CRITICAL: THIS IS A PULL REQUEST FROM A FORK. YOU DO NOT HAVE PUSH PERMISSIONS TO THIS REPOSITORY. ONLY READ FILES, VALIDATE CODE, AND PROVIDE YOUR COMMENTS/FEEDBACK DIRECTLY IN THIS PR VIA COMMENTS. DO NOT ATTEMPT TO PUSH OR CREATE PULL REQUESTS.\n\n' || (steps.check_fork.outputs.is_pr == 'true' && 'NOTE: The branch is under MervinPraison/PraisonAI (not a fork). You are able to make modifications and push directly to this branch.\n\n' || 'NOTE: This is an issue (not a PR). Create a new fix branch per STEP 3 below.\n\n') }}
|
||||
${{ steps.check_fork.outputs.is_pr == 'true' && format('CONTEXT: PR #{0} on branch {1}. head.repo={2}, is_fork={3}, merge_state={4}. NEVER treat cursor/*, claude/* branch names or bot authors as forks — only is_fork=true means fork.\n\n', steps.check_fork.outputs.pr_number, steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.head_repo, steps.check_fork.outputs.is_fork, steps.check_fork.outputs.merge_state) || '' }}
|
||||
You are working on the PraisonAI SDK. Follow AGENTS.md strictly.
|
||||
|
||||
STEP 0 — SETUP GIT IDENTITY & AUTH (GLOBAL — required for all repos):
|
||||
@@ -233,15 +253,9 @@ jobs:
|
||||
- Is there a name collision with existing exports in __init__.py?
|
||||
If ANY of these conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
|
||||
|
||||
STEP 3 — IMPLEMENT (for changes in THIS repo — PraisonAI):
|
||||
- Create a fix branch: git checkout -b claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
- Implement a minimal, focused fix
|
||||
- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper
|
||||
- Keep changes small and backward-compatible
|
||||
- Commit: git add -A && git commit -m "fix: <description> (fixes #$ISSUE_NUMBER)"
|
||||
- Push: git push origin claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
CRITICAL: After pushing, you MUST immediately run: gh pr create --title "fix: <title>" --body "Fixes #$ISSUE_NUMBER" --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
Do NOT stop after pushing. Do NOT say "manually create a PR". Always create the PR automatically.
|
||||
${{ steps.check_fork.outputs.is_pr == 'true' && steps.check_fork.outputs.is_fork != 'true' && steps.check_fork.outputs.merge_state == 'DIRTY' && format('STEP 3 — RESOLVE MERGE CONFLICTS ON PR (branch: {0}, PR #{1}):\n- DO NOT create a new branch or PR\n- git fetch origin main && git rebase origin/main\n- Resolve conflicts: keep this PR intent, merge in main newer logic\n- git add -A && git rebase --continue (repeat until done)\n- git push --force-with-lease origin {0}\n- Run targeted pytest on changed paths\n- Comment files resolved and what you skipped\n', steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.pr_number) || '' }}
|
||||
${{ steps.check_fork.outputs.is_pr == 'true' && steps.check_fork.outputs.is_fork != 'true' && steps.check_fork.outputs.merge_state != 'DIRTY' && format('STEP 3 — IMPLEMENT ON EXISTING PR (branch: {0}, PR #{1}):\n- DO NOT create a new branch or PR\n- git checkout {0}\n- Implement minimal, focused fixes from reviewer feedback\n- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper\n- Keep changes small and backward-compatible\n- Commit: git add -A && git commit -m "fix: <description>"\n- Push: git push origin {0}\n- Comment a summary of exact files modified and what you skipped\n', steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.pr_number) || '' }}
|
||||
${{ steps.check_fork.outputs.is_pr != 'true' && 'STEP 3 — IMPLEMENT (for changes in THIS repo — PraisonAI):\n- Create a fix branch: git checkout -b claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\n- Implement a minimal, focused fix\n- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper\n- Keep changes small and backward-compatible\n- Commit: git add -A && git commit -m "fix: <description> (fixes #$ISSUE_NUMBER)"\n- Push: git push origin claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\nCRITICAL: After pushing, you MUST immediately run: gh pr create --title "fix: <title>" --body "Fixes #$ISSUE_NUMBER" --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)\nDo NOT stop after pushing. Do NOT say "manually create a PR". Always create the PR automatically.\n' || '' }}
|
||||
|
||||
STEP 3-ALT — IMPLEMENT IN EXTERNAL REPO (for PraisonAI-Tools, PraisonAIDocs, PraisonAI-Plugins):
|
||||
When work must happen in a different repository, follow these steps EXACTLY. Do NOT attempt to use `cd`, as directory state is not preserved. Use `git -C` for all commands.
|
||||
@@ -267,10 +281,7 @@ jobs:
|
||||
- For docs changes: verify files exist and are valid markdown/MDX
|
||||
- Ensure no regressions
|
||||
|
||||
STEP 5 — CREATE PR:
|
||||
- Commit with descriptive message, push, and create PR using `gh pr create`
|
||||
- For external repos, use `gh pr create -R MervinPraison/<REPO_NAME>`
|
||||
CRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link or say "manual push required".
|
||||
${{ steps.check_fork.outputs.is_pr == 'true' && 'STEP 5 — SKIP: Already on an open PR. Do NOT run gh pr create.\n' || 'STEP 5 — CREATE PR:\n- Commit with descriptive message, push, and create PR using `gh pr create`\n- For external repos, use `gh pr create -R MervinPraison/<REPO_NAME>`\nCRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link or say "manual push required".\n' }}
|
||||
NOTE: If you worked on an external repo, the wrapper action will report "No commits" for the main repo at the end. This is expected, do not worry about it.
|
||||
allowed_tools: |
|
||||
Bash(git:*)
|
||||
|
||||
@@ -22,8 +22,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -36,9 +36,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Run Gemini Issue Triage
|
||||
|
||||
@@ -37,9 +37,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Set up Git
|
||||
|
||||
@@ -48,9 +48,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
ref: ${{ steps.checkout_ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -60,16 +60,16 @@ jobs:
|
||||
- name: Run PraisonAI integration tests
|
||||
run: |
|
||||
cd PraisonAI/src/praisonai
|
||||
python -m pytest tests/integration/test_aiui_* -v --timeout=300
|
||||
python -m pytest tests/integration/test_aiui_* -v
|
||||
|
||||
- name: Run PraisonAIUI integration tests
|
||||
run: |
|
||||
cd PraisonAIUI
|
||||
if [ -f tests/integration/test_agentic_roundtrip.py ]; then
|
||||
python -m pytest tests/integration/test_agentic_roundtrip.py -v --timeout=300
|
||||
python -m pytest tests/integration/test_agentic_roundtrip.py -v
|
||||
fi
|
||||
if [ -f tests/test_feature_sdk_backends.py ]; then
|
||||
python -m pytest tests/test_feature_sdk_backends.py -v --timeout=300
|
||||
python -m pytest tests/test_feature_sdk_backends.py -v
|
||||
fi
|
||||
|
||||
- name: Test Pattern C CLI
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
run: |
|
||||
cd PraisonAI/src/praisonai
|
||||
if [ -n "$OPENAI_API_KEY" ]; then
|
||||
python -m pytest tests/integration/test_aiui_host_agentic.py -v --timeout=600
|
||||
python -m pytest tests/integration/test_aiui_host_agentic.py -v
|
||||
else
|
||||
echo "OPENAI_API_KEY not available, skipping agentic tests"
|
||||
fi
|
||||
@@ -15,8 +15,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
@@ -26,11 +26,8 @@ jobs:
|
||||
# First checkout the upstream repo at the specified tag
|
||||
- name: Checkout upstream repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: MervinPraison/PraisonAI
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
fetch-depth: 0
|
||||
@@ -48,11 +45,8 @@ jobs:
|
||||
# Now checkout the fork at main branch
|
||||
- name: Checkout fork
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.inputs.fork_owner }}/PraisonAI
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
@@ -449,11 +443,8 @@ jobs:
|
||||
# First checkout the fork's main branch
|
||||
- name: Checkout fork repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
repository: ${{ github.event.inputs.fork_owner }}/PraisonAI
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Sync Claude Secrets to PraisonAIUI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync secrets to PraisonAIUI
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
|
||||
TARGET: MervinPraison/PraisonAIUI
|
||||
CLAUDE_APP_ID: ${{ secrets.CLAUDE_APP_ID }}
|
||||
CLAUDE_APP_PRIVATE_KEY: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
GH_TOKEN_VALUE: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${GH_TOKEN:-}" ]; then
|
||||
echo "SECRETS_ADMIN_PAT is not configured in this repository"
|
||||
exit 1
|
||||
fi
|
||||
echo -n "$CLAUDE_APP_ID" | gh secret set CLAUDE_APP_ID --repo "$TARGET"
|
||||
echo -n "$CLAUDE_APP_PRIVATE_KEY" | gh secret set CLAUDE_APP_PRIVATE_KEY --repo "$TARGET"
|
||||
echo -n "$CLAUDE_CODE_OAUTH_TOKEN" | gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$TARGET"
|
||||
echo -n "$GH_TOKEN_VALUE" | gh secret set GH_TOKEN --repo "$TARGET"
|
||||
echo "Synced CLAUDE_APP_ID, CLAUDE_APP_PRIVATE_KEY, CLAUDE_CODE_OAUTH_TOKEN, GH_TOKEN → $TARGET"
|
||||
@@ -37,10 +37,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -22,8 +22,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
@@ -13,10 +13,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -81,10 +79,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -33,10 +33,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -45,8 +45,8 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -105,8 +105,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -187,8 +187,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -259,8 +259,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -318,8 +318,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -377,8 +377,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -436,8 +436,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -495,8 +495,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
|
||||
@@ -45,10 +45,8 @@ jobs:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -88,6 +88,9 @@ issue_details.json
|
||||
.internal
|
||||
.praisonai
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
|
||||
# Rust / Cargo
|
||||
target/
|
||||
Cargo.lock
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Agent Instructions
|
||||
|
||||
You are working on the PraisonAI project.
|
||||
|
||||
## Project Guidelines
|
||||
- Follow the existing code style and conventions
|
||||
- Be concise and helpful in responses
|
||||
- Test implementation thoroughly
|
||||
- Ensure backward compatibility with existing APIs
|
||||
- Follow protocol-driven design: core protocols in praisonaiagents/, heavy implementations in praisonai/
|
||||
@@ -0,0 +1,14 @@
|
||||
# Contributing to PraisonAI
|
||||
|
||||
Thank you for your interest! Here's how to get started:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Follow existing code style
|
||||
- Add tests for new features
|
||||
- Update documentation as needed
|
||||
@@ -770,11 +770,11 @@ agent.start('Write a movie script about a robot in Mars');
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
PraisonAI is built for speed, with agent instantiation in under 4μs. This reduces overhead, improves responsiveness, and helps multi-agent systems scale efficiently in real-world production workloads.
|
||||
PraisonAI is built for speed, with agent instantiation in around 14μs. This reduces overhead, improves responsiveness, and helps multi-agent systems scale efficiently in real-world production workloads.
|
||||
|
||||
| Performance Metric | PraisonAI |
|
||||
|--------------------|-----------|
|
||||
| Avg Instantiation Time | **3.77 μs** |
|
||||
| Avg Instantiation Time | **14 μs** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Methods:
|
||||
* <code title="class GuardrailResult">GuardrailResult.<a href="./src/praisonai-agents/praisonaiagents/guardrails/guardrail_result.py">from_tuple</a>(result: Tuple[bool, Any]) -> 'GuardrailResult'</code>
|
||||
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_dict</a>(data: Dict[str, Any]) -> 'HandoffConfig'</code>
|
||||
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class HandoffResult">HandoffResult.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_outcome</a>(outcome: AgentRunOutcome, target_agent: Optional[str] = None, source_agent: Optional[str] = None, handoff_depth: int = 0) -> 'HandoffResult'</code>
|
||||
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">json</a>() -> Optional[str]</code>
|
||||
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">to_dict</a>() -> dict</code>
|
||||
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
@@ -58,6 +59,8 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to_async</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">is_closed</a>() -> bool</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_instance</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_instance</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_model</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>(value: Optional[str]) -> None</code>
|
||||
@@ -75,7 +78,7 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until_async</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs: Any) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', 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>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>(value: Optional[Any]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>() -> Optional[int]</code>
|
||||
@@ -175,6 +178,7 @@ Methods:
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_schema_roundtrip</a>() -> bool</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">__call__</a>(*args, **kwargs) -> Any</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">check_availability</a>() -> tuple[bool, str]</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">get_schema</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">injected_params</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">run</a>(**kwargs) -> Any</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">clear</a>() -> None</code>
|
||||
@@ -182,16 +186,18 @@ Methods:
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_single_file_plugins</a>() -> int</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_all</a>() -> Dict[str, Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_available_tools</a>(context: Optional[Dict[str, Any]] = None) -> List[Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool_definitions</a>() -> List[Dict[str, Any]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_trust_level</a>(name: str) -> Optional[str]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_available_tools</a>(context: Optional[Dict[str, Any]] = None, ttl_seconds: Optional[float] = None) -> List[Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_base_tools</a>() -> List[BaseTool]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools</a>() -> List[str]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools_with_allowed_filter</a>(context: Optional[Dict[str, Any]] = None) -> List[str]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False) -> None</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False, trust_level: Optional[str] = None, dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None) -> None</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">unregister</a>(name: str) -> bool</code>
|
||||
* <code title="class Tools">Tools.<a href="./src/praisonai-agents/praisonaiagents/tools/tools.py">internet_search</a>(*args, **kwargs)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_registry</a>() -> ToolRegistry</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None) -> None</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, trust_level: Optional[str] = None, dynamic_schema_overrides: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None) -> None</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">tool</a>(func: Optional[Callable] = None) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_tool</a>(tool: Any) -> bool</code>
|
||||
|
||||
@@ -236,6 +242,7 @@ from praisonaiagents import Memory, MemoryBackend, MemoryConfig
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_cache_optimized_context</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_cache_boundary: bool = True, include_in_output: Optional[bool] = None) -> Dict[str, str]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_context_for_task</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_in_output: Optional[bool] = None) -> str</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">calculate_quality_metrics</a>(output: str, expected_output: str, llm: Optional[str] = None, custom_prompt: Optional[str] = None) -> Dict[str, float]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">close_connections</a>()</code>
|
||||
@@ -353,7 +360,7 @@ Methods:
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_tool_function</a>(source_agent: 'Agent') -> Callable</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_description</a>() -> str</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_name</a>() -> str</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None) -> Handoff</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None, tool_policy_mode: Optional[Literal['intersect', 'passthrough']] = None, blocked_tools: Optional[List[str]] = None) -> Handoff</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">compress_history</a>(data: HandoffInputData) -> HandoffInputData</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">keep_last_n_messages</a>(n: int) -> Callable[[HandoffInputData], HandoffInputData]</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_all_tools</a>(data: HandoffInputData) -> HandoffInputData</code>
|
||||
@@ -511,10 +518,12 @@ Methods:
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_memory</a>(text: str, memory_type: str = 'long', **metadata) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">chat</a>(message: str, **kwargs) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">clear_memory</a>(memory_type: str = 'all') -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">close</a>() -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">create_agent</a>(*args, **kwargs) -> 'Agent'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_context</a>(query: str, max_items: int = 3) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">increment_state</a>(key: str, increment: int = 1, default: int = 0) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">is_expired</a>() -> bool</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">knowledge</a>() -> 'Knowledge'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">memory</a>() -> 'Memory'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">restore_state</a>() -> Dict[str, Any]</code>
|
||||
@@ -523,6 +532,7 @@ Methods:
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_memory</a>(query: str, memory_type: str = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">send_message</a>(message: str, **kwargs) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">time_to_expiry</a>() -> Optional[float]</code>
|
||||
|
||||
# MCP
|
||||
|
||||
@@ -605,7 +615,7 @@ Methods:
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">register_snapshot_callback</a>(callback: Callable[[SnapshotHookData], None]) -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">reset</a>() -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">set_tool_budget</a>(tool_name: str, max_tokens: int, protected: bool = False) -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str) -> str</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str, tool_call_id: str = None, run_id: str = None) -> str</code>
|
||||
|
||||
# UI
|
||||
|
||||
@@ -626,6 +636,7 @@ Methods:
|
||||
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/autonomy.py">effective_track_changes</a>() -> bool</code>
|
||||
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/autonomy.py">from_dict</a>(data: Dict[str, Any]) -> 'AutonomyConfig'</code>
|
||||
* <code title="class CachingConfig">CachingConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">from_dict</a>(data: Dict[str, Any]) -> 'ExecutionConfig'</code>
|
||||
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class HooksConfig">HooksConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
@@ -693,7 +704,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, aembedding, aembeddings, embedding, embeddings, get_dimensions
|
||||
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, __version__, aembedding, aembeddings, embedding, embeddings, get_dimensions
|
||||
```
|
||||
|
||||
Methods:
|
||||
@@ -718,12 +729,16 @@ Methods:
|
||||
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>(value: bool)</code>
|
||||
* <code title="class AgentOSProtocol">AgentOSProtocol.<a href="./src/praisonai-agents/praisonaiagents/app/protocols.py">get_app</a>() -> Any</code>
|
||||
* <code title="class AgentOSProtocol">AgentOSProtocol.<a href="./src/praisonai-agents/praisonaiagents/app/protocols.py">serve</a>(host: Optional[str] = None, port: Optional[int] = None, reload: bool = False, **kwargs: Any) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aannounce_completion</a>(agent_id: str, task_id: str, result: Any, success: bool = True, error: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">announce_completion</a>(agent_id: str, task_id: str, result: Any, success: bool = True, error: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aspawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">await_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">close</a>() -> None</code>
|
||||
@@ -738,6 +753,7 @@ Methods:
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_spawned_agents</a>() -> List[SpawnedSubAgent]</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
|
||||
@@ -754,10 +770,12 @@ Methods:
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">spawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
|
||||
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">wait_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
|
||||
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval</a>(request: ApprovalRequest) -> ApprovalDecision</code>
|
||||
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval_sync</a>(request: ApprovalRequest) -> ApprovalDecision</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
|
||||
@@ -777,11 +795,17 @@ Methods:
|
||||
|
||||
* <code title="cli">praisonai <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai acp acp-main <a href="./src/praisonai/praisonai/cli/commands/acp.py">--help</a></code>
|
||||
* <code title="cli">praisonai agent list <a href="./src/praisonai/praisonai/cli/commands/agent.py">--help</a></code>
|
||||
* <code title="cli">praisonai agent show <a href="./src/praisonai/praisonai/cli/commands/agent.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents create <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents info <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents list <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai app app <a href="./src/praisonai/praisonai/cli/commands/app.py">--help</a></code>
|
||||
* <code title="cli">praisonai audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.py">--help</a></code>
|
||||
* <code title="cli">praisonai auth list <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
|
||||
* <code title="cli">praisonai auth login <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
|
||||
* <code title="cli">praisonai auth logout <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
|
||||
* <code title="cli">praisonai auth status <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch batch-run <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch list <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
* <code title="cli">praisonai batch report <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
|
||||
@@ -822,6 +846,8 @@ Methods:
|
||||
* <code title="cli">praisonai claw claw <a href="./src/praisonai/praisonai/cli/commands/claw.py">--help</a></code>
|
||||
* <code title="cli">praisonai code <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai code code-main <a href="./src/praisonai/praisonai/cli/commands/code.py">--help</a></code>
|
||||
* <code title="cli">praisonai command list <a href="./src/praisonai/praisonai/cli/commands/command.py">--help</a></code>
|
||||
* <code title="cli">praisonai command show <a href="./src/praisonai/praisonai/cli/commands/command.py">--help</a></code>
|
||||
* <code title="cli">praisonai commit commit-main <a href="./src/praisonai/praisonai/cli/commands/commit.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion bash <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion completion-callback <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
@@ -834,6 +860,9 @@ Methods:
|
||||
* <code title="cli">praisonai config path <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config reset <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config set <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config show <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config sources <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config validate <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai context add <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context clear <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context compact <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
@@ -868,10 +897,12 @@ Methods:
|
||||
* <code title="cli">praisonai doctor db <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor doctor-callback <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor env <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor fix <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor mcp <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor network <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor packaging <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor performance <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor runtime <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor selftest <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor skills <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor tools <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
@@ -902,6 +933,9 @@ Methods:
|
||||
* <code title="cli">praisonai gateway install <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway logs <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway mint-link <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway pause <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway reconnect <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway resume <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
@@ -992,6 +1026,14 @@ Methods:
|
||||
* <code title="cli">praisonai pairing list <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
|
||||
* <code title="cli">praisonai pairing revoke <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
|
||||
* <code title="cli">praisonai paths show <a href="./src/praisonai/praisonai/cli/commands/paths.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions allow <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions ask <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions deny <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions export <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions import-rules <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions list <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions remove <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai permissions reset <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins create <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins disable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins discover <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
@@ -1290,7 +1332,7 @@ export { // Python parity additions
|
||||
export { // Subagent Tool (agent-as-tool pattern)
|
||||
SubagentTool, BaseTool, FunctionTool, ToolRegistry, ToolResult, ToolValidationError, createDelegator, createSubagentTool, createSubagentTools, createTool, getRegistry, getTool, registerTool, tool, validateTool } from "./tools";
|
||||
export { airweaveSearch, bedrockBrowserClick, bedrockBrowserFill, bedrockBrowserNavigate, bedrockCodeInterpreter, codeExecution, codeMode, createCustomTool, exaSearch, firecrawlCrawl, firecrawlScrape, parallelSearch, perplexitySearch, registerCustomTool, registerLocalTool, registerNpmTool, superagentGuard, superagentRedact, superagentVerify, tavilyCrawl, tavilyExtract, tavilySearch, valyuBioSearch, valyuCompanyResearch, valyuEconomicsSearch, valyuFinanceSearch, valyuPaperSearch, valyuPatentSearch, valyuSecSearch, valyuWebSearch } from "./tools/builtins";
|
||||
export { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getRegistry, getTool, getToolsRegistry, get_registry, get_tool, registerTool, register_tool, resetToolsRegistry, validateTool, validate_tool } from "./tools/registry";
|
||||
export { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, get_registry, get_tool, register_tool, resetToolsRegistry, validate_tool } from "./tools/registry";
|
||||
export type { InstallHints, PraisonTool, RedactionHooks, RegisteredTool, ToolCapabilities, ToolExecutionContext, ToolExecutionResult, ToolFactory, ToolHooks, ToolInstallStatus, ToolLimits, ToolLogger, ToolMetadata, ToolMiddleware, ToolParameterProperty, ToolParameterSchema } from "./tools/registry";
|
||||
export { registerBuiltinTools, tools } from "./tools/tools";
|
||||
export { // Classes
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.52" \
|
||||
"praisonai>=4.6.68" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.52" \
|
||||
"praisonai>=4.6.68" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.52" \
|
||||
"praisonai>=4.6.68" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example of using LLMConfig with Agent for model fallback configuration."""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, 'src/praisonai-agents')
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config import LLMConfig
|
||||
|
||||
# Example 1: Pass LLMConfig via llm parameter
|
||||
def example_llm_param():
|
||||
"""Using LLMConfig via the llm parameter."""
|
||||
config = LLMConfig(
|
||||
model="gpt-4o",
|
||||
fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
|
||||
base_url="https://api.example.com",
|
||||
api_key="test-key"
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant",
|
||||
llm=config # Pass LLMConfig object directly
|
||||
)
|
||||
|
||||
print(f"Primary model: {agent.llm}")
|
||||
print(f"Fallback models: {agent.fallback_models}")
|
||||
print(f"Base URL: {agent.base_url}")
|
||||
print(f"API Key: {agent.api_key}")
|
||||
|
||||
# Example 2: Pass LLMConfig via model parameter
|
||||
def example_model_param():
|
||||
"""Using LLMConfig via the model parameter (preferred)."""
|
||||
config = LLMConfig(
|
||||
model="gpt-4o",
|
||||
fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"],
|
||||
base_url="https://api.example.com",
|
||||
api_key="test-key"
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant",
|
||||
model=config # Pass LLMConfig via model parameter
|
||||
)
|
||||
|
||||
print(f"Primary model: {agent.llm}")
|
||||
print(f"Fallback models: {agent.fallback_models}")
|
||||
print(f"Base URL: {agent.base_url}")
|
||||
print(f"API Key: {agent.api_key}")
|
||||
|
||||
# Example 3: Traditional usage still works
|
||||
def example_traditional():
|
||||
"""Traditional usage without LLMConfig (no fallback support)."""
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant",
|
||||
model="gpt-4o",
|
||||
base_url="https://api.example.com",
|
||||
api_key="test-key"
|
||||
)
|
||||
|
||||
print(f"Primary model: {agent.llm}")
|
||||
print(f"Fallback models: {agent.fallback_models}") # Will be empty
|
||||
print(f"Base URL: {agent.base_url}")
|
||||
print(f"API Key: {agent.api_key}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Example 1: LLMConfig via llm parameter ===")
|
||||
example_llm_param()
|
||||
print()
|
||||
|
||||
print("=== Example 2: LLMConfig via model parameter ===")
|
||||
example_model_param()
|
||||
print()
|
||||
|
||||
print("=== Example 3: Traditional usage (no fallback) ===")
|
||||
example_traditional()
|
||||
@@ -2,7 +2,7 @@
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents import AgentFlow, Task
|
||||
from praisonaiagents.workflows.workflow_configs import (
|
||||
WorkflowPlanningConfig, WorkflowMemoryConfig,
|
||||
WorkflowOutputConfig, WorkflowPlanningConfig, WorkflowMemoryConfig,
|
||||
)
|
||||
|
||||
# Full-featured workflow with all consolidated params
|
||||
@@ -32,7 +32,7 @@ workflow = AgentFlow(
|
||||
),
|
||||
],
|
||||
# Workflow-level consolidated params
|
||||
output="verbose",
|
||||
output=WorkflowOutputConfig(verbose=True, stream=True),
|
||||
planning=WorkflowPlanningConfig(enabled=True, reasoning=True),
|
||||
memory=WorkflowMemoryConfig(backend="file"),
|
||||
# Agent-like params
|
||||
|
||||
@@ -5,12 +5,12 @@ Demonstrates web search with consolidated params.
|
||||
Presets: duckduckgo, tavily, google, bing, serper, search_only, fetch_only
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents import Agent, WebConfig
|
||||
|
||||
# Basic: Enable web search with preset
|
||||
# Basic: Enable web search with preset (search only — fetch needs Claude)
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant with web access.",
|
||||
web="duckduckgo", # Presets: duckduckgo, tavily, google, bing, serper
|
||||
web=WebConfig(search=True, fetch=False, search_provider="duckduckgo"),
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -30,5 +30,5 @@ if __name__ == "__main__":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
result = workflow.run()
|
||||
result = workflow.run("AI agents")
|
||||
print(result)
|
||||
@@ -17,10 +17,8 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="CockroachDB Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
memory={
|
||||
"session_id": "crdb-demo-session",
|
||||
"db": {"database_url": os.getenv("COCKROACHDB_URL")},
|
||||
},
|
||||
db={"database_url": os.getenv("COCKROACHDB_URL")},
|
||||
session_id="crdb-demo-session",
|
||||
)
|
||||
|
||||
result = agent.start("What is distributed SQL? Explain in 2 sentences.")
|
||||
|
||||
@@ -18,10 +18,8 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Neon Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
memory={
|
||||
"session_id": "neon-demo-session",
|
||||
"db": {"database_url": os.getenv("NEON_DATABASE_URL")},
|
||||
},
|
||||
db={"database_url": os.getenv("NEON_DATABASE_URL")},
|
||||
session_id="neon-demo-session",
|
||||
)
|
||||
|
||||
# Option 2: Direct URL
|
||||
|
||||
@@ -20,10 +20,8 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Supabase Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
memory={
|
||||
"session_id": "supabase-demo-session",
|
||||
"db": {"database_url": os.getenv("SUPABASE_DATABASE_URL")},
|
||||
},
|
||||
db={"database_url": os.getenv("SUPABASE_DATABASE_URL")},
|
||||
session_id="supabase-demo-session",
|
||||
)
|
||||
|
||||
result = agent.start("What is Supabase? Explain in 2 sentences.")
|
||||
|
||||
@@ -18,13 +18,11 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Turso Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
memory={
|
||||
"session_id": "turso-demo-session",
|
||||
"db": {
|
||||
"database_url": os.getenv("TURSO_DATABASE_URL"),
|
||||
"auth_token": os.getenv("TURSO_AUTH_TOKEN"),
|
||||
},
|
||||
db={
|
||||
"database_url": os.getenv("TURSO_DATABASE_URL"),
|
||||
"auth_token": os.getenv("TURSO_AUTH_TOKEN"),
|
||||
},
|
||||
session_id="turso-demo-session",
|
||||
)
|
||||
|
||||
result = agent.start("What is edge computing? Explain in 2 sentences.")
|
||||
|
||||
@@ -10,7 +10,8 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="FlexBot",
|
||||
instructions="You are a helpful assistant.",
|
||||
llm={"model": "gpt-4o-mini", "configurable": True},
|
||||
llm="gpt-4o-mini",
|
||||
llm_config={"configurable": True}
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -21,7 +21,7 @@ agent = Agent(
|
||||
name="ContextBot",
|
||||
instructions="You help show context information.",
|
||||
tools=[show_context],
|
||||
memory={"session_id": "my-session-123"},
|
||||
session_id="my-session-123"
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -52,10 +52,8 @@ agent = Agent(
|
||||
instructions="You are a helpful assistant with access to technical documentation.",
|
||||
knowledge={
|
||||
"sources": [doc_path],
|
||||
"vector_store": {
|
||||
"provider": "qdrant",
|
||||
"url": "http://localhost:6333",
|
||||
},
|
||||
"vector_store": "qdrant",
|
||||
"url": "http://localhost:6333",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ def main():
|
||||
agent = Agent(
|
||||
name="SecureAgent",
|
||||
instructions="You are a file management assistant.",
|
||||
policy=engine
|
||||
)
|
||||
agent.policy = engine
|
||||
|
||||
print("\n--- Agent with Policy Enforcement Created ---")
|
||||
print(f"Agent: {agent.name}")
|
||||
|
||||
@@ -6,10 +6,18 @@ It includes self-reflection for improving code quality and multiple specialized
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.tools import CodeInterpreter
|
||||
|
||||
# Initialize code execution tool
|
||||
code_interpreter = CodeInterpreter()
|
||||
def run_python_code(code: str) -> str:
|
||||
"""Execute Python code safely and return stdout or errors."""
|
||||
import io
|
||||
import contextlib
|
||||
try:
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
exec(code, {"__builtins__": __builtins__})
|
||||
return buf.getvalue() or "Executed successfully (no output)"
|
||||
except Exception as e:
|
||||
return f"Execution error: {e}"
|
||||
|
||||
# Custom code analysis tool
|
||||
def analyze_code_complexity(code: str) -> str:
|
||||
@@ -32,9 +40,7 @@ code_writer = Agent(
|
||||
goal="Write clean, efficient, and well-documented Python code",
|
||||
backstory="You are a senior Python developer with 10+ years of experience in writing production-quality code.",
|
||||
instructions="Write Python code that follows PEP 8 standards, includes proper error handling, and has clear documentation.",
|
||||
reflection=True,
|
||||
|
||||
|
||||
output="silent",
|
||||
)
|
||||
|
||||
code_reviewer = Agent(
|
||||
@@ -52,7 +58,7 @@ code_executor = Agent(
|
||||
goal="Safely execute and test Python code",
|
||||
backstory="You are responsible for running code and reporting results or errors.",
|
||||
instructions="Execute the provided code safely and report the output or any errors encountered.",
|
||||
tools=[code_interpreter]
|
||||
tools=[run_python_code]
|
||||
)
|
||||
|
||||
# Example tasks
|
||||
@@ -87,7 +93,7 @@ def demonstrate_code_agent():
|
||||
workflow = AgentTeam(
|
||||
agents=[code_writer, code_reviewer, code_executor],
|
||||
tasks=[write_task, review_task, execute_task],
|
||||
process="sequential", output="verbose"
|
||||
process="sequential", output="silent"
|
||||
)
|
||||
|
||||
# Run the workflow
|
||||
@@ -106,10 +112,8 @@ general_code_agent = Agent(
|
||||
3. Explain code concepts
|
||||
4. Refactor code for better performance
|
||||
5. Add tests and documentation""",
|
||||
tools=[code_interpreter, analyze_code_complexity],
|
||||
reflection=True,
|
||||
|
||||
|
||||
tools=[run_python_code, analyze_code_complexity],
|
||||
output="silent",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -41,21 +41,25 @@ def calculate_statistics(numbers: list) -> str:
|
||||
"""Calculate basic statistics for a list of numbers."""
|
||||
if not numbers:
|
||||
return "No numbers provided"
|
||||
|
||||
n = len(numbers)
|
||||
mean = sum(numbers) / n
|
||||
sorted_nums = sorted(numbers)
|
||||
try:
|
||||
nums = [float(x) for x in numbers]
|
||||
except (TypeError, ValueError):
|
||||
return "Error: all values must be numbers"
|
||||
|
||||
n = len(nums)
|
||||
mean = sum(nums) / n
|
||||
sorted_nums = sorted(nums)
|
||||
median = sorted_nums[n//2] if n % 2 else (sorted_nums[n//2-1] + sorted_nums[n//2]) / 2
|
||||
variance = sum((x - mean)**2 for x in numbers) / n
|
||||
variance = sum((x - mean)**2 for x in nums) / n
|
||||
std_dev = math.sqrt(variance)
|
||||
|
||||
|
||||
return f"""Statistics:
|
||||
- Count: {n}
|
||||
- Mean: {mean:.4f}
|
||||
- Median: {median:.4f}
|
||||
- Standard Deviation: {std_dev:.4f}
|
||||
- Min: {min(numbers)}
|
||||
- Max: {max(numbers)}"""
|
||||
- Min: {min(nums)}
|
||||
- Max: {max(nums)}"""
|
||||
|
||||
# Create specialized math agents
|
||||
calculation_agent = Agent(
|
||||
@@ -66,8 +70,7 @@ calculation_agent = Agent(
|
||||
instructions="Perform the requested calculations step by step, showing your work clearly.",
|
||||
tools=[basic_calculator, solve_quadratic, calculate_statistics],
|
||||
reflection=True,
|
||||
|
||||
|
||||
output="silent",
|
||||
)
|
||||
|
||||
problem_solver_agent = Agent(
|
||||
@@ -82,8 +85,7 @@ problem_solver_agent = Agent(
|
||||
4. Show each step of the solution
|
||||
5. Verify the answer makes sense""",
|
||||
reflection=True,
|
||||
|
||||
|
||||
output="silent",
|
||||
)
|
||||
|
||||
math_tutor_agent = Agent(
|
||||
@@ -91,7 +93,8 @@ math_tutor_agent = Agent(
|
||||
role="Mathematics tutor and explainer",
|
||||
goal="Explain mathematical concepts clearly and help users understand math",
|
||||
backstory="You are a patient math tutor who can explain concepts at any level from elementary to advanced.",
|
||||
instructions="Explain mathematical concepts using simple language, provide examples, and check for understanding."
|
||||
instructions="Explain mathematical concepts using simple language, provide examples, and check for understanding.",
|
||||
output="silent",
|
||||
)
|
||||
|
||||
# Example workflow for solving complex problems
|
||||
@@ -126,7 +129,8 @@ def solve_math_problem_workflow():
|
||||
workflow = AgentTeam(
|
||||
agents=[problem_solver_agent, calculation_agent, math_tutor_agent],
|
||||
tasks=[understand_task, solve_task, verify_task],
|
||||
process="sequential", output="verbose"
|
||||
process="sequential",
|
||||
output="silent",
|
||||
)
|
||||
|
||||
return workflow.start()
|
||||
@@ -148,8 +152,7 @@ math_assistant = Agent(
|
||||
Always show your work step by step.""",
|
||||
tools=[basic_calculator, solve_quadratic, calculate_statistics],
|
||||
reflection=True,
|
||||
|
||||
|
||||
output="silent",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Bot In-Flight Run Control
|
||||
|
||||
Demonstrates how to use SessionRunControl to provide better UX for bots
|
||||
during long-running agent operations. Shows busy feedback, pending message
|
||||
handling, and /stop command support.
|
||||
|
||||
This example shows the "before" and "after" behavior described in issue #1914.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
# Mock agent for demonstration
|
||||
class MockAgent:
|
||||
"""Simple mock agent that simulates long-running tasks."""
|
||||
|
||||
def __init__(self, name="assistant"):
|
||||
self.name = name
|
||||
self.chat_history = []
|
||||
self._interrupt_controller = None
|
||||
|
||||
def chat(self, prompt: str) -> str:
|
||||
"""Simulate a long-running chat response."""
|
||||
print(f"[Agent] Starting to process: '{prompt[:50]}...'")
|
||||
|
||||
# Simulate long processing with interrupt checking
|
||||
for i in range(30): # 3 second task (30 x 0.1s)
|
||||
time.sleep(0.1)
|
||||
|
||||
# Check for interruption
|
||||
if (self._interrupt_controller and
|
||||
hasattr(self._interrupt_controller, 'is_set') and
|
||||
self._interrupt_controller.is_set()):
|
||||
reason = getattr(self._interrupt_controller, 'reason', 'unknown')
|
||||
print(f"[Agent] ⚠️ Interrupted: {reason}")
|
||||
raise InterruptedError(f"Task cancelled: {reason}")
|
||||
|
||||
response = f"Completed analysis of: {prompt}"
|
||||
print(f"[Agent] ✅ Finished: {response}")
|
||||
return response
|
||||
|
||||
|
||||
async def demo_without_run_control():
|
||||
"""Demonstrate the old behavior (silent blocking)."""
|
||||
print("\n" + "="*60)
|
||||
print("🔴 BEFORE: Without Run Control (Silent Blocking)")
|
||||
print("="*60)
|
||||
|
||||
from praisonai.bots._session import BotSessionManager
|
||||
|
||||
# Standard session manager (no run control)
|
||||
session_mgr = BotSessionManager()
|
||||
agent = MockAgent("research-agent")
|
||||
user_id = "user123"
|
||||
|
||||
print("\n1. User sends long task:")
|
||||
print(" User: 'research quantum computing trends'")
|
||||
|
||||
# Start first task (this will run for 3 seconds)
|
||||
task1 = asyncio.create_task(
|
||||
session_mgr.chat(agent, user_id, "research quantum computing trends")
|
||||
)
|
||||
|
||||
# Simulate user sending follow-up messages during execution
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n2. User sends follow-up (this will block silently):")
|
||||
print(" User: 'actually focus on business impact'")
|
||||
|
||||
task2 = asyncio.create_task(
|
||||
session_mgr.chat(agent, user_id, "actually focus on business impact")
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n3. User tries to send /stop (this will also block):")
|
||||
print(" User: '/stop'")
|
||||
|
||||
task3 = asyncio.create_task(
|
||||
session_mgr.chat(agent, user_id, "/stop")
|
||||
)
|
||||
|
||||
print("\n⏳ Waiting for all messages to complete...")
|
||||
results = await asyncio.gather(task1, task2, task3)
|
||||
|
||||
print("\n📊 Results (all processed sequentially with no feedback):")
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f" {i}. {result}")
|
||||
|
||||
print("\n❌ Problems with this approach:")
|
||||
print(" - No immediate feedback on follow-up messages")
|
||||
print(" - No way to cancel long-running tasks")
|
||||
print(" - User doesn't know if bot is working or broken")
|
||||
|
||||
|
||||
async def demo_with_run_control():
|
||||
"""Demonstrate the new behavior with run control."""
|
||||
print("\n" + "="*60)
|
||||
print("🟢 AFTER: With Run Control (Better UX)")
|
||||
print("="*60)
|
||||
|
||||
from praisonai.bots._run_control import SessionRunControl
|
||||
from praisonai.bots._session import BotSessionManager
|
||||
from praisonai.bots._commands import handle_stop_command
|
||||
|
||||
# Session manager with run control
|
||||
run_control = SessionRunControl(
|
||||
busy_mode="queue",
|
||||
busy_ack_template="⏳ {action} — will process after current task finishes"
|
||||
)
|
||||
session_mgr = BotSessionManager(run_control=run_control)
|
||||
agent = MockAgent("research-agent")
|
||||
user_id = "user123"
|
||||
|
||||
print("\n1. User sends long task:")
|
||||
print(" User: 'research quantum computing trends'")
|
||||
|
||||
# Start first task
|
||||
task1 = asyncio.create_task(
|
||||
session_mgr.chat_with_run_control(agent, user_id, "research quantum computing trends")
|
||||
)
|
||||
|
||||
# Immediate follow-up messages get acknowledgments
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n2. User sends follow-up (gets immediate feedback):")
|
||||
print(" User: 'actually focus on business impact'")
|
||||
|
||||
result2 = await session_mgr.chat_with_run_control(
|
||||
agent, user_id, "actually focus on business impact"
|
||||
)
|
||||
print(f" Bot: {result2['response']}")
|
||||
print(f" Metadata: {result2['metadata']}")
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n3. User decides to stop current task:")
|
||||
print(" User: '/stop'")
|
||||
|
||||
stop_response = await handle_stop_command(user_id, run_control)
|
||||
print(f" Bot: {stop_response}")
|
||||
|
||||
# Wait for first task to complete (it should be cancelled)
|
||||
try:
|
||||
result1 = await task1
|
||||
print(f"\n📊 Task 1 result: {result1['response']}")
|
||||
print(f" Metadata: {result1['metadata']}")
|
||||
except asyncio.CancelledError:
|
||||
print("\n📊 Task 1 was cancelled as expected")
|
||||
|
||||
print("\n4. User sends fresh request after stopping:")
|
||||
print(" User: 'what's the weather like?'")
|
||||
|
||||
result4 = await session_mgr.chat_with_run_control(
|
||||
agent, user_id, "what's the weather like?"
|
||||
)
|
||||
print(f" Bot: {result4['response']}")
|
||||
|
||||
print("\n✅ Benefits of this approach:")
|
||||
print(" - Immediate feedback on all messages")
|
||||
print(" - Can cancel long-running tasks with /stop")
|
||||
print(" - Pending messages are queued and processed in order")
|
||||
print(" - User always knows what's happening")
|
||||
|
||||
|
||||
async def demo_interrupt_mode():
|
||||
"""Demonstrate interrupt mode."""
|
||||
print("\n" + "="*60)
|
||||
print("⚡ BONUS: Interrupt Mode (Cancel and Restart)")
|
||||
print("="*60)
|
||||
|
||||
from praisonai.bots._run_control import SessionRunControl
|
||||
from praisonai.bots._session import BotSessionManager
|
||||
|
||||
# Use interrupt mode instead of queue mode
|
||||
run_control = SessionRunControl(busy_mode="interrupt")
|
||||
session_mgr = BotSessionManager(run_control=run_control)
|
||||
agent = MockAgent("research-agent")
|
||||
user_id = "user123"
|
||||
|
||||
print("\n1. User starts long task:")
|
||||
print(" User: 'research renewable energy for 30 pages'")
|
||||
|
||||
task1 = asyncio.create_task(
|
||||
session_mgr.chat_with_run_control(agent, user_id, "research renewable energy for 30 pages")
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
print("\n2. User changes mind (interrupts and restarts):")
|
||||
print(" User: 'actually just summarize solar panel efficiency'")
|
||||
|
||||
result2 = await session_mgr.chat_with_run_control(
|
||||
agent, user_id, "actually just summarize solar panel efficiency"
|
||||
)
|
||||
print(f" Bot: {result2['response']}")
|
||||
|
||||
# Wait for original task (should be interrupted)
|
||||
try:
|
||||
result1 = await task1
|
||||
print(f"\n📊 Original task result: {result1['response']}")
|
||||
if result1['metadata'].get('interrupted'):
|
||||
print(" ✅ Original task was properly interrupted")
|
||||
except:
|
||||
print("\n📊 Original task was cancelled")
|
||||
|
||||
print("\n✅ Interrupt mode is great for:")
|
||||
print(" - When users frequently change direction")
|
||||
print(" - Interactive sessions where latest intent matters most")
|
||||
print(" - Real-time collaboration scenarios")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all demonstrations."""
|
||||
print("🚀 Bot In-Flight Run Control Demo")
|
||||
print("Solving the silent lock problem described in issue #1914")
|
||||
|
||||
# Show the problem
|
||||
await demo_without_run_control()
|
||||
|
||||
# Show the solution
|
||||
await demo_with_run_control()
|
||||
|
||||
# Show advanced feature
|
||||
await demo_interrupt_mode()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🎯 Summary")
|
||||
print("="*60)
|
||||
print("SessionRunControl solves the silent blocking problem by providing:")
|
||||
print("1. Immediate feedback for mid-run messages")
|
||||
print("2. /stop command support via InterruptController")
|
||||
print("3. Configurable policies (queue/interrupt/steer)")
|
||||
print("4. Run generation tracking to prevent race conditions")
|
||||
print("5. Pending message merging and ordering")
|
||||
print("\nThis makes bot interactions feel responsive and predictable!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -75,7 +75,7 @@ class ContinuousCameraMonitor:
|
||||
agents=[self.vision_agent],
|
||||
tasks=[task],
|
||||
process="sequential",
|
||||
output="silent", # Reduced verbosity for continuous monitoring
|
||||
verbose=0 # Reduced verbosity for continuous monitoring
|
||||
)
|
||||
|
||||
return agents.start()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
@@ -19,11 +20,9 @@ config = {
|
||||
}
|
||||
}
|
||||
|
||||
# Create sample file in system temp directory
|
||||
_sample_path = os.path.join(tempfile.gettempdir(), "sample_knowledge.txt")
|
||||
if not os.path.exists(_sample_path):
|
||||
with open(_sample_path, "w", encoding="utf-8") as f:
|
||||
f.write("Mervin Praison is the creator of PraisonAI.\n")
|
||||
_sample_path = Path(__file__).with_name("sample_knowledge.txt")
|
||||
if not _sample_path.exists():
|
||||
_sample_path.write_text("Mervin Praison is the creator of PraisonAI.\n", encoding="utf-8")
|
||||
|
||||
# Create an agent with knowledge capabilities
|
||||
knowledge_agent = Agent(
|
||||
@@ -31,7 +30,7 @@ knowledge_agent = Agent(
|
||||
role="Information Specialist",
|
||||
goal="Store and retrieve knowledge efficiently",
|
||||
backstory="Expert in managing and utilizing stored knowledge",
|
||||
knowledge={**config, "sources": [_sample_path]},
|
||||
knowledge={**config, "sources": [str(_sample_path)]},
|
||||
)
|
||||
|
||||
# Define a task for the agent
|
||||
|
||||
@@ -16,7 +16,7 @@ agents = AgentTeam(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process="workflow",
|
||||
execution={"max_iter": 30},
|
||||
max_iter=30
|
||||
)
|
||||
|
||||
agents.start()
|
||||
@@ -61,6 +61,7 @@ def example_with_wrappers():
|
||||
role="Data Analyst",
|
||||
goal="Analyze data and provide insights",
|
||||
llm="gpt-4o-mini",
|
||||
request_id="request_2" # Unique request ID
|
||||
)
|
||||
|
||||
# Operations are automatically tracked
|
||||
|
||||
@@ -132,8 +132,7 @@ agents = AgentTeam(
|
||||
agents=[qa_generator, total_questions_evaluator, cot_generator, upload_to_huggingface],
|
||||
tasks=[generate_task, evaluate_total_questions_task, generate_cot_task, upload_to_huggingface_task],
|
||||
process="workflow",
|
||||
execution={"max_iter": 30},
|
||||
output="minimal",
|
||||
max_iter=30, output="minimal"
|
||||
)
|
||||
|
||||
agents.start()
|
||||
@@ -18,7 +18,7 @@ openai_profile = AuthProfile(
|
||||
provider="openai",
|
||||
api_key=os.environ.get("OPENAI_API_KEY", "sk-..."),
|
||||
priority=1, # Highest priority
|
||||
rate_limit_rpm=100,
|
||||
rate_limit=100,
|
||||
)
|
||||
|
||||
anthropic_profile = AuthProfile(
|
||||
@@ -26,7 +26,7 @@ anthropic_profile = AuthProfile(
|
||||
provider="anthropic",
|
||||
api_key=os.environ.get("ANTHROPIC_API_KEY", "sk-ant-..."),
|
||||
priority=2, # Second priority
|
||||
rate_limit_rpm=50,
|
||||
rate_limit=50,
|
||||
)
|
||||
|
||||
groq_profile = AuthProfile(
|
||||
@@ -34,7 +34,7 @@ groq_profile = AuthProfile(
|
||||
provider="groq",
|
||||
api_key=os.environ.get("GROQ_API_KEY", "gsk-..."),
|
||||
priority=3, # Third priority (fast but limited)
|
||||
rate_limit_rpm=30,
|
||||
rate_limit=30,
|
||||
)
|
||||
|
||||
# Configure failover behavior
|
||||
|
||||
@@ -105,13 +105,13 @@ def main():
|
||||
agents=[researcher, retriever],
|
||||
tasks=[store_task, verify_task, query_task, query_both_task], # Use same verbose level as memory
|
||||
memory={
|
||||
"backend": "sqlite",
|
||||
"embedder": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "text-embedding-3-small"
|
||||
}
|
||||
},
|
||||
"config": {"provider": "sqlite"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ built-in validation and safety mechanisms for secure agent operations.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.config.feature_configs import GuardrailConfig
|
||||
from praisonaiagents.tools import internet_search
|
||||
|
||||
print("=== Production Guardrails Patterns Example ===\n")
|
||||
@@ -39,8 +38,8 @@ production_agent = Agent(
|
||||
goal="Generate safe, compliant content for production use",
|
||||
backstory="Production-ready agent with built-in safety and compliance checks",
|
||||
tools=[internet_search],
|
||||
guardrails=GuardrailConfig(validator=content_guardrail, max_retries=3),
|
||||
)
|
||||
guardrails=content_guardrail,
|
||||
max_retries=3)
|
||||
|
||||
# Create secure task with validation
|
||||
secure_task = Task(
|
||||
|
||||
@@ -40,7 +40,7 @@ agent = Agent(
|
||||
"execute_command"
|
||||
],
|
||||
memory=True,
|
||||
web=True,
|
||||
web_search=True,
|
||||
auto_approve_tools=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ print("=== Advanced Graph Memory Integration Example ===\n")
|
||||
|
||||
# Configure simple graph memory
|
||||
memory_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"provider": "mem0",
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
|
||||
@@ -12,8 +12,8 @@ print("=== Advanced Memory Search and Optimization Example ===\n")
|
||||
|
||||
# Configure optimized memory settings
|
||||
memory_config = {
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"provider": "mem0",
|
||||
"vector_store": {
|
||||
"provider": "chroma",
|
||||
"config": {
|
||||
|
||||
@@ -86,13 +86,13 @@ def main():
|
||||
agents=[researcher, retriever],
|
||||
tasks=[store_task, retrieve_task],
|
||||
memory={
|
||||
"backend": "sqlite",
|
||||
"embedder": {
|
||||
"provider": "gemini",
|
||||
"config": {
|
||||
"model": "text-embedding-004",
|
||||
}
|
||||
},
|
||||
"config": {"provider": "sqlite"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Message Steering Example
|
||||
|
||||
Demonstrates real-time message steering for agents during execution.
|
||||
Shows how to send guidance messages to agents while they're running tasks.
|
||||
|
||||
Usage:
|
||||
# Python API
|
||||
python message_steering_example.py
|
||||
|
||||
# CLI with steering enabled
|
||||
praisonai --message-steering "Write a detailed report about AI trends"
|
||||
|
||||
# YAML (add message_steering: true to agent config)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
from praisonaiagents import Agent
|
||||
|
||||
|
||||
def basic_message_steering():
|
||||
"""Basic message steering example - enable steering and send guidance."""
|
||||
print("🎯 Basic Message Steering Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Create agent with message steering enabled
|
||||
agent = Agent(
|
||||
name="researcher",
|
||||
instructions="You are a helpful research assistant",
|
||||
message_steering=True, # Enable real-time message steering
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("✅ Agent created with message steering enabled")
|
||||
print(f"📊 Steering status: {agent.get_steering_status()}")
|
||||
|
||||
# Send some guidance
|
||||
msg_id_1 = agent.steer("Focus on recent developments from 2024")
|
||||
msg_id_2 = agent.steer("Keep response under 200 words", priority=10)
|
||||
|
||||
print(f"📨 Sent steering messages: {msg_id_1}, {msg_id_2}")
|
||||
print(f"📊 Steering status: {agent.get_steering_status()}")
|
||||
|
||||
# Execute task - steering will be automatically injected
|
||||
result = agent.start("Summarize the latest AI trends")
|
||||
|
||||
print("\n📝 Result:")
|
||||
print(result)
|
||||
|
||||
|
||||
def threaded_message_steering():
|
||||
"""Advanced example - send steering messages while agent is running."""
|
||||
print("\n🚀 Threaded Message Steering Example")
|
||||
print("=" * 50)
|
||||
|
||||
agent = Agent(
|
||||
name="writer",
|
||||
instructions="You are a content writer",
|
||||
message_steering=True,
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
result_holder = [None]
|
||||
|
||||
def run_agent():
|
||||
"""Run agent in background thread."""
|
||||
result = agent.start("Write a comprehensive article about quantum computing")
|
||||
result_holder[0] = result
|
||||
|
||||
# Start agent in background
|
||||
thread = threading.Thread(target=run_agent)
|
||||
thread.start()
|
||||
|
||||
# Send steering messages while it's running
|
||||
time.sleep(0.5) # Let execution start
|
||||
agent.steer("Make it accessible to beginners")
|
||||
|
||||
time.sleep(1.0)
|
||||
agent.steer("Include practical applications", priority=15)
|
||||
|
||||
time.sleep(1.5)
|
||||
agent.steer("Keep it under 300 words total", priority=20)
|
||||
|
||||
# Wait for completion
|
||||
thread.join(timeout=60)
|
||||
|
||||
if result_holder[0]:
|
||||
print("\n📝 Final Result:")
|
||||
print(result_holder[0])
|
||||
else:
|
||||
print("⚠️ Agent execution timed out")
|
||||
|
||||
|
||||
async def async_message_steering():
|
||||
"""Async example showing message steering with async execution."""
|
||||
print("\n⚡ Async Message Steering Example")
|
||||
print("=" * 50)
|
||||
|
||||
agent = Agent(
|
||||
name="analyst",
|
||||
instructions="You are a data analyst",
|
||||
message_steering=True,
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Start async task
|
||||
async def run_analysis():
|
||||
return await agent.astart("Analyze the impact of AI on job markets")
|
||||
|
||||
# Create task
|
||||
task = asyncio.create_task(run_analysis())
|
||||
|
||||
# Send steering while running
|
||||
await asyncio.sleep(0.3)
|
||||
agent.steer("Focus on positive opportunities")
|
||||
|
||||
await asyncio.sleep(0.7)
|
||||
agent.steer("Include specific examples", priority=12)
|
||||
|
||||
# Wait for result
|
||||
result = await task
|
||||
|
||||
print("\n📝 Analysis Result:")
|
||||
print(result)
|
||||
|
||||
|
||||
def cli_yaml_examples():
|
||||
"""Show CLI and YAML usage examples."""
|
||||
print("\n💻 CLI & YAML Examples")
|
||||
print("=" * 50)
|
||||
|
||||
print("🔧 CLI Usage:")
|
||||
print(" praisonai --message-steering 'Your task here'")
|
||||
print(" # Then in another terminal:")
|
||||
print(" # agent.steer('Additional guidance')")
|
||||
|
||||
print("\n📄 YAML Configuration:")
|
||||
yaml_example = """
|
||||
# agents.yaml
|
||||
framework: praisonai
|
||||
roles:
|
||||
researcher:
|
||||
role: Research Assistant
|
||||
goal: Conduct thorough research
|
||||
backstory: Expert researcher with steering support
|
||||
message_steering: true # Enable steering for this agent
|
||||
tools:
|
||||
- internet_search
|
||||
- write_file
|
||||
tasks:
|
||||
research_task:
|
||||
description: Research AI trends and write report
|
||||
expected_output: Comprehensive research report
|
||||
"""
|
||||
print(yaml_example.strip())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run examples
|
||||
basic_message_steering()
|
||||
threaded_message_steering()
|
||||
|
||||
# Run async example
|
||||
asyncio.run(async_message_steering())
|
||||
|
||||
# Show CLI/YAML usage
|
||||
cli_yaml_examples()
|
||||
|
||||
print("\n✨ Message steering examples completed!")
|
||||
print("📚 See docs for more advanced patterns and priority levels")
|
||||
@@ -30,11 +30,7 @@ config = {
|
||||
agent = Agent(
|
||||
name="Knowledge Agent",
|
||||
instructions="You answer questions based on the provided knowledge.",
|
||||
knowledge={
|
||||
"sources": ["kag-research-paper.pdf"],
|
||||
"vector_store": config["vector_store"],
|
||||
"embedder": config["embedder"],
|
||||
},
|
||||
knowledge={**config, "sources": ["kag-research-paper.pdf"]},
|
||||
memory={"user_id": "user1"},
|
||||
llm="deepseek-r1"
|
||||
)
|
||||
|
||||
@@ -31,9 +31,10 @@ llm_config = {
|
||||
agent = Agent(
|
||||
instructions="You are a helpful Assistant specialized in scientific explanations. "
|
||||
"Provide clear, accurate, and engaging responses.",
|
||||
llm=llm_config,
|
||||
output={"markdown": True},
|
||||
reflection=True,
|
||||
llm=llm_config, # Pass the detailed configuration
|
||||
# Enable detailed output
|
||||
markdown=True, # Format responses in markdown
|
||||
reflection=True # Enable self-reflection
|
||||
)
|
||||
|
||||
# Test the agent
|
||||
|
||||
@@ -28,8 +28,8 @@ if not os.environ.get("OPENAI_API_KEY"):
|
||||
def main():
|
||||
# MongoDB memory configuration
|
||||
mongodb_memory_config = {
|
||||
"provider": "mongodb",
|
||||
"config": {
|
||||
"provider": "mongodb",
|
||||
"connection_string": "mongodb://localhost:27017/", # Replace with your MongoDB connection string
|
||||
"database": "praisonai_memory",
|
||||
"use_vector_search": True, # Enable Atlas Vector Search (requires MongoDB Atlas)
|
||||
|
||||
@@ -12,7 +12,6 @@ Environment:
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from praisonaiagents import Agent, AutoRagAgent
|
||||
|
||||
# Ensure API key is set
|
||||
@@ -21,7 +20,8 @@ if not os.environ.get("OPENAI_API_KEY"):
|
||||
exit(1)
|
||||
|
||||
# Create a sample document for testing
|
||||
sample_doc = os.path.join(tempfile.gettempdir(), "sample_doc.txt")
|
||||
from pathlib import Path
|
||||
sample_doc = str(Path(__file__).with_name("sample_doc.txt"))
|
||||
with open(sample_doc, "w", encoding="utf-8") as f:
|
||||
f.write("""
|
||||
PraisonAI Framework Documentation
|
||||
|
||||
@@ -28,7 +28,7 @@ def example_basic_persistence():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
memory={"session_id": session_id},
|
||||
session_id=session_id,
|
||||
output="minimal",
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ def example_session_restoration():
|
||||
agent1 = Agent(
|
||||
name="MemoryBot",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
memory={"session_id": session_id},
|
||||
session_id=session_id,
|
||||
output="minimal",
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ def example_session_restoration():
|
||||
agent2 = Agent(
|
||||
name="MemoryBot",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
memory={"session_id": session_id},
|
||||
session_id=session_id,
|
||||
output="minimal",
|
||||
)
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ def main():
|
||||
|
||||
context = memory.build_context_for_task(
|
||||
task_descr="Explain AI alignment challenges",
|
||||
user_id="quality_demo_user",
|
||||
memory={"user_id": "quality_demo_user"},
|
||||
max_items=3
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ def main():
|
||||
# Create a session with persistent storage
|
||||
session = Session(
|
||||
session_id="demo_chat_001",
|
||||
user_id="demo_user",
|
||||
memory={"user_id": "demo_user"},
|
||||
)
|
||||
|
||||
print(f"📝 Created session: {session}")
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
goal="Find information about a topic",
|
||||
backstory="You are an expert researcher who finds comprehensive information.",
|
||||
llm="gpt-4o-mini", # Use a cost-effective model for examples
|
||||
output={"metrics": True} # Token metrics via consolidated output config
|
||||
metrics=True # 🎯 SIMPLIFIED: Just add this parameter!
|
||||
)
|
||||
|
||||
# Create a simple task
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
goal="Analyze data and provide insights",
|
||||
backstory="You are an experienced data analyst with expertise in market research.",
|
||||
llm="gpt-4o-mini",
|
||||
output={"metrics": True}
|
||||
metrics=True # 🎯 Enable metrics tracking
|
||||
)
|
||||
|
||||
writer_agent = Agent(
|
||||
@@ -23,7 +23,7 @@ def main():
|
||||
goal="Create well-structured content",
|
||||
backstory="You are a skilled technical writer who creates clear, engaging content.",
|
||||
llm="gpt-4o-mini",
|
||||
output={"metrics": True}
|
||||
metrics=True # 🎯 Enable metrics tracking
|
||||
)
|
||||
|
||||
# Create multiple tasks
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
goal="Conduct comprehensive market research",
|
||||
backstory="You are a seasoned market researcher with 10+ years of experience.",
|
||||
llm="gpt-4o-mini",
|
||||
output={"metrics": True}
|
||||
metrics=True # 🎯 Enable automatic metrics tracking
|
||||
)
|
||||
|
||||
strategy_agent = Agent(
|
||||
@@ -23,7 +23,7 @@ def main():
|
||||
goal="Develop actionable business strategies",
|
||||
backstory="You are a strategic consultant who translates research into business plans.",
|
||||
llm="gpt-4o-mini",
|
||||
output={"metrics": True}
|
||||
metrics=True # 🎯 Enable automatic metrics tracking
|
||||
)
|
||||
|
||||
# Create tasks that will generate substantial token usage
|
||||
|
||||
@@ -22,8 +22,7 @@ rag_agent = Agent(
|
||||
|
||||
You can use the internet_search function to search the web when needed.""",
|
||||
llm="gpt-4o",
|
||||
output={"markdown": True},
|
||||
)
|
||||
markdown=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🤖 Thai Recipe RAG Agent is ready!")
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JIRA Agent Watch Example
|
||||
|
||||
This example demonstrates how to use the JIRA watch tools with PraisonAI agents.
|
||||
The agent can monitor JIRA issues and projects for changes, search for issues,
|
||||
and get detailed issue information.
|
||||
|
||||
Setup:
|
||||
1. Install JIRA library: pip install jira
|
||||
2. Set environment variables:
|
||||
- [email protected] (for cloud JIRA)
|
||||
- JIRA_API_TOKEN=your_api_token
|
||||
- Or use JIRA_USERNAME + JIRA_API_TOKEN for server JIRA
|
||||
|
||||
Usage:
|
||||
python jira_agent_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import jira_tools
|
||||
|
||||
|
||||
def main():
|
||||
"""Demonstrate JIRA agent watch capabilities."""
|
||||
|
||||
# Check if JIRA credentials are available
|
||||
jira_email = os.getenv('JIRA_EMAIL')
|
||||
jira_token = os.getenv('JIRA_API_TOKEN')
|
||||
jira_username = os.getenv('JIRA_USERNAME')
|
||||
|
||||
if not jira_token:
|
||||
print("❌ JIRA_API_TOKEN environment variable not set")
|
||||
print("Please set your JIRA API token:")
|
||||
print("export JIRA_API_TOKEN='your_token_here'")
|
||||
return
|
||||
|
||||
if not (jira_email or jira_username):
|
||||
print("❌ JIRA credentials incomplete")
|
||||
print("Please set either:")
|
||||
print("- JIRA_EMAIL (for cloud JIRA)")
|
||||
print("- JIRA_USERNAME (for server JIRA)")
|
||||
return
|
||||
|
||||
# Create agent with JIRA tools
|
||||
agent = Agent(
|
||||
name="JIRA Monitor Agent",
|
||||
instructions="""
|
||||
You are a JIRA monitoring agent. You can:
|
||||
|
||||
1. Watch specific JIRA issues for changes
|
||||
2. Monitor JIRA projects for new issues and updates
|
||||
3. Search for issues using JQL queries
|
||||
4. Get detailed information about specific issues
|
||||
|
||||
When using JIRA tools, always provide the full JIRA URL
|
||||
(e.g., https://yourcompany.atlassian.net).
|
||||
|
||||
Be helpful and provide clear summaries of JIRA activity.
|
||||
""",
|
||||
tools=jira_tools(), # Add all JIRA tools
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("🎯 JIRA Agent Watch Example")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print("Available JIRA tools:")
|
||||
print("- jira_watch_issue: Monitor a specific issue for changes")
|
||||
print("- jira_watch_project: Monitor a project for new/updated issues")
|
||||
print("- jira_get_issue_info: Get detailed info about an issue")
|
||||
print("- jira_search_issues: Search issues using JQL")
|
||||
print()
|
||||
|
||||
# Example interactions
|
||||
example_prompts = [
|
||||
"Get information about JIRA issue DEMO-1 from https://praisonai.atlassian.net",
|
||||
"Search for open issues in project DEMO from https://praisonai.atlassian.net using JQL: 'project = DEMO AND status = Open'",
|
||||
"Watch JIRA issue DEMO-1 from https://praisonai.atlassian.net for 2 minutes (check every 60 seconds, max 2 checks)",
|
||||
]
|
||||
|
||||
print("📝 Example prompts you can try:")
|
||||
for i, prompt in enumerate(example_prompts, 1):
|
||||
print(f"{i}. {prompt}")
|
||||
print()
|
||||
|
||||
# Interactive mode
|
||||
print("💬 Interactive JIRA Agent (type 'quit' to exit)")
|
||||
print("-" * 50)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n🧑 You: ").strip()
|
||||
|
||||
if user_input.lower() in ['quit', 'exit', 'q']:
|
||||
print("👋 Goodbye!")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
print("🤖 Agent: ", end="", flush=True)
|
||||
response = agent.start(user_input)
|
||||
print(response)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Goodbye!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Named Toolsets Example
|
||||
|
||||
This example demonstrates how to use named toolsets to organize and reuse
|
||||
groups of tools across agents and workflows.
|
||||
|
||||
Toolsets provide:
|
||||
- Organized tool collections (web, research, development, etc.)
|
||||
- Progressive disclosure (safe → research → development)
|
||||
- Composable design (toolsets can include other toolsets)
|
||||
- YAML configuration support
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.toolsets import (
|
||||
list_toolsets,
|
||||
resolve_toolset,
|
||||
register_toolset
|
||||
)
|
||||
|
||||
# Set up environment (replace with your actual API key)
|
||||
os.environ.setdefault("OPENAI_API_KEY", "your-openai-api-key-here")
|
||||
|
||||
|
||||
def demo_basic_toolsets():
|
||||
"""Demonstrate basic toolset functionality."""
|
||||
print("=== Basic Toolsets Demo ===")
|
||||
|
||||
# List available toolsets
|
||||
available_toolsets = list_toolsets()
|
||||
print(f"Available toolsets: {available_toolsets}")
|
||||
|
||||
# Show what tools are in each prebuilt toolset
|
||||
for toolset_name in ["safe", "web", "research"]:
|
||||
tools = resolve_toolset(toolset_name)
|
||||
print(f"\n{toolset_name.upper()} toolset contains: {tools}")
|
||||
|
||||
|
||||
def demo_agent_with_toolsets():
|
||||
"""Demonstrate agents using toolsets."""
|
||||
print("\n=== Agent with Toolsets Demo ===")
|
||||
|
||||
# Create a research agent with research toolset
|
||||
researcher = Agent(
|
||||
name="researcher",
|
||||
role="Research Specialist",
|
||||
goal="Research topics thoroughly using web tools",
|
||||
instructions="You are a research specialist with access to web search and file tools.",
|
||||
toolsets=["research"], # includes web tools + file tools
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print(f"Research agent has {len(researcher.tools)} tools available")
|
||||
print(f"Tools: {[getattr(t, '__name__', str(t)) for t in researcher.tools]}")
|
||||
|
||||
# Create a safe agent with minimal tools
|
||||
safe_agent = Agent(
|
||||
name="safe_assistant",
|
||||
role="Safe Assistant",
|
||||
goal="Help users safely without risky operations",
|
||||
instructions="You are a helpful assistant with access to only safe, read-only tools.",
|
||||
toolsets=["safe"],
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print(f"\nSafe agent has {len(safe_agent.tools)} tools available")
|
||||
print(f"Safe tools: {[getattr(t, '__name__', str(t)) for t in safe_agent.tools]}")
|
||||
|
||||
|
||||
def demo_custom_toolsets():
|
||||
"""Demonstrate creating and using custom toolsets."""
|
||||
print("\n=== Custom Toolsets Demo ===")
|
||||
|
||||
# Register a custom toolset for data analysis
|
||||
register_toolset(
|
||||
"data_analysis",
|
||||
tools=["read_file", "write_file"],
|
||||
includes=["safe"], # Include safe tools too
|
||||
description="Tools for data analysis workflows"
|
||||
)
|
||||
|
||||
# Create an agent with the custom toolset
|
||||
analyst = Agent(
|
||||
name="data_analyst",
|
||||
role="Data Analyst",
|
||||
goal="Analyze data files and generate reports",
|
||||
instructions="You specialize in data analysis. Read files, process data, and write reports.",
|
||||
toolsets=["data_analysis"],
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print(f"Data analyst has {len(analyst.tools)} tools available")
|
||||
print(f"Analysis tools: {[getattr(t, '__name__', str(t)) for t in analyst.tools]}")
|
||||
|
||||
|
||||
def demo_mixed_tools_and_toolsets():
|
||||
"""Demonstrate mixing explicit tools with toolsets."""
|
||||
print("\n=== Mixed Tools and Toolsets Demo ===")
|
||||
|
||||
# Create agent with both explicit tools and toolsets
|
||||
hybrid_agent = Agent(
|
||||
name="hybrid_assistant",
|
||||
role="Hybrid Assistant",
|
||||
goal="Assist with both research and custom tasks",
|
||||
instructions="You can research topics and perform custom analysis tasks.",
|
||||
tools=["write_file"], # Explicit tool
|
||||
toolsets=["research"], # Toolset with web + file tools
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print(f"Hybrid agent has {len(hybrid_agent.tools)} tools available")
|
||||
tool_names = [getattr(t, '__name__', str(t)) for t in hybrid_agent.tools]
|
||||
print(f"Hybrid tools: {tool_names}")
|
||||
|
||||
# Note: write_file might appear twice (once explicit, once from toolset)
|
||||
# The agent handles deduplication automatically
|
||||
|
||||
|
||||
def demo_multi_agent_with_toolsets():
|
||||
"""Demonstrate multi-agent workflows with different toolsets."""
|
||||
print("\n=== Multi-Agent with Toolsets Demo ===")
|
||||
|
||||
# Create agents with different toolsets for different roles
|
||||
researcher = Agent(
|
||||
name="researcher",
|
||||
role="Research Specialist",
|
||||
goal="Research topics and gather information",
|
||||
toolsets=["research"],
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="writer",
|
||||
role="Content Writer",
|
||||
goal="Write content based on research",
|
||||
toolsets=["safe"], # Safe tools for writing
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create tasks for each agent
|
||||
research_task = Task(
|
||||
name="research_task",
|
||||
description="Research the benefits of renewable energy",
|
||||
expected_output="A summary of key benefits of renewable energy",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
writing_task = Task(
|
||||
name="writing_task",
|
||||
description="Write a blog post about renewable energy based on the research",
|
||||
expected_output="A well-written blog post about renewable energy benefits",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
# Create team with different toolsets per agent
|
||||
team = AgentTeam(
|
||||
agents=[researcher, writer],
|
||||
tasks=[research_task, writing_task],
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
print(f"Team created with {len(team.agents)} agents using different toolsets")
|
||||
print(f"Research agent tools: {len(researcher.tools)}")
|
||||
print(f"Writer agent tools: {len(writer.tools)}")
|
||||
|
||||
|
||||
def demo_yaml_config():
|
||||
"""Show how toolsets work in YAML configuration."""
|
||||
print("\n=== YAML Configuration Example ===")
|
||||
|
||||
yaml_config = """
|
||||
# agents.yaml - Example using toolsets
|
||||
agents:
|
||||
researcher:
|
||||
role: Research Specialist
|
||||
goal: Research topics thoroughly
|
||||
toolsets:
|
||||
- research # Includes web search, file tools
|
||||
llm: gpt-4o-mini
|
||||
|
||||
analyst:
|
||||
role: Data Analyst
|
||||
goal: Analyze research data
|
||||
tools:
|
||||
- write_file # Explicit tool
|
||||
toolsets:
|
||||
- safe # Safe toolset
|
||||
llm: gpt-4o-mini
|
||||
|
||||
tasks:
|
||||
research_task:
|
||||
description: Research renewable energy trends
|
||||
agent: researcher
|
||||
expected_output: Research summary
|
||||
|
||||
analysis_task:
|
||||
description: Analyze the research data
|
||||
agent: analyst
|
||||
expected_output: Analysis report
|
||||
"""
|
||||
|
||||
print("Example YAML configuration with toolsets:")
|
||||
print(yaml_config)
|
||||
print("Use: praisonai run 'Research renewable energy' --toolset research")
|
||||
print("Or: praisonai chat --toolset web,files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🛠️ PraisonAI Named Toolsets Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Run all demos
|
||||
demo_basic_toolsets()
|
||||
demo_agent_with_toolsets()
|
||||
demo_custom_toolsets()
|
||||
demo_mixed_tools_and_toolsets()
|
||||
demo_multi_agent_with_toolsets()
|
||||
demo_yaml_config()
|
||||
|
||||
print("\n✅ Toolsets example completed!")
|
||||
print("\n💡 Key Benefits:")
|
||||
print(" • Organized tool collections")
|
||||
print(" • Progressive disclosure (safe → research → development)")
|
||||
print(" • Reusable across agents and workflows")
|
||||
print(" • YAML configuration support")
|
||||
print(" • Composable design with includes")
|
||||
@@ -5,7 +5,7 @@ Demonstrates using Agent instances directly as workflow steps.
|
||||
Each agent processes the input and passes output to the next agent.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents import Agent, AgentFlow
|
||||
|
||||
# Create agents with specific roles
|
||||
researcher = Agent(
|
||||
|
||||
@@ -6,7 +6,6 @@ to different next steps based on output content.
|
||||
"""
|
||||
|
||||
from praisonaiagents import AgentFlow, Task
|
||||
from praisonaiagents import AgentFlowManager
|
||||
|
||||
# Create a workflow with branching
|
||||
workflow = AgentFlow(
|
||||
@@ -36,16 +35,9 @@ workflow = AgentFlow(
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create manager and register workflow
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Decision Workflow"] = workflow
|
||||
|
||||
# Execute - will branch to success_handler since 42 is positive
|
||||
result = manager.execute(
|
||||
"Decision Workflow",
|
||||
default_llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
|
||||
print("Workflow completed!")
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
for step in result.get("steps", []):
|
||||
name = step.get("step", step.get("name", "unknown"))
|
||||
status = step.get("status", "completed")
|
||||
print(f" {name}: {status}")
|
||||
@@ -54,11 +54,13 @@ workflow = AgentFlow(
|
||||
analyzer, # Always runs
|
||||
Task(
|
||||
name="tech_review",
|
||||
description="Review technical aspects of the content",
|
||||
agent=tech_reviewer,
|
||||
should_run=is_technical # Only runs for technical content
|
||||
),
|
||||
Task(
|
||||
name="creative_enhance",
|
||||
description="Enhance the creative aspects of the content",
|
||||
agent=creative_enhancer,
|
||||
should_run=needs_creative # Only runs for creative content
|
||||
),
|
||||
|
||||
@@ -6,9 +6,8 @@ functions that return StepResult with stop_workflow=True.
|
||||
"""
|
||||
|
||||
from praisonaiagents import (
|
||||
Workflow, Task, WorkflowContext, StepResult
|
||||
AgentFlow, Task, WorkflowContext, StepResult
|
||||
)
|
||||
from praisonaiagents import AgentFlowManager
|
||||
|
||||
# Custom validator that can stop the workflow
|
||||
def validate_data(context: WorkflowContext) -> StepResult:
|
||||
@@ -50,20 +49,13 @@ workflow = AgentFlow(
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Data Processing"] = workflow
|
||||
|
||||
# Test with invalid data (will stop early)
|
||||
print("=== Testing with invalid data ===")
|
||||
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
|
||||
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['output']}")
|
||||
|
||||
# Test with valid data (will complete)
|
||||
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
|
||||
for step in result.get("steps", []):
|
||||
print(f" {step.get('step', 'step')}: {step.get('output', step.get('status'))}")
|
||||
|
||||
print("\n=== Testing with valid data ===")
|
||||
workflow.variables["data"] = {"value": 42}
|
||||
result = manager.execute("Data Processing", default_llm="gpt-4o-mini")
|
||||
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
|
||||
for step in result.get("steps", []):
|
||||
print(f" {step.get('step', 'step')}: {step.get('status', 'completed')}")
|
||||
@@ -6,7 +6,6 @@ Each item is processed individually with the loop variable.
|
||||
"""
|
||||
|
||||
from praisonaiagents import AgentFlow, Task
|
||||
from praisonaiagents import AgentFlowManager
|
||||
|
||||
# Create a workflow with loop
|
||||
workflow = AgentFlow(
|
||||
@@ -30,18 +29,12 @@ workflow = AgentFlow(
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create manager and register workflow
|
||||
manager = WorkflowManager()
|
||||
manager.workflows["Process Items"] = workflow
|
||||
|
||||
# Execute - will process each fruit in the list
|
||||
result = manager.execute(
|
||||
"Process Items",
|
||||
default_llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
result = workflow.run("", llm="gpt-4o-mini", verbose=True)
|
||||
print("Workflow completed!")
|
||||
for step_result in result["results"]:
|
||||
print(f" {step_result['step']}: {step_result['status']}")
|
||||
if isinstance(step_result.get("output"), list):
|
||||
print(f" Loop processed {len(step_result['output'])} items")
|
||||
for step in result.get("steps", []):
|
||||
name = step.get("step", step.get("name", "unknown"))
|
||||
status = step.get("status", "completed")
|
||||
print(f" {name}: {status}")
|
||||
out = step.get("output")
|
||||
if isinstance(out, list):
|
||||
print(f" Loop processed {len(out)} items")
|
||||
@@ -53,7 +53,7 @@ workflow = AgentFlow(
|
||||
# Step 4: Function - formats output
|
||||
format_output
|
||||
],
|
||||
llm="gpt-4o-mini", # Used for action-based steps
|
||||
llm="gpt-4o-mini" # Used for action-based steps
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -5,7 +5,7 @@ Demonstrates running multiple agents concurrently and
|
||||
combining their results with an aggregator agent.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents import Agent, AgentFlow
|
||||
from praisonaiagents.workflows import parallel
|
||||
|
||||
# Create parallel research agents
|
||||
|
||||
@@ -5,7 +5,7 @@ Demonstrates decision-based routing where an agent classifier
|
||||
routes to specialized handler agents based on the request type.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
from praisonaiagents import Agent, AgentFlow
|
||||
from praisonaiagents.workflows import route
|
||||
|
||||
# Create classifier agent
|
||||
|
||||
@@ -51,15 +51,13 @@ print("=" * 60)
|
||||
agent = Agent(
|
||||
name="Knowledge Agent",
|
||||
instructions="You are a helpful assistant that answers questions based on the provided knowledge. Be concise.",
|
||||
knowledge={
|
||||
"sources": [".praison/test_docs/overview.txt"],
|
||||
"config": {
|
||||
"policy": "always",
|
||||
"top_k": 3,
|
||||
"citations": True,
|
||||
"max_context_tokens": 2000,
|
||||
},
|
||||
},
|
||||
knowledge=[".praison/test_docs/overview.txt"],
|
||||
retrieval_config={
|
||||
"policy": "always", # Always retrieve for this demo
|
||||
"top_k": 3, # Get top 3 chunks
|
||||
"citations": True, # Include citations
|
||||
"max_context_tokens": 2000,
|
||||
}
|
||||
)
|
||||
|
||||
# Test 1: Basic chat with retrieval
|
||||
|
||||
@@ -60,7 +60,7 @@ print("=" * 50)
|
||||
agent_with_discovery = Agent(
|
||||
name="Multi-Skill Agent",
|
||||
instructions="You are a versatile assistant with multiple skills.",
|
||||
skills=["./"], # Scan current directory for skill subdirectories
|
||||
skills_dirs=["./"], # Scan current directory for skill subdirectories
|
||||
)
|
||||
|
||||
if agent_with_discovery.skill_manager:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
framework: praisonai
|
||||
topic: AI Research with Message Steering
|
||||
|
||||
roles:
|
||||
researcher:
|
||||
role: AI Research Analyst
|
||||
goal: Conduct comprehensive AI research with real-time guidance
|
||||
backstory: You are an expert AI researcher who can receive and incorporate real-time guidance during your work
|
||||
message_steering: true # Enable real-time message steering
|
||||
tools:
|
||||
- internet_search
|
||||
- write_file
|
||||
tasks:
|
||||
research_task:
|
||||
description: |
|
||||
Research the latest developments in AI and write a comprehensive report.
|
||||
You may receive additional guidance during execution via message steering.
|
||||
expected_output: |
|
||||
A well-structured research report covering recent AI developments,
|
||||
incorporating any real-time guidance received during execution.
|
||||
|
||||
writer:
|
||||
role: Technical Writer
|
||||
goal: Write clear, engaging content based on research
|
||||
backstory: You are a skilled technical writer who can adapt content based on real-time feedback
|
||||
message_steering: true # Enable steering for dynamic content adjustment
|
||||
tools:
|
||||
- read_file
|
||||
- write_file
|
||||
tasks:
|
||||
writing_task:
|
||||
description: |
|
||||
Take the research findings and create an accessible article for a general audience.
|
||||
Adjust style and focus based on any steering messages received.
|
||||
expected_output: |
|
||||
A polished article that transforms technical research into accessible content,
|
||||
refined based on real-time steering guidance.
|
||||
|
||||
dependencies:
|
||||
- writing_task: [research_task]
|
||||
|
||||
# Usage Instructions:
|
||||
# 1. Run: praisonai --agent-file message_steering_example.yaml
|
||||
# 2. While running, use steering API to send guidance:
|
||||
# agent.steer("Focus more on practical applications")
|
||||
# agent.steer("Keep it under 500 words", priority=15)
|
||||
# agent.steer("Add more examples", priority=10)
|
||||
@@ -0,0 +1,148 @@
|
||||
# Named Toolsets YAML Example
|
||||
# This example demonstrates using toolsets in YAML configuration
|
||||
|
||||
# Global configuration
|
||||
config:
|
||||
llm: gpt-4o-mini
|
||||
memory: true
|
||||
|
||||
# Define agents with different toolsets
|
||||
agents:
|
||||
# Research agent with research toolset (includes web tools)
|
||||
researcher:
|
||||
role: Senior Research Analyst
|
||||
goal: Research topics thoroughly using web search and analysis tools
|
||||
instructions: |
|
||||
You are a senior research analyst with access to web search and file tools.
|
||||
Research topics comprehensively and provide detailed findings.
|
||||
toolsets:
|
||||
- research # Includes: web search, file tools, basic analysis
|
||||
|
||||
# Writer agent with safe toolset for writing tasks
|
||||
writer:
|
||||
role: Content Writer
|
||||
goal: Create engaging content based on research
|
||||
instructions: |
|
||||
You are a skilled content writer. Transform research into clear,
|
||||
engaging content for the target audience.
|
||||
toolsets:
|
||||
- safe # Safe, read-only tools for writing
|
||||
|
||||
# Developer agent with development toolset
|
||||
developer:
|
||||
role: Software Developer
|
||||
goal: Create and modify code based on requirements
|
||||
instructions: |
|
||||
You are a software developer with access to development tools.
|
||||
Write, test, and debug code according to specifications.
|
||||
toolsets:
|
||||
- development # Includes: code tools, file operations, testing
|
||||
|
||||
# Data analyst with mixed tools and toolsets
|
||||
analyst:
|
||||
role: Data Analyst
|
||||
goal: Analyze data and generate insights
|
||||
instructions: |
|
||||
You are a data analyst. Process data files and generate
|
||||
actionable insights and visualizations.
|
||||
tools:
|
||||
- read_file # Explicit tool
|
||||
- write_file # Explicit tool
|
||||
toolsets:
|
||||
- safe # Safe toolset for analysis
|
||||
|
||||
# Security-focused agent with minimal toolset
|
||||
security_auditor:
|
||||
role: Security Auditor
|
||||
goal: Review and audit for security issues
|
||||
instructions: |
|
||||
You are a security auditor with minimal tool access.
|
||||
Focus on reading and analyzing without making changes.
|
||||
toolsets:
|
||||
- safe # Minimal, safe tools only
|
||||
|
||||
# Define tasks for the workflow
|
||||
tasks:
|
||||
# Research task using research toolset
|
||||
research_phase:
|
||||
description: |
|
||||
Research the latest trends in AI agent frameworks.
|
||||
Focus on:
|
||||
- Popular frameworks and their features
|
||||
- Industry adoption patterns
|
||||
- Security considerations
|
||||
- Performance benchmarks
|
||||
expected_output: |
|
||||
A comprehensive research report covering AI agent frameworks,
|
||||
including trends, adoption, security, and performance data.
|
||||
agent: researcher
|
||||
|
||||
# Content creation using safe toolset
|
||||
content_creation:
|
||||
description: |
|
||||
Create a technical blog post based on the research findings.
|
||||
The post should be:
|
||||
- Well-structured with clear sections
|
||||
- Accessible to technical and non-technical audiences
|
||||
- Include actionable insights
|
||||
- 1000-1500 words
|
||||
expected_output: |
|
||||
A polished blog post about AI agent frameworks suitable
|
||||
for publication on a technical blog.
|
||||
agent: writer
|
||||
|
||||
# Code development using development toolset
|
||||
code_examples:
|
||||
description: |
|
||||
Create code examples demonstrating key concepts from the research.
|
||||
Include:
|
||||
- Simple "hello world" style examples
|
||||
- Intermediate use cases
|
||||
- Best practices examples
|
||||
- Proper documentation
|
||||
expected_output: |
|
||||
A collection of well-documented code examples that illustrate
|
||||
the concepts discussed in the research and blog post.
|
||||
agent: developer
|
||||
|
||||
# Data analysis using mixed tools/toolsets
|
||||
trend_analysis:
|
||||
description: |
|
||||
Analyze the research data to identify trends and patterns.
|
||||
Create:
|
||||
- Summary statistics
|
||||
- Trend analysis
|
||||
- Recommendations based on findings
|
||||
expected_output: |
|
||||
A data analysis report with trends, patterns, and
|
||||
actionable recommendations for AI framework adoption.
|
||||
agent: analyst
|
||||
|
||||
# Security review using minimal toolset
|
||||
security_review:
|
||||
description: |
|
||||
Review all outputs for security considerations.
|
||||
Check for:
|
||||
- Potential security vulnerabilities in code examples
|
||||
- Security best practices in recommendations
|
||||
- Privacy considerations in data handling
|
||||
expected_output: |
|
||||
A security review report identifying any security concerns
|
||||
and recommendations for secure implementation.
|
||||
agent: security_auditor
|
||||
|
||||
# Workflow process (sequential execution)
|
||||
process: sequential
|
||||
|
||||
# Optional: Define workflow dependencies
|
||||
# tasks:
|
||||
# research_phase:
|
||||
# dependencies: []
|
||||
# content_creation:
|
||||
# dependencies: [research_phase]
|
||||
# code_examples:
|
||||
# dependencies: [research_phase]
|
||||
# trend_analysis:
|
||||
# dependencies: [research_phase]
|
||||
# security_review:
|
||||
# dependencies: [content_creation, code_examples, trend_analysis]
|
||||
@@ -93,7 +93,7 @@ def example_3_execute_yaml_directly():
|
||||
result = manager.execute_yaml(
|
||||
yaml_file,
|
||||
input_data="The future of AI",
|
||||
verbose=True,
|
||||
output="verbose"
|
||||
)
|
||||
|
||||
print(f"Status: {result['status']}")
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
Prototype implementation of Gateway Server with backpressure and flow control.
|
||||
|
||||
This demonstrates how the bounded queues and slow consumer detection would be
|
||||
implemented in the wrapper layer (praisonai package) using the configuration
|
||||
from the core SDK (praisonaiagents).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# For the prototype, we'll define minimal config inline
|
||||
# from praisonaiagents.gateway import GatewayConfig
|
||||
|
||||
@dataclass
|
||||
class SessionConfig:
|
||||
max_inbox: int = 256
|
||||
|
||||
@dataclass
|
||||
class GatewayConfig:
|
||||
session_config: SessionConfig = field(default_factory=SessionConfig)
|
||||
max_buffered_bytes: int = 1024 * 1024 # 1MB default
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GatewaySession:
|
||||
"""Gateway session with bounded inbox for backpressure."""
|
||||
|
||||
_session_id: str
|
||||
_agent_id: str
|
||||
_client_id: Optional[str] = None
|
||||
_inbox: asyncio.Queue = field(default_factory=asyncio.Queue)
|
||||
_max_inbox: int = 256
|
||||
_is_executing: bool = False
|
||||
|
||||
def setup_inbox(self, max_inbox: int = 256) -> None:
|
||||
"""Set up the inbox with proper bounds."""
|
||||
self._max_inbox = max_inbox
|
||||
if max_inbox > 0:
|
||||
self._inbox = asyncio.Queue(maxsize=max_inbox)
|
||||
else:
|
||||
self._inbox = asyncio.Queue() # Unbounded for backward compat if 0
|
||||
|
||||
async def queue_message(self, message: str) -> bool:
|
||||
"""Queue a user message with backpressure handling.
|
||||
|
||||
Returns:
|
||||
True if message was queued, False if inbox is full.
|
||||
"""
|
||||
if self._max_inbox > 0:
|
||||
# Bounded queue - check if full
|
||||
try:
|
||||
self._inbox.put_nowait(message)
|
||||
return True
|
||||
except asyncio.QueueFull:
|
||||
return False
|
||||
else:
|
||||
# Unbounded queue for backward compatibility
|
||||
await self._inbox.put(message)
|
||||
return True
|
||||
|
||||
|
||||
class GatewayServer:
|
||||
"""Gateway server with flow control."""
|
||||
|
||||
def __init__(self, config: GatewayConfig):
|
||||
self.config = config
|
||||
self._sessions: Dict[str, GatewaySession] = {}
|
||||
self._clients: Dict[str, Any] = {} # client_id -> WebSocket
|
||||
|
||||
def create_session(self, session_id: str, agent_id: str, client_id: str) -> GatewaySession:
|
||||
"""Create a new session with bounded inbox."""
|
||||
session = GatewaySession(
|
||||
_session_id=session_id,
|
||||
_agent_id=agent_id,
|
||||
_client_id=client_id,
|
||||
)
|
||||
# Set up inbox with configured bounds
|
||||
session.setup_inbox(self.config.session_config.max_inbox)
|
||||
self._sessions[session_id] = session
|
||||
return session
|
||||
|
||||
async def handle_message(self, session: GatewaySession, message: str, client_id: str) -> str:
|
||||
"""Handle incoming message with inbox overflow protection."""
|
||||
# Try to queue the message
|
||||
queued = await session.queue_message(message)
|
||||
if not queued:
|
||||
# Inbox is full, send error response
|
||||
await self._send_to_client(
|
||||
client_id,
|
||||
{
|
||||
"type": "error",
|
||||
"code": "inbox_full",
|
||||
"message": "Message queue is full. Please wait for current messages to be processed."
|
||||
}
|
||||
)
|
||||
return "Inbox full - message rejected."
|
||||
|
||||
# Start processing if not already running
|
||||
if not session._is_executing:
|
||||
session._is_executing = True
|
||||
asyncio.create_task(self._drain_session_queue(session, client_id))
|
||||
return "Processing started."
|
||||
|
||||
return "Message queued."
|
||||
|
||||
async def _drain_session_queue(self, session: GatewaySession, client_id: str) -> None:
|
||||
"""Process all messages from the session's inbox queue."""
|
||||
try:
|
||||
while not session._inbox.empty():
|
||||
msg = await session._inbox.get()
|
||||
# Process message here (simplified for prototype)
|
||||
logger.info(f"Processing message for session {session._session_id}: {msg}")
|
||||
|
||||
# Simulate processing delay
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Send response to client (simplified)
|
||||
await self._send_to_client(
|
||||
client_id,
|
||||
{
|
||||
"type": "response",
|
||||
"message": f"Processed: {msg}"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing queue for session {session._session_id}: {e}")
|
||||
finally:
|
||||
session._is_executing = False
|
||||
|
||||
async def _send_to_client(self, client_id: str, data: Dict[str, Any]) -> None:
|
||||
"""Send data to client with slow consumer detection."""
|
||||
ws = self._clients.get(client_id)
|
||||
if ws:
|
||||
try:
|
||||
# Check for slow consumer (buffered bytes)
|
||||
if self.config.max_buffered_bytes > 0:
|
||||
# Try to get transport buffer size
|
||||
transport = getattr(ws, 'transport', None) or getattr(ws, '_transport', None)
|
||||
if transport and hasattr(transport, 'get_write_buffer_size'):
|
||||
buffered_bytes = transport.get_write_buffer_size()
|
||||
if buffered_bytes > self.config.max_buffered_bytes:
|
||||
# Check if this is a droppable event type
|
||||
event_type = data.get("type", "")
|
||||
droppable_types = {"presence", "typing", "status"}
|
||||
|
||||
if event_type in droppable_types:
|
||||
# Drop the message for slow consumers
|
||||
logger.warning(f"Dropping {event_type} event for slow consumer {client_id} (buffered: {buffered_bytes} bytes)")
|
||||
return
|
||||
else:
|
||||
# Critical event - close the connection
|
||||
logger.warning(f"Closing slow consumer {client_id} (buffered: {buffered_bytes} bytes > {self.config.max_buffered_bytes})")
|
||||
await ws.close(code=1013, reason="slow consumer")
|
||||
return
|
||||
|
||||
# Send the message
|
||||
await ws.send_json(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending to client {client_id}: {e}", exc_info=True)
|
||||
|
||||
|
||||
# Example usage demonstrating the flow control
|
||||
async def demo():
|
||||
"""Demonstrate the flow control implementation."""
|
||||
|
||||
# Create config with bounded inbox and slow consumer detection
|
||||
config = GatewayConfig()
|
||||
config.session_config.max_inbox = 10 # Small inbox for demo
|
||||
config.max_buffered_bytes = 1024 # 1KB for demo
|
||||
|
||||
# Create server and session
|
||||
server = GatewayServer(config)
|
||||
session = server.create_session("sess1", "agent1", "client1")
|
||||
|
||||
# Simulate filling up the inbox
|
||||
print(f"Inbox max size: {session._inbox.maxsize}")
|
||||
|
||||
# Fill the inbox
|
||||
for i in range(10):
|
||||
success = await session.queue_message(f"Message {i}")
|
||||
print(f"Queued message {i}: {success}")
|
||||
|
||||
# Try to add one more - should fail
|
||||
success = await session.queue_message("Overflow message")
|
||||
print(f"Overflow message queued: {success}")
|
||||
|
||||
# Process messages
|
||||
while not session._inbox.empty():
|
||||
msg = await session._inbox.get()
|
||||
print(f"Processing: {msg}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(demo())
|
||||
@@ -378,7 +378,7 @@ Agent parameters are consolidated into Config objects following the pattern:
|
||||
|
||||
| Config Object | Replaces (Deprecated) |
|
||||
|--------------|----------------------|
|
||||
| `execution=ExecutionConfig(code_execution=True, code_mode="safe", rate_limiter=obj)` | `allow_code_execution`, `code_execution_mode`, `rate_limiter` |
|
||||
| `execution=ExecutionConfig(code_execution=True, code_mode="safe", rate_limiter=obj, context_compaction=False)` | `allow_code_execution`, `code_execution_mode`, `rate_limiter` |
|
||||
| `memory=MemoryConfig(auto_save="session_name")` | `auto_save` |
|
||||
| `autonomy=AutonomyConfig(verification_hooks=[...])` | `verification_hooks` |
|
||||
| `handoffs=[other_agent]` | `allow_delegation` |
|
||||
@@ -390,6 +390,8 @@ Agent parameters are consolidated into Config objects following the pattern:
|
||||
|
||||
**Deprecated standalone params still work** (backward compatible) but emit `DeprecationWarning`.
|
||||
|
||||
> **📢 Deprecation Notice:** `ExecutionConfig.context_compaction` currently defaults to `False` during a deprecation period. In the next release, it will default to `True` for automatic proactive context overflow protection. To opt out explicitly, set `context_compaction=False`. To use the new behavior early, set `context_compaction=True`.
|
||||
|
||||
```python
|
||||
# ❌ Old way (deprecated)
|
||||
agent = Agent(
|
||||
@@ -413,6 +415,7 @@ agent = Agent(
|
||||
code_execution=True,
|
||||
code_mode="safe",
|
||||
rate_limiter=my_limiter,
|
||||
context_compaction=False, # prepare for next release when default becomes True; warning still fires during deprecation period
|
||||
),
|
||||
memory=MemoryConfig(auto_save="my_session"),
|
||||
autonomy=AutonomyConfig(verification_hooks=[my_hook]),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Documentation Parity Tracker (Python)
|
||||
|
||||
> **Categories:** 60 | **Documented:** 57 | **Parity:** 95.0%
|
||||
> **Categories:** 64 | **Documented:** 61 | **Parity:** 95.3%
|
||||
|
||||
This report compares **Python SDK feature categories** against **Python documentation** (docs/concepts, docs/features, etc.).
|
||||
|
||||
@@ -8,72 +8,76 @@ This report compares **Python SDK feature categories** against **Python document
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Feature Categories | 60 |
|
||||
| **Documented Categories** | **57** |
|
||||
| Feature Categories | 64 |
|
||||
| **Documented Categories** | **61** |
|
||||
| **Undocumented Categories** | **3** |
|
||||
| **Parity** | **95.0%** |
|
||||
| **Parity** | **95.3%** |
|
||||
|
||||
## Documented Categories
|
||||
|
||||
| Category | Features | Docs | Lines |
|
||||
|----------|----------|------|-------|
|
||||
| ✅ AGUI | 1 | 2 | 420 |
|
||||
| ✅ Agent | 19 | 30 | 8367 |
|
||||
| ✅ Agent-to-Agent (A2A) | 1 | 3 | 625 |
|
||||
| ✅ Approval | 1 | 2 | 889 |
|
||||
| ✅ Agent | 21 | 34 | 9360 |
|
||||
| ✅ Agent-to-Agent (A2A) | 1 | 7 | 2880 |
|
||||
| ✅ Agent-to-User (A2U) | 1 | 3 | 547 |
|
||||
| ✅ Approval | 1 | 3 | 1283 |
|
||||
| ✅ Audio | 2 | 10 | 649 |
|
||||
| ✅ Auto Generation | 3 | 6 | 1386 |
|
||||
| ✅ Autonomy | 3 | 4 | 1146 |
|
||||
| ✅ Bots | 5 | 2 | 573 |
|
||||
| ✅ Auto Generation | 4 | 6 | 1724 |
|
||||
| ✅ Autonomy | 3 | 5 | 1919 |
|
||||
| ✅ Bots | 7 | 14 | 5228 |
|
||||
| ✅ Budget | 1 | 1 | 287 |
|
||||
| ✅ CLI | 5 | 98 | 24103 |
|
||||
| ✅ Chunking | 2 | 2 | 285 |
|
||||
| ✅ Citations | 2 | 1 | 202 |
|
||||
| ✅ Code Execution | 3 | 13 | 3395 |
|
||||
| ✅ Code Execution | 3 | 13 | 3551 |
|
||||
| ✅ Conditions | 1 | 3 | 1086 |
|
||||
| ✅ Configuration | 2 | 7 | 1753 |
|
||||
| ✅ Context Management | 13 | 29 | 7576 |
|
||||
| ✅ Database | 1 | 31 | 3037 |
|
||||
| ✅ Configuration | 3 | 7 | 1812 |
|
||||
| ✅ Context Management | 16 | 31 | 9294 |
|
||||
| ✅ Database | 1 | 34 | 4339 |
|
||||
| ✅ Display | 6 | 2 | 1175 |
|
||||
| ✅ Embeddings | 6 | 23 | 1966 |
|
||||
| ✅ Evaluation | 1 | 6 | 2538 |
|
||||
| ✅ Events | 1 | 2 | 699 |
|
||||
| ✅ Execution | 3 | 2 | 515 |
|
||||
| ✅ Files | 2 | 5 | 863 |
|
||||
| ✅ Flow | 1 | 1 | 284 |
|
||||
| ✅ Gateway | 6 | 1 | 256 |
|
||||
| ✅ Guardrails | 4 | 4 | 2279 |
|
||||
| ✅ Handoffs | 10 | 4 | 1674 |
|
||||
| ✅ Hooks | 2 | 6 | 1606 |
|
||||
| ✅ Evaluation | 1 | 6 | 2539 |
|
||||
| ✅ Events | 1 | 2 | 704 |
|
||||
| ✅ Execution | 3 | 3 | 1059 |
|
||||
| ✅ Files | 2 | 6 | 1100 |
|
||||
| ✅ Flow | 1 | 2 | 435 |
|
||||
| ✅ Gateway | 6 | 10 | 3924 |
|
||||
| ✅ Guardrails | 4 | 4 | 2008 |
|
||||
| ✅ Handoffs | 11 | 5 | 2155 |
|
||||
| ✅ Hooks | 2 | 6 | 1916 |
|
||||
| ✅ Image | 1 | 11 | 1096 |
|
||||
| ✅ Knowledge | 3 | 12 | 3262 |
|
||||
| ✅ LLM | 2 | 2 | 1049 |
|
||||
| ✅ Loops | 4 | 1 | 250 |
|
||||
| ✅ MCP | 2 | 47 | 9754 |
|
||||
| ✅ Memory | 6 | 12 | 4474 |
|
||||
| ✅ Output | 3 | 4 | 868 |
|
||||
| ✅ Parallel Execution | 2 | 2 | 347 |
|
||||
| ✅ Planning | 6 | 5 | 1506 |
|
||||
| ✅ Plugins | 8 | 3 | 1494 |
|
||||
| ✅ Prompts | 2 | 4 | 796 |
|
||||
| ✅ Providers | 2 | 50 | 5396 |
|
||||
| ✅ Knowledge | 3 | 12 | 3466 |
|
||||
| ✅ LLM | 3 | 5 | 2251 |
|
||||
| ✅ Loops | 4 | 3 | 774 |
|
||||
| ✅ MCP | 2 | 51 | 10960 |
|
||||
| ✅ Memory | 6 | 16 | 5855 |
|
||||
| ✅ Observability | 2 | 23 | 2590 |
|
||||
| ✅ Output | 3 | 4 | 903 |
|
||||
| ✅ Parallel Execution | 3 | 2 | 399 |
|
||||
| ✅ Planning | 6 | 5 | 1519 |
|
||||
| ✅ Plugins | 8 | 3 | 1502 |
|
||||
| ✅ Prompts | 2 | 7 | 1570 |
|
||||
| ✅ Providers | 2 | 51 | 5911 |
|
||||
| ✅ Query | 1 | 2 | 616 |
|
||||
| ✅ RAG | 5 | 14 | 2910 |
|
||||
| ✅ RAG | 5 | 15 | 3190 |
|
||||
| ✅ Realtime | 2 | 5 | 584 |
|
||||
| ✅ Reflection | 3 | 2 | 469 |
|
||||
| ✅ Retrieval | 2 | 5 | 941 |
|
||||
| ✅ Retrieval | 2 | 5 | 944 |
|
||||
| ✅ Routing | 1 | 2 | 597 |
|
||||
| ✅ Sandbox | 5 | 3 | 1002 |
|
||||
| ✅ Security | 1 | 1 | 875 |
|
||||
| ✅ Sessions | 2 | 8 | 2128 |
|
||||
| ✅ Skills | 5 | 5 | 2002 |
|
||||
| ✅ Tasks | 2 | 5 | 2590 |
|
||||
| ✅ Telemetry | 1 | 2 | 541 |
|
||||
| ✅ Sandbox | 5 | 6 | 2369 |
|
||||
| ✅ Security | 1 | 3 | 2518 |
|
||||
| ✅ Sessions | 4 | 9 | 3826 |
|
||||
| ✅ Skills | 5 | 9 | 3415 |
|
||||
| ✅ Tasks | 2 | 6 | 3064 |
|
||||
| ✅ Telemetry | 1 | 2 | 560 |
|
||||
| ✅ Templates | 1 | 7 | 1411 |
|
||||
| ✅ Tools | 4 | 97 | 17827 |
|
||||
| ✅ Tools | 12 | 113 | 23809 |
|
||||
| ✅ Tracing | 3 | 2 | 139 |
|
||||
| ✅ Video | 2 | 5 | 460 |
|
||||
| ✅ Video | 2 | 6 | 510 |
|
||||
| ✅ Vision | 2 | 1 | 283 |
|
||||
| ✅ Web | 4 | 6 | 1267 |
|
||||
| ✅ Workflows | 4 | 9 | 3579 |
|
||||
| ✅ Web | 4 | 6 | 1297 |
|
||||
| ✅ Workflows | 4 | 11 | 4302 |
|
||||
|
||||
## Stub Documentation (Need Content)
|
||||
|
||||
@@ -94,21 +98,18 @@ These categories have documentation files but < 50 lines (stubs):
|
||||
|
||||
These docs exist but don't match any implemented feature category:
|
||||
|
||||
- ℹ️ AI SDK (1 docs, 112 lines)
|
||||
- ℹ️ Agent-to-User (A2U) (3 docs, 770 lines)
|
||||
- ℹ️ CLI (91 docs, 21386 lines)
|
||||
- ℹ️ Callbacks (1 docs, 985 lines)
|
||||
- ℹ️ Chat (3 docs, 546 lines)
|
||||
- ℹ️ AI SDK (2 docs, 527 lines)
|
||||
- ℹ️ Callbacks (1 docs, 1122 lines)
|
||||
- ℹ️ Chat (3 docs, 634 lines)
|
||||
- ℹ️ Deep Research (2 docs, 437 lines)
|
||||
- ℹ️ Deployment (29 docs, 5931 lines)
|
||||
- ℹ️ Jobs (1 docs, 358 lines)
|
||||
- ℹ️ Deployment (29 docs, 6013 lines)
|
||||
- ℹ️ Jobs (2 docs, 961 lines)
|
||||
- ℹ️ Middleware (1 docs, 60 lines)
|
||||
- ℹ️ Observability (21 docs, 1580 lines)
|
||||
- ℹ️ Process (1 docs, 506 lines)
|
||||
- ℹ️ Scheduler (2 docs, 858 lines)
|
||||
- ℹ️ Storage (4 docs, 722 lines)
|
||||
- ℹ️ Streaming (3 docs, 485 lines)
|
||||
- ℹ️ Token Management (3 docs, 1020 lines)
|
||||
- ℹ️ Process (1 docs, 545 lines)
|
||||
- ℹ️ Scheduler (2 docs, 1084 lines)
|
||||
- ℹ️ Storage (5 docs, 957 lines)
|
||||
- ℹ️ Streaming (4 docs, 1163 lines)
|
||||
- ℹ️ Token Management (4 docs, 1383 lines)
|
||||
- ℹ️ Vector Store (11 docs, 733 lines)
|
||||
|
||||
---
|
||||
|
||||
@@ -1,35 +1,70 @@
|
||||
# PraisonAI Agents - Benchmark Results
|
||||
|
||||
**Generated:** 2025-12-18 14:40:22
|
||||
**Iterations:** 100
|
||||
**Test:** Agent instantiation (without tools)
|
||||
**Generated:** 2026-06-17
|
||||
**Methodology:** 10 warmup runs + 1000 measured iterations (`performance_benchmark.py` config)
|
||||
**Config:** `Agent(name=..., instructions=..., model="gpt-4o-mini", tools=[get_weather], output="silent")`
|
||||
|
||||
## Results
|
||||
## Agent Instantiation Time
|
||||
|
||||
| Framework | Avg Time (μs) | Relative |
|
||||
|-----------|---------------|----------|
|
||||
| **PraisonAI** | **3.77** | **1.00x (fastest)** |
|
||||
| OpenAI Agents SDK | 5.26 | 1.39x |
|
||||
| Agno | 5.64 | 1.49x |
|
||||
| PraisonAI (LiteLLM) | 7.56 | 2.00x |
|
||||
| PydanticAI | 226.94 | 60.16x |
|
||||
| LangGraph | 4,558.71 | 1,209x |
|
||||
| CrewAI | 15,607.92 | 4,138x |
|
||||
| Framework | Avg Time (μs) | Min (μs) | Median (μs) | Relative |
|
||||
|-----------|---------------|----------|-------------|----------|
|
||||
| OpenAI Agents SDK | 5.05 | — | — | 1.00x (fastest) |
|
||||
| Agno | 5.27 | — | — | 1.04x |
|
||||
| **PraisonAI** | **13.62** | **12.58** | **12.96** | **2.69x** |
|
||||
| PraisonAI (LiteLLM) | 14.49 | 13.37 | 13.71 | 2.86x |
|
||||
|
||||
### PraisonAI detail (1000 iter)
|
||||
|
||||
| Variant | Avg (μs) | Min (μs) | Median (μs) | Std (μs) |
|
||||
|---------|----------|----------|-------------|----------|
|
||||
| With tools (`gpt-4o-mini`) | 13.62 | 12.58 | 12.96 | 2.73 |
|
||||
| With tools (`openai/gpt-4o-mini`) | 14.49 | 13.37 | 13.71 | 4.14 |
|
||||
| Minimal, no tools | 13.40 | — | — | — |
|
||||
|
||||
## Historical baseline
|
||||
|
||||
| Source | Config | Avg (μs) | Notes |
|
||||
|--------|--------|----------|-------|
|
||||
| `performance_benchmark.py` (Dec 2025) | tools + 1000 iter | **3.20** | Pre-init-bloat baseline |
|
||||
| `simple_benchmark.py` (Dec 2025) | no tools, 100 iter | **3.77** | Saved in prior `BENCHMARK_RESULTS.md` |
|
||||
| Current (Jun 2026) | tools + 1000 iter | **13.62** | After lazy-init optimisations (was ~31 μs pre-fix) |
|
||||
|
||||
## Package Versions
|
||||
|
||||
| Package | Version |
|
||||
|---------|--------|
|
||||
| PraisonAI | 0.1.5 |
|
||||
| Agno | 2.3.14 |
|
||||
| PydanticAI | 1.35.0 |
|
||||
| OpenAI Agents SDK | 0.6.3 |
|
||||
| LangGraph | 1.0.5 |
|
||||
| CrewAI | 1.6.1 |
|
||||
|---------|---------|
|
||||
| PraisonAI | 1.6.60 |
|
||||
| Agno | 2.4.2 |
|
||||
| OpenAI Agents SDK | 0.7.0 |
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
PraisonAI-only (fast; avoids CrewAI/LangGraph import hang):
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
python benchmarks/simple_benchmark.py
|
||||
PYTHONWARNINGS='ignore::DeprecationWarning' python -c "
|
||||
import time, statistics
|
||||
from typing import Literal
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def get_weather(city: Literal['nyc', 'sf']):
|
||||
return 'cloudy' if city == 'nyc' else 'sunny'
|
||||
|
||||
def bench(fn, n=1000, w=10):
|
||||
for _ in range(w): fn()
|
||||
times = []
|
||||
for _ in range(n):
|
||||
s = time.perf_counter(); fn(); times.append(time.perf_counter()-s)
|
||||
print(f'avg={statistics.mean(times)*1e6:.2f} μs')
|
||||
|
||||
bench(lambda: Agent(name='Test', instructions='Hi', model='gpt-4o-mini', tools=[get_weather], output='silent'))
|
||||
"
|
||||
```
|
||||
|
||||
Full multi-framework benchmark:
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
PYTHONWARNINGS='ignore::DeprecationWarning' python benchmarks/performance_benchmark.py --save
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# Agent Instantiation — Root Cause Analysis
|
||||
|
||||
**Date:** 2026-06-17
|
||||
**Version:** praisonaiagents 1.6.58 (dev)
|
||||
**Methodology:** 10 warmup + 1000 measured iterations, `time.perf_counter()`
|
||||
|
||||
## Summary
|
||||
|
||||
Instantiation regressed from **~3.2 μs** (Dec 2025) to **~31 μs** (~10×). After fixes, steady-state is **~16 μs** (~2× historical). PraisonAI is competitive with Agno (~5 μs) on warmed benchmarks using `model=`.
|
||||
|
||||
## Before vs After Fixes
|
||||
|
||||
| Config | Historical | Pre-fix | Post-fix |
|
||||
|--------|------------|---------|----------|
|
||||
| Minimal (`name`, `output="silent"`) | 3.20 μs | 31.36 μs | **16.35 μs** |
|
||||
| `model="gpt-4o-mini"` | 3.77 μs | 32.19 μs | **16.86 μs** |
|
||||
| `llm="gpt-4o-mini"` (deprecated) | 3.77 μs | 46.48 μs | **~18 μs** (warn once) |
|
||||
| LiteLLM `openai/gpt-4o-mini` cold | 7.31 μs | **3.7 s** | **53.63 μs** |
|
||||
| LiteLLM warmed instantiate | — | 60 μs | **19.83 μs** |
|
||||
| Import `Agent` | — | 20.8 ms | **29.6 ms** |
|
||||
|
||||
## Root Causes (validated)
|
||||
|
||||
| ID | Cause | Status | Fix applied |
|
||||
|----|-------|--------|-------------|
|
||||
| H1 | Config loader cold start (+9 ms first init) | CONFIRMED | Fast path when no config defaults (`_defaults_has_any_values`) |
|
||||
| H2 | LoopGuard on every init (~2 μs) | CONFIRMED | Lazy `_ensure_loop_guard()` on first tool call |
|
||||
| H3 | Approval presets | REJECTED | No change needed |
|
||||
| H4 | `llm=` deprecation every init (+16 μs) | CONFIRMED | Warn once per process in `warn_deprecated_param` |
|
||||
| H5 | Init bloat / config resolution (~25 μs) | CONFIRMED | Partially addressed via loader fast path |
|
||||
| H6 | LiteLLM eager `LLM()` import (+3.7 s) | CONFIRMED | Deferred `_llm_init_params` + lazy `llm_instance` property |
|
||||
| H7 | `ExecutionConfig` deprecation every init | CONFIRMED | Fixed stack walk; internal SDK path cached |
|
||||
|
||||
## cProfile — Post-fix (500 warmed `model=` inits)
|
||||
|
||||
| Function | Time (ms) | Per call |
|
||||
|----------|-----------|----------|
|
||||
| `Agent.__init__` | 8 | ~16 μs |
|
||||
| `apply_config_defaults` | 1 | ~2 μs |
|
||||
| `ExecutionConfig.__post_init__` | 1 | ~2 μs (first call only walks stack) |
|
||||
| `_warnings.warn` | **0** | removed from hot path |
|
||||
|
||||
## Fixes implemented
|
||||
|
||||
1. **`praisonaiagents/config/feature_configs.py`** — Fix `ExecutionConfig.__post_init__` deprecation guard; cache internal SDK detection.
|
||||
2. **`praisonaiagents/utils/deprecation.py`** — Warn once per process for deprecated params.
|
||||
3. **`praisonaiagents/config/loader.py`** — Skip `apply_config_defaults` lookups when config has no defaults.
|
||||
4. **`praisonaiagents/agent/agent.py`** — Lazy LoopGuard, lazy LLM instance, deferred LiteLLM params.
|
||||
5. **`benchmarks/simple_benchmark.py`** — Use `model=`, add 10-iteration warmup.
|
||||
6. **`benchmarks/performance_benchmark.py`** — Use `model=` instead of deprecated `llm=`.
|
||||
|
||||
## Remaining gap (~16 μs vs ~3 μs)
|
||||
|
||||
The residual ~13 μs is primarily **`Agent.__init__` size** (~1200 lines of consolidated config resolution added since v0.1.5). Further optimisation requires a dedicated slim fast-path for `Agent(name=..., output="silent")` without full config object churn.
|
||||
|
||||
## How to reproduce
|
||||
|
||||
```bash
|
||||
cd praisonai-agents
|
||||
PYTHONWARNINGS='ignore::DeprecationWarning' python benchmarks/simple_benchmark.py
|
||||
PYTHONWARNINGS='ignore::DeprecationWarning' python benchmarks/performance_benchmark.py
|
||||
```
|
||||
@@ -120,8 +120,8 @@ def run_benchmark(iterations: int = 100) -> List[ParamResult]:
|
||||
('backstory', 'I am an AI assistant.', 'backstory="..."'),
|
||||
|
||||
# LLM configuration
|
||||
('llm', 'gpt-4o-mini', 'llm="gpt-4o-mini"'),
|
||||
('llm', 'openai/gpt-4o-mini', 'llm="openai/gpt-4o-mini" (LiteLLM)'),
|
||||
('model', 'gpt-4o-mini', 'model="gpt-4o-mini"'),
|
||||
('model', 'openai/gpt-4o-mini', 'model="openai/gpt-4o-mini" (LiteLLM)'),
|
||||
|
||||
# Tools
|
||||
('tools', [sample_tool], 'tools=[sample_tool]'),
|
||||
@@ -298,7 +298,7 @@ def save_results(results: List[ParamResult], filename: str = 'PARAM_IMPACT_RESUL
|
||||
f.write('### Best Practices\n\n')
|
||||
f.write('1. Use `output="silent"` for production workloads\n')
|
||||
f.write('2. Only enable `context=True` when needed for long conversations\n')
|
||||
f.write('3. Use `llm="gpt-4o-mini"` (OpenAI SDK) for fastest instantiation\n')
|
||||
f.write('3. Use `model="gpt-4o-mini"` for fastest instantiation\n')
|
||||
f.write('4. Avoid `memory=True` unless persistent memory is required\n')
|
||||
|
||||
f.write('\n## How to Reproduce\n\n')
|
||||
|
||||
@@ -185,7 +185,7 @@ def instantiate_praisonai_agent():
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="gpt-4o-mini",
|
||||
model="gpt-4o-mini",
|
||||
tools=tools,
|
||||
output="silent"
|
||||
)
|
||||
@@ -196,7 +196,7 @@ def instantiate_praisonai_litellm_agent():
|
||||
return PraisonAgent(
|
||||
name="Test Agent",
|
||||
instructions="Be concise, reply with one sentence.",
|
||||
llm="openai/gpt-4o-mini",
|
||||
model="openai/gpt-4o-mini",
|
||||
tools=tools,
|
||||
output="silent"
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ import argparse
|
||||
|
||||
|
||||
ITERATIONS = 100
|
||||
WARMUP = 10
|
||||
|
||||
|
||||
def sample_tool(city: Literal['nyc', 'sf']):
|
||||
@@ -23,8 +24,10 @@ def sample_tool(city: Literal['nyc', 'sf']):
|
||||
return 'sunny'
|
||||
|
||||
|
||||
def measure_instantiation(create_fn, iterations=ITERATIONS):
|
||||
def measure_instantiation(create_fn, iterations=ITERATIONS, warmup=WARMUP):
|
||||
"""Measure average instantiation time in microseconds."""
|
||||
for _ in range(warmup):
|
||||
create_fn()
|
||||
times = []
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
@@ -48,11 +51,11 @@ def run_benchmark():
|
||||
from praisonaiagents import Agent as PraisonAgent
|
||||
|
||||
results['PraisonAI'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', output="silent")
|
||||
lambda: PraisonAgent(name='Test', model='gpt-4o-mini', output="silent")
|
||||
)
|
||||
|
||||
results['PraisonAI (LiteLLM)'] = measure_instantiation(
|
||||
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', output="silent")
|
||||
lambda: PraisonAgent(name='Test', model='openai/gpt-4o-mini', output="silent")
|
||||
)
|
||||
|
||||
# Other frameworks for comparison
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Context Compaction Example - Python API
|
||||
|
||||
Demonstrates how to configure context compaction policies in PraisonAI agents.
|
||||
This provides examples for CLI/YAML parity.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import ExecutionConfig
|
||||
from praisonaiagents.context.adapters import ContextCompactionPolicyAdapter
|
||||
from praisonaiagents.context.protocols import CompactionStrategy
|
||||
|
||||
|
||||
def example_default_behavior():
|
||||
"""Example 1: Default behavior (during deprecation period)."""
|
||||
print("=== Example 1: Default Behavior ===")
|
||||
|
||||
# Currently defaults to False with deprecation warning
|
||||
agent = Agent(
|
||||
name="default_agent",
|
||||
instructions="You are a helpful assistant."
|
||||
)
|
||||
|
||||
print(f"Context compaction enabled: {agent.execution.context_compaction}")
|
||||
print("Note: Currently False during deprecation period, will change to True in next release\n")
|
||||
|
||||
|
||||
def example_explicit_disable():
|
||||
"""Example 2: Explicitly disable context compaction."""
|
||||
print("=== Example 2: Explicitly Disabled ===")
|
||||
|
||||
agent = Agent(
|
||||
name="no_compaction_agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
execution=ExecutionConfig(context_compaction=False)
|
||||
)
|
||||
|
||||
print(f"Context compaction enabled: {agent.execution.context_compaction}")
|
||||
print("No automatic compaction will occur\n")
|
||||
|
||||
|
||||
def example_default_policy():
|
||||
"""Example 3: Use default compaction policy."""
|
||||
print("=== Example 3: Default Policy ===")
|
||||
|
||||
agent = Agent(
|
||||
name="default_policy_agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
execution=ExecutionConfig(context_compaction=True)
|
||||
)
|
||||
|
||||
print(f"Context compaction enabled: {agent.execution.context_compaction}")
|
||||
print("Will use balanced default policy (trigger at 90%, preserve 5 recent turns)\n")
|
||||
|
||||
|
||||
def example_conservative_policy():
|
||||
"""Example 4: Conservative compaction policy."""
|
||||
print("=== Example 4: Conservative Policy ===")
|
||||
|
||||
conservative_policy = ContextCompactionPolicyAdapter(
|
||||
trigger_at=0.80, # Trigger at 80% capacity
|
||||
strategy=CompactionStrategy.DROP_OLDEST_TOOLS,
|
||||
preserve_last_n_turns=8, # Keep more conversation history
|
||||
target_utilization=0.60, # Target 60% utilization after compaction
|
||||
aggressive_tool_truncation=False
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="conservative_agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
execution=ExecutionConfig(context_compaction=conservative_policy)
|
||||
)
|
||||
|
||||
print(f"Policy trigger: {conservative_policy.trigger_at}")
|
||||
print(f"Strategy: {conservative_policy.strategy}")
|
||||
print(f"Preserve turns: {conservative_policy.preserve_last_n_turns}")
|
||||
print(f"Target utilization: {conservative_policy.target_utilization}\n")
|
||||
|
||||
|
||||
def example_aggressive_policy():
|
||||
"""Example 5: Aggressive compaction policy."""
|
||||
print("=== Example 5: Aggressive Policy ===")
|
||||
|
||||
aggressive_policy = ContextCompactionPolicyAdapter(
|
||||
trigger_at=0.95, # Wait until 95% capacity
|
||||
strategy=CompactionStrategy.SUMMARISE, # Use LLM summarization
|
||||
preserve_last_n_turns=3, # Keep only recent exchanges
|
||||
target_utilization=0.75, # Target higher utilization
|
||||
aggressive_tool_truncation=True, # Truncate large tool outputs
|
||||
model_overrides={
|
||||
"gpt-4": {"trigger_at": 0.92}, # Different threshold for gpt-4
|
||||
"claude-3": {"trigger_at": 0.88}
|
||||
}
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="aggressive_agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
execution=ExecutionConfig(context_compaction=aggressive_policy)
|
||||
)
|
||||
|
||||
print(f"Policy trigger: {aggressive_policy.trigger_at}")
|
||||
print(f"Strategy: {aggressive_policy.strategy}")
|
||||
print(f"Preserve turns: {aggressive_policy.preserve_last_n_turns}")
|
||||
print(f"Model overrides: {aggressive_policy.model_overrides}\n")
|
||||
|
||||
|
||||
def example_yaml_config():
|
||||
"""Example 6: YAML configuration equivalent."""
|
||||
print("=== Example 6: YAML Configuration Equivalent ===")
|
||||
print("""
|
||||
# agent_config.yaml
|
||||
name: "helpful_agent"
|
||||
instructions: "You are a helpful assistant."
|
||||
llm: "gpt-4o-mini"
|
||||
execution:
|
||||
context_compaction:
|
||||
trigger_at: 0.85
|
||||
strategy: "drop_oldest_tools"
|
||||
preserve_last_n_turns: 6
|
||||
target_utilization: 0.65
|
||||
aggressive_tool_truncation: true
|
||||
model_overrides:
|
||||
"gpt-4":
|
||||
trigger_at: 0.90
|
||||
strategy: "summarise"
|
||||
""")
|
||||
|
||||
# Python equivalent:
|
||||
policy = ContextCompactionPolicyAdapter(
|
||||
trigger_at=0.85,
|
||||
strategy="drop_oldest_tools",
|
||||
preserve_last_n_turns=6,
|
||||
target_utilization=0.65,
|
||||
aggressive_tool_truncation=True,
|
||||
model_overrides={
|
||||
"gpt-4": {
|
||||
"trigger_at": 0.90,
|
||||
"strategy": "summarise"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="helpful_agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
llm="gpt-4o-mini",
|
||||
execution=ExecutionConfig(context_compaction=policy)
|
||||
)
|
||||
|
||||
print(f"Loaded policy trigger: {policy.trigger_at}")
|
||||
print(f"Loaded strategy: {policy.strategy}\n")
|
||||
|
||||
|
||||
def example_cli_equivalent():
|
||||
"""Example 7: CLI equivalent commands."""
|
||||
print("=== Example 7: CLI Equivalent ===")
|
||||
print("""
|
||||
# Basic usage (will use default policy when enabled)
|
||||
praisonai agent run --name "assistant" --instructions "Be helpful"
|
||||
|
||||
# Disable compaction explicitly
|
||||
praisonai agent run --name "assistant" --execution '{"context_compaction": false}'
|
||||
|
||||
# Use conservative policy
|
||||
praisonai agent run --name "assistant" \\
|
||||
--execution '{
|
||||
"context_compaction": {
|
||||
"trigger_at": 0.80,
|
||||
"strategy": "drop_oldest_tools",
|
||||
"preserve_last_n_turns": 8
|
||||
}
|
||||
}'
|
||||
|
||||
# Load from YAML config file
|
||||
praisonai agent run --config agent_config.yaml
|
||||
""")
|
||||
|
||||
|
||||
def example_serialization_round_trip():
|
||||
"""Example 8: Serialization round-trip."""
|
||||
print("=== Example 8: Serialization Round-Trip ===")
|
||||
|
||||
# Create agent with custom policy
|
||||
original_policy = ContextCompactionPolicyAdapter(
|
||||
trigger_at=0.88,
|
||||
strategy=CompactionStrategy.SUMMARISE
|
||||
)
|
||||
|
||||
config = ExecutionConfig(context_compaction=original_policy)
|
||||
|
||||
# Serialize to dict (for JSON/YAML storage)
|
||||
config_dict = config.to_dict()
|
||||
print(f"Serialized context_compaction type: {type(config_dict['context_compaction'])}")
|
||||
|
||||
# Deserialize back to object
|
||||
restored_config = ExecutionConfig.from_dict(config_dict)
|
||||
restored_policy = restored_config.context_compaction
|
||||
|
||||
print(f"Restored policy type: {type(restored_policy)}")
|
||||
print(f"Attributes preserved: trigger_at={restored_policy.trigger_at}, strategy={restored_policy.strategy}")
|
||||
print("✓ Round-trip serialization successful\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Context Compaction Configuration Examples\n")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
example_default_behavior()
|
||||
example_explicit_disable()
|
||||
example_default_policy()
|
||||
example_conservative_policy()
|
||||
example_aggressive_policy()
|
||||
example_yaml_config()
|
||||
example_cli_equivalent()
|
||||
example_serialization_round_trip()
|
||||
|
||||
print("=" * 50)
|
||||
print("✅ All examples completed successfully!")
|
||||
print("\nFor more information, see:")
|
||||
print("- Documentation: https://docs.praisonai.com/context-compaction")
|
||||
print("- Examples: examples/context_compaction_example.py")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Example failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@@ -114,6 +114,9 @@ def _get_lazy_cache():
|
||||
# ============================================================================
|
||||
|
||||
_LAZY_IMPORTS = {
|
||||
# Version information (lazy loaded to avoid file I/O on import)
|
||||
'__version__': ('praisonaiagents._version', '__version__'),
|
||||
|
||||
# Tools (moved from eager imports for lazy loading)
|
||||
'Tools': ('praisonaiagents.tools.tools', 'Tools'),
|
||||
'BaseTool': ('praisonaiagents.tools.base', 'BaseTool'),
|
||||
@@ -127,6 +130,23 @@ _LAZY_IMPORTS = {
|
||||
'get_tool': ('praisonaiagents.tools.registry', 'get_tool'),
|
||||
'ToolRegistry': ('praisonaiagents.tools.registry', 'ToolRegistry'),
|
||||
|
||||
# Toolsets (named toolset groups)
|
||||
'ToolsetSpec': ('praisonaiagents.toolsets', 'ToolsetSpec'),
|
||||
'ToolsetRegistry': ('praisonaiagents.toolsets', 'ToolsetRegistry'),
|
||||
'get_toolset_registry': ('praisonaiagents.toolsets', 'get_toolset_registry'),
|
||||
'register_toolset': ('praisonaiagents.toolsets', 'register_toolset'),
|
||||
'resolve_toolset': ('praisonaiagents.toolsets', 'resolve_toolset'),
|
||||
'resolve_toolsets': ('praisonaiagents.toolsets', 'resolve_toolsets'),
|
||||
'list_toolsets': ('praisonaiagents.toolsets', 'list_toolsets'),
|
||||
'get_toolset': ('praisonaiagents.toolsets', 'get_toolset'),
|
||||
'unregister_toolset': ('praisonaiagents.toolsets', 'unregister_toolset'),
|
||||
'has_toolset': ('praisonaiagents.toolsets', 'has_toolset'),
|
||||
|
||||
# Run outcomes - typed validation and agent execution results
|
||||
'AgentRunOutcome': ('praisonaiagents.run_outcome', 'AgentRunOutcome'),
|
||||
'RunStatus': ('praisonaiagents.run_outcome', 'RunStatus'),
|
||||
'validate_decision_string': ('praisonaiagents.run_outcome', 'validate_decision_string'),
|
||||
|
||||
# Main display utilities (imports rich)
|
||||
'TaskOutput': ('praisonaiagents.main', 'TaskOutput'),
|
||||
'ReflectionOutput': ('praisonaiagents.main', 'ReflectionOutput'),
|
||||
@@ -174,6 +194,7 @@ _LAZY_IMPORTS = {
|
||||
'RECOMMENDED_PROMPT_PREFIX': ('praisonaiagents.agent.handoff', 'RECOMMENDED_PROMPT_PREFIX'),
|
||||
'prompt_with_handoff_instructions': ('praisonaiagents.agent.handoff', 'prompt_with_handoff_instructions'),
|
||||
'HandoffConfig': ('praisonaiagents.agent.handoff', 'HandoffConfig'),
|
||||
'HandoffToolPolicy': ('praisonaiagents.agent.handoff', 'HandoffToolPolicy'),
|
||||
'HandoffResult': ('praisonaiagents.agent.handoff', 'HandoffResult'),
|
||||
'HandoffInputData': ('praisonaiagents.agent.handoff', 'HandoffInputData'),
|
||||
'ContextPolicy': ('praisonaiagents.agent.handoff', 'ContextPolicy'),
|
||||
@@ -386,6 +407,17 @@ _LAZY_IMPORTS = {
|
||||
'MultiAgentMemoryConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentMemoryConfig'),
|
||||
'ToolSearchConfig': ('praisonaiagents.config.feature_configs', 'ToolSearchConfig'),
|
||||
|
||||
# Context compaction policies
|
||||
'ContextCompactionPolicy': ('praisonaiagents.context.policy', 'ContextCompactionPolicy'),
|
||||
'ContextCompactionPolicyProtocol': ('praisonaiagents.context.policy', 'ContextCompactionPolicyProtocol'),
|
||||
'CompactionRoute': ('praisonaiagents.context.policy', 'CompactionRoute'),
|
||||
'CompactionStrategy': ('praisonaiagents.context.policy', 'CompactionStrategy'),
|
||||
'ContextBudgetResult': ('praisonaiagents.context.policy', 'ContextBudgetResult'),
|
||||
'get_default_policy': ('praisonaiagents.context.policy', 'get_default_policy'),
|
||||
'CONSERVATIVE_POLICY': ('praisonaiagents.context.policy', 'CONSERVATIVE_POLICY'),
|
||||
'BALANCED_POLICY': ('praisonaiagents.context.policy', 'BALANCED_POLICY'),
|
||||
'AGGRESSIVE_POLICY': ('praisonaiagents.context.policy', 'AGGRESSIVE_POLICY'),
|
||||
|
||||
# Parameter resolver
|
||||
'resolve': ('praisonaiagents.config.param_resolver', 'resolve'),
|
||||
'ArrayMode': ('praisonaiagents.config.param_resolver', 'ArrayMode'),
|
||||
@@ -444,7 +476,7 @@ _LAZY_IMPORTS = {
|
||||
'GatewayClientProtocol': ('praisonaiagents.gateway.protocols', 'GatewayClientProtocol'),
|
||||
'GatewayEvent': ('praisonaiagents.gateway.protocols', 'GatewayEvent'),
|
||||
'GatewayMessage': ('praisonaiagents.gateway.protocols', 'GatewayMessage'),
|
||||
'EventType': ('praisonaiagents.gateway.protocols', 'EventType'),
|
||||
'GatewayEventType': ('praisonaiagents.gateway.protocols', 'EventType'),
|
||||
'GatewayConfig': ('praisonaiagents.gateway.config', 'GatewayConfig'),
|
||||
'SessionConfig': ('praisonaiagents.gateway.config', 'SessionConfig'),
|
||||
|
||||
@@ -626,6 +658,11 @@ def _custom_handler(name, cache):
|
||||
mod = importlib.import_module('.db', 'praisonaiagents')
|
||||
cache['db'] = mod
|
||||
return mod
|
||||
if name == 'toolsets':
|
||||
import importlib
|
||||
mod = importlib.import_module('.toolsets', 'praisonaiagents')
|
||||
cache['toolsets'] = mod
|
||||
return mod
|
||||
|
||||
raise AttributeError(f"Not handled by custom_handler: {name}")
|
||||
|
||||
@@ -741,6 +778,9 @@ def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
|
||||
# ============================================================================
|
||||
|
||||
__all__ = [
|
||||
# Version information
|
||||
'__version__',
|
||||
|
||||
# Core classes - the essentials
|
||||
'Agent',
|
||||
'AgentTeam', # Primary class for multi-agent coordination (v1.0+)
|
||||
|
||||
Whitespace-only changes.
@@ -111,32 +111,3 @@ def _get_plugins_list() -> list:
|
||||
|
||||
PLUGINS_ENABLED = _get_plugins_enabled()
|
||||
PLUGINS_LIST = _get_plugins_list()
|
||||
|
||||
|
||||
def missing_dependency_error(
|
||||
package_name: str,
|
||||
extra_name: Optional[str] = None,
|
||||
feature_name: Optional[str] = None
|
||||
) -> ImportError:
|
||||
"""
|
||||
Create a standardized ImportError for missing optional dependencies.
|
||||
|
||||
Args:
|
||||
package_name: The name of the missing package (e.g., 'litellm')
|
||||
extra_name: The pip extra to install (e.g., 'llm' for [llm])
|
||||
feature_name: Human-readable feature name (e.g., 'LLM support')
|
||||
|
||||
Returns:
|
||||
ImportError with helpful install instructions
|
||||
"""
|
||||
if extra_name:
|
||||
install_cmd = f"pip install 'praisonaiagents[{extra_name}]'"
|
||||
else:
|
||||
install_cmd = f"pip install {package_name}"
|
||||
|
||||
feature_desc = f" for {feature_name}" if feature_name else ""
|
||||
|
||||
return ImportError(
|
||||
f"{package_name} is required{feature_desc} but not installed. "
|
||||
f"Please install with: {install_cmd}"
|
||||
)
|
||||
@@ -1,49 +0,0 @@
|
||||
"""
|
||||
Resolver helper functions for consolidated parameters.
|
||||
|
||||
These helpers wrap the canonical resolver for specific parameter types,
|
||||
handling special cases like callable validators for guardrails.
|
||||
"""
|
||||
|
||||
from typing import Any, Type
|
||||
|
||||
from .config.param_resolver import resolve, ArrayMode
|
||||
from .config.presets import GUARDRAIL_PRESETS
|
||||
|
||||
|
||||
def resolve_guardrails(value: Any, config_class: Type) -> Any:
|
||||
"""
|
||||
Resolve guardrails parameter.
|
||||
|
||||
Handles special case of callable validators which have highest precedence.
|
||||
|
||||
Args:
|
||||
value: The guardrails parameter value
|
||||
config_class: GuardrailConfig class
|
||||
|
||||
Returns:
|
||||
Resolved config, callable, or string
|
||||
"""
|
||||
# Handle None
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Handle False (disabled)
|
||||
if value is False:
|
||||
return None
|
||||
|
||||
# Handle callable (highest precedence after None check)
|
||||
# This includes functions, lambdas, and callable objects
|
||||
if callable(value) and not isinstance(value, type):
|
||||
return value
|
||||
|
||||
# Use canonical resolver for everything else
|
||||
return resolve(
|
||||
value=value,
|
||||
param_name="guardrails",
|
||||
config_class=config_class,
|
||||
presets=GUARDRAIL_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
string_mode="llm_prompt",
|
||||
default=None,
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Version utilities for praisonaiagents package.
|
||||
|
||||
This module provides version information by reading from pyproject.toml
|
||||
to avoid duplication and ensure the single source of truth.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
"""
|
||||
Get the version string from pyproject.toml.
|
||||
|
||||
Returns:
|
||||
str: Version string (e.g., "1.6.52")
|
||||
|
||||
Raises:
|
||||
RuntimeError: If version cannot be found or parsed
|
||||
"""
|
||||
try:
|
||||
# Get the package root (praisonaiagents directory)
|
||||
package_root = Path(__file__).parent
|
||||
# Go up to src/praisonai-agents directory
|
||||
pyproject_path = package_root.parent / "pyproject.toml"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
raise RuntimeError(f"pyproject.toml not found at {pyproject_path}")
|
||||
|
||||
content = pyproject_path.read_text()
|
||||
|
||||
# Look for version = "X.Y.Z" pattern
|
||||
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
||||
if not match:
|
||||
raise RuntimeError("Version not found in pyproject.toml")
|
||||
|
||||
return match.group(1)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to "unknown" to avoid breaking imports
|
||||
import warnings
|
||||
warnings.warn(
|
||||
f"Failed to read version from pyproject.toml: {e}. Using 'unknown'.",
|
||||
RuntimeWarning
|
||||
)
|
||||
return "unknown"
|
||||
|
||||
|
||||
# Cache the version to avoid reading the file multiple times
|
||||
__version__ = get_version()
|
||||
@@ -114,11 +114,12 @@ def __getattr__(name):
|
||||
|
||||
# Handoff - lightweight
|
||||
_handoff_names = {
|
||||
'Handoff', 'handoff', 'handoff_filters', 'parallel_handoffs',
|
||||
'Handoff', 'handoff_filters', 'parallel_handoffs',
|
||||
'RECOMMENDED_PROMPT_PREFIX', 'prompt_with_handoff_instructions',
|
||||
'HandoffConfig', 'HandoffResult', 'HandoffInputData',
|
||||
'ContextPolicy', 'HandoffError', 'HandoffCycleError',
|
||||
'HandoffDepthError', 'HandoffTimeoutError'
|
||||
'HandoffDepthError', 'HandoffTimeoutError', 'HandoffValidationError',
|
||||
'TypedHandoff'
|
||||
}
|
||||
if name in _handoff_names:
|
||||
from . import handoff as _handoff_module
|
||||
@@ -165,6 +166,21 @@ def __getattr__(name):
|
||||
_lazy_cache[name] = value
|
||||
return value
|
||||
|
||||
# Message Steering - lightweight protocols and implementations
|
||||
_steering_names = {
|
||||
'MessageSteeringProtocol', 'AgentSteeringProtocol', 'SteeringPriority',
|
||||
'SteeringMessage', 'MessageSteering', 'SteeringMixin'
|
||||
}
|
||||
if name in _steering_names:
|
||||
if name in {'MessageSteeringProtocol', 'AgentSteeringProtocol', 'SteeringPriority', 'SteeringMessage'}:
|
||||
from . import protocols as _protocols_module
|
||||
value = getattr(_protocols_module, name)
|
||||
else:
|
||||
from . import message_steering as _steering_module
|
||||
value = getattr(_steering_module, name)
|
||||
_lazy_cache[name] = value
|
||||
return value
|
||||
|
||||
# Decomposed mixins - for advanced use cases
|
||||
if name == 'ToolExecutionMixin':
|
||||
from .tool_execution import ToolExecutionMixin
|
||||
@@ -230,6 +246,8 @@ __all__ = [
|
||||
'HandoffCycleError',
|
||||
'HandoffDepthError',
|
||||
'HandoffTimeoutError',
|
||||
'HandoffValidationError',
|
||||
'TypedHandoff',
|
||||
'RouterAgent',
|
||||
'DeepResearchAgent',
|
||||
'DeepResearchResponse',
|
||||
@@ -253,6 +271,13 @@ __all__ = [
|
||||
'MemoryAwareAgentProtocol',
|
||||
'FullAgentProtocol',
|
||||
'ContextEngineerProtocol',
|
||||
# Message Steering
|
||||
'MessageSteeringProtocol',
|
||||
'AgentSteeringProtocol',
|
||||
'SteeringPriority',
|
||||
'SteeringMessage',
|
||||
'MessageSteering',
|
||||
'SteeringMixin',
|
||||
# Decomposed mixins (for advanced use cases)
|
||||
'ToolExecutionMixin',
|
||||
'ChatHandlerMixin',
|
||||
|
||||
@@ -7,6 +7,7 @@ import asyncio
|
||||
import contextlib
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import random
|
||||
from typing import List, Optional, Any, Dict, Union, Literal, TYPE_CHECKING, Callable, Generator
|
||||
from collections import OrderedDict
|
||||
import inspect
|
||||
@@ -16,12 +17,13 @@ from .chat_mixin import ChatMixin
|
||||
from .execution_mixin import ExecutionMixin
|
||||
from .memory_mixin import MemoryMixin
|
||||
from .async_memory_mixin import AsyncMemoryMixin
|
||||
from .tool_execution import ToolExecutionMixin
|
||||
from .tool_execution import ToolExecutionMixin, BackoffPolicy
|
||||
from .chat_handler import ChatHandlerMixin
|
||||
from .session_manager import SessionManagerMixin
|
||||
from .async_safety import AsyncSafeState
|
||||
from .unified_execution_mixin import UnifiedExecutionMixin
|
||||
from .sandbox_mixin import SandboxMixin
|
||||
from .message_steering import SteeringMixin
|
||||
|
||||
# Module-level logger for thread safety errors and debugging
|
||||
logger = get_logger(__name__)
|
||||
@@ -242,7 +244,7 @@ def _get_default_server_registry() -> ServerRegistry:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..approval.protocols import ApprovalConfig, ApprovalProtocol
|
||||
from ..config.feature_configs import LearnConfig, MemoryConfig
|
||||
from ..config.feature_configs import LearnConfig, MemoryConfig, ToolConfig
|
||||
from ..context.models import ContextConfig
|
||||
from ..context.manager import ContextManager
|
||||
from ..knowledge.knowledge import Knowledge
|
||||
@@ -252,11 +254,12 @@ if TYPE_CHECKING:
|
||||
from .handoff import Handoff, HandoffConfig, HandoffResult
|
||||
from ..rag.models import RAGResult, ContextPack
|
||||
from ..eval.results import EvaluationLoopResult
|
||||
from ..tools.retry import RetryPolicy
|
||||
|
||||
# Import structured error from central errors module
|
||||
from ..errors import BudgetExceededError
|
||||
|
||||
class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin, ExecutionMixin, MemoryMixin, AsyncMemoryMixin):
|
||||
class Agent(SteeringMixin, SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin, ExecutionMixin, MemoryMixin, AsyncMemoryMixin):
|
||||
# Class-level counter for generating unique display names for nameless agents
|
||||
_agent_counter = 0
|
||||
_agent_counter_lock = threading.Lock()
|
||||
@@ -311,6 +314,38 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
cls._default_model_checked = True
|
||||
return cls._default_model
|
||||
|
||||
def _ensure_llm_instance(self):
|
||||
"""Lazy-create LLM instance from deferred init params (avoids import at Agent())."""
|
||||
if self._llm_instance is not None:
|
||||
return self._llm_instance
|
||||
if self._llm_init_params:
|
||||
try:
|
||||
from ..llm.llm import LLM
|
||||
self._llm_instance = LLM(**self._llm_init_params)
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"LLM features requested but dependencies not installed. "
|
||||
"Please install with: pip install \"praisonaiagents[llm]\""
|
||||
) from e
|
||||
return self._llm_instance
|
||||
|
||||
@property
|
||||
def llm_instance(self):
|
||||
return self._ensure_llm_instance()
|
||||
|
||||
@llm_instance.setter
|
||||
def llm_instance(self, value):
|
||||
self._llm_instance = value
|
||||
if value is not None:
|
||||
self._llm_init_params = None
|
||||
|
||||
def _ensure_loop_guard(self):
|
||||
"""Lazy-create loop guard on first tool execution."""
|
||||
if self._loop_guard is None:
|
||||
from ..escalation.loop_guard import LoopGuard, LoopGuardConfig
|
||||
self._loop_guard = LoopGuard(LoopGuardConfig(enabled=True))
|
||||
return self._loop_guard
|
||||
|
||||
@classmethod
|
||||
def _configure_logging(cls):
|
||||
"""Configure logging settings once for all agent instances."""
|
||||
@@ -516,13 +551,14 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
backstory: Optional[str] = None,
|
||||
instructions: Optional[str] = None,
|
||||
# LLM configuration
|
||||
llm: Optional[Union[str, Any]] = None,
|
||||
llm: Optional[Union[str, Any]] = None, # Can be string, dict, or LLMConfig object
|
||||
model: Optional[Union[str, Any]] = None, # Alias for llm=
|
||||
base_url: Optional[str] = None, # Kept separate (connection/auth)
|
||||
api_key: Optional[str] = None, # Kept separate (connection/auth)
|
||||
auth: Optional[str] = None, # Subscription auth provider: "claude-code", "codex", etc.
|
||||
# Tools
|
||||
tools: Optional[List[Any]] = None,
|
||||
toolsets: Optional[List[str]] = None, # Named toolset groups to resolve
|
||||
allow_delegation: bool = False, # Deprecated: use handoffs= instead
|
||||
allow_code_execution: Optional[bool] = False, # Deprecated: use execution=ExecutionConfig(code_execution=True)
|
||||
code_execution_mode: Literal["safe", "unsafe"] = "safe", # Deprecated: use execution=ExecutionConfig(code_mode="safe")
|
||||
@@ -550,13 +586,14 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
hooks: Optional[Union[List[Any], Dict[str, Any], 'HooksConfig']] = None,
|
||||
skills: Optional[Union[List[str], str, Dict[str, Any], 'SkillsConfig']] = None,
|
||||
approval: Optional[Union[bool, str, Dict[str, Any], 'ApprovalConfig', 'ApprovalProtocol']] = None,
|
||||
tool_timeout: Optional[int] = None, # P8/G11: Timeout in seconds for each tool call
|
||||
parallel_tool_calls: bool = False, # Gap 2: Enable parallel execution of batched LLM tool calls
|
||||
tool_config: Optional[Union[bool, 'ToolConfig']] = None, # Tool execution configuration (timeout, retry, parallel)
|
||||
learn: Optional[Union[bool, str, Dict[str, Any], 'LearnConfig']] = None, # Continuous learning (peer to memory)
|
||||
backend: Optional[Any] = None, # External managed agent backend (e.g., ManagedAgentIntegration)
|
||||
cli_backend: Optional[Union[str, Any]] = None, # CLI backend for delegating turns (e.g., "claude-code")
|
||||
cli_backend: Optional[Union[str, Any]] = None, # CLI backend for delegating turns (e.g., "claude-code") - DEPRECATED
|
||||
runtime: Optional[Union[str, Dict[str, Any], 'AgentRuntimeConfig']] = None, # Model-scoped runtime configuration
|
||||
interrupt_controller: Optional['InterruptController'] = None, # G2: Cooperative cancellation
|
||||
tool_search: Optional[Union[bool, str, Dict[str, Any], 'ToolSearchConfig']] = False, # Progressive tool disclosure
|
||||
message_steering: Optional[Union[bool, 'MessageSteeringProtocol']] = False, # Real-time message steering during execution
|
||||
sandbox: Optional[Union[bool, 'SandboxConfig']] = None, # Sandbox for safe code execution
|
||||
):
|
||||
"""Initialize an Agent instance.
|
||||
@@ -567,9 +604,10 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
goal: Primary objective the agent aims to achieve.
|
||||
backstory: Background context shaping personality and decisions.
|
||||
instructions: Direct instructions (overrides role/goal/backstory). Recommended for simple agents.
|
||||
llm: Model name string ("gpt-4o", "anthropic/claude-3-sonnet") or LLM object.
|
||||
llm: Model name string ("gpt-4o", "anthropic/claude-3-sonnet"), LLMConfig object, or custom LLM.
|
||||
Can accept LLMConfig(model="gpt-4o", fallback_models=["claude-3-5-sonnet", "gpt-4o-mini"]).
|
||||
Defaults to OPENAI_MODEL_NAME env var or "gpt-4o-mini".
|
||||
model: Alias for llm parameter.
|
||||
model: Alias for llm parameter. Also accepts LLMConfig objects.
|
||||
base_url: Custom LLM endpoint URL (e.g., for Ollama). Kept separate for auth.
|
||||
api_key: API key for LLM provider. Kept separate for auth.
|
||||
tools: List of tools, functions, callables, or MCP instances.
|
||||
@@ -645,22 +683,24 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
- LearnConfig: Custom configuration
|
||||
Learning is a first-class citizen, peer to memory. It captures patterns,
|
||||
preferences, and insights from interactions to improve future responses.
|
||||
parallel_tool_calls: Enable parallel execution of batched LLM tool calls (default False).
|
||||
When True and LLM returns multiple tool calls in a single response, they execute
|
||||
concurrently instead of sequentially. Provides ~3x speedup for I/O-bound tools.
|
||||
Maintains backward compatibility with False default.
|
||||
tool_config: Tool execution configuration. Accepts:
|
||||
- bool: True enables defaults, False disables
|
||||
- ToolConfig: Custom configuration for timeout, retry policy, parallel execution
|
||||
Consolidates tool timeout, retry policy, and parallel execution settings.
|
||||
backend: External managed agent backend for hybrid execution. Accepts:
|
||||
- ManagedAgentIntegration: External managed agent service
|
||||
- None: Use local execution (default)
|
||||
When provided, agent can delegate execution to managed infrastructure
|
||||
for long-running tasks or when local resources are constrained.
|
||||
cli_backend: CLI backend for delegating full turns to external CLI tools. Accepts:
|
||||
- str: Backend ID ("claude-code", "codex-cli", "gemini-cli")
|
||||
- CliBackendProtocol: Custom CLI backend instance
|
||||
- None: Use standard LLM execution (default)
|
||||
When provided, agent delegates entire conversation turns to the CLI tool
|
||||
instead of using the built-in LLM. Enables session continuity and
|
||||
tool integration through external AI coding assistants.
|
||||
cli_backend: **DEPRECATED** CLI backend for delegating full turns. Use 'runtime' parameter instead.
|
||||
This parameter is deprecated in favor of model-scoped runtime configuration.
|
||||
Will emit deprecation warning when used.
|
||||
runtime: Model-scoped runtime configuration for turn delegation. Accepts:
|
||||
- str: Runtime ID ("claude-code", "praisonai")
|
||||
- Dict[str, Any]: Runtime config {"runtime": "claude-code", "config_overrides": {...}}
|
||||
- AgentRuntimeConfig: Pre-configured runtime instance
|
||||
- None: Use model-based resolution (default)
|
||||
Enables turn delegation with per-model runtime selection and fail-closed behavior.
|
||||
tool_search: Progressive tool disclosure configuration. Accepts:
|
||||
- bool: False=disabled (default), True=auto mode
|
||||
- str: Mode ("auto", "on", "off")
|
||||
@@ -688,6 +728,7 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
- auto_save → memory=MemoryConfig(auto_save=)
|
||||
- rate_limiter → execution=ExecutionConfig(rate_limiter=)
|
||||
- verification_hooks → autonomy=AutonomyConfig(verification_hooks=)
|
||||
- Use tool_config=ToolConfig(...) for tool execution configuration
|
||||
"""
|
||||
# Add check at start if memory is requested
|
||||
if memory is not None:
|
||||
@@ -714,16 +755,16 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
# Precedence: Explicit params > Config file > Built-in defaults
|
||||
# Only applies when param is None (not explicitly set)
|
||||
# ============================================================
|
||||
from ..config.loader import apply_config_defaults, get_default
|
||||
from ..config.loader import apply_config_defaults, get_default, _defaults_has_any_values
|
||||
|
||||
# Apply config defaults for LLM if not explicitly set
|
||||
if llm is None and model is None:
|
||||
if llm is None and model is None and _defaults_has_any_values():
|
||||
config_model = get_default("model")
|
||||
if config_model:
|
||||
llm = config_model
|
||||
|
||||
# Apply config defaults for base_url if not explicitly set
|
||||
if base_url is None:
|
||||
if base_url is None and _defaults_has_any_values():
|
||||
config_base_url = get_default("base_url")
|
||||
if config_base_url:
|
||||
base_url = config_base_url
|
||||
@@ -798,8 +839,7 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
alternative="use 'execution=ExecutionConfig(rate_limiter=obj)' instead",
|
||||
stacklevel=3
|
||||
)
|
||||
# Note: parallel_tool_calls is NOT deprecated - it's a new Gap 2 feature
|
||||
# Both direct parameter and ExecutionConfig.parallel_tool_calls are supported
|
||||
# Note: parallel_tool_calls moved to tool_config pattern
|
||||
if verification_hooks is not None:
|
||||
warn_deprecated_param(
|
||||
"verification_hooks",
|
||||
@@ -975,8 +1015,8 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
allow_code_execution = True
|
||||
if _exec_config.code_mode != "safe":
|
||||
code_execution_mode = _exec_config.code_mode
|
||||
# Get parallel_tool_calls from ExecutionConfig, fall back to parameter
|
||||
parallel_tool_calls = getattr(_exec_config, 'parallel_tool_calls', parallel_tool_calls)
|
||||
# Get parallel_tool_calls from ExecutionConfig
|
||||
parallel_tool_calls = getattr(_exec_config, 'parallel_tool_calls', False)
|
||||
# Budget guard extraction
|
||||
_max_budget = getattr(_exec_config, 'max_budget', None)
|
||||
_on_budget_exceeded = getattr(_exec_config, 'on_budget_exceeded', 'stop') or 'stop'
|
||||
@@ -984,8 +1024,9 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
max_iter, max_rpm, max_execution_time, max_retry_limit = 20, None, None, 2
|
||||
_max_budget = None
|
||||
_on_budget_exceeded = 'stop'
|
||||
# Keep parallel_tool_calls parameter value when no ExecutionConfig provided
|
||||
# (already set from parameter, no need to override)
|
||||
# Default to False when no ExecutionConfig provided
|
||||
parallel_tool_calls = False
|
||||
_exec_config = ExecutionConfig() # Default config for non-execution case
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve TEMPLATES param - FAST PATH
|
||||
@@ -1388,7 +1429,7 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
# String could be LLM prompt - passthrough for later processing
|
||||
_guardrails_config = guardrails
|
||||
else:
|
||||
from .._resolver_helpers import resolve_guardrails as _resolve_guardrails
|
||||
from ..config.param_resolver import resolve_guardrails as _resolve_guardrails
|
||||
_guardrails_config = _resolve_guardrails(
|
||||
value=guardrails,
|
||||
config_class=GuardrailConfig,
|
||||
@@ -1473,6 +1514,9 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
|
||||
# Initialize autonomy features (agent-centric escalation/doom-loop)
|
||||
self._init_autonomy(autonomy, verification_hooks=verification_hooks)
|
||||
|
||||
# Initialize loop guard lazily on first tool execution (zero init overhead)
|
||||
self._loop_guard = None
|
||||
|
||||
# If instructions are provided, use them to set role, goal, and backstory
|
||||
if instructions:
|
||||
@@ -1506,12 +1550,20 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
self.self_reflect = True if self_reflect is None else self_reflect
|
||||
|
||||
self.instructions = instructions
|
||||
|
||||
# Resolve tool_config for tool execution settings
|
||||
_tool_config = Agent._resolve_tool_config(
|
||||
tool_config=tool_config
|
||||
)
|
||||
|
||||
# Gap 2: Store parallel tool calls setting for ToolCallExecutor selection
|
||||
self.parallel_tool_calls = parallel_tool_calls
|
||||
self.parallel_tool_calls = _tool_config.parallel if _tool_config else parallel_tool_calls
|
||||
# G2: Store interrupt controller for cooperative cancellation
|
||||
self.interrupt_controller = interrupt_controller
|
||||
# Check for model name in environment variable if not provided
|
||||
self._using_custom_llm = False
|
||||
self._llm_instance = None
|
||||
self._llm_init_params = None
|
||||
# Flag to track if final result has been displayed to prevent duplicates
|
||||
self._final_display_shown = False
|
||||
|
||||
@@ -1526,7 +1578,22 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
|
||||
# Handle llm= deprecation: model= is the preferred parameter name
|
||||
# llm= still works but shows deprecation warning
|
||||
if llm is not None and model is None:
|
||||
fallback_models = None # Initialize for internal use
|
||||
|
||||
# Check if llm is an LLMConfig object
|
||||
from ..config import LLMConfig
|
||||
if isinstance(llm, LLMConfig):
|
||||
# Extract values from LLMConfig
|
||||
llm_config_obj = llm
|
||||
llm = llm_config_obj.model
|
||||
fallback_models = llm_config_obj.fallback_models
|
||||
if base_url is None:
|
||||
base_url = llm_config_obj.base_url
|
||||
if api_key is None:
|
||||
api_key = llm_config_obj.api_key
|
||||
if auth is None:
|
||||
auth = llm_config_obj.auth
|
||||
elif llm is not None and model is None:
|
||||
warn_deprecated_param(
|
||||
"llm",
|
||||
since="1.0.0",
|
||||
@@ -1534,9 +1601,23 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
alternative="use 'model' instead. Example: Agent(model='gpt-4o-mini')",
|
||||
stacklevel=3
|
||||
)
|
||||
|
||||
# model= is the preferred parameter (no warning)
|
||||
if model is not None:
|
||||
llm = model # model= takes precedence
|
||||
# Check if model is an LLMConfig object
|
||||
if isinstance(model, LLMConfig):
|
||||
# Extract values from LLMConfig (same as llm handling)
|
||||
llm = model.model
|
||||
if fallback_models is None:
|
||||
fallback_models = model.fallback_models
|
||||
if base_url is None:
|
||||
base_url = model.base_url
|
||||
if api_key is None:
|
||||
api_key = model.api_key
|
||||
if auth is None:
|
||||
auth = model.auth
|
||||
else:
|
||||
llm = model # model= takes precedence
|
||||
|
||||
# Store rate limiter (optional, zero overhead when None)
|
||||
self._rate_limiter = rate_limiter
|
||||
@@ -1550,98 +1631,74 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
|
||||
# If base_url is provided, always create a custom LLM instance
|
||||
# If base_url is provided, defer custom LLM instance creation
|
||||
if base_url:
|
||||
try:
|
||||
from ..llm.llm import LLM
|
||||
# Handle different llm parameter types with base_url
|
||||
if isinstance(llm, dict):
|
||||
# Merge base_url and api_key into the dict
|
||||
llm_config = llm.copy()
|
||||
llm_config['base_url'] = base_url
|
||||
if api_key:
|
||||
llm_config['api_key'] = api_key
|
||||
if auth:
|
||||
llm_config['auth'] = auth
|
||||
llm_config['metrics'] = metrics
|
||||
self.llm_instance = LLM(**llm_config)
|
||||
self.llm = llm.get('model', Agent._get_default_model())
|
||||
else:
|
||||
# Create LLM with model string and base_url (cached for performance)
|
||||
model_name = llm or Agent._get_default_model()
|
||||
self.llm_instance = LLM(
|
||||
model=model_name,
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
auth=auth,
|
||||
metrics=metrics,
|
||||
web_search=web_search,
|
||||
web_fetch=web_fetch,
|
||||
prompt_caching=prompt_caching,
|
||||
claude_memory=claude_memory
|
||||
)
|
||||
self.llm = model_name
|
||||
self._using_custom_llm = True
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"LLM features requested but dependencies not installed. "
|
||||
"Please install with: pip install \"praisonaiagents[llm]\""
|
||||
) from e
|
||||
if isinstance(llm, dict):
|
||||
llm_config = llm.copy()
|
||||
llm_config['base_url'] = base_url
|
||||
if api_key:
|
||||
llm_config['api_key'] = api_key
|
||||
if auth:
|
||||
llm_config['auth'] = auth
|
||||
llm_config['metrics'] = metrics
|
||||
llm_config['max_iter'] = max_iter
|
||||
self._llm_init_params = llm_config
|
||||
self.llm = llm.get('model', Agent._get_default_model())
|
||||
else:
|
||||
model_name = llm or Agent._get_default_model()
|
||||
self._llm_init_params = {
|
||||
'model': model_name,
|
||||
'base_url': base_url,
|
||||
'api_key': api_key,
|
||||
'auth': auth,
|
||||
'metrics': metrics,
|
||||
'max_iter': max_iter,
|
||||
'web_search': web_search,
|
||||
'web_fetch': web_fetch,
|
||||
'prompt_caching': prompt_caching,
|
||||
'claude_memory': claude_memory,
|
||||
}
|
||||
self.llm = model_name
|
||||
self._using_custom_llm = True
|
||||
# If the user passes a dictionary (for advanced configuration)
|
||||
elif isinstance(llm, dict) and "model" in llm:
|
||||
try:
|
||||
from ..llm.llm import LLM
|
||||
# Add api_key if provided and not in dict
|
||||
if api_key and 'api_key' not in llm:
|
||||
llm = llm.copy()
|
||||
llm['api_key'] = api_key
|
||||
# Add auth if provided and not in dict
|
||||
if auth and 'auth' not in llm:
|
||||
llm = llm.copy()
|
||||
llm['auth'] = auth
|
||||
# Add metrics parameter
|
||||
llm = llm.copy()
|
||||
llm['metrics'] = metrics
|
||||
self.llm_instance = LLM(**llm) # Pass all dict items as kwargs
|
||||
self._using_custom_llm = True
|
||||
self.llm = llm.get('model', Agent._get_default_model())
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"LLM features requested but dependencies not installed. "
|
||||
"Please install with: pip install \"praisonaiagents[llm]\""
|
||||
) from e
|
||||
# If the user passes a string with a slash (provider/model)
|
||||
llm_config = llm.copy()
|
||||
if api_key and 'api_key' not in llm_config:
|
||||
llm_config['api_key'] = api_key
|
||||
if auth and 'auth' not in llm_config:
|
||||
llm_config['auth'] = auth
|
||||
llm_config['metrics'] = metrics
|
||||
llm_config['max_iter'] = max_iter
|
||||
self._llm_init_params = llm_config
|
||||
self._using_custom_llm = True
|
||||
self.llm = llm_config.get('model', Agent._get_default_model())
|
||||
# If the user passes a string with a slash (provider/model) — defer LiteLLM import
|
||||
elif isinstance(llm, str) and "/" in llm:
|
||||
try:
|
||||
from ..llm.llm import LLM
|
||||
# Pass the entire string so LiteLLM can parse provider/model
|
||||
llm_params = {'model': llm}
|
||||
if api_key:
|
||||
llm_params['api_key'] = api_key
|
||||
if auth:
|
||||
llm_params['auth'] = auth
|
||||
llm_params['metrics'] = metrics
|
||||
llm_params['web_search'] = web_search
|
||||
llm_params['web_fetch'] = web_fetch
|
||||
llm_params['prompt_caching'] = prompt_caching
|
||||
llm_params['claude_memory'] = claude_memory
|
||||
self.llm_instance = LLM(**llm_params)
|
||||
self._using_custom_llm = True
|
||||
self.llm = llm
|
||||
|
||||
# Ensure tools are properly accessible when using custom LLM
|
||||
if tools:
|
||||
logging.debug(f"Tools passed to Agent with custom LLM: {tools}")
|
||||
# Store the tools for later use
|
||||
self.tools = tools
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"LLM features requested but dependencies not installed. "
|
||||
"Please install with: pip install \"praisonaiagents[llm]\""
|
||||
) from e
|
||||
llm_params = {'model': llm}
|
||||
if api_key:
|
||||
llm_params['api_key'] = api_key
|
||||
if auth:
|
||||
llm_params['auth'] = auth
|
||||
llm_params['metrics'] = metrics
|
||||
llm_params['web_search'] = web_search
|
||||
llm_params['web_fetch'] = web_fetch
|
||||
llm_params['prompt_caching'] = prompt_caching
|
||||
llm_params['claude_memory'] = claude_memory
|
||||
llm_params['max_iter'] = max_iter
|
||||
self._llm_init_params = llm_params
|
||||
self._using_custom_llm = True
|
||||
self.llm = llm
|
||||
|
||||
if tools:
|
||||
logging.debug(f"Tools passed to Agent with custom LLM: {tools}")
|
||||
self.tools = tools
|
||||
# Otherwise, fall back to OpenAI environment/name (cached for performance)
|
||||
else:
|
||||
self.llm = llm or Agent._get_default_model()
|
||||
|
||||
# Store fallback models for resilience (defensive copy to avoid external mutations)
|
||||
self.fallback_models = list(fallback_models) if fallback_models else []
|
||||
|
||||
# Handle tools parameter - ensure it's always a list
|
||||
if callable(tools):
|
||||
# If a single function/callable is passed, wrap it in a list
|
||||
@@ -1658,6 +1715,32 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
else:
|
||||
self.tools = tools or []
|
||||
|
||||
# Handle toolsets parameter - resolve named toolset groups
|
||||
if toolsets:
|
||||
try:
|
||||
from ..toolsets import resolve_toolsets
|
||||
toolset_tool_names = resolve_toolsets(toolsets)
|
||||
# Remove duplicates with existing tools
|
||||
existing_tool_names = set()
|
||||
for tool in self.tools:
|
||||
name = getattr(tool, 'name', getattr(tool, '__name__', str(tool)))
|
||||
existing_tool_names.add(name)
|
||||
|
||||
unique_tool_names = [name for name in toolset_tool_names if name not in existing_tool_names]
|
||||
toolset_tools = self._resolve_tool_names(unique_tool_names)
|
||||
self.tools.extend(toolset_tools)
|
||||
logging.debug(f"Resolved toolsets {toolsets} to {len(toolset_tools)} tools: {[getattr(t, '__name__', str(t)) for t in toolset_tools]}")
|
||||
except (ValueError, KeyError) as e:
|
||||
raise ValueError(
|
||||
f"Agent '{getattr(self, 'display_name', 'unknown')}' failed to resolve toolsets {toolsets}: {e}. "
|
||||
"Verify names with praisonaiagents.toolsets.list_toolsets()."
|
||||
) from e
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
f"Agent '{getattr(self, 'display_name', 'unknown')}' failed to import toolsets module: {e}. "
|
||||
"Ensure praisonaiagents is properly installed."
|
||||
) from e
|
||||
|
||||
# Inject default tools for autonomy mode (after self.tools is initialized)
|
||||
# ONLY inject if caller didn't provide tools - avoid duplicates with CLI/wrapper tools
|
||||
if self.autonomy_enabled and hasattr(self, '_autonomy_config_obj'):
|
||||
@@ -1693,6 +1776,7 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
self.max_execution_time = max_execution_time
|
||||
self._memory_instance = None
|
||||
self._init_memory(memory, user_id)
|
||||
self._init_message_steering(message_steering)
|
||||
self.verbose = verbose
|
||||
self._has_explicit_output_config = _has_explicit_output # Track if user set output mode
|
||||
self.tool_output_limit = tool_output_limit # Configurable tool output limit
|
||||
@@ -1714,9 +1798,14 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
|
||||
self.allow_code_execution = allow_code_execution
|
||||
self.max_retry_limit = max_retry_limit
|
||||
self.code_execution_mode = code_execution_mode
|
||||
# Store execution config for guardrail retry backoff
|
||||
self._execution_config = _exec_config
|
||||
self.embedder_config = embedder_config
|
||||
self.knowledge = knowledge
|
||||
self.use_system_prompt = use_system_prompt
|
||||
|
||||
# Store ExecutionConfig for context compaction policy access
|
||||
self.execution = _exec_config
|
||||
# Async-safe chat_history with dual-lock protection
|
||||
self.__chat_history_state = AsyncSafeState([])
|
||||
|
||||
@@ -1806,20 +1895,24 @@ Your Goal: {self.goal}
|
||||
self._approval_backend = None
|
||||
self._approve_all_tools = False
|
||||
self._approval_timeout = 0
|
||||
self._approval_permissions = None
|
||||
else:
|
||||
# Unknown string — treat as no approval
|
||||
self._approval_backend = None
|
||||
self._approve_all_tools = False
|
||||
self._approval_timeout = 0
|
||||
self._approval_permissions = None
|
||||
elif approval is True:
|
||||
from ..approval.backends import AutoApproveBackend
|
||||
self._approval_backend = AutoApproveBackend()
|
||||
self._approve_all_tools = False
|
||||
self._approval_timeout = 0 # 0 = use backend default
|
||||
self._approval_permissions = None
|
||||
elif approval is False or approval is None:
|
||||
self._approval_backend = None
|
||||
self._approve_all_tools = False
|
||||
self._approval_timeout = 0
|
||||
self._approval_permissions = None
|
||||
# No explicit approval kwarg — honour PRAISONAI_TOOL_SAFETY.
|
||||
# Default preset "default" blocks only destructive ops
|
||||
# (delete_*, execute_command, execute_code, kill_process, move/copy)
|
||||
@@ -1849,17 +1942,21 @@ Your Goal: {self.goal}
|
||||
self._approval_backend = approval.backend
|
||||
self._approve_all_tools = approval.all_tools
|
||||
self._approval_timeout = approval.timeout # None = indefinite, 0 = backend default
|
||||
# Store permissions if provided (for CI-safe declarative policies)
|
||||
self._approval_permissions = getattr(approval, 'permissions', None)
|
||||
elif isinstance(approval, dict):
|
||||
# Dict config: convert to ApprovalConfig
|
||||
approval_config = ApprovalConfig(**approval)
|
||||
self._approval_backend = approval_config.backend
|
||||
self._approve_all_tools = approval_config.all_tools
|
||||
self._approval_timeout = approval_config.timeout
|
||||
self._approval_permissions = getattr(approval_config, 'permissions', None)
|
||||
else:
|
||||
# Plain backend object — dangerous tools only, backend default timeout
|
||||
self._approval_backend = approval
|
||||
self._approve_all_tools = False
|
||||
self._approval_timeout = 0
|
||||
self._approval_permissions = None
|
||||
|
||||
# Per-agent autonomy→approval bridge (G-BRIDGE-1 fix)
|
||||
# If autonomy level is full_auto and no explicit approval was set,
|
||||
@@ -1872,8 +1969,14 @@ Your Goal: {self.goal}
|
||||
self._pending_approvals = {}
|
||||
self._approvals_lock = asyncio.Lock()
|
||||
|
||||
# Store the resolved tool_config for cloning (resolved earlier in __init__)
|
||||
self._tool_config = _tool_config
|
||||
|
||||
# P8/G11: Tool timeout - prevent slow tools from blocking
|
||||
self._tool_timeout = tool_timeout
|
||||
self._tool_timeout = _tool_config.timeout if _tool_config else None
|
||||
|
||||
# Store tool retry policy for tool execution with exponential backoff
|
||||
self._tool_retry_policy = _tool_config.retry_policy if _tool_config else None
|
||||
|
||||
# Cache for system prompts and formatted tools with eager thread-safe lock
|
||||
# Use OrderedDict for LRU behavior
|
||||
@@ -1977,10 +2080,26 @@ Your Goal: {self.goal}
|
||||
# Backend - external managed agent backend for hybrid execution
|
||||
self.backend = backend
|
||||
|
||||
# CLI Backend - external CLI backend for delegating full turns
|
||||
# CLI Backend - external CLI backend for delegating full turns (DEPRECATED)
|
||||
self._cli_backend = None
|
||||
if cli_backend is not None:
|
||||
from ..utils.deprecation import warn_deprecated_param
|
||||
warn_deprecated_param(
|
||||
"cli_backend",
|
||||
since="1.0.0",
|
||||
removal="2.0.0",
|
||||
alternative="use 'runtime=' instead for model-scoped runtime configuration. For migration assistance, run: praisonai doctor fix --execute",
|
||||
stacklevel=3
|
||||
)
|
||||
self._cli_backend = self._resolve_cli_backend(cli_backend)
|
||||
|
||||
# Runtime Configuration - model-scoped runtime selection
|
||||
self._runtime_config = None
|
||||
if runtime is not None:
|
||||
self._runtime_config = self._resolve_runtime_config(runtime)
|
||||
|
||||
# Runtime resolver for per-turn resolution
|
||||
self._runtime_resolver = None # Will be initialized on first use
|
||||
|
||||
# Telemetry - lazy initialized via property for performance
|
||||
self.__telemetry = None
|
||||
@@ -1989,6 +2108,19 @@ Your Goal: {self.goal}
|
||||
# Sandbox configuration - initialize SandboxMixin
|
||||
super().__init__(sandbox=sandbox)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_tool_config(tool_config):
|
||||
"""Resolve tool_config parameter with backward compatibility."""
|
||||
# Import here to avoid circular imports
|
||||
from ..config.feature_configs import ToolConfig, resolve_tools
|
||||
|
||||
# Handle the new consolidated parameter
|
||||
if tool_config is not None:
|
||||
resolved_config = resolve_tools(tool_config)
|
||||
return resolved_config
|
||||
|
||||
return None
|
||||
|
||||
def __deepcopy__(self, memo: dict) -> "Agent":
|
||||
"""Custom deepcopy that creates fresh threading primitives.
|
||||
|
||||
@@ -2035,6 +2167,7 @@ Your Goal: {self.goal}
|
||||
|
||||
# LLM configuration
|
||||
'llm': self.llm,
|
||||
'fallback_models': list(self.fallback_models) if hasattr(self, 'fallback_models') and self.fallback_models else None,
|
||||
'base_url': getattr(self, 'base_url', None),
|
||||
'api_key': getattr(self, 'api_key', None),
|
||||
'auth': getattr(self, 'auth', None),
|
||||
@@ -2065,9 +2198,8 @@ Your Goal: {self.goal}
|
||||
'learn': getattr(self, '_learn_config', None),
|
||||
'tool_search': getattr(self, '_tool_search_config', None),
|
||||
|
||||
# Tool configuration
|
||||
'tool_timeout': getattr(self, '_tool_timeout', None),
|
||||
'parallel_tool_calls': getattr(self, 'parallel_tool_calls', False),
|
||||
# Tool configuration - use consolidated config when available
|
||||
'tool_config': getattr(self, '_tool_config', None),
|
||||
|
||||
# CLI backend
|
||||
'cli_backend': getattr(self, '_cli_backend', None),
|
||||
@@ -4245,33 +4377,95 @@ Summary:"""
|
||||
|
||||
return ""
|
||||
|
||||
def store_memory(self, content: str, memory_type: str = "short_term", **kwargs: Any) -> None:
|
||||
def store_memory(self, content: str, memory_type: str = "short_term", action: str = "add", **kwargs: Any) -> None:
|
||||
"""
|
||||
Store content in memory.
|
||||
|
||||
Args:
|
||||
content: Content to store
|
||||
memory_type: Type of memory (short_term, long_term, entity, episodic)
|
||||
action: Type of operation ("add", "replace", "remove")
|
||||
**kwargs: Additional arguments for the memory method
|
||||
"""
|
||||
if not self._memory_instance:
|
||||
return
|
||||
|
||||
# Use protocol names first (store_*), fallback to legacy names (add_*)
|
||||
if memory_type == "short_term":
|
||||
if hasattr(self._memory_instance, 'store_short_term'):
|
||||
self._memory_instance.store_short_term(content, **kwargs)
|
||||
elif hasattr(self._memory_instance, 'add_short_term'):
|
||||
self._memory_instance.add_short_term(content, **kwargs)
|
||||
elif memory_type == "long_term":
|
||||
if hasattr(self._memory_instance, 'store_long_term'):
|
||||
self._memory_instance.store_long_term(content, **kwargs)
|
||||
elif hasattr(self._memory_instance, 'add_long_term'):
|
||||
self._memory_instance.add_long_term(content, **kwargs)
|
||||
elif memory_type == "entity" and hasattr(self._memory_instance, 'add_entity'):
|
||||
self._memory_instance.add_entity(content, **kwargs)
|
||||
elif memory_type == "episodic" and hasattr(self._memory_instance, 'add_episodic'):
|
||||
self._memory_instance.add_episodic(content, **kwargs)
|
||||
# Validate memory_type
|
||||
supported_types = ["short_term", "long_term", "entity", "episodic"]
|
||||
if memory_type not in supported_types:
|
||||
raise ValueError(f"Unsupported memory_type '{memory_type}'. Supported: {supported_types}")
|
||||
|
||||
# Perform the actual storage operation first
|
||||
storage_success = False
|
||||
try:
|
||||
if action == "add":
|
||||
# Use protocol names first (store_*), fallback to legacy names (add_*)
|
||||
if memory_type == "short_term":
|
||||
if hasattr(self._memory_instance, 'store_short_term'):
|
||||
self._memory_instance.store_short_term(content, **kwargs)
|
||||
storage_success = True
|
||||
elif hasattr(self._memory_instance, 'add_short_term'):
|
||||
self._memory_instance.add_short_term(content, **kwargs)
|
||||
storage_success = True
|
||||
elif memory_type == "long_term":
|
||||
if hasattr(self._memory_instance, 'store_long_term'):
|
||||
self._memory_instance.store_long_term(content, **kwargs)
|
||||
storage_success = True
|
||||
elif hasattr(self._memory_instance, 'add_long_term'):
|
||||
self._memory_instance.add_long_term(content, **kwargs)
|
||||
storage_success = True
|
||||
elif memory_type == "entity" and hasattr(self._memory_instance, 'add_entity'):
|
||||
self._memory_instance.add_entity(content, **kwargs)
|
||||
storage_success = True
|
||||
elif memory_type == "episodic" and hasattr(self._memory_instance, 'add_episodic'):
|
||||
self._memory_instance.add_episodic(content, **kwargs)
|
||||
storage_success = True
|
||||
elif action == "replace":
|
||||
# For replace operations - look for replace_ or update_ methods
|
||||
method_name = f"replace_{memory_type}" if hasattr(self._memory_instance, f'replace_{memory_type}') else f"update_{memory_type}"
|
||||
if hasattr(self._memory_instance, method_name):
|
||||
getattr(self._memory_instance, method_name)(content, **kwargs)
|
||||
storage_success = True
|
||||
else:
|
||||
raise ValueError(f"Memory provider does not support 'replace' for {memory_type}")
|
||||
elif action == "remove":
|
||||
# For remove operations - look for remove_ or delete_ methods
|
||||
method_name = f"remove_{memory_type}" if hasattr(self._memory_instance, f'remove_{memory_type}') else f"delete_{memory_type}"
|
||||
if hasattr(self._memory_instance, method_name):
|
||||
getattr(self._memory_instance, method_name)(content, **kwargs)
|
||||
storage_success = True
|
||||
else:
|
||||
raise ValueError(f"Memory provider does not support 'remove' for {memory_type}")
|
||||
else:
|
||||
raise ValueError(f"Unsupported action '{action}'. Supported: add, replace, remove")
|
||||
|
||||
if not storage_success:
|
||||
raise ValueError(f"Memory provider does not support {memory_type} storage")
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"[{self.name}] Memory storage failed: {e}")
|
||||
raise
|
||||
|
||||
# Only call on_memory_write hook AFTER successful storage
|
||||
if storage_success:
|
||||
try:
|
||||
metadata = kwargs.get('metadata', None)
|
||||
# Try async version first if in async context, fallback to sync
|
||||
import asyncio
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
# In async context - prefer async hook if available
|
||||
if hasattr(self._memory_instance, 'aon_memory_write'):
|
||||
# Schedule async hook without blocking (fire and forget)
|
||||
asyncio.create_task(self._memory_instance.aon_memory_write(action, memory_type, content, metadata))
|
||||
elif hasattr(self._memory_instance, 'on_memory_write'):
|
||||
self._memory_instance.on_memory_write(action, memory_type, content, metadata)
|
||||
except RuntimeError:
|
||||
# Not in async context - use sync hook
|
||||
if hasattr(self._memory_instance, 'on_memory_write'):
|
||||
self._memory_instance.on_memory_write(action, memory_type, content, metadata)
|
||||
except Exception as e:
|
||||
logging.warning(f"[{self.name}] Memory on_memory_write hook failed: {e}")
|
||||
|
||||
def _display_memory_info(self):
|
||||
"""Display memory information to user in a friendly format."""
|
||||
@@ -4825,6 +5019,22 @@ Answer:"""
|
||||
retry_count += 1
|
||||
logging.warning(f"Agent {self.name}: Guardrail validation failed (retry {retry_count}/{self.max_guardrail_retries}): {error}")
|
||||
|
||||
# Add exponential backoff delay to avoid hammering the LLM
|
||||
execution_config = getattr(self, '_execution_config', None)
|
||||
if execution_config is not None:
|
||||
total_delay = BackoffPolicy.delay(
|
||||
retry_count,
|
||||
execution_config.retry_initial_delay,
|
||||
execution_config.retry_backoff_factor,
|
||||
execution_config.retry_jitter
|
||||
)
|
||||
else:
|
||||
# Fall back to simple backoff if no execution config
|
||||
total_delay = 1.0 * (2.0 ** (retry_count - 1))
|
||||
|
||||
logging.info(f"Agent {self.name}: Waiting {total_delay:.2f}s before guardrail retry")
|
||||
time.sleep(total_delay)
|
||||
|
||||
# Regenerate response for retry
|
||||
try:
|
||||
retry_prompt = f"{prompt}\n\nNote: Previous response failed validation due to: {error}. Please provide an improved response."
|
||||
@@ -4862,6 +5072,22 @@ Answer:"""
|
||||
retry_count += 1
|
||||
logging.warning(f"Agent {self.name}: Guardrail validation failed (retry {retry_count}/{self.max_guardrail_retries}): {error}")
|
||||
|
||||
# Add exponential backoff delay to avoid hammering the LLM
|
||||
execution_config = getattr(self, '_execution_config', None)
|
||||
if execution_config is not None:
|
||||
total_delay = BackoffPolicy.delay(
|
||||
retry_count,
|
||||
execution_config.retry_initial_delay,
|
||||
execution_config.retry_backoff_factor,
|
||||
execution_config.retry_jitter
|
||||
)
|
||||
else:
|
||||
# Fall back to simple backoff if no execution config
|
||||
total_delay = 1.0 * (2.0 ** (retry_count - 1))
|
||||
|
||||
logging.info(f"Agent {self.name}: Waiting {total_delay:.2f}s before guardrail retry")
|
||||
await asyncio.sleep(total_delay)
|
||||
|
||||
# Regenerate response for retry (async version)
|
||||
try:
|
||||
retry_prompt = f"{prompt}\n\nNote: Previous response failed validation due to: {error}. Please provide an improved response."
|
||||
@@ -4950,17 +5176,163 @@ Answer:"""
|
||||
|
||||
raise TypeError(f"Invalid cli_backend type: {type(cli_backend)}. Expected CliBackendProtocol instance or factory callable.")
|
||||
|
||||
async def _chat_via_cli_backend(self, prompt: str, **kwargs) -> Optional[str]:
|
||||
def _resolve_runtime_config(self, runtime):
|
||||
"""Resolve runtime parameter to AgentRuntimeConfig instance.
|
||||
|
||||
Args:
|
||||
runtime: Runtime configuration parameter
|
||||
|
||||
Returns:
|
||||
AgentRuntimeConfig instance
|
||||
|
||||
Raises:
|
||||
TypeError: If runtime type is invalid
|
||||
ValueError: If runtime configuration is invalid
|
||||
"""
|
||||
try:
|
||||
from ..runtime import AgentRuntimeConfig
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Runtime configuration features requested but not available. "
|
||||
"This should not happen in a properly installed praisonaiagents package."
|
||||
)
|
||||
|
||||
# If already an AgentRuntimeConfig instance, return as-is
|
||||
if isinstance(runtime, AgentRuntimeConfig):
|
||||
return runtime
|
||||
|
||||
# If string, create config with runtime ID
|
||||
if isinstance(runtime, str):
|
||||
return AgentRuntimeConfig.from_runtime_id(runtime)
|
||||
|
||||
# If dictionary, create config from dict
|
||||
if isinstance(runtime, dict):
|
||||
return AgentRuntimeConfig.from_dict(runtime)
|
||||
|
||||
raise TypeError(f"Invalid runtime type: {type(runtime)}. Expected str, dict, or AgentRuntimeConfig instance.")
|
||||
|
||||
def _get_runtime_resolver(self):
|
||||
"""Get or create runtime resolver instance (lazy initialization)."""
|
||||
if self._runtime_resolver is None:
|
||||
try:
|
||||
from ..runtime.resolver import RuntimeResolver
|
||||
self._runtime_resolver = RuntimeResolver()
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Runtime resolution features requested but not available. "
|
||||
"This should not happen in a properly installed praisonaiagents package."
|
||||
)
|
||||
return self._runtime_resolver
|
||||
|
||||
async def _resolve_turn_runtime(self):
|
||||
"""Resolve runtime for current turn based on model and configuration.
|
||||
|
||||
Returns:
|
||||
Runtime instance if resolution succeeds, None otherwise
|
||||
"""
|
||||
try:
|
||||
from ..runtime.resolver import RuntimeResolutionContext
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
# Create resolution context with current model and provider info
|
||||
context = RuntimeResolutionContext(
|
||||
model_name=getattr(self, 'llm', None),
|
||||
provider_name=self._extract_provider_from_model(getattr(self, 'llm', None)),
|
||||
agent_config={'agent_id': getattr(self, 'agent_id', None)}
|
||||
)
|
||||
|
||||
try:
|
||||
resolver = self._get_runtime_resolver()
|
||||
result = resolver.resolve_runtime_instance(
|
||||
context=context,
|
||||
model_runtime_configs=getattr(self, '_model_runtime_configs', None),
|
||||
provider_runtime_configs=getattr(self, '_provider_runtime_configs', None),
|
||||
legacy_cli_backend=getattr(self, '_cli_backend', None)
|
||||
)
|
||||
|
||||
if result and result.runtime:
|
||||
return result.runtime
|
||||
|
||||
except (ValueError, RuntimeError) as e:
|
||||
# Only fall back for registry not initialized, propagate configuration errors
|
||||
if "not initialized" in str(e).lower():
|
||||
# Registry not initialized - this is expected in SDK-only mode
|
||||
pass
|
||||
else:
|
||||
# Configuration error (unknown runtime ID etc.) - fail closed
|
||||
raise RuntimeError(
|
||||
f"Runtime resolution failed for agent={getattr(self, 'display_name', 'unknown')!r}, "
|
||||
f"model={getattr(self, 'llm', None)!r}: {e}. "
|
||||
"Fix the runtime ID/configuration or remove the runtime override."
|
||||
) from e
|
||||
|
||||
return None
|
||||
|
||||
def _extract_provider_from_model(self, model_name):
|
||||
"""Extract provider name from model name.
|
||||
|
||||
Args:
|
||||
model_name: Model name string (e.g., "anthropic/claude-3-sonnet")
|
||||
|
||||
Returns:
|
||||
Provider name if detectable, None otherwise
|
||||
"""
|
||||
if not model_name or not isinstance(model_name, str):
|
||||
return None
|
||||
|
||||
# Common provider patterns
|
||||
if '/' in model_name:
|
||||
return model_name.split('/')[0]
|
||||
|
||||
# Provider inference based on model name patterns
|
||||
model_lower = model_name.lower()
|
||||
if 'claude' in model_lower or 'anthropic' in model_lower:
|
||||
return 'anthropic'
|
||||
elif 'gpt' in model_lower or 'openai' in model_lower:
|
||||
return 'openai'
|
||||
elif 'gemini' in model_lower or 'google' in model_lower:
|
||||
return 'google'
|
||||
elif 'llama' in model_lower:
|
||||
return 'meta'
|
||||
|
||||
return None
|
||||
|
||||
async def _chat_via_runtime(self, runtime_instance, prompt: str, **kwargs) -> Optional[str]:
|
||||
"""Chat implementation using resolved runtime instance.
|
||||
|
||||
This is similar to _chat_via_cli_backend but uses the resolved runtime
|
||||
from the model-scoped runtime resolution system.
|
||||
|
||||
Args:
|
||||
runtime_instance: Resolved runtime instance (CliBackendProtocol)
|
||||
prompt: User prompt
|
||||
**kwargs: Additional chat parameters
|
||||
|
||||
Returns:
|
||||
Runtime response content
|
||||
"""
|
||||
if not runtime_instance:
|
||||
raise RuntimeError("Runtime instance is None")
|
||||
|
||||
# Delegate to CLI backend implementation with runtime instance
|
||||
# Pass runtime instance directly to avoid mutating shared state
|
||||
return await self._chat_via_cli_backend(prompt=prompt, cli_backend=runtime_instance, **kwargs)
|
||||
|
||||
async def _chat_via_cli_backend(self, prompt: str, cli_backend: Any = None, **kwargs) -> Optional[str]:
|
||||
"""Chat implementation using CLI backend delegation.
|
||||
|
||||
Args:
|
||||
prompt: User prompt
|
||||
cli_backend: Optional specific backend instance to use (for runtime delegation)
|
||||
**kwargs: Additional chat parameters (passed through as metadata)
|
||||
|
||||
Returns:
|
||||
CLI backend response content
|
||||
"""
|
||||
if not self._cli_backend:
|
||||
# Use provided backend or fall back to instance backend
|
||||
backend = cli_backend or self._cli_backend
|
||||
if not backend:
|
||||
raise RuntimeError("CLI backend not configured")
|
||||
|
||||
try:
|
||||
@@ -4999,7 +5371,7 @@ Answer:"""
|
||||
images = None
|
||||
|
||||
# Execute CLI backend
|
||||
result = await self._cli_backend.execute(
|
||||
result = await backend.execute(
|
||||
prompt=prompt,
|
||||
session=session_binding,
|
||||
images=images,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -9,6 +9,7 @@ import time
|
||||
import json
|
||||
import logging
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
@@ -805,7 +806,25 @@ Write the complete compiled report:"""
|
||||
# Suppress verbose during animation - we'll display result ourselves
|
||||
self.verbose = False
|
||||
current_status[0] = "Sending to LLM..."
|
||||
result_holder[0] = self.chat(prompt, **kwargs)
|
||||
|
||||
# Check for steering messages before starting chat
|
||||
if hasattr(self, '_check_steering_messages'):
|
||||
try:
|
||||
steering_msg = self._check_steering_messages()
|
||||
if steering_msg:
|
||||
# Inject steering message into prompt with clear separator
|
||||
prompt_with_steering = f"{prompt}\n\n{steering_msg}"
|
||||
current_status[0] = "Processing steering guidance..."
|
||||
result_holder[0] = self.chat(prompt_with_steering, **kwargs)
|
||||
else:
|
||||
result_holder[0] = self.chat(prompt, **kwargs)
|
||||
except Exception as e:
|
||||
logger.warning(f"Steering check failed, continuing without steering: {e}")
|
||||
current_status[0] = "Steering failed, proceeding normally..."
|
||||
result_holder[0] = self.chat(prompt, **kwargs)
|
||||
else:
|
||||
result_holder[0] = self.chat(prompt, **kwargs)
|
||||
|
||||
current_status[0] = "Finalizing response..."
|
||||
except Exception as e:
|
||||
error_holder[0] = e
|
||||
@@ -947,7 +966,80 @@ Write the complete compiled report:"""
|
||||
return await self.achat(prompt, task_name=task_name, task_description=task_description, task_id=task_id)
|
||||
|
||||
async def execute_tool_async(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None, tools_override: Optional[List] = None) -> Any:
|
||||
"""Async version of execute_tool"""
|
||||
"""Async version of execute_tool with retry policy support"""
|
||||
|
||||
# Get retry policy (tool-level > agent-level > default)
|
||||
retry_policy = self._get_tool_retry_policy(function_name)
|
||||
|
||||
last_exception = None
|
||||
|
||||
# Retry loop with exponential backoff
|
||||
for attempt in range(retry_policy.max_attempts):
|
||||
try:
|
||||
result = await self._execute_tool_async_impl(function_name, arguments, tool_call_id, tools_override)
|
||||
|
||||
# Check if result is an error that should be retried
|
||||
if isinstance(result, dict) and result.get("error"):
|
||||
# Skip retry for non-retryable errors (approval, permission, etc.)
|
||||
if (result.get("approval_denied") or
|
||||
result.get("permission_denied") or
|
||||
result.get("approval_error") or
|
||||
result.get("circuit_open")):
|
||||
return result
|
||||
|
||||
# Determine error type for retry policy
|
||||
error_type = self._classify_error_type(result, last_exception)
|
||||
|
||||
# Check if we should retry
|
||||
if not retry_policy.should_retry(error_type, attempt):
|
||||
return result
|
||||
|
||||
# Don't retry on last attempt
|
||||
if attempt == retry_policy.max_attempts - 1:
|
||||
return result
|
||||
|
||||
# Emit retry hook event
|
||||
delay_ms = retry_policy.get_delay_ms(attempt)
|
||||
await self._emit_retry_hook_async(function_name, attempt + 1, delay_ms, result.get("error", "Unknown error"), retry_policy.max_attempts, error_type)
|
||||
|
||||
# Wait before retry (async)
|
||||
await asyncio.sleep(delay_ms / 1000.0)
|
||||
continue
|
||||
else:
|
||||
# Success - return result
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
# Determine if retryable
|
||||
is_retryable = not isinstance(e, (ValueError, TypeError, AttributeError))
|
||||
|
||||
# Check if retryable and not last attempt
|
||||
if not is_retryable or attempt == retry_policy.max_attempts - 1:
|
||||
# Return error dict instead of raising for consistency with sync version
|
||||
return {"error": f"Error in execute_tool_async: {str(e)}"}
|
||||
|
||||
# Determine error type
|
||||
error_type = self._classify_error_type(None, e)
|
||||
|
||||
if not retry_policy.should_retry(error_type, attempt):
|
||||
return {"error": f"Error in execute_tool_async: {str(e)}"}
|
||||
|
||||
# Emit retry hook event
|
||||
delay_ms = retry_policy.get_delay_ms(attempt)
|
||||
await self._emit_retry_hook_async(function_name, attempt + 1, delay_ms, str(e), retry_policy.max_attempts, error_type)
|
||||
|
||||
# Wait before retry (async)
|
||||
await asyncio.sleep(delay_ms / 1000.0)
|
||||
continue
|
||||
|
||||
# Should never reach here due to loop logic, but safety fallback
|
||||
if last_exception:
|
||||
return {"error": f"Error in execute_tool_async: {str(last_exception)}"}
|
||||
return {"error": "Maximum retry attempts exceeded"}
|
||||
|
||||
async def _execute_tool_async_impl(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None, tools_override: Optional[List] = None) -> Any:
|
||||
"""Internal async tool execution implementation"""
|
||||
try:
|
||||
logging.info(f"Executing async tool: {function_name} with arguments: {arguments}")
|
||||
|
||||
@@ -1286,3 +1378,51 @@ Write the complete compiled report:"""
|
||||
print(f"\nTo add MCP capabilities, install: pip install {missing_module}")
|
||||
return None
|
||||
|
||||
async def _emit_retry_hook_async(self, tool_name, attempt, delay_ms, error, max_attempts, error_type):
|
||||
"""Emit ON_RETRY hook event (async version).
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool being retried
|
||||
attempt: Current attempt number (1-based)
|
||||
delay_ms: Delay before retry in milliseconds
|
||||
error: Error message or description
|
||||
max_attempts: Maximum number of attempts configured
|
||||
error_type: Classified error type
|
||||
"""
|
||||
try:
|
||||
from ..hooks import HookEvent, OnRetryInput
|
||||
|
||||
# Only emit if we have a hook runner
|
||||
hook_runner = getattr(self, '_hook_runner', None)
|
||||
if hook_runner is None:
|
||||
return
|
||||
|
||||
retry_input = OnRetryInput(
|
||||
session_id=getattr(self, '_session_id', 'default'),
|
||||
cwd=os.getcwd(),
|
||||
event_name=HookEvent.ON_RETRY,
|
||||
timestamp=str(time.time()),
|
||||
agent_name=self.name,
|
||||
tool_name=tool_name,
|
||||
attempt=attempt,
|
||||
delay_ms=delay_ms,
|
||||
error=error,
|
||||
max_attempts=max_attempts,
|
||||
error_type=error_type
|
||||
)
|
||||
|
||||
# Execute hook asynchronously if supported, otherwise use sync
|
||||
if hasattr(hook_runner, 'execute_async'):
|
||||
await hook_runner.execute_async(HookEvent.ON_RETRY, retry_input, target=tool_name)
|
||||
else:
|
||||
# Fallback to sync execution in executor to avoid blocking event loop
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
lambda: hook_runner.execute_sync(HookEvent.ON_RETRY, retry_input, target=tool_name)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Don't let hook failures break retry logic
|
||||
logging.debug(f"Failed to emit async retry hook: {e}")
|
||||
|
||||
@@ -10,7 +10,7 @@ Unified Handoff System:
|
||||
- Replaces/absorbs Agent.delegate() and SubagentDelegator functionality
|
||||
"""
|
||||
|
||||
from typing import Optional, Any, Callable, Dict, List, Union, TYPE_CHECKING
|
||||
from typing import Optional, Any, Callable, Dict, List, Union, TYPE_CHECKING, Literal, TypeVar, Type, Generic
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import inspect
|
||||
@@ -19,6 +19,22 @@ from praisonaiagents._logging import get_logger
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
|
||||
try:
|
||||
from pydantic import BaseModel, ValidationError as PydanticValidationError
|
||||
PYDANTIC_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYDANTIC_AVAILABLE = False
|
||||
if TYPE_CHECKING:
|
||||
from pydantic import BaseModel, ValidationError as PydanticValidationError
|
||||
else:
|
||||
# Provide a safe sentinel for runtime
|
||||
class BaseModel: # type: ignore
|
||||
pass
|
||||
PydanticValidationError = Exception
|
||||
|
||||
from ..run_outcome import AgentRunOutcome, RunStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .agent import Agent
|
||||
@@ -32,6 +48,24 @@ class ContextPolicy(Enum):
|
||||
NONE = "none" # No context sharing
|
||||
LAST_N = "last_n" # Share last N messages
|
||||
|
||||
@dataclass
|
||||
class HandoffToolPolicy:
|
||||
"""
|
||||
Policy for tool boundary enforcement during handoff.
|
||||
|
||||
Defines how tools are filtered when handing off to a target agent.
|
||||
Default mode is 'intersect' for security by default - sub-agents only
|
||||
get tools that both they and the source agent have access to.
|
||||
|
||||
Attributes:
|
||||
mode: Tool filtering mode
|
||||
- "intersect": Target gets intersection of source and target tools (DEFAULT - secure)
|
||||
- "passthrough": Target keeps its full tool set (legacy behavior - opt-in)
|
||||
blocked_tools: List of tool names to always strip, regardless of intersection
|
||||
"""
|
||||
mode: Literal["intersect", "passthrough"] = "intersect"
|
||||
blocked_tools: List[str] = field(default_factory=list)
|
||||
|
||||
@dataclass
|
||||
class HandoffConfig:
|
||||
"""
|
||||
@@ -45,6 +79,7 @@ class HandoffConfig:
|
||||
max_context_tokens: Maximum tokens to include in context
|
||||
max_context_messages: Maximum messages to include (for LAST_N policy)
|
||||
preserve_system: Whether to preserve system messages in context
|
||||
tool_policy: Tool boundary enforcement policy (NEW - security by default)
|
||||
timeout_seconds: Timeout for handoff execution
|
||||
max_concurrent: Maximum concurrent handoffs (0 = unlimited)
|
||||
detect_cycles: Enable cycle detection to prevent infinite loops
|
||||
@@ -61,6 +96,9 @@ class HandoffConfig:
|
||||
max_context_messages: int = 10
|
||||
preserve_system: bool = True
|
||||
|
||||
# Tool security boundary (NEW)
|
||||
tool_policy: HandoffToolPolicy = field(default_factory=HandoffToolPolicy)
|
||||
|
||||
# Execution control
|
||||
timeout_seconds: float = 300.0
|
||||
max_concurrent: int = 5
|
||||
@@ -85,6 +123,10 @@ class HandoffConfig:
|
||||
"max_context_tokens": self.max_context_tokens,
|
||||
"max_context_messages": self.max_context_messages,
|
||||
"preserve_system": self.preserve_system,
|
||||
"tool_policy": {
|
||||
"mode": self.tool_policy.mode,
|
||||
"blocked_tools": self.tool_policy.blocked_tools.copy()
|
||||
},
|
||||
"timeout_seconds": self.timeout_seconds,
|
||||
"max_concurrent": self.max_concurrent,
|
||||
"detect_cycles": self.detect_cycles,
|
||||
@@ -96,16 +138,29 @@ class HandoffConfig:
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'HandoffConfig':
|
||||
"""Create config from dictionary."""
|
||||
if "context_policy" in data and isinstance(data["context_policy"], str):
|
||||
data["context_policy"] = ContextPolicy(data["context_policy"])
|
||||
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
||||
data_copy = data.copy()
|
||||
|
||||
# Handle context_policy enum conversion
|
||||
if "context_policy" in data_copy and isinstance(data_copy["context_policy"], str):
|
||||
data_copy["context_policy"] = ContextPolicy(data_copy["context_policy"])
|
||||
|
||||
# Handle tool_policy conversion
|
||||
if "tool_policy" in data_copy and isinstance(data_copy["tool_policy"], dict):
|
||||
tool_policy_data = data_copy["tool_policy"]
|
||||
data_copy["tool_policy"] = HandoffToolPolicy(
|
||||
mode=tool_policy_data.get("mode", "intersect"),
|
||||
blocked_tools=tool_policy_data.get("blocked_tools", [])
|
||||
)
|
||||
|
||||
return cls(**{k: v for k, v in data_copy.items() if k in cls.__dataclass_fields__})
|
||||
|
||||
# Import structured error hierarchy from central errors module
|
||||
from ..errors import (
|
||||
HandoffError,
|
||||
HandoffCycleError,
|
||||
HandoffDepthError,
|
||||
HandoffTimeoutError
|
||||
HandoffTimeoutError,
|
||||
HandoffValidationError
|
||||
)
|
||||
|
||||
# Thread-local storage for tracking handoff chains
|
||||
@@ -147,7 +202,12 @@ class HandoffInputData:
|
||||
|
||||
@dataclass
|
||||
class HandoffResult:
|
||||
"""Result of a handoff operation."""
|
||||
"""
|
||||
Result of a handoff operation.
|
||||
|
||||
Now includes typed outcome information while maintaining backward
|
||||
compatibility with the legacy boolean success field.
|
||||
"""
|
||||
success: bool
|
||||
response: Optional[str] = None
|
||||
target_agent: Optional[str] = None
|
||||
@@ -156,6 +216,64 @@ class HandoffResult:
|
||||
error: Optional[str] = None
|
||||
handoff_depth: int = 0
|
||||
|
||||
# New typed outcome field
|
||||
outcome: Optional[AgentRunOutcome] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Initialize outcome field if not provided."""
|
||||
if self.outcome is None:
|
||||
# Create outcome based on legacy success field
|
||||
if self.success:
|
||||
self.outcome = AgentRunOutcome.success(
|
||||
output=self.response or "",
|
||||
elapsed_s=self.duration_seconds,
|
||||
agent_name=self.target_agent,
|
||||
context={
|
||||
"source_agent": self.source_agent,
|
||||
"handoff_depth": self.handoff_depth,
|
||||
}
|
||||
)
|
||||
else:
|
||||
error_text = (self.error or "").lower()
|
||||
context = {
|
||||
"source_agent": self.source_agent,
|
||||
"handoff_depth": self.handoff_depth,
|
||||
}
|
||||
if any(keyword in error_text for keyword in ["timeout", "timed out"]):
|
||||
self.outcome = AgentRunOutcome.timeout(
|
||||
error=self.error or "Handoff failed",
|
||||
elapsed_s=self.duration_seconds,
|
||||
agent_name=self.target_agent,
|
||||
context=context,
|
||||
)
|
||||
else:
|
||||
self.outcome = AgentRunOutcome.failure(
|
||||
error=self.error or "Handoff failed",
|
||||
elapsed_s=self.duration_seconds,
|
||||
agent_name=self.target_agent,
|
||||
context=context,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_outcome(
|
||||
cls,
|
||||
outcome: AgentRunOutcome,
|
||||
target_agent: Optional[str] = None,
|
||||
source_agent: Optional[str] = None,
|
||||
handoff_depth: int = 0,
|
||||
) -> "HandoffResult":
|
||||
"""Create HandoffResult from AgentRunOutcome."""
|
||||
return cls(
|
||||
success=outcome.is_success(),
|
||||
response=outcome.output,
|
||||
target_agent=target_agent or outcome.agent_name,
|
||||
source_agent=source_agent,
|
||||
duration_seconds=outcome.elapsed_s,
|
||||
error=outcome.error,
|
||||
handoff_depth=handoff_depth,
|
||||
outcome=outcome,
|
||||
)
|
||||
|
||||
|
||||
class Handoff:
|
||||
"""
|
||||
@@ -241,6 +359,52 @@ class Handoff:
|
||||
agent_desc += f" - {self.agent.goal}"
|
||||
return agent_desc
|
||||
|
||||
def _compute_effective_tools(self, source_agent: 'Agent') -> List[Any] | None:
|
||||
"""
|
||||
Compute the effective tool set for the target agent based on tool policy.
|
||||
|
||||
Args:
|
||||
source_agent: The agent initiating the handoff
|
||||
|
||||
Returns:
|
||||
List of tools the target agent should have access to during handoff.
|
||||
Returns None to indicate unrestricted access (for passthrough mode without blocked tools).
|
||||
Returns [] to indicate no tools allowed (for intersect mode with empty intersection).
|
||||
"""
|
||||
policy = self.config.tool_policy
|
||||
|
||||
if policy.mode == "passthrough":
|
||||
# Legacy behavior - target keeps its full tool set, minus blocked tools
|
||||
if not policy.blocked_tools:
|
||||
# No restrictions - return None so chat() uses the agent's configured tools
|
||||
return None
|
||||
else:
|
||||
# Filter out blocked tools
|
||||
target_tools = getattr(self.agent, 'tools', None) or []
|
||||
effective_tools = [
|
||||
tool for tool in target_tools
|
||||
if getattr(tool, 'name', getattr(tool, '__name__', str(tool))) not in policy.blocked_tools
|
||||
]
|
||||
return effective_tools
|
||||
else: # intersect mode (default)
|
||||
# Security by default - intersection of source and target tools
|
||||
source_tools = getattr(source_agent, 'tools', None) or []
|
||||
source_tool_names = {
|
||||
getattr(tool, 'name', getattr(tool, '__name__', str(tool)))
|
||||
for tool in source_tools
|
||||
}
|
||||
|
||||
target_tools = getattr(self.agent, 'tools', None) or []
|
||||
effective_tools = []
|
||||
for tool in target_tools:
|
||||
tool_name = getattr(tool, 'name', getattr(tool, '__name__', str(tool)))
|
||||
if (tool_name in source_tool_names and
|
||||
tool_name not in policy.blocked_tools):
|
||||
effective_tools.append(tool)
|
||||
|
||||
# In intersect mode, empty list means no tools allowed - this is the security boundary
|
||||
return effective_tools
|
||||
|
||||
def _check_safety(self, source_agent: 'Agent') -> None:
|
||||
"""
|
||||
Check safety constraints before handoff.
|
||||
@@ -402,8 +566,11 @@ class Handoff:
|
||||
|
||||
logger.info(f"Programmatic handoff to {self.agent.name}")
|
||||
|
||||
# Execute with timeout if sync
|
||||
response = self.agent.chat(full_prompt)
|
||||
# Compute effective tools based on tool policy
|
||||
effective_tools = self._compute_effective_tools(source_agent)
|
||||
|
||||
# Execute with tool boundary enforcement
|
||||
response = self.agent.chat(full_prompt, tools=effective_tools)
|
||||
|
||||
result = HandoffResult(
|
||||
success=True,
|
||||
@@ -417,6 +584,20 @@ class Handoff:
|
||||
# Execute on_complete callback
|
||||
self._execute_callback(self.config.on_complete, source_agent, kwargs, result)
|
||||
|
||||
# Call on_delegation hook on source agent's memory
|
||||
if (hasattr(source_agent, '_memory_instance') and
|
||||
source_agent._memory_instance and
|
||||
hasattr(source_agent._memory_instance, 'on_delegation')):
|
||||
try:
|
||||
source_agent._memory_instance.on_delegation(
|
||||
task=full_prompt,
|
||||
result=result.response or "",
|
||||
agent_name=self.agent.name,
|
||||
metadata={"handoff_depth": result.handoff_depth}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[{source_agent.name}] Memory on_delegation hook failed: {e}")
|
||||
|
||||
return result
|
||||
|
||||
except (HandoffCycleError, HandoffDepthError, HandoffTimeoutError) as e:
|
||||
@@ -491,15 +672,18 @@ class Handoff:
|
||||
|
||||
logger.info(f"Async handoff to {self.agent.name}")
|
||||
|
||||
# Execute - check for async chat method
|
||||
if hasattr(self.agent, 'achat'):
|
||||
response = await self.agent.achat(full_prompt)
|
||||
else:
|
||||
# Run sync chat in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, self.agent.chat, full_prompt)
|
||||
# Compute effective tools based on tool policy
|
||||
effective_tools = self._compute_effective_tools(source_agent)
|
||||
|
||||
return HandoffResult(
|
||||
# Execute with tool boundary enforcement - check for async chat method
|
||||
if hasattr(self.agent, 'achat'):
|
||||
response = await self.agent.achat(full_prompt, tools=effective_tools)
|
||||
else:
|
||||
# Run sync chat in executor with tool constraint
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, lambda: self.agent.chat(full_prompt, tools=effective_tools))
|
||||
|
||||
result = HandoffResult(
|
||||
success=True,
|
||||
response=str(response) if response else "",
|
||||
target_agent=self.agent.name,
|
||||
@@ -507,6 +691,22 @@ class Handoff:
|
||||
duration_seconds=time.time() - start_time,
|
||||
handoff_depth=_get_handoff_depth(),
|
||||
)
|
||||
|
||||
# Call on_delegation hook on source agent's memory
|
||||
if (hasattr(source_agent, '_memory_instance') and
|
||||
source_agent._memory_instance and
|
||||
hasattr(source_agent._memory_instance, 'on_delegation')):
|
||||
try:
|
||||
source_agent._memory_instance.on_delegation(
|
||||
task=full_prompt,
|
||||
result=result.response or "",
|
||||
agent_name=self.agent.name,
|
||||
metadata={"handoff_depth": result.handoff_depth}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[{source_agent.name}] Memory on_delegation hook failed: {e}")
|
||||
|
||||
return result
|
||||
finally:
|
||||
_pop_handoff()
|
||||
|
||||
@@ -609,11 +809,15 @@ class Handoff:
|
||||
if kwargs and self.input_type:
|
||||
context_info += f"Context: {kwargs} "
|
||||
|
||||
# Execute the target agent
|
||||
# Execute the target agent with tool boundary enforcement
|
||||
if last_message:
|
||||
prompt = context_info + last_message
|
||||
logger.info(f"Handing off to {self.agent.name} with prompt: {prompt}")
|
||||
response = self.agent.chat(prompt)
|
||||
|
||||
# Compute effective tools based on tool policy
|
||||
effective_tools = self._compute_effective_tools(source_agent)
|
||||
|
||||
response = self.agent.chat(prompt, tools=effective_tools)
|
||||
|
||||
result = HandoffResult(
|
||||
success=True,
|
||||
@@ -625,6 +829,20 @@ class Handoff:
|
||||
)
|
||||
self._execute_callback(self.config.on_complete, source_agent, kwargs, result)
|
||||
|
||||
# Call on_delegation hook on source agent's memory
|
||||
if (hasattr(source_agent, '_memory_instance') and
|
||||
source_agent._memory_instance and
|
||||
hasattr(source_agent._memory_instance, 'on_delegation')):
|
||||
try:
|
||||
source_agent._memory_instance.on_delegation(
|
||||
task=prompt,
|
||||
result=result.response or "",
|
||||
agent_name=self.agent.name,
|
||||
metadata={"handoff_depth": result.handoff_depth}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[{source_agent.name}] Memory on_delegation hook failed: {e}")
|
||||
|
||||
return f"Handoff successful. {self.agent.name} response: {response}"
|
||||
|
||||
return f"Handoff to {self.agent.name} completed, but no specific task was provided."
|
||||
@@ -682,13 +900,16 @@ def handoff(
|
||||
max_concurrent: Optional[int] = None,
|
||||
detect_cycles: Optional[bool] = None,
|
||||
max_depth: Optional[int] = None,
|
||||
# Tool security boundary kwargs (NEW)
|
||||
tool_policy_mode: Optional[Literal["intersect", "passthrough"]] = None,
|
||||
blocked_tools: Optional[List[str]] = None,
|
||||
) -> Handoff:
|
||||
"""
|
||||
Create a handoff configuration for delegating tasks to another agent.
|
||||
|
||||
This is a convenience function that creates a Handoff instance with the
|
||||
specified configuration. It supports both the legacy API and the new
|
||||
unified HandoffConfig.
|
||||
unified HandoffConfig with tool security boundary enforcement.
|
||||
|
||||
Args:
|
||||
agent: The target agent to hand off to
|
||||
@@ -703,29 +924,39 @@ def handoff(
|
||||
max_concurrent: Shorthand for config.max_concurrent
|
||||
detect_cycles: Shorthand for config.detect_cycles
|
||||
max_depth: Shorthand for config.max_depth
|
||||
tool_policy_mode: Tool boundary mode ("intersect" = secure by default, "passthrough" = legacy)
|
||||
blocked_tools: List of tool names to always block regardless of intersection
|
||||
|
||||
Returns:
|
||||
A configured Handoff instance
|
||||
|
||||
Example:
|
||||
```python
|
||||
from praisonaiagents import Agent, handoff, HandoffConfig
|
||||
from praisonaiagents import Agent, handoff, HandoffConfig, HandoffToolPolicy
|
||||
|
||||
billing_agent = Agent(name="Billing Agent")
|
||||
refund_agent = Agent(name="Refund Agent")
|
||||
refund_agent = Agent(name="Refund Agent")
|
||||
|
||||
# Simple usage
|
||||
# Simple usage (secure by default - tools intersected)
|
||||
triage_agent = Agent(
|
||||
name="Triage Agent",
|
||||
handoffs=[billing_agent, handoff(refund_agent)]
|
||||
)
|
||||
|
||||
# With config
|
||||
# With tool security config
|
||||
triage_agent = Agent(
|
||||
name="Triage Agent",
|
||||
name="Triage Agent",
|
||||
handoffs=[
|
||||
handoff(billing_agent, context_policy="summary", timeout_seconds=60),
|
||||
handoff(refund_agent, config=HandoffConfig(detect_cycles=True))
|
||||
handoff(billing_agent,
|
||||
tool_policy_mode="intersect", # Only shared tools
|
||||
blocked_tools=["execute_code", "shell_tools"]),
|
||||
handoff(refund_agent,
|
||||
config=HandoffConfig(
|
||||
tool_policy=HandoffToolPolicy(
|
||||
mode="passthrough", # Legacy behavior
|
||||
blocked_tools=["dangerous_tool"]
|
||||
)
|
||||
))
|
||||
]
|
||||
)
|
||||
```
|
||||
@@ -746,6 +977,14 @@ def handoff(
|
||||
if max_depth is not None:
|
||||
config.max_depth = max_depth
|
||||
|
||||
# Apply tool policy kwargs to config
|
||||
if tool_policy_mode is not None or blocked_tools is not None:
|
||||
# Create new tool policy with updated values
|
||||
config.tool_policy = HandoffToolPolicy(
|
||||
mode=tool_policy_mode if tool_policy_mode is not None else config.tool_policy.mode,
|
||||
blocked_tools=blocked_tools if blocked_tools is not None else config.tool_policy.blocked_tools
|
||||
)
|
||||
|
||||
return Handoff(
|
||||
agent=agent,
|
||||
tool_name_override=tool_name_override,
|
||||
@@ -924,6 +1163,402 @@ async def parallel_handoffs(
|
||||
return processed_results
|
||||
|
||||
|
||||
# Type variable for generic typed handoff
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class TypedHandoff(Handoff, Generic[T]):
|
||||
"""
|
||||
Typed handoff with schema validation using Pydantic models.
|
||||
|
||||
This class extends the base Handoff to add type safety through Pydantic schema validation.
|
||||
The sender declares an output schema, the receiver expects that schema, and the framework
|
||||
validates the payload at the boundary, raising HandoffValidationError if validation fails.
|
||||
|
||||
The receiving agent's prompt is constructed from validated JSON rather than raw string
|
||||
concatenation, enabling proper deserialization of structured data.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from praisonaiagents.agent.handoff import TypedHandoff, HandoffValidationError
|
||||
|
||||
class ResearchResult(BaseModel):
|
||||
summary: str
|
||||
citations: list[str]
|
||||
confidence: float
|
||||
|
||||
# Create typed handoff
|
||||
typed_handoff = TypedHandoff(
|
||||
agent=writer_agent,
|
||||
input_schema=ResearchResult
|
||||
)
|
||||
|
||||
# Valid payload
|
||||
result = ResearchResult(
|
||||
summary="AI research findings",
|
||||
citations=["ref1", "ref2"],
|
||||
confidence=0.92
|
||||
)
|
||||
response = typed_handoff.execute_programmatic(source_agent, result)
|
||||
|
||||
# Invalid payload raises HandoffValidationError
|
||||
bad_payload = {"summary": "...", "citations": "not-a-list"}
|
||||
typed_handoff.execute_programmatic(source_agent, bad_payload) # Raises error
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent: 'Agent',
|
||||
input_schema: Type[T],
|
||||
tool_name_override: Optional[str] = None,
|
||||
tool_description_override: Optional[str] = None,
|
||||
on_handoff: Optional[Callable] = None,
|
||||
input_filter: Optional[Union[Callable[[HandoffInputData], HandoffInputData], List[Callable[[HandoffInputData], HandoffInputData]]]] = None,
|
||||
config: Optional[HandoffConfig] = None,
|
||||
):
|
||||
"""
|
||||
Initialize a TypedHandoff with schema validation.
|
||||
|
||||
Args:
|
||||
agent: The target agent to hand off to
|
||||
input_schema: Pydantic model class for payload validation
|
||||
tool_name_override: Custom tool name (defaults to transfer_to_<agent_name>)
|
||||
tool_description_override: Custom tool description
|
||||
on_handoff: Callback function executed when handoff is invoked
|
||||
input_filter: Function or list of functions to filter/transform input
|
||||
config: HandoffConfig for advanced settings (context policy, timeouts, etc.)
|
||||
|
||||
Raises:
|
||||
ImportError: If Pydantic is not available
|
||||
TypeError: If input_schema is not a Pydantic model
|
||||
"""
|
||||
if not PYDANTIC_AVAILABLE:
|
||||
raise ImportError(
|
||||
"Pydantic is required for TypedHandoff. Install with: pip install pydantic"
|
||||
)
|
||||
|
||||
if not (inspect.isclass(input_schema) and issubclass(input_schema, BaseModel)):
|
||||
raise TypeError(
|
||||
f"input_schema must be a Pydantic BaseModel class, got {type(input_schema)}"
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
agent=agent,
|
||||
tool_name_override=tool_name_override,
|
||||
tool_description_override=tool_description_override,
|
||||
on_handoff=on_handoff,
|
||||
input_type=None, # We handle typing ourselves
|
||||
input_filter=input_filter,
|
||||
config=config,
|
||||
)
|
||||
self._input_schema = input_schema
|
||||
|
||||
def _validate_payload(self, payload: Union[T, dict, Any]) -> T:
|
||||
"""
|
||||
Validate payload against the input schema.
|
||||
|
||||
Args:
|
||||
payload: The payload to validate - can be a Pydantic model instance,
|
||||
dict, or any object with model_dump() method
|
||||
|
||||
Returns:
|
||||
Validated Pydantic model instance
|
||||
|
||||
Raises:
|
||||
HandoffValidationError: If validation fails
|
||||
"""
|
||||
try:
|
||||
if isinstance(payload, self._input_schema):
|
||||
# Already the correct type, validate by re-parsing
|
||||
validated = self._input_schema.model_validate(payload.model_dump())
|
||||
elif isinstance(payload, dict):
|
||||
# Dictionary - validate directly
|
||||
validated = self._input_schema.model_validate(payload)
|
||||
elif hasattr(payload, 'model_dump'):
|
||||
# Another Pydantic model - convert via dict
|
||||
validated = self._input_schema.model_validate(payload.model_dump())
|
||||
else:
|
||||
# Try to convert to dict first
|
||||
if hasattr(payload, '__dict__'):
|
||||
payload_dict = payload.__dict__
|
||||
else:
|
||||
payload_dict = dict(payload) if hasattr(payload, '__iter__') else {}
|
||||
validated = self._input_schema.model_validate(payload_dict)
|
||||
|
||||
return validated
|
||||
|
||||
except PydanticValidationError as exc:
|
||||
validation_errors = [
|
||||
f"{err['loc']}: {err['msg']}" for err in exc.errors()
|
||||
] if hasattr(exc, 'errors') else [str(exc)]
|
||||
|
||||
raise HandoffValidationError(
|
||||
f"Handoff to '{self.agent.name}' received an invalid payload: {exc}",
|
||||
source_agent="unknown",
|
||||
target_agent=self.agent.name,
|
||||
agent_id="unknown",
|
||||
validation_errors=validation_errors
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
raise HandoffValidationError(
|
||||
f"Handoff to '{self.agent.name}' failed to validate payload: {exc}",
|
||||
source_agent="unknown",
|
||||
target_agent=self.agent.name,
|
||||
agent_id="unknown"
|
||||
) from exc
|
||||
|
||||
def execute_programmatic(
|
||||
self,
|
||||
source_agent: 'Agent',
|
||||
payload: Union[T, dict, str],
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
) -> HandoffResult:
|
||||
"""
|
||||
Execute typed handoff programmatically with schema validation.
|
||||
|
||||
Args:
|
||||
source_agent: The agent initiating the handoff
|
||||
payload: The typed payload (Pydantic model instance or dict) OR
|
||||
a string prompt (for backward compatibility)
|
||||
context: Optional additional context
|
||||
|
||||
Returns:
|
||||
HandoffResult with response or error
|
||||
|
||||
Raises:
|
||||
HandoffValidationError: If payload validation fails
|
||||
"""
|
||||
# Handle backward compatibility: if payload is a string, treat as regular handoff
|
||||
if isinstance(payload, str):
|
||||
return super().execute_programmatic(source_agent, payload, context)
|
||||
|
||||
start_time = time.time()
|
||||
kwargs = context or {}
|
||||
|
||||
try:
|
||||
# Safety checks
|
||||
self._check_safety(source_agent)
|
||||
|
||||
# Track handoff chain
|
||||
_push_handoff(source_agent.name)
|
||||
|
||||
# Execute on_handoff callback
|
||||
self._execute_callback(self.config.on_handoff or self.on_handoff, source_agent, kwargs)
|
||||
|
||||
# Apply input filter if provided (fixes Greptile P1 issue)
|
||||
_ = self._prepare_context(source_agent, kwargs)
|
||||
|
||||
# Validate payload against schema
|
||||
try:
|
||||
validated_payload = self._validate_payload(payload)
|
||||
except HandoffValidationError as e:
|
||||
# Update error context with proper agent info (fixes Greptile P2 issue)
|
||||
e.source_agent = source_agent.name
|
||||
e.agent_id = source_agent.name
|
||||
e.context["source_agent"] = source_agent.name
|
||||
e.context["agent_id"] = source_agent.name
|
||||
raise
|
||||
|
||||
# Convert validated payload to structured JSON for the prompt
|
||||
payload_json = validated_payload.model_dump_json(indent=2)
|
||||
|
||||
# Build prompt with structured JSON instead of string concatenation
|
||||
context_prefix = f"[Handoff from {source_agent.name}] "
|
||||
if kwargs:
|
||||
context_prefix += f"Additional context: {json.dumps(kwargs, indent=2)}\n"
|
||||
|
||||
# The key improvement: structured JSON instead of str() concatenation
|
||||
full_prompt = f"{context_prefix}Payload:\n{payload_json}"
|
||||
|
||||
logger.info(f"Typed handoff to {self.agent.name} with validated payload")
|
||||
|
||||
# Execute with timeout if sync
|
||||
response = self.agent.chat(full_prompt)
|
||||
|
||||
result = HandoffResult(
|
||||
success=True,
|
||||
response=str(response) if response else "",
|
||||
target_agent=self.agent.name,
|
||||
source_agent=source_agent.name,
|
||||
duration_seconds=time.time() - start_time,
|
||||
handoff_depth=_get_handoff_depth(),
|
||||
)
|
||||
|
||||
# Execute on_complete callback
|
||||
self._execute_callback(self.config.on_complete, source_agent, kwargs, result)
|
||||
|
||||
return result
|
||||
|
||||
except (HandoffCycleError, HandoffDepthError, HandoffTimeoutError, HandoffValidationError) as e:
|
||||
result = HandoffResult(
|
||||
success=False,
|
||||
target_agent=self.agent.name,
|
||||
source_agent=source_agent.name,
|
||||
duration_seconds=time.time() - start_time,
|
||||
error=str(e),
|
||||
handoff_depth=_get_handoff_depth(),
|
||||
)
|
||||
self._execute_callback(self.config.on_error, source_agent, kwargs, result)
|
||||
raise
|
||||
except Exception as e:
|
||||
result = HandoffResult(
|
||||
success=False,
|
||||
target_agent=self.agent.name,
|
||||
source_agent=source_agent.name,
|
||||
duration_seconds=time.time() - start_time,
|
||||
error=str(e),
|
||||
handoff_depth=_get_handoff_depth(),
|
||||
)
|
||||
self._execute_callback(self.config.on_error, source_agent, kwargs, result)
|
||||
logger.error(f"Typed handoff error: {e}")
|
||||
return result
|
||||
finally:
|
||||
_pop_handoff()
|
||||
|
||||
async def execute_async(
|
||||
self,
|
||||
source_agent: 'Agent',
|
||||
payload: Union[T, dict, str],
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
) -> HandoffResult:
|
||||
"""
|
||||
Execute typed handoff asynchronously with schema validation.
|
||||
|
||||
Args:
|
||||
source_agent: The agent initiating the handoff
|
||||
payload: The typed payload (Pydantic model instance or dict) OR
|
||||
a string prompt (for backward compatibility)
|
||||
context: Optional additional context
|
||||
|
||||
Returns:
|
||||
HandoffResult with response or error
|
||||
|
||||
Raises:
|
||||
HandoffValidationError: If payload validation fails
|
||||
"""
|
||||
# Handle backward compatibility: if payload is a string, treat as regular handoff
|
||||
if isinstance(payload, str):
|
||||
return await super().execute_async(source_agent, payload, context)
|
||||
|
||||
# Initialize semaphore if needed (thread-safe)
|
||||
if self.config.max_concurrent > 0 and Handoff._semaphore is None:
|
||||
with Handoff._semaphore_lock:
|
||||
if Handoff._semaphore is None:
|
||||
Handoff._semaphore = asyncio.Semaphore(self.config.max_concurrent)
|
||||
|
||||
start_time = time.time()
|
||||
kwargs = context or {}
|
||||
|
||||
async def _execute():
|
||||
try:
|
||||
# Safety checks
|
||||
self._check_safety(source_agent)
|
||||
_push_handoff(source_agent.name)
|
||||
|
||||
# Execute callback
|
||||
self._execute_callback(self.config.on_handoff or self.on_handoff, source_agent, kwargs)
|
||||
|
||||
# Apply input filter if provided (fixes Greptile P1 issue)
|
||||
_ = self._prepare_context(source_agent, kwargs)
|
||||
|
||||
# Validate payload against schema
|
||||
try:
|
||||
validated_payload = self._validate_payload(payload)
|
||||
except HandoffValidationError as e:
|
||||
# Update error context with proper agent info (fixes Greptile P2 issue)
|
||||
e.source_agent = source_agent.name
|
||||
e.agent_id = source_agent.name
|
||||
e.context["source_agent"] = source_agent.name
|
||||
e.context["agent_id"] = source_agent.name
|
||||
raise
|
||||
|
||||
# Convert to structured JSON
|
||||
payload_json = validated_payload.model_dump_json(indent=2)
|
||||
|
||||
# Build prompt
|
||||
context_prefix = f"[Handoff from {source_agent.name}] "
|
||||
if kwargs:
|
||||
context_prefix += f"Additional context: {json.dumps(kwargs, indent=2)}\n"
|
||||
|
||||
full_prompt = f"{context_prefix}Payload:\n{payload_json}"
|
||||
|
||||
logger.info(f"Async typed handoff to {self.agent.name}")
|
||||
|
||||
# Execute - check for async chat method
|
||||
if hasattr(self.agent, 'achat'):
|
||||
response = await self.agent.achat(full_prompt)
|
||||
else:
|
||||
# Run sync chat in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, self.agent.chat, full_prompt)
|
||||
|
||||
return HandoffResult(
|
||||
success=True,
|
||||
response=str(response) if response else "",
|
||||
target_agent=self.agent.name,
|
||||
source_agent=source_agent.name,
|
||||
duration_seconds=time.time() - start_time,
|
||||
handoff_depth=_get_handoff_depth(),
|
||||
)
|
||||
finally:
|
||||
_pop_handoff()
|
||||
|
||||
try:
|
||||
if self.config.max_concurrent > 0 and Handoff._semaphore:
|
||||
async with Handoff._semaphore:
|
||||
if self.config.timeout_seconds > 0:
|
||||
result = await asyncio.wait_for(
|
||||
_execute(),
|
||||
timeout=self.config.timeout_seconds
|
||||
)
|
||||
else:
|
||||
result = await _execute()
|
||||
else:
|
||||
if self.config.timeout_seconds > 0:
|
||||
result = await asyncio.wait_for(
|
||||
_execute(),
|
||||
timeout=self.config.timeout_seconds
|
||||
)
|
||||
else:
|
||||
result = await _execute()
|
||||
|
||||
self._execute_callback(self.config.on_complete, source_agent, kwargs, result)
|
||||
return result
|
||||
|
||||
except asyncio.TimeoutError as err:
|
||||
result = HandoffResult(
|
||||
success=False,
|
||||
target_agent=self.agent.name,
|
||||
source_agent=source_agent.name,
|
||||
duration_seconds=time.time() - start_time,
|
||||
error=f"Timeout after {self.config.timeout_seconds}s",
|
||||
handoff_depth=_get_handoff_depth(),
|
||||
)
|
||||
self._execute_callback(self.config.on_error, source_agent, kwargs, result)
|
||||
raise HandoffTimeoutError(
|
||||
f"Typed handoff to {self.agent.name} timed out after {self.config.timeout_seconds}s",
|
||||
timeout_seconds=self.config.timeout_seconds,
|
||||
source_agent=source_agent.name,
|
||||
target_agent=self.agent.name,
|
||||
agent_id=source_agent.name
|
||||
) from err
|
||||
except Exception as e:
|
||||
result = HandoffResult(
|
||||
success=False,
|
||||
target_agent=self.agent.name,
|
||||
source_agent=source_agent.name,
|
||||
duration_seconds=time.time() - start_time,
|
||||
error=str(e),
|
||||
handoff_depth=_get_handoff_depth(),
|
||||
)
|
||||
self._execute_callback(self.config.on_error, source_agent, kwargs, result)
|
||||
if isinstance(e, (HandoffCycleError, HandoffDepthError, HandoffValidationError)):
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def prompt_with_handoff_instructions(base_prompt: str, agent: 'Agent') -> str:
|
||||
"""
|
||||
Add handoff instructions to an agent's prompt.
|
||||
|
||||
@@ -317,8 +317,9 @@ class MemoryMixin:
|
||||
return
|
||||
|
||||
try:
|
||||
from ..session import get_default_session_store
|
||||
self._session_store = get_default_session_store()
|
||||
if self._session_store is None:
|
||||
from ..session import get_default_session_store
|
||||
self._session_store = get_default_session_store()
|
||||
|
||||
# Restore chat history from previous session
|
||||
history = self._session_store.get_chat_history(self._session_id)
|
||||
@@ -403,6 +404,12 @@ class MemoryMixin:
|
||||
self._session_store.add_user_message(self._session_id, content)
|
||||
elif role == "assistant":
|
||||
self._session_store.add_assistant_message(self._session_id, content)
|
||||
# Keep auto_save index in sync when per-turn persist shares session_id
|
||||
if self.auto_save and self.auto_save == self._session_id:
|
||||
with self._history_lock:
|
||||
self._auto_save_last_index = (
|
||||
getattr(self, "_auto_save_last_index", 0) + 1
|
||||
)
|
||||
self._persist_session_stats()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to persist message to session store: {e}")
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
Message Steering Implementation.
|
||||
|
||||
Core implementation of real-time message steering during agent execution.
|
||||
Integrates with existing message queue infrastructure.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any
|
||||
from .message_queue import AgentMessageQueue, MessagePriority
|
||||
from .protocols import (
|
||||
MessageSteeringProtocol,
|
||||
SteeringMessage,
|
||||
SteeringPriority,
|
||||
AgentSteeringProtocol
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MessageSteering:
|
||||
"""
|
||||
Core message steering implementation.
|
||||
|
||||
Provides real-time message queuing and processing for agents
|
||||
during execution. Uses the existing AgentMessageQueue infrastructure.
|
||||
"""
|
||||
|
||||
def __init__(self, max_messages: int = 50):
|
||||
"""
|
||||
Initialize message steering.
|
||||
|
||||
Args:
|
||||
max_messages: Maximum number of pending messages
|
||||
"""
|
||||
self._message_queue = AgentMessageQueue(max_size=max_messages)
|
||||
self._enabled = True
|
||||
self._last_check = 0.0
|
||||
self._check_interval = 0.1 # Check every 100ms
|
||||
|
||||
def queue_message(self, message: str, priority: int = 5) -> str:
|
||||
"""Queue a steering message."""
|
||||
if not self._enabled:
|
||||
return ""
|
||||
|
||||
# Convert priority to SteeringPriority
|
||||
try:
|
||||
steering_priority = SteeringPriority(priority)
|
||||
except ValueError:
|
||||
steering_priority = SteeringPriority.NORMAL
|
||||
|
||||
steering_msg = SteeringMessage(
|
||||
content=message,
|
||||
priority=steering_priority,
|
||||
metadata={"timestamp": time.time()}
|
||||
)
|
||||
|
||||
# Use existing message queue with priority mapping
|
||||
# Special handling for INTERRUPT - give it maximum priority
|
||||
if steering_priority == SteeringPriority.INTERRUPT:
|
||||
queue_priority = MessagePriority.URGENT.value + 1 # Higher than URGENT
|
||||
else:
|
||||
queue_priority = min(priority, MessagePriority.URGENT.value)
|
||||
success = self._message_queue.enqueue(
|
||||
content=steering_msg,
|
||||
priority=queue_priority,
|
||||
metadata={"type": "steering", "original_priority": priority}
|
||||
)
|
||||
|
||||
if success:
|
||||
msg_id = f"steer_{int(time.time() * 1000)}"
|
||||
logger.debug(f"Queued steering message: {msg_id}")
|
||||
return msg_id
|
||||
else:
|
||||
logger.warning("Failed to queue steering message - queue full")
|
||||
return ""
|
||||
|
||||
def get_pending_messages(self) -> List[SteeringMessage]:
|
||||
"""Get all pending steering messages."""
|
||||
if not self._enabled:
|
||||
return []
|
||||
|
||||
messages = []
|
||||
all_messages = self._message_queue.get_all()
|
||||
|
||||
for msg in all_messages:
|
||||
if isinstance(msg, SteeringMessage):
|
||||
messages.append(msg)
|
||||
|
||||
return messages
|
||||
|
||||
def process_steering(self, context: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Process pending steering messages.
|
||||
|
||||
This is called during agent execution to check for and process
|
||||
any steering messages. Uses rate limiting to avoid excessive checking,
|
||||
but allows INTERRUPT priority messages to bypass rate limiting.
|
||||
|
||||
Args:
|
||||
context: Execution context that can be updated with steering info
|
||||
|
||||
Returns:
|
||||
True if messages were processed, False otherwise
|
||||
"""
|
||||
if not self._enabled:
|
||||
return False
|
||||
|
||||
# Check if we have high priority messages that bypass rate limiting
|
||||
has_urgent_messages = False
|
||||
all_messages = self._message_queue.get_all()
|
||||
for msg in all_messages:
|
||||
if isinstance(msg, SteeringMessage) and msg.priority.value >= SteeringPriority.HIGH.value:
|
||||
has_urgent_messages = True
|
||||
break
|
||||
|
||||
# Rate limiting - only check every 100ms unless we have urgent messages
|
||||
current_time = time.time()
|
||||
if not has_urgent_messages and current_time - self._last_check < self._check_interval:
|
||||
return False
|
||||
|
||||
self._last_check = current_time
|
||||
|
||||
if self._message_queue.is_empty():
|
||||
return False
|
||||
|
||||
# Process one message per check to avoid blocking
|
||||
msg_content = self._message_queue.dequeue(timeout=0)
|
||||
if msg_content is None:
|
||||
return False
|
||||
|
||||
if not isinstance(msg_content, SteeringMessage):
|
||||
return False
|
||||
|
||||
# Update context with steering message
|
||||
if context is not None:
|
||||
if "steering_messages" not in context:
|
||||
context["steering_messages"] = []
|
||||
context["steering_messages"].append({
|
||||
"content": msg_content.content,
|
||||
"priority": msg_content.priority.name,
|
||||
"timestamp": msg_content.metadata.get("timestamp") if msg_content.metadata else None
|
||||
})
|
||||
|
||||
logger.info(f"Processed steering message: {msg_content.content[:50]}...")
|
||||
return True
|
||||
|
||||
def clear_messages(self) -> int:
|
||||
"""Clear all pending messages."""
|
||||
return self._message_queue.clear()
|
||||
|
||||
def has_pending_messages(self) -> bool:
|
||||
"""Check if there are pending steering messages."""
|
||||
return not self._message_queue.is_empty()
|
||||
|
||||
def enable(self) -> None:
|
||||
"""Enable message steering."""
|
||||
self._enabled = True
|
||||
|
||||
def disable(self) -> None:
|
||||
"""Disable message steering."""
|
||||
self._enabled = False
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Whether message steering is enabled."""
|
||||
return self._enabled
|
||||
|
||||
|
||||
class SteeringMixin:
|
||||
"""
|
||||
Mixin to add message steering capabilities to Agent class.
|
||||
|
||||
This provides the agent interface for message steering while
|
||||
delegating implementation to MessageSteering.
|
||||
"""
|
||||
|
||||
def _init_message_steering(self, message_steering=False):
|
||||
"""Initialize message steering if enabled."""
|
||||
if message_steering is True:
|
||||
# Use default implementation
|
||||
self._message_steering = MessageSteering()
|
||||
elif message_steering is False or message_steering is None:
|
||||
# Disabled
|
||||
self._message_steering = None
|
||||
else:
|
||||
# Custom implementation provided
|
||||
self._message_steering = message_steering
|
||||
|
||||
def steer(self, message: str, priority: int = 5) -> str:
|
||||
"""
|
||||
Send a steering message to the agent during execution.
|
||||
|
||||
This can be called while the agent is running to provide
|
||||
real-time guidance or course corrections.
|
||||
|
||||
Args:
|
||||
message: The steering message
|
||||
priority: Message priority (1=low, 5=normal, 10=high, 20=urgent, 30=interrupt)
|
||||
|
||||
Returns:
|
||||
Message ID for tracking, empty string if steering disabled
|
||||
|
||||
Example:
|
||||
```python
|
||||
agent = Agent(name="research", message_steering=True)
|
||||
|
||||
# Start long-running task
|
||||
import threading
|
||||
def run_task():
|
||||
return agent.start("Research AI trends comprehensively")
|
||||
thread = threading.Thread(target=run_task)
|
||||
thread.start()
|
||||
|
||||
# Send steering messages while running
|
||||
agent.steer("Focus on the business impact, not technical details")
|
||||
agent.steer("Also include information about market size", priority=10)
|
||||
```
|
||||
"""
|
||||
if self._message_steering is None:
|
||||
logger.warning("Message steering not enabled - call ignored")
|
||||
return ""
|
||||
|
||||
return self._message_steering.queue_message(message, priority)
|
||||
|
||||
def get_steering_status(self) -> Dict[str, Any]:
|
||||
"""Get current steering status."""
|
||||
if self._message_steering is None:
|
||||
return {"enabled": False, "pending_count": 0}
|
||||
|
||||
return {
|
||||
"enabled": self._message_steering.enabled,
|
||||
"pending_count": len(self._message_steering.get_pending_messages()),
|
||||
"has_pending": self._message_steering.has_pending_messages()
|
||||
}
|
||||
|
||||
@property
|
||||
def message_steering_enabled(self) -> bool:
|
||||
"""Whether message steering is enabled for this agent."""
|
||||
return self._message_steering is not None and self._message_steering.enabled
|
||||
|
||||
def _check_steering_messages(self) -> Optional[str]:
|
||||
"""
|
||||
Check for and process steering messages.
|
||||
|
||||
This is called during agent execution loops to process
|
||||
any pending steering messages.
|
||||
|
||||
Returns:
|
||||
Steering message to inject into conversation, or None
|
||||
"""
|
||||
if self._message_steering is None:
|
||||
return None
|
||||
|
||||
# Process steering messages
|
||||
context = {}
|
||||
if self._message_steering.process_steering(context):
|
||||
steering_messages = context.get("steering_messages", [])
|
||||
if steering_messages:
|
||||
# Get the most recent message
|
||||
latest = steering_messages[-1]
|
||||
content = latest["content"]
|
||||
priority = latest["priority"]
|
||||
|
||||
# Format as system message
|
||||
if priority == "INTERRUPT":
|
||||
return f"\n[INTERRUPT USER GUIDANCE]: {content}\nPlease stop current work and follow this guidance immediately."
|
||||
elif priority in ("HIGH", "URGENT"):
|
||||
return f"\n[URGENT USER GUIDANCE]: {content}\nPlease acknowledge and adjust your approach accordingly."
|
||||
else:
|
||||
return f"\n[USER GUIDANCE]: {content}\nPlease consider this feedback as you continue."
|
||||
|
||||
return None
|
||||
@@ -10,6 +10,25 @@ This enables:
|
||||
These protocols are lightweight and have zero performance impact.
|
||||
"""
|
||||
from typing import Protocol, runtime_checkable, Optional, Any, AsyncIterator, Dict, List
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class SteeringPriority(Enum):
|
||||
"""Priority levels for steering messages."""
|
||||
LOW = 1
|
||||
NORMAL = 5
|
||||
HIGH = 10
|
||||
URGENT = 20
|
||||
INTERRUPT = 30 # Immediate interruption
|
||||
|
||||
|
||||
@dataclass
|
||||
class SteeringMessage:
|
||||
"""A steering message with priority and metadata."""
|
||||
content: str
|
||||
priority: SteeringPriority = SteeringPriority.NORMAL
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -431,6 +450,140 @@ class ManagedBackendProtocol(Protocol):
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class MessageSteeringProtocol(Protocol):
|
||||
"""
|
||||
Protocol for message steering during agent execution.
|
||||
|
||||
This protocol enables real-time communication with agents during
|
||||
long-running operations, allowing users to provide mid-execution
|
||||
guidance, feedback, and course corrections.
|
||||
|
||||
Example:
|
||||
```python
|
||||
class BasicMessageSteering:
|
||||
def queue_message(self, message: str, priority: int = 5) -> str:
|
||||
# Add to internal queue
|
||||
return "msg_001"
|
||||
|
||||
def get_pending_messages(self) -> List[SteeringMessage]:
|
||||
# Return queued messages
|
||||
return []
|
||||
|
||||
def process_steering(self, context: dict) -> bool:
|
||||
# Check for messages and update context
|
||||
return False
|
||||
|
||||
agent = Agent(name="assistant", message_steering=BasicMessageSteering())
|
||||
```
|
||||
"""
|
||||
|
||||
def queue_message(self, message: str, priority: int = 5) -> str:
|
||||
"""
|
||||
Queue a steering message for the agent.
|
||||
|
||||
Args:
|
||||
message: The steering message content
|
||||
priority: Message priority (higher = more urgent)
|
||||
|
||||
Returns:
|
||||
Message ID for tracking
|
||||
"""
|
||||
...
|
||||
|
||||
def get_pending_messages(self) -> List[Any]:
|
||||
"""
|
||||
Get all pending steering messages.
|
||||
|
||||
Returns:
|
||||
List of pending messages ordered by priority
|
||||
"""
|
||||
...
|
||||
|
||||
def process_steering(self, context: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Process any pending steering messages and update execution context.
|
||||
|
||||
This method is called during agent execution to check for
|
||||
and process any steering messages that have been queued.
|
||||
|
||||
Args:
|
||||
context: Optional execution context to update
|
||||
|
||||
Returns:
|
||||
True if messages were processed, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def clear_messages(self) -> int:
|
||||
"""
|
||||
Clear all pending messages.
|
||||
|
||||
Returns:
|
||||
Number of messages cleared
|
||||
"""
|
||||
...
|
||||
|
||||
def has_pending_messages(self) -> bool:
|
||||
"""
|
||||
Check if there are pending steering messages.
|
||||
|
||||
Returns:
|
||||
True if messages are pending, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""
|
||||
Whether message steering is enabled.
|
||||
|
||||
Returns:
|
||||
True if steering is enabled, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AgentSteeringProtocol(Protocol):
|
||||
"""
|
||||
Protocol for agents that support message steering.
|
||||
|
||||
This extends the agent interface with steering capabilities,
|
||||
allowing real-time message injection during execution.
|
||||
"""
|
||||
|
||||
def steer(self, message: str, priority: int = 5) -> str:
|
||||
"""
|
||||
Send a steering message to the agent during execution.
|
||||
|
||||
This can be called while the agent is running to provide
|
||||
real-time guidance or course corrections.
|
||||
|
||||
Args:
|
||||
message: The steering message
|
||||
priority: Message priority (higher = more urgent)
|
||||
|
||||
Returns:
|
||||
Message ID for tracking
|
||||
"""
|
||||
...
|
||||
|
||||
def get_steering_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get current steering status.
|
||||
|
||||
Returns:
|
||||
Dict with pending message count, last processed time, etc.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def message_steering_enabled(self) -> bool:
|
||||
"""Whether message steering is enabled for this agent."""
|
||||
...
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AgentProtocol',
|
||||
'RunnableAgentProtocol',
|
||||
@@ -441,4 +594,8 @@ __all__ = [
|
||||
'HttpLauncherProtocol',
|
||||
'McpLauncherProtocol',
|
||||
'ManagedBackendProtocol',
|
||||
'SteeringPriority',
|
||||
'SteeringMessage',
|
||||
'MessageSteeringProtocol',
|
||||
'AgentSteeringProtocol',
|
||||
]
|
||||
@@ -5,15 +5,10 @@ Provides safe code execution capabilities to agents via the sandbox framework.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, Union
|
||||
from typing import Optional, Dict, Any, Union, TYPE_CHECKING
|
||||
|
||||
from ..sandbox import (
|
||||
SandboxConfig,
|
||||
SandboxManager,
|
||||
SandboxResult,
|
||||
check_code_safety,
|
||||
format_warnings,
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
from ..sandbox import SandboxConfig, SandboxManager, SandboxResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,27 +21,33 @@ class SandboxMixin:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Extract sandbox config from kwargs
|
||||
self.sandbox_config: Optional[SandboxConfig] = kwargs.pop('sandbox', None)
|
||||
sandbox = kwargs.pop('sandbox', None)
|
||||
|
||||
# Convert boolean True to default config
|
||||
if self.sandbox_config is True:
|
||||
self.sandbox_config = SandboxConfig.subprocess()
|
||||
if sandbox is True:
|
||||
from ..sandbox import SandboxConfig
|
||||
sandbox = SandboxConfig.subprocess()
|
||||
elif sandbox is False:
|
||||
sandbox = None
|
||||
|
||||
self.sandbox_config: Optional['SandboxConfig'] = sandbox
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._sandbox_manager: Optional[SandboxManager] = None
|
||||
self._sandbox_manager: Optional['SandboxManager'] = None
|
||||
|
||||
@property
|
||||
def has_sandbox(self) -> bool:
|
||||
"""Check if sandbox is configured for this agent."""
|
||||
return self.sandbox_config is not None
|
||||
|
||||
def get_sandbox_manager(self) -> Optional[SandboxManager]:
|
||||
def get_sandbox_manager(self) -> Optional['SandboxManager']:
|
||||
"""Get or create sandbox manager."""
|
||||
if not self.has_sandbox:
|
||||
return None
|
||||
|
||||
if self._sandbox_manager is None:
|
||||
from ..sandbox import SandboxManager
|
||||
self._sandbox_manager = SandboxManager(self.sandbox_config)
|
||||
|
||||
return self._sandbox_manager
|
||||
@@ -57,7 +58,7 @@ class SandboxMixin:
|
||||
language: str = "python",
|
||||
check_security: bool = True,
|
||||
**kwargs
|
||||
) -> SandboxResult:
|
||||
) -> 'SandboxResult':
|
||||
"""Execute code safely in configured sandbox.
|
||||
|
||||
Args:
|
||||
@@ -79,6 +80,7 @@ class SandboxMixin:
|
||||
|
||||
# Security pre-check if enabled
|
||||
if check_security:
|
||||
from ..sandbox import check_code_safety, format_warnings
|
||||
warnings = check_code_safety(code, language)
|
||||
if warnings:
|
||||
warning_text = format_warnings(warnings)
|
||||
@@ -97,7 +99,7 @@ class SandboxMixin:
|
||||
language: str = "python",
|
||||
check_security: bool = True,
|
||||
**kwargs
|
||||
) -> SandboxResult:
|
||||
) -> 'SandboxResult':
|
||||
"""Synchronous wrapper for execute_code.
|
||||
|
||||
Args:
|
||||
@@ -117,7 +119,7 @@ class SandboxMixin:
|
||||
command: Union[str, list],
|
||||
check_security: bool = True,
|
||||
**kwargs
|
||||
) -> SandboxResult:
|
||||
) -> 'SandboxResult':
|
||||
"""Run a shell command in the sandbox.
|
||||
|
||||
Args:
|
||||
@@ -139,6 +141,7 @@ class SandboxMixin:
|
||||
|
||||
# Security pre-check
|
||||
if check_security:
|
||||
from ..sandbox import check_code_safety, format_warnings
|
||||
warnings = check_code_safety(command_str, "bash")
|
||||
if warnings:
|
||||
warning_text = format_warnings(warnings)
|
||||
|
||||
Loaded 100 of 545 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user