mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 01:08:39 -10:00
Compare commits
230
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e4a1c77f4 | ||
|
|
7ae9a70ba2 | ||
|
|
8e6b50c11a | ||
|
|
f015ac7753 | ||
|
|
1277a7a08b | ||
|
|
a5add02561 | ||
|
|
c25c1cc18e | ||
|
|
8f35439583 | ||
|
|
21ed0a4379 | ||
|
|
75e17ae7c2 | ||
|
|
3c3188c8c1 | ||
|
|
d76b56ec6b | ||
|
|
f5d3e26de9 | ||
|
|
ec52231f62 | ||
|
|
81dfb6a555 | ||
|
|
bc52cd67fd | ||
|
|
d4b563987f | ||
|
|
4c68e61438 | ||
|
|
e42a75d5d7 | ||
|
|
c13b9a7489 | ||
|
|
6693a7552e | ||
|
|
135412c338 | ||
|
|
0c37498118 | ||
|
|
be72b83521 | ||
|
|
b868e20f61 | ||
|
|
83a1e95f20 | ||
|
|
33cc2a9614 | ||
|
|
d4a8b4766d | ||
|
|
7a5635ff3d | ||
|
|
5f7f155ea7 | ||
|
|
ab62317ea9 | ||
|
|
1eafb07846 | ||
|
|
40e40174db | ||
|
|
0156893f56 | ||
|
|
6ea089db11 | ||
|
|
a2edba05c8 | ||
|
|
fb6d8ed427 | ||
|
|
635ab502a1 | ||
|
|
56c4f7d626 | ||
|
|
dfcb1c8392 | ||
|
|
fc75ed8671 | ||
|
|
86acdf59b7 | ||
|
|
70085486a3 | ||
|
|
e788d6f355 | ||
|
|
b93f6489cc | ||
|
|
4a838852b1 | ||
|
|
40fde37e1e | ||
|
|
462b7eb809 | ||
|
|
f25c8c2e88 | ||
|
|
2ab04d2160 | ||
|
|
7e0536ab63 | ||
|
|
184194ea7a | ||
|
|
0f689671f6 | ||
|
|
0af82c8cfc | ||
|
|
17dce2cbe0 | ||
|
|
d4c4872478 | ||
|
|
a67ad5fe2e | ||
|
|
13811df460 | ||
|
|
70116adba3 | ||
|
|
e7feea03f2 | ||
|
|
f3653b9759 | ||
|
|
64d7b96a99 | ||
|
|
2cdc08e61d | ||
|
|
0e9a67e3ee | ||
|
|
e9444030ab | ||
|
|
961f5046c9 | ||
|
|
0c663e6b7c | ||
|
|
00c6f77b28 | ||
|
|
1248e7791c | ||
|
|
3414e55b1b | ||
|
|
6e363e470a | ||
|
|
dd0e02c056 | ||
|
|
365f75040f | ||
|
|
88f364c34d | ||
|
|
4ac0d883cf | ||
|
|
78af42457c | ||
|
|
0accebb2e3 | ||
|
|
757559f67f | ||
|
|
43a5d097dd | ||
|
|
18b4914de2 | ||
|
|
4675577a3f | ||
|
|
426ecfc90a | ||
|
|
c9b8685d27 | ||
|
|
7ea4b9ad13 | ||
|
|
defa325754 | ||
|
|
64d048c897 | ||
|
|
2806f47093 | ||
|
|
8db093245e | ||
|
|
78523f8911 | ||
|
|
51b8761303 | ||
|
|
c8144d6532 | ||
|
|
9327462a89 | ||
|
|
24f1945c70 | ||
|
|
9ac9dc5aed | ||
|
|
fde17acdc8 | ||
|
|
5ed5f1a6a9 | ||
|
|
cc1853f0dc | ||
|
|
147aa701dd | ||
|
|
6496455c45 | ||
|
|
239660715a | ||
|
|
8907022b3a | ||
|
|
bb0d1968ad | ||
|
|
2e7ffe7338 | ||
|
|
a2b44dc100 | ||
|
|
ca4ee2e42b | ||
|
|
40f8d0d62c | ||
|
|
f74012e374 | ||
|
|
1145252406 | ||
|
|
f74ee5d619 | ||
|
|
fe30293e85 | ||
|
|
6865574255 | ||
|
|
b4e3a8a84a | ||
|
|
0889f800c1 | ||
|
|
e84d2b18da | ||
|
|
dfbc68f92f | ||
|
|
4b806f8eca | ||
|
|
f00763937b | ||
|
|
d0523d5b64 | ||
|
|
dd41a3c426 | ||
|
|
e87cc650dc | ||
|
|
3ac10ab851 | ||
|
|
2ecc383e6e | ||
|
|
35761ea88e | ||
|
|
e122d7d227 | ||
|
|
2284535f65 | ||
|
|
eb2cb40b30 | ||
|
|
459688d32d | ||
|
|
b7f3e55669 | ||
|
|
1cd48fde4e | ||
|
|
584d85f305 | ||
|
|
42807f1740 | ||
|
|
0d40fa93aa | ||
|
|
75439026e7 | ||
|
|
616532d4ed | ||
|
|
6693274671 | ||
|
|
142db6c5d5 | ||
|
|
a2d915f6ab | ||
|
|
e40269d194 | ||
|
|
d2ce315ce1 | ||
|
|
f069930d67 | ||
|
|
b58a85a669 | ||
|
|
6d60068661 | ||
|
|
5fa2a7fc47 | ||
|
|
fbdcb92ff2 | ||
|
|
18da39991a | ||
|
|
1c32d811f7 | ||
|
|
27de2635eb | ||
|
|
d6bdc7e0de | ||
|
|
92c6a13cac | ||
|
|
5882eb0ffc | ||
|
|
7bcba30b14 | ||
|
|
ce3c3c8d59 | ||
|
|
4dd917e303 | ||
|
|
16c08ba682 | ||
|
|
9b7f458b35 | ||
|
|
83f7babd89 | ||
|
|
0d65b5aaf9 | ||
|
|
ab90f57c8e | ||
|
|
6fbf238628 | ||
|
|
22e1e0bdb0 | ||
|
|
cc5683c5c4 | ||
|
|
d4e82d3909 | ||
|
|
35146cd1c8 | ||
|
|
3ad181950c | ||
|
|
78d07eb09f | ||
|
|
09970cd348 | ||
|
|
387b81d324 | ||
|
|
ae1cf98fc9 | ||
|
|
52354bf3f4 | ||
|
|
5bd317178a | ||
|
|
94bc2ce611 | ||
|
|
55287d4cc3 | ||
|
|
a8b72b046f | ||
|
|
69fce71ad5 | ||
|
|
e4e2657f17 | ||
|
|
56443bf3f8 | ||
|
|
258ab1cd20 | ||
|
|
63248a3a46 | ||
|
|
331b47013e | ||
|
|
162c10c16f | ||
|
|
cdefa98c09 | ||
|
|
5b2d924121 | ||
|
|
9fcf1b4acd | ||
|
|
7bec5453d0 | ||
|
|
7c87046753 | ||
|
|
81ea4d7706 | ||
|
|
f1d53a409b | ||
|
|
384bb06210 | ||
|
|
2dd1a80fde | ||
|
|
d4a1b6e64b | ||
|
|
09e85a24d2 | ||
|
|
5990367b3d | ||
|
|
01a20e307c | ||
|
|
ed3a689f8c | ||
|
|
7313b64544 | ||
|
|
dacb1dc42c | ||
|
|
74241c1459 | ||
|
|
b1048da6a5 | ||
|
|
ffc8cb1bbc | ||
|
|
eceaa96141 | ||
|
|
e632aadd42 | ||
|
|
bbcc505693 | ||
|
|
cc0544a4e3 | ||
|
|
b8455205f6 | ||
|
|
a10af4e839 | ||
|
|
ff37cbcf6f | ||
|
|
a42c6d325e | ||
|
|
724e4af963 | ||
|
|
7cc9718c41 | ||
|
|
72e477f4a3 | ||
|
|
ecfd858269 | ||
|
|
92c6690ab2 | ||
|
|
efb92fdd2e | ||
|
|
9c2f14e042 | ||
|
|
8b0567262d | ||
|
|
d80bff2d9a | ||
|
|
3e29936e86 | ||
|
|
34c6423dbc | ||
|
|
6b4e1b3590 | ||
|
|
7eb01489fd | ||
|
|
731e222a53 | ||
|
|
df50822ff6 | ||
|
|
7480c4389a | ||
|
|
494cd8b44a | ||
|
|
2e733e16d7 | ||
|
|
97141c75bd | ||
|
|
c660495911 | ||
|
|
0cb35a4fe3 | ||
|
|
3622b1a3d9 | ||
|
|
20d418e1de |
No files matched your search
@@ -70,6 +70,8 @@ runs:
|
||||
|
||||
- name: Install GitHub MCP Server
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
run: |
|
||||
claude mcp add-json github '{
|
||||
"command": "docker",
|
||||
@@ -82,7 +84,7 @@ runs:
|
||||
"ghcr.io/github/github-mcp-server:sha-ff3036d"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ inputs.github_token }}"
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "'"${INPUT_GITHUB_TOKEN}"'"
|
||||
}
|
||||
}'
|
||||
|
||||
@@ -207,21 +209,44 @@ runs:
|
||||
User Request:
|
||||
$USER_REQUEST
|
||||
|
||||
Please analyze the request and provide helpful assistance. You have access to repository tools and can help with:
|
||||
- Code analysis and implementation
|
||||
- GitHub workflows and automation
|
||||
- Pull request reviews and feedback
|
||||
- Issue resolution and bug fixes
|
||||
- Documentation updates
|
||||
- Testing and deployment
|
||||
|
||||
Please analyze the request and provide helpful assistance.
|
||||
|
||||
CRITICAL INSTRUCTION: If your task involves modifying codebase files to add a feature or fix a bug:
|
||||
1. ALWAYS create a new git branch appropriately named (e.g. \`feature/issue-<number>\`).
|
||||
2. MANDATORY: Before writing any code, you MUST carefully read "src/praisonai-agents/AGENTS.md" to understand this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy.
|
||||
3. Make your necessary code changes honoring the rules in AGENTS.md.
|
||||
4. Commit your changes and push the branch to the remote repository.
|
||||
5. Use the GitHub CLI (\`gh pr create --title "..." --body "Fixes #<number>"\`) to open a Pull Request containing your work.
|
||||
|
||||
|
||||
STEP 0 — SETUP GIT IDENTITY:
|
||||
git config user.name "MervinPraison"
|
||||
git config user.email "[email protected]"
|
||||
|
||||
STEP 1 — READ GUIDELINES:
|
||||
MANDATORY: Before writing any code, you MUST carefully read "src/praisonai-agents/AGENTS.md" to understand this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy.
|
||||
|
||||
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
|
||||
Before implementing anything, answer these questions:
|
||||
- CORE vs WRAPPER vs TOOLS ROUTING:
|
||||
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
|
||||
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
|
||||
3. Tools (PraisonAI-Tools): Specific integrations, external tools (e.g. SurrealDB, Slack), and community extensions MUST go to the `MervinPraison/PraisonAI-Tools` repository. If the feature is a tool, clone `https://github.com/MervinPraison/PraisonAI-Tools`, implement it there, and create the PR using `gh pr create -R MervinPraison/PraisonAI-Tools`.
|
||||
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory).
|
||||
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
|
||||
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
|
||||
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead.
|
||||
- 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:
|
||||
- Create a new git branch appropriately named (e.g. \`feature/issue-<number>\`)
|
||||
- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper
|
||||
- Keep changes small and backward-compatible
|
||||
|
||||
STEP 4 — TEST:
|
||||
- Run: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
|
||||
- Ensure no regressions
|
||||
|
||||
STEP 5 — CREATE PR:
|
||||
- Commit with descriptive message, push the branch
|
||||
- Use the GitHub CLI (\`gh pr create --title "..." --body "Fixes #<number>"\`) to open a Pull Request
|
||||
CRITICAL: You MUST create the PR automatically using \`gh pr create\`. Do NOT just provide a link.
|
||||
|
||||
Respond naturally and helpfully to the user's request using the available tools.
|
||||
EOF
|
||||
|
||||
@@ -231,6 +256,14 @@ runs:
|
||||
- name: Run Claude Code
|
||||
if: env.SKIP_EXECUTION != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
INPUT_MAX_TURNS: ${{ inputs.max_turns }}
|
||||
INPUT_ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
||||
INPUT_DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
|
||||
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
|
||||
run: |
|
||||
echo "🚀 Running Claude Code with GitHub context..."
|
||||
|
||||
@@ -238,30 +271,26 @@ runs:
|
||||
CMD_ARGS=("-p" "--verbose" "--output-format" "stream-json")
|
||||
|
||||
# Add max turns if specified
|
||||
if [[ -n "${{ inputs.max_turns }}" ]]; then
|
||||
CMD_ARGS+=("--max-turns" "${{ inputs.max_turns }}")
|
||||
if [[ -n "$INPUT_MAX_TURNS" ]]; then
|
||||
CMD_ARGS+=("--max-turns" "$INPUT_MAX_TURNS")
|
||||
fi
|
||||
|
||||
# Add allowed tools (include GitHub tools by default)
|
||||
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_comment,Read,Write,Edit,Bash"
|
||||
if [[ -n "${{ inputs.allowed_tools }}" ]]; then
|
||||
TOOLS="$TOOLS,${{ inputs.allowed_tools }}"
|
||||
if [[ -n "$INPUT_ALLOWED_TOOLS" ]]; then
|
||||
TOOLS="$TOOLS,$INPUT_ALLOWED_TOOLS"
|
||||
fi
|
||||
CMD_ARGS+=("--allowedTools" "$TOOLS")
|
||||
|
||||
# Add disallowed tools
|
||||
if [[ -n "${{ inputs.disallowed_tools }}" ]]; then
|
||||
CMD_ARGS+=("--disallowedTools" "${{ inputs.disallowed_tools }}")
|
||||
if [[ -n "$INPUT_DISALLOWED_TOOLS" ]]; then
|
||||
CMD_ARGS+=("--disallowedTools" "$INPUT_DISALLOWED_TOOLS")
|
||||
fi
|
||||
|
||||
echo "📝 Executing Claude Code with prompt from file..."
|
||||
|
||||
# Execute Claude Code with timeout, using stdin for the prompt
|
||||
TIMEOUT_SECONDS=$((${{ inputs.timeout_minutes }} * 60))
|
||||
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "${{ env.PROMPT_FILE }}"
|
||||
TIMEOUT_SECONDS=$(($INPUT_TIMEOUT_MINUTES * 60))
|
||||
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "$PROMPT_FILE"
|
||||
|
||||
echo "✅ Claude Code execution completed"
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
echo "✅ Claude Code execution completed"
|
||||
@@ -19,6 +19,7 @@ runs:
|
||||
- name: Checkout repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create prompt
|
||||
|
||||
@@ -57,13 +57,15 @@ runs:
|
||||
|
||||
- name: Install Gemini CLI
|
||||
id: install
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [[ "${{ inputs.version }}" == "latest" || "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.-]+)?(\+[a-zA-Z0-9\.-]+)?$ ]]; then
|
||||
echo "Installing Gemini CLI from npm: @google/gemini-cli@${{ inputs.version }}"
|
||||
npm install -g @google/gemini-cli@${{ inputs.version }}
|
||||
if [[ "$INPUT_VERSION" == "latest" || "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.-]+)?(\+[a-zA-Z0-9\.-]+)?$ ]]; then
|
||||
echo "Installing Gemini CLI from npm: @google/gemini-cli@$INPUT_VERSION"
|
||||
npm install -g @google/gemini-cli@$INPUT_VERSION
|
||||
else
|
||||
echo "Installing Gemini CLI from GitHub: github:google-gemini/gemini-cli#${{ inputs.version }}"
|
||||
npm install -g github:google-gemini/gemini-cli#${{ inputs.version }}
|
||||
echo "Installing Gemini CLI from GitHub: github:google-gemini/gemini-cli#$INPUT_VERSION"
|
||||
npm install -g github:google-gemini/gemini-cli#$INPUT_VERSION
|
||||
fi
|
||||
echo "Verifying installation:"
|
||||
if command -v gemini >/dev/null 2>&1; then
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
@@ -0,0 +1,72 @@
|
||||
framework: "praisonai"
|
||||
process: workflow
|
||||
topic: "Autonomous Issue Resolution"
|
||||
planning: false
|
||||
|
||||
roles:
|
||||
issue_analyst:
|
||||
role: "GitHub Issue Analyst"
|
||||
goal: "Read the GitHub issue, understand the required changes, and map the problem to the local codebase"
|
||||
backstory: "You are an expert software analyst who reads bug reports and feature requests. You excel at taking a problem description and finding exactly which files in the codebase need to be modified. Your primary job is discovery and planning."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
software_engineer:
|
||||
role: "Autonomous Software Engineer"
|
||||
goal: "Implement required code changes on a branch, run tests, commit, and push"
|
||||
backstory: "You are a senior software engineer. You always branch first, implement changes using shell commands, verify with git diff, run tests, then commit and push. You never create a Pull Request unless the issue body explicitly requests one."
|
||||
tools:
|
||||
- "execute_command"
|
||||
- "github_create_branch"
|
||||
- "github_commit_and_push"
|
||||
- "github_create_pull_request"
|
||||
|
||||
steps:
|
||||
- name: Analyze Issue
|
||||
agent: issue_analyst
|
||||
action: >
|
||||
Thoroughly analyze the issue and the repository. Follow these steps in order.
|
||||
|
||||
First, set up git identity: execute_command 'git config user.name PraisonAI' then
|
||||
execute_command 'git config user.email [email protected]'. Do NOT pass cwd.
|
||||
|
||||
Second, read the issue: execute_command 'cat .github/triage-context.md'. Do NOT pass cwd.
|
||||
|
||||
Third, read architecture guidelines: execute_command 'cat src/praisonai-agents/AGENTS.md'.
|
||||
|
||||
Fourth, decide if this issue requires code changes:
|
||||
- If YES: identify the exact files and lines that must change. Output a concrete file-level plan.
|
||||
- If NO (question, discussion, duplicate, invalid): say so clearly. No code changes needed.
|
||||
|
||||
Output: routing decision (Core SDK / Wrapper / Tools / No-change), validation result, and exact files/lines to change.
|
||||
expected_output: A decision on whether code changes are needed, and if so a concrete file-level plan.
|
||||
|
||||
- name: Implement Fix
|
||||
agent: software_engineer
|
||||
action: >
|
||||
You are working inside the repository at /Users/praison/praisonai-package.
|
||||
Do NOT ask for confirmation. Do NOT use placeholder paths. Act immediately.
|
||||
|
||||
Read the analyst's output first. If the analyst said NO code changes are needed, stop here and
|
||||
output a brief explanation — do NOT create a branch or PR.
|
||||
|
||||
If code changes ARE needed, follow these steps:
|
||||
|
||||
Step 1 — Create branch: github_create_branch with branch_name=praisonai/issue-{{ISSUE_NUMBER}}.
|
||||
|
||||
Step 2 — Implement changes using execute_command (no cwd argument).
|
||||
To insert a line: python3 -c "lines=open('FILE').readlines(); lines.insert(N,'LINE\n'); open('FILE','w').writelines(lines)"
|
||||
To replace text: python3 -c "t=open('FILE').read(); open('FILE','w').write(t.replace('OLD','NEW'))"
|
||||
After each edit verify: execute_command 'git diff'
|
||||
|
||||
Step 3 — Run tests: execute_command 'python3 -m pytest src/praisonai-agents/tests/ -x -q --timeout=30 -p no:warnings'
|
||||
|
||||
Step 4 — Commit and push: github_commit_and_push with a descriptive commit_message.
|
||||
|
||||
Step 5 — Check if the issue body EXPLICITLY contains the phrase "create pull request" or "create PR"
|
||||
(case-insensitive). ONLY in that case create a PR using github_create_pull_request with
|
||||
title='fix: resolve issue #{{ISSUE_NUMBER}}',
|
||||
body='Fixes #{{ISSUE_NUMBER}}.\n\nAutonomously resolved by PraisonAI.',
|
||||
head_branch=praisonai/issue-{{ISSUE_NUMBER}}, base_branch=main.
|
||||
Otherwise do NOT create a PR — just confirm the branch was pushed successfully.
|
||||
expected_output: Either "No code changes needed", or "Branch pushed, no PR requested", or "Branch pushed and PR created at URL".
|
||||
@@ -0,0 +1,116 @@
|
||||
framework: "praisonai"
|
||||
topic: "Pull Request Code Review Analysis"
|
||||
|
||||
roles:
|
||||
security_reviewer:
|
||||
role: "Security Code Reviewer"
|
||||
goal: "Identify security vulnerabilities, authentication issues, input validation problems, and unsafe code practices in the PR changes"
|
||||
backstory: "You are a cybersecurity expert specializing in code analysis. You have extensive experience in identifying common security flaws like injection attacks, authentication bypasses, exposed secrets, and unsafe data handling patterns."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
performance_reviewer:
|
||||
role: "Performance Code Reviewer"
|
||||
goal: "Analyze code changes for performance implications, identify bottlenecks, inefficient algorithms, and resource usage issues"
|
||||
backstory: "You are a performance optimization specialist with deep knowledge of algorithm efficiency, memory management, and system performance. You excel at spotting code that could cause performance degradation."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
maintainability_reviewer:
|
||||
role: "Code Quality & Maintainability Reviewer"
|
||||
goal: "Evaluate code structure, readability, documentation, naming conventions, and adherence to best practices"
|
||||
backstory: "You are a software engineering expert focused on code quality and maintainability. You ensure code follows established patterns, is well-documented, and will be easy for future developers to understand and modify."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
lead_reviewer:
|
||||
role: "Lead Technical Reviewer"
|
||||
goal: "Synthesize all review feedback, make final recommendations, and post comprehensive review comments to the GitHub PR"
|
||||
backstory: "You are a senior technical lead responsible for final review decisions. You coordinate input from security, performance, and maintainability reviewers to provide balanced, actionable feedback to the development team."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
steps:
|
||||
- name: security_analysis
|
||||
agent: security_reviewer
|
||||
action: |
|
||||
Analyze the PR diff for security issues:
|
||||
1. Extract PR details: `echo "$PR_DATA"`
|
||||
2. Get changed files: `echo "$CHANGED_FILES"`
|
||||
3. Review full diff: `gh pr diff ${PR_NUMBER}`
|
||||
4. Look for:
|
||||
- Hardcoded secrets, API keys, passwords
|
||||
- SQL injection vulnerabilities
|
||||
- XSS vulnerabilities
|
||||
- Authentication/authorization bypasses
|
||||
- Unsafe file operations
|
||||
- Command injection risks
|
||||
- Missing input validation
|
||||
- Exposed sensitive data
|
||||
5. Document findings with file paths and line numbers
|
||||
expected_output: "Detailed security analysis report with specific vulnerabilities found, severity levels, and remediation suggestions"
|
||||
|
||||
- name: performance_analysis
|
||||
agent: performance_reviewer
|
||||
action: |
|
||||
Analyze the PR diff for performance issues:
|
||||
1. Review algorithm complexity changes
|
||||
2. Check for inefficient database queries
|
||||
3. Identify memory leaks or excessive allocations
|
||||
4. Look for hot-path regressions
|
||||
5. Check for blocking I/O operations
|
||||
6. Analyze module-level imports that could slow startup
|
||||
7. Review caching implementations
|
||||
8. Check for recursive operations without termination
|
||||
9. Document performance concerns with impact estimates
|
||||
expected_output: "Performance analysis report highlighting bottlenecks, efficiency concerns, and optimization recommendations"
|
||||
|
||||
- name: maintainability_analysis
|
||||
agent: maintainability_reviewer
|
||||
action: |
|
||||
Analyze the PR diff for code quality and maintainability:
|
||||
1. Check naming conventions consistency
|
||||
2. Evaluate code structure and organization
|
||||
3. Review documentation completeness
|
||||
4. Check for DRY violations
|
||||
5. Analyze error handling patterns
|
||||
6. Review test coverage for changes
|
||||
7. Check adherence to repository coding standards
|
||||
8. Evaluate API design consistency
|
||||
9. Look for technical debt introduction
|
||||
expected_output: "Code quality analysis with specific recommendations for improving maintainability and readability"
|
||||
|
||||
- name: final_review
|
||||
agent: lead_reviewer
|
||||
action: |
|
||||
Synthesize all reviews and post final comprehensive feedback:
|
||||
1. Collect input from security, performance, and maintainability reviews
|
||||
2. Prioritize findings by severity (Critical, High, Medium, Low)
|
||||
3. Create structured review comment using this format:
|
||||
|
||||
## 📋 Review Summary
|
||||
[2-3 sentence overview and assessment]
|
||||
|
||||
## 🔍 General Feedback
|
||||
[Overall patterns and observations]
|
||||
|
||||
## 🎯 Specific Feedback
|
||||
### 🔴 Critical (if any)
|
||||
[Security vulnerabilities, breaking changes, major bugs]
|
||||
|
||||
### 🟡 High (if any)
|
||||
[Performance issues, design flaws, significant bugs]
|
||||
|
||||
### 🟢 Medium (if any)
|
||||
[Code quality improvements, minor optimizations]
|
||||
|
||||
### 🔵 Low (if any)
|
||||
[Documentation, naming suggestions, minor refactoring]
|
||||
|
||||
## ✅ Highlights (if any)
|
||||
[Positive aspects worth mentioning]
|
||||
|
||||
4. Post comprehensive review: `echo "[REVIEW_CONTENT]" | gh pr comment ${PR_NUMBER} -F -`
|
||||
5. If critical issues found, suggest not merging until resolved
|
||||
expected_output: "Final review comment posted to GitHub PR with comprehensive analysis from all reviewers"
|
||||
dependencies: [security_analysis, performance_analysis, maintainability_analysis]
|
||||
@@ -0,0 +1,3 @@
|
||||
# GitHub Issue #1350: Add debug log on successful branch checkout
|
||||
|
||||
The github_create_branch function in src/praisonai-agents/praisonaiagents/tools/github_tools.py should call logger.debug after a successful checkout.
|
||||
@@ -1,11 +1,14 @@
|
||||
name: Auto PR Comment
|
||||
|
||||
# Review chain: CodeRabbit/Qodo/Gemini review first → THEN trigger Copilot
|
||||
# For human PRs: CodeRabbit/Qodo auto-review → this workflow triggers Copilot
|
||||
# Review chain: CodeRabbit/Qodo/Gemini review first → Copilot → Claude (final)
|
||||
# For human PRs: CodeRabbit/Qodo auto-review → this workflow triggers Copilot → then Claude
|
||||
# For bot PRs: CodeRabbit/Qodo skip bots, so we trigger them explicitly first
|
||||
#
|
||||
# IMPORTANT: @copilot ignores bot comments. All @copilot mentions must use
|
||||
# PAT_TOKEN so the comment posts as MervinPraison (a real user).
|
||||
#
|
||||
# Claude trigger runs IN-BAND (same workflow) to avoid GitHub Actions
|
||||
# blocking bot-triggered workflows with action_required approval gates.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
@@ -14,6 +17,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
env:
|
||||
MAX_POLL_ATTEMPTS: 20
|
||||
POLL_DELAY_MS: 30000
|
||||
CLAUDE_TRIGGER_LOGINS: '["MervinPraison","github-actions[bot]"]'
|
||||
|
||||
jobs:
|
||||
# ================================================================
|
||||
# HUMAN PRs: Trigger @copilot AFTER CodeRabbit posts its summary
|
||||
@@ -23,12 +31,16 @@ jobs:
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
github.event.comment.user.login == 'coderabbitai[bot]' &&
|
||||
contains(github.event.comment.body, 'summary by coderabbit')
|
||||
contains(github.event.comment.body, 'summarize by coderabbit')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
pr_number: ${{ steps.trigger.outputs.pr_number }}
|
||||
triggered: ${{ steps.trigger.outputs.triggered }}
|
||||
steps:
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
id: trigger
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
@@ -45,14 +57,18 @@ jobs:
|
||||
);
|
||||
if (alreadyPosted) {
|
||||
console.log('Copilot already triggered, skipping');
|
||||
core.setOutput('triggered', 'false');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
return;
|
||||
}
|
||||
github.rest.issues.createComment({
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused? Any unnecessary code or scope creep?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. **Address reviewer feedback**: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible'
|
||||
})
|
||||
});
|
||||
core.setOutput('triggered', 'true');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
|
||||
# Fallback: Trigger @copilot after Qodo review (if Coderabbit doesn't fire)
|
||||
copilot-after-qodo:
|
||||
@@ -62,8 +78,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
pr_number: ${{ steps.trigger.outputs.pr_number }}
|
||||
triggered: ${{ steps.trigger.outputs.triggered }}
|
||||
steps:
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
id: trigger
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
@@ -79,14 +99,377 @@ jobs:
|
||||
);
|
||||
if (alreadyPosted) {
|
||||
console.log('Copilot already triggered, skipping');
|
||||
core.setOutput('triggered', 'false');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
return;
|
||||
}
|
||||
github.rest.issues.createComment({
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above first.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. Suggest specific improvements with code examples where possible'
|
||||
})
|
||||
});
|
||||
core.setOutput('triggered', 'true');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
|
||||
# ================================================================
|
||||
# FALLBACK: Claude trigger for cases where Copilot chain fails/skips
|
||||
# Ensures Claude always runs after sufficient review time
|
||||
# ================================================================
|
||||
claude-fallback-timeout:
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for review window (15 min)
|
||||
run: sleep 900
|
||||
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
core.setOutput('comments_count', comments.data.length.toString());
|
||||
|
||||
// Collect all bot reviewer comments for context
|
||||
const botReviews = comments.data.filter(c =>
|
||||
['coderabbitai[bot]', 'qodo-code-review[bot]', 'gemini-code-assist[bot]',
|
||||
'copilot-swe-agent', 'Copilot', 'greptile-apps[bot]'].some(bot =>
|
||||
c.user.login.toLowerCase().includes(bot.toLowerCase())
|
||||
)
|
||||
);
|
||||
core.setOutput('review_count', botReviews.length.toString());
|
||||
|
||||
- name: Aggregate reviews and trigger Claude
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, per_page: 100});
|
||||
const reviews = {coderabbit: [], qodo: [], gemini: [], copilot: [], greptile: [], others: []};
|
||||
comments.data.forEach(c => {
|
||||
const login = c.user.login.toLowerCase();
|
||||
const body = c.body.substring(0, 500);
|
||||
if (login.includes('coderabbit')) reviews.coderabbit.push(body);
|
||||
else if (login.includes('qodo')) reviews.qodo.push(body);
|
||||
else if (login.includes('gemini')) reviews.gemini.push(body);
|
||||
else if (login.includes('copilot')) reviews.copilot.push(body);
|
||||
else if (login.includes('greptile')) reviews.greptile.push(body);
|
||||
else if (c.user.type === 'Bot') reviews.others.push(body);
|
||||
});
|
||||
const summaryParts = [];
|
||||
if (reviews.coderabbit.length) summaryParts.push('CodeRabbit: ' + reviews.coderabbit.length + ' comments');
|
||||
if (reviews.qodo.length) summaryParts.push('Qodo: ' + reviews.qodo.length + ' comments');
|
||||
if (reviews.gemini.length) summaryParts.push('Gemini: ' + reviews.gemini.length + ' comments');
|
||||
if (reviews.copilot.length) summaryParts.push('Copilot: ' + reviews.copilot.length + ' comments');
|
||||
if (reviews.greptile.length) summaryParts.push('Greptile: ' + reviews.greptile.length + ' comments');
|
||||
const reviewSummary = summaryParts.length ? summaryParts.join(' | ') : 'No bot reviews detected';
|
||||
const claudeBody = '@claude You are the FINAL architecture reviewer. ' + reviewSummary + '.\n\n' +
|
||||
'Review Context: ' + comments.data.length + ' total comments, ' +
|
||||
(reviews.coderabbit.length + reviews.qodo.length + reviews.gemini.length + reviews.copilot.length + reviews.greptile.length) + ' bot reviews.\n\n' +
|
||||
'Phase 1: Review per AGENTS.md principles (protocol-driven, backward compatible, no perf impact).\n' +
|
||||
'Phase 2: FIX valid issues found by prior reviewers.\n' +
|
||||
'Phase 3: Provide final verdict (approve or request changes).';
|
||||
await github.rest.issues.createComment({issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: claudeBody});
|
||||
console.log('Claude fallback triggered on PR #' + context.issue.number + ' with ' + reviewSummary);
|
||||
|
||||
# ================================================================
|
||||
# Claude FINAL review — polls for Copilot's response, then triggers
|
||||
# This runs IN-BAND under the PAT context (MervinPraison), avoiding
|
||||
# the action_required approval gate that blocks bot-triggered workflows.
|
||||
# ================================================================
|
||||
claude-after-copilot:
|
||||
needs: [copilot-after-coderabbit]
|
||||
if: |
|
||||
always() &&
|
||||
needs.copilot-after-coderabbit.result == 'success' &&
|
||||
needs.copilot-after-coderabbit.outputs.triggered == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for Copilot to respond (poll)
|
||||
id: poll
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
|
||||
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Poll for Copilot's response — check every 30s for up to 10 minutes
|
||||
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
|
||||
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
|
||||
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner,
|
||||
repo,
|
||||
per_page: 50,
|
||||
sort: 'created',
|
||||
direction: 'desc'
|
||||
});
|
||||
|
||||
// Look for Copilot's response (copilot-swe-agent or Copilot)
|
||||
const copilotResponse = comments.data.find(c =>
|
||||
(c.user.login === 'copilot-swe-agent' ||
|
||||
c.user.login === 'Copilot' ||
|
||||
c.user.login.toLowerCase().includes('copilot')) &&
|
||||
c.user.type === 'Bot'
|
||||
);
|
||||
|
||||
if (copilotResponse) {
|
||||
console.log(`Copilot responded at ${copilotResponse.created_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check PR reviews for Copilot
|
||||
const reviews = await github.rest.pulls.listReviews({
|
||||
pull_number: pr,
|
||||
owner,
|
||||
repo
|
||||
});
|
||||
|
||||
const copilotReview = reviews.data.find(r =>
|
||||
r.user.login.toLowerCase().includes('copilot')
|
||||
);
|
||||
|
||||
if (copilotReview) {
|
||||
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
if (i < maxAttempts - 1) {
|
||||
console.log(`Copilot hasn't responded yet, waiting ${delayMs/1000}s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Copilot did not respond within timeout, triggering Claude anyway');
|
||||
core.setOutput('copilot_responded', 'timeout');
|
||||
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Trigger Claude final review
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the FINAL architecture reviewer. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
console.log(`Claude triggered on PR #${pr}`);
|
||||
|
||||
# ================================================================
|
||||
# Claude after Qodo fallback path (same polling approach)
|
||||
# ================================================================
|
||||
claude-after-copilot-qodo:
|
||||
needs: [copilot-after-qodo]
|
||||
if: |
|
||||
always() &&
|
||||
needs.copilot-after-qodo.result == 'success' &&
|
||||
needs.copilot-after-qodo.outputs.triggered == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for Copilot to respond (poll)
|
||||
id: poll
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
|
||||
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
|
||||
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr, owner, repo,
|
||||
per_page: 50, sort: 'created', direction: 'desc'
|
||||
});
|
||||
const copilotResponse = comments.data.find(c =>
|
||||
(c.user.login === 'copilot-swe-agent' ||
|
||||
c.user.login === 'Copilot' ||
|
||||
c.user.login.toLowerCase().includes('copilot')) &&
|
||||
c.user.type === 'Bot'
|
||||
);
|
||||
if (copilotResponse) {
|
||||
console.log(`Copilot responded at ${copilotResponse.created_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
const reviews = await github.rest.pulls.listReviews({
|
||||
pull_number: pr, owner, repo
|
||||
});
|
||||
const copilotReview = reviews.data.find(r =>
|
||||
r.user.login.toLowerCase().includes('copilot')
|
||||
);
|
||||
if (copilotReview) {
|
||||
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
if (i < maxAttempts - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
console.log('Copilot did not respond within timeout, triggering Claude anyway');
|
||||
core.setOutput('copilot_responded', 'timeout');
|
||||
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Trigger Claude final review
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the FINAL architecture reviewer. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
console.log(`Claude triggered on PR #${pr}`);
|
||||
|
||||
# ================================================================
|
||||
# Claude after Gemini — restores the trigger lost from the deleted
|
||||
# chain-claude-after-copilot.yml workflow.
|
||||
# Fires when Gemini posts a review comment on a PR or issue.
|
||||
# ================================================================
|
||||
claude-after-gemini:
|
||||
if: |
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.user.login == 'gemini-code-assist[bot]' ||
|
||||
contains(github.event.comment.body, 'Review completed by Gemini CLI') ||
|
||||
contains(github.event.comment.body, 'The fix is ready for review. Please test')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Post Claude analysis request (Lead Engineer)
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the Lead Engineer. Read ALL analysis and reviews above carefully (Gemini, CodeRabbit, Qodo, Copilot, etc).\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
});
|
||||
|
||||
# ================================================================
|
||||
# BOT PRs: CodeRabbit/Qodo skip bot-authored PRs by default.
|
||||
@@ -106,7 +489,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Trigger CodeRabbit, Qodo, and Gemini reviews
|
||||
- name: Trigger CodeRabbit and Qodo reviews
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
@@ -129,12 +512,12 @@ jobs:
|
||||
});
|
||||
console.log(`Triggered Qodo for bot PR #${pr}`);
|
||||
|
||||
// 3. Trigger Gemini review
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr, owner, repo,
|
||||
body: '@gemini review this PR'
|
||||
});
|
||||
console.log(`Triggered Gemini for bot PR #${pr}`);
|
||||
// 3. Trigger Gemini review (commented out)
|
||||
// await github.rest.issues.createComment({
|
||||
// issue_number: pr, owner, repo,
|
||||
// body: '@gemini review this PR'
|
||||
// });
|
||||
// console.log(`Triggered Gemini for bot PR #${pr}`);
|
||||
|
||||
// NOTE: @copilot is NOT triggered here.
|
||||
// It will be triggered by copilot-after-coderabbit or copilot-after-qodo
|
||||
|
||||
@@ -20,6 +20,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
@@ -35,6 +37,8 @@ jobs:
|
||||
|
||||
- name: Run Import Time Benchmark
|
||||
id: import_time
|
||||
env:
|
||||
FAIL_ON_REGRESSION: ${{ inputs.fail_on_regression }}
|
||||
run: |
|
||||
cd src/praisonai-agents
|
||||
python benchmarks/import_time.py 2>&1 | tee import_time_output.txt
|
||||
@@ -46,7 +50,7 @@ jobs:
|
||||
# Check threshold (200ms)
|
||||
if (( $(echo "$IMPORT_TIME > 200" | bc -l) )); then
|
||||
echo "::warning::Import time ${IMPORT_TIME}ms exceeds 200ms threshold"
|
||||
if [ "${{ inputs.fail_on_regression }}" == "true" ]; then
|
||||
if [ "$FAIL_ON_REGRESSION" == "true" ]; then
|
||||
echo "import_passed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
@@ -55,6 +59,8 @@ jobs:
|
||||
|
||||
- name: Run Memory Usage Benchmark
|
||||
id: memory
|
||||
env:
|
||||
FAIL_ON_REGRESSION: ${{ inputs.fail_on_regression }}
|
||||
run: |
|
||||
cd src/praisonai-agents
|
||||
python benchmarks/memory_usage.py 2>&1 | tee memory_output.txt
|
||||
@@ -66,7 +72,7 @@ jobs:
|
||||
# Check threshold (30MB)
|
||||
if (( $(echo "$PEAK_MEMORY > 30" | bc -l) )); then
|
||||
echo "::warning::Memory usage ${PEAK_MEMORY}MB exceeds 30MB threshold"
|
||||
if [ "${{ inputs.fail_on_regression }}" == "true" ]; then
|
||||
if [ "$FAIL_ON_REGRESSION" == "true" ]; then
|
||||
echo "memory_passed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
@@ -95,9 +101,11 @@ jobs:
|
||||
|
||||
- name: Run Parameter Impact Benchmark
|
||||
id: param_impact
|
||||
env:
|
||||
INPUT_ITERATIONS: ${{ inputs.iterations }}
|
||||
run: |
|
||||
cd src/praisonai-agents
|
||||
python benchmarks/param_impact_benchmark.py --no-save --iterations ${{ inputs.iterations }} 2>&1 | tee param_impact_output.txt
|
||||
python benchmarks/param_impact_benchmark.py --no-save --iterations "$INPUT_ITERATIONS" 2>&1 | tee param_impact_output.txt
|
||||
|
||||
- name: Check Lazy Imports
|
||||
id: lazy_imports
|
||||
|
||||
@@ -18,6 +18,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
name: Chain Claude After Reviewers
|
||||
|
||||
# Event-driven: triggers when Copilot finishes reviewing a PR
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
# Trigger Claude after Copilot finishes on a PR — Claude is the FINAL reviewer
|
||||
# Claude reads ALL previous reviews (Qodo, Coderabbit, Copilot) and fixes any valid issues
|
||||
claude-final-review:
|
||||
if: |
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.user.login, 'copilot')) ||
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.user.login, 'copilot'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Post Claude final review + fix request
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the FINAL architecture reviewer. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
})
|
||||
|
||||
# Trigger Claude after Gemini finishes on an Issue or PR
|
||||
claude-after-gemini:
|
||||
if: |
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.user.login == 'gemini-code-assist[bot]' ||
|
||||
contains(github.event.comment.body, 'Review completed by Gemini CLI') ||
|
||||
contains(github.event.comment.body, 'The fix is ready for review. Please test')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Post Claude analysis request (final reviewer)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the Lead Engineer. Read ALL analysis and reviews above carefully (Gemini, CodeRabbit, Qodo, Copilot, etc).\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
})
|
||||
@@ -15,15 +15,18 @@ jobs:
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Run Claude Issue Triage
|
||||
uses: ./.github/actions/claude-issue-triage-action
|
||||
|
||||
+193
-15
@@ -11,10 +11,116 @@ on:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
# ================================================================
|
||||
# Job 1: Lightweight triage for ALL new issues (including external users)
|
||||
# Posts welcome comment + adds triage labels. No Claude code execution.
|
||||
# ================================================================
|
||||
issue-triage:
|
||||
if: |
|
||||
github.event_name == 'issues' &&
|
||||
github.event.action == 'opened' &&
|
||||
!contains(toJSON(github.event.issue.labels), 'claude')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Triage and acknowledge issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = (issue.title || '').toLowerCase();
|
||||
const body = (issue.body || '').toLowerCase();
|
||||
const content = title + ' ' + body;
|
||||
const isOwner = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(
|
||||
issue.author_association
|
||||
);
|
||||
|
||||
// --- Label triage ---
|
||||
const labels = [];
|
||||
|
||||
// Kind labels
|
||||
if (content.includes('feature request') || content.includes('[feature')) {
|
||||
labels.push('enhancement');
|
||||
} else if (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback')) {
|
||||
labels.push('bug');
|
||||
} else if (content.includes('question') || content.includes('how do i') || content.includes('how to')) {
|
||||
labels.push('question');
|
||||
}
|
||||
|
||||
// Area labels
|
||||
if (content.includes('security') || content.includes('cve') || content.includes('vulnerability')) {
|
||||
labels.push('security');
|
||||
}
|
||||
if (content.includes('performance') || content.includes('slow') || content.includes('memory leak')) {
|
||||
labels.push('performance');
|
||||
}
|
||||
if (content.includes('documentation') || content.includes('docs')) {
|
||||
labels.push('documentation');
|
||||
}
|
||||
if (content.includes('typescript') || content.includes('javascript') || content.includes('npm')) {
|
||||
labels.push('javascript');
|
||||
}
|
||||
|
||||
// Apply labels
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels
|
||||
});
|
||||
}
|
||||
|
||||
// --- Acknowledgment comment (external users only) ---
|
||||
if (!isOwner) {
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: [
|
||||
`👋 Thanks for opening this issue, @${issue.user.login}!`,
|
||||
'',
|
||||
'A maintainer will review this shortly. In the meantime:',
|
||||
'- Make sure you\'ve included steps to reproduce (for bugs)',
|
||||
'- Check [existing issues](https://github.com/MervinPraison/PraisonAI/issues) for duplicates',
|
||||
'- Review the [documentation](https://docs.praison.ai) for related guides',
|
||||
'',
|
||||
'_A maintainer can trigger deeper analysis by commenting `@claude` on this issue._'
|
||||
].join('\n')
|
||||
});
|
||||
}
|
||||
|
||||
// --- For owner: add 'claude' label to trigger the code-fix job ---
|
||||
if (isOwner) {
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['claude']
|
||||
});
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# Job 2: Full Claude code-fix (owner/collaborator only)
|
||||
# Triggers on: labeled 'claude', @claude comments, assignments
|
||||
# ================================================================
|
||||
claude-response:
|
||||
# Allow: human users, github-actions[bot] (auto-comments/labels)
|
||||
# Block: dependabot, cursor, renovate, other bots
|
||||
# Skip 'opened' events — triage job handles those above
|
||||
if: |
|
||||
github.event.action != 'opened' &&
|
||||
(github.event.action != 'labeled' || github.event.label.name == 'claude') &&
|
||||
(github.event_name != 'issue_comment' || contains(github.event.comment.body, '@claude')) &&
|
||||
(
|
||||
@@ -33,58 +139,130 @@ jobs:
|
||||
actions: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Check for Fork PR
|
||||
id: check_fork
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
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;
|
||||
}
|
||||
}
|
||||
core.setOutput('is_fork', 'false');
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Fetch PR branch and Setup Remote
|
||||
if: github.event.issue.pull_request
|
||||
run: |
|
||||
# Fetch PR head from base repo and put it into local branch
|
||||
git fetch origin pull/${{ github.event.issue.number }}/head:${{ steps.check_fork.outputs.pr_branch }}
|
||||
|
||||
# If it's a fork, make origin point to local to trick claude-code-action's `git fetch origin <branch>`
|
||||
if [ "${{ steps.check_fork.outputs.is_fork }}" == "true" ]; then
|
||||
git remote set-url origin file://$(pwd)
|
||||
fi
|
||||
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
with:
|
||||
allowed_bots: 'praisonai-triage-agent[bot]'
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
trigger_phrase: "@claude"
|
||||
label_trigger: "claude"
|
||||
allowed_non_write_users: "*"
|
||||
|
||||
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' || '' }}
|
||||
You are working on the PraisonAI SDK. Follow AGENTS.md strictly.
|
||||
|
||||
STEP 0 — SETUP GIT IDENTITY:
|
||||
git config user.name "MervinPraison"
|
||||
git config user.email "[email protected]"
|
||||
STEP 0 — SETUP GIT IDENTITY & AUTH (GLOBAL — required for all repos):
|
||||
git config --global user.name "MervinPraison"
|
||||
git config --global user.email "[email protected]"
|
||||
gh auth setup-git
|
||||
|
||||
|
||||
STEP 1 — READ GUIDELINES:
|
||||
Read AGENTS.md to understand the architecture rules.
|
||||
|
||||
STEP 2 — ARCHITECTURE VALIDATION (MANDATORY before writing code):
|
||||
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
|
||||
Before implementing anything, answer these questions:
|
||||
- Does this add heavy implementations to the core SDK (praisonaiagents)? If YES → move it to the wrapper (praisonai) or make it a plugin.
|
||||
- CORE vs WRAPPER vs TOOLS vs DOCS vs PLUGINS ROUTING:
|
||||
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
|
||||
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
|
||||
3. Tools (PraisonAI-Tools): Capabilities Agents actively call during execution (e.g. SurrealDB tool, Slack tool).
|
||||
4. Documentation (PraisonAIDocs): Documentation pages, guides, API references.
|
||||
5. Plugins (PraisonAI-Plugins): Framework lifecycle extensions replacing systemic behavior (e.g. tracing, logging, metrics, hooks, guardrails).
|
||||
For items routed to an EXTERNAL repository (Tools, Docs, Plugins), follow STEP 3-ALT below.
|
||||
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory).
|
||||
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
|
||||
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
|
||||
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead.
|
||||
- Is there a name collision with existing exports in __init__.py?
|
||||
If ANY of these checks fail, add a comment to the issue explaining why and close it. Do NOT create a PR.
|
||||
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:
|
||||
STEP 3 — IMPLEMENT (for changes in THIS repo — PraisonAI):
|
||||
- Create a fix branch and implement a minimal, focused fix
|
||||
- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper
|
||||
- Keep changes small and backward-compatible
|
||||
|
||||
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.
|
||||
a) Clone the repository:
|
||||
gh repo clone MervinPraison/<REPO_NAME> /tmp/<REPO_NAME>
|
||||
b) Copy GitHub authentication from the main repository so push works seamlessly:
|
||||
git -C /tmp/<REPO_NAME> config http."https://github.com/".extraheader "$(git config --get http."https://github.com/".extraheader)"
|
||||
c) Create a feature branch (NEVER commit to main):
|
||||
git -C /tmp/<REPO_NAME> checkout -b claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
d) Make your changes using absolute paths (e.g. edit /tmp/<REPO_NAME>/docs.json)
|
||||
e) Commit:
|
||||
git -C /tmp/<REPO_NAME> add -A
|
||||
git -C /tmp/<REPO_NAME> commit -m "feat: <description> (fixes #$ISSUE_NUMBER)"
|
||||
f) Push the branch:
|
||||
git -C /tmp/<REPO_NAME> push origin claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
g) Create PR in the EXTERNAL repo:
|
||||
gh pr create -R MervinPraison/<REPO_NAME> --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d) --title "feat: <title>" --body "Fixes MervinPraison/PraisonAI#$ISSUE_NUMBER
|
||||
|
||||
<body>"
|
||||
|
||||
STEP 4 — TEST:
|
||||
- Run: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
|
||||
- For SDK changes: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
|
||||
- For docs changes: verify files exist and are valid markdown/MDX
|
||||
- Ensure no regressions
|
||||
|
||||
STEP 5 — CREATE PR:
|
||||
- Commit with descriptive message, push, and create PR using `gh pr create`
|
||||
CRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link.
|
||||
- For external repos, use `gh pr create -R MervinPraison/<REPO_NAME>`
|
||||
CRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link or say "manual push required".
|
||||
NOTE: If you worked on an external repo, the wrapper action will report "No commits" for the main repo at the end. This is expected, do not worry about it.
|
||||
allowed_tools: |
|
||||
Bash(git:*)
|
||||
Bash(python:*)
|
||||
|
||||
@@ -20,6 +20,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -34,6 +34,10 @@ jobs:
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ jobs:
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ jobs:
|
||||
|
||||
- name: Checkout PR code
|
||||
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 }}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
name: PraisonAI Issue Triage
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue Number'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
!github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@praisonai') &&
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
github.event.comment.user.type != 'Bot' &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'))
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.PRAISONAI_APP_ID }}
|
||||
private_key: ${{ secrets.PRAISONAI_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --local user.email "[email protected]"
|
||||
git config --local user.name "PraisonAI Bot"
|
||||
|
||||
- name: Install PraisonAI
|
||||
run: |
|
||||
pip install uv
|
||||
cd src/praisonai-agents && uv pip install --system -e ".[all]"
|
||||
cd ../praisonai && uv pip install --system -e ".[all]"
|
||||
|
||||
- name: Run PraisonAI Issue Triage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
PRAISONAI_AUTO_APPROVE: "true"
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
export ISSUE_NUMBER="${{ inputs.issue_number }}"
|
||||
else
|
||||
export ISSUE_NUMBER="${{ github.event.issue.number }}"
|
||||
fi
|
||||
praisonai github triage --issue $ISSUE_NUMBER --agent-file .github/praisonai-issue-triage.yaml
|
||||
@@ -0,0 +1,72 @@
|
||||
name: PraisonAI PR Reviewer
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Pull Request Number'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@praisonai') &&
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
github.event.comment.user.type != 'Bot' &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'))
|
||||
|
||||
steps:
|
||||
- name: Determine checkout ref
|
||||
id: dest
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "issue_comment" ]; then
|
||||
echo "ref=refs/pull/${{ github.event.issue.number }}/head" >> "$GITHUB_OUTPUT"
|
||||
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "ref=refs/pull/${{ inputs.pr_number }}/head" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
ref: ${{ steps.dest.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.PRAISONAI_APP_ID }}
|
||||
private_key: ${{ secrets.PRAISONAI_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install PraisonAI
|
||||
run: pip install "praisonai[all]"
|
||||
|
||||
- name: Run PraisonAI PR Review
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
praisonai workflow run .github/praisonai-reviewer.yaml
|
||||
@@ -15,7 +15,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
@@ -22,6 +22,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
@@ -26,6 +26,10 @@ 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:
|
||||
repository: MervinPraison/PraisonAI
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
@@ -34,14 +38,20 @@ jobs:
|
||||
# Get the commit hash from the tag
|
||||
- name: Get commit hash
|
||||
id: get_commit
|
||||
env:
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse HEAD)
|
||||
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
|
||||
echo "Found commit: $COMMIT_HASH for tag ${{ github.event.inputs.tag }}"
|
||||
echo "Found commit: $COMMIT_HASH for tag $INPUT_TAG"
|
||||
|
||||
# Now checkout the fork at main branch
|
||||
- name: Checkout fork
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
repository: ${{ github.event.inputs.fork_owner }}/PraisonAI
|
||||
ref: main
|
||||
@@ -60,8 +70,11 @@ jobs:
|
||||
# Fetch from upstream
|
||||
git fetch upstream --tags --force
|
||||
|
||||
# Reset to upstream commit
|
||||
git reset --hard ${{ steps.get_commit.outputs.commit_hash }}
|
||||
- name: Hard reset to tag commit
|
||||
env:
|
||||
COMMIT_HASH: ${{ steps.get_commit.outputs.commit_hash }}
|
||||
run: |
|
||||
git reset --hard $COMMIT_HASH
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -436,6 +449,10 @@ 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:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
@@ -445,55 +462,64 @@ jobs:
|
||||
# Add upstream and get commit from tag
|
||||
- name: Get upstream tag commit
|
||||
id: get_upstream_commit
|
||||
env:
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
# Add upstream remote
|
||||
git remote add upstream https://github.com/MervinPraison/PraisonAI.git
|
||||
|
||||
# Delete local tag if it exists
|
||||
git tag -d ${{ github.event.inputs.tag }} || true
|
||||
git tag -d "$INPUT_TAG" || true
|
||||
|
||||
# Fetch from upstream including tags with force
|
||||
git fetch upstream --tags --force
|
||||
|
||||
# Verify tag exists in upstream
|
||||
if ! git ls-remote --tags upstream | grep -q "refs/tags/${{ github.event.inputs.tag }}$"; then
|
||||
echo "::error::Tag ${{ github.event.inputs.tag }} not found in upstream repository"
|
||||
if ! git ls-remote --tags upstream | grep -q "refs/tags/$INPUT_TAG$"; then
|
||||
echo "::error::Tag $INPUT_TAG not found in upstream repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the commit hash from the upstream tag
|
||||
UPSTREAM_COMMIT=$(git rev-list -n 1 refs/tags/${{ github.event.inputs.tag }})
|
||||
UPSTREAM_COMMIT=$(git rev-list -n 1 refs/tags/"$INPUT_TAG")
|
||||
if [ -z "$UPSTREAM_COMMIT" ]; then
|
||||
echo "::error::Could not find commit for tag ${{ github.event.inputs.tag }}"
|
||||
echo "::error::Could not find commit for tag $INPUT_TAG"
|
||||
exit 1
|
||||
fi
|
||||
echo "upstream_commit=$UPSTREAM_COMMIT" >> $GITHUB_OUTPUT
|
||||
echo "Found commit: $UPSTREAM_COMMIT for tag ${{ github.event.inputs.tag }}"
|
||||
echo "Found commit: $UPSTREAM_COMMIT for tag $INPUT_TAG"
|
||||
|
||||
# Create tag at the same commit
|
||||
- name: Create tag at commit
|
||||
env:
|
||||
GIT_ASKPASS: /bin/echo
|
||||
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
git config --global user.name "MervinPraison"
|
||||
git config --global user.email "[email protected]"
|
||||
|
||||
# Remove tag if it exists locally
|
||||
git tag -d ${{ github.event.inputs.tag }} || true
|
||||
git tag -d "$INPUT_TAG" || true
|
||||
|
||||
# Remove tag if it exists remotely
|
||||
git push origin :refs/tags/${{ github.event.inputs.tag }} || true
|
||||
git push origin :refs/tags/"$INPUT_TAG" || true
|
||||
|
||||
# Fetch the specific commit from upstream
|
||||
git fetch upstream ${{ steps.get_upstream_commit.outputs.upstream_commit }} --force
|
||||
- name: Fetch tag from upstream
|
||||
env:
|
||||
UPSTREAM_COMMIT: ${{ steps.get_upstream_commit.outputs.upstream_commit }}
|
||||
run: |
|
||||
git fetch upstream $UPSTREAM_COMMIT --force
|
||||
|
||||
# Reset to upstream commit
|
||||
git reset --hard ${{ steps.get_upstream_commit.outputs.upstream_commit }}
|
||||
- name: Reset to upstream commit
|
||||
env:
|
||||
UPSTREAM_COMMIT: ${{ steps.get_upstream_commit.outputs.upstream_commit }}
|
||||
run: |
|
||||
git reset --hard $UPSTREAM_COMMIT
|
||||
|
||||
# Create and push tag
|
||||
git tag -a ${{ github.event.inputs.tag }} -m "Release ${{ github.event.inputs.tag }} (synced from upstream MervinPraison/PraisonAI@${{ github.event.inputs.tag }})"
|
||||
git push origin ${{ github.event.inputs.tag }}
|
||||
env:
|
||||
GIT_ASKPASS: /bin/echo
|
||||
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
git tag -a "$INPUT_TAG" -m "Release $INPUT_TAG (synced from upstream MervinPraison/PraisonAI@$INPUT_TAG)"
|
||||
git push origin "$INPUT_TAG"
|
||||
|
||||
# Create the release
|
||||
- name: Create Release
|
||||
|
||||
@@ -37,6 +37,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -67,9 +71,11 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Run Comprehensive Test Suite
|
||||
env:
|
||||
INPUT_TEST_TYPE: ${{ github.event.inputs.test_type || 'all' }}
|
||||
run: |
|
||||
# Determine test type from input or default to 'all'
|
||||
TEST_TYPE="${{ github.event.inputs.test_type || 'all' }}"
|
||||
TEST_TYPE="$INPUT_TEST_TYPE"
|
||||
|
||||
echo "🧪 Running comprehensive test suite (type: $TEST_TYPE)"
|
||||
|
||||
@@ -102,13 +108,16 @@ jobs:
|
||||
|
||||
- name: Generate Comprehensive Test Report
|
||||
if: always()
|
||||
env:
|
||||
INPUT_TEST_TYPE: ${{ github.event.inputs.test_type || 'all' }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
echo "# 📋 Comprehensive Test Report" > comprehensive_report.md
|
||||
echo "" >> comprehensive_report.md
|
||||
echo "**Python Version:** ${{ matrix.python-version }}" >> comprehensive_report.md
|
||||
echo "**Test Type:** ${{ github.event.inputs.test_type || 'all' }}" >> comprehensive_report.md
|
||||
echo "**Trigger:** ${{ github.event_name }}" >> comprehensive_report.md
|
||||
echo "**Date:** $(date -u)" >> comprehensive_report.md
|
||||
echo "**Test Type:** $INPUT_TEST_TYPE" >> comprehensive_report.md
|
||||
echo "**Trigger:** $EVENT_NAME" >> comprehensive_report.md
|
||||
echo "**Status:** 🟢 All Tests Passed" >> comprehensive_report.md
|
||||
echo "" >> comprehensive_report.md
|
||||
|
||||
echo "## 🧪 Test Categories Covered:" >> comprehensive_report.md
|
||||
|
||||
@@ -22,7 +22,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
@@ -13,6 +13,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -77,6 +81,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -33,6 +33,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -45,7 +45,8 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -104,7 +105,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -185,6 +187,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -255,6 +259,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -312,6 +318,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -369,6 +377,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -426,6 +436,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
@@ -483,6 +495,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.check-key.outputs.has_key == 'true'
|
||||
|
||||
- name: Set up Python 3.11
|
||||
|
||||
@@ -34,14 +34,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 🚨 Cost Warning
|
||||
env:
|
||||
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
|
||||
INPUT_CONFIRM_COSTS: ${{ github.event.inputs.confirm_costs }}
|
||||
run: |
|
||||
echo "⚠️ WARNING: This workflow will make real API calls!"
|
||||
echo "💰 This may incur charges on your API accounts"
|
||||
echo "🎯 Framework: ${{ github.event.inputs.framework }}"
|
||||
echo "✅ Cost confirmation: ${{ github.event.inputs.confirm_costs }}"
|
||||
echo "🎯 Framework: $INPUT_FRAMEWORK"
|
||||
echo "✅ Cost confirmation: $INPUT_CONFIRM_COSTS"
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -93,21 +100,23 @@ jobs:
|
||||
|
||||
- name: Generate Real Test Report
|
||||
if: always()
|
||||
env:
|
||||
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
|
||||
run: |
|
||||
echo "# 🔥 Real End-to-End Test Report" > real_test_report.md
|
||||
echo "" >> real_test_report.md
|
||||
echo "⚠️ **WARNING: This report represents tests that made real API calls**" >> real_test_report.md
|
||||
echo "" >> real_test_report.md
|
||||
echo "**Framework Tested:** ${{ github.event.inputs.framework }}" >> real_test_report.md
|
||||
echo "**Framework Tested:** $INPUT_FRAMEWORK" >> real_test_report.md
|
||||
echo "**Python Version:** ${{ matrix.python-version }}" >> real_test_report.md
|
||||
echo "**Date:** $(date -u)" >> real_test_report.md
|
||||
echo "**Triggered by:** ${{ github.actor }}" >> real_test_report.md
|
||||
echo "**Triggered by:** $GITHUB_ACTOR" >> real_test_report.md
|
||||
echo "" >> real_test_report.md
|
||||
|
||||
echo "## 🧪 Test Results" >> real_test_report.md
|
||||
echo "" >> real_test_report.md
|
||||
|
||||
if [ "${{ github.event.inputs.framework }}" == "autogen" ] || [ "${{ github.event.inputs.framework }}" == "all" ]; then
|
||||
if [ "$INPUT_FRAMEWORK" == "autogen" ] || [ "$INPUT_FRAMEWORK" == "all" ]; then
|
||||
echo "### AutoGen Real Tests:" >> real_test_report.md
|
||||
echo "- Environment verification" >> real_test_report.md
|
||||
echo "- Agent creation with real API calls" >> real_test_report.md
|
||||
@@ -115,7 +124,7 @@ jobs:
|
||||
echo "" >> real_test_report.md
|
||||
fi
|
||||
|
||||
if [ "${{ github.event.inputs.framework }}" == "crewai" ] || [ "${{ github.event.inputs.framework }}" == "all" ]; then
|
||||
if [ "$INPUT_FRAMEWORK" == "crewai" ] || [ "$INPUT_FRAMEWORK" == "all" ]; then
|
||||
echo "### CrewAI Real Tests:" >> real_test_report.md
|
||||
echo "- Environment verification" >> real_test_report.md
|
||||
echo "- Crew creation with real API calls" >> real_test_report.md
|
||||
@@ -150,11 +159,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 🛡️ Safety Check Failed
|
||||
env:
|
||||
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
|
||||
INPUT_CONFIRM_COSTS: ${{ github.event.inputs.confirm_costs }}
|
||||
run: |
|
||||
echo "🚨 Real tests not executed due to safety checks:"
|
||||
echo ""
|
||||
echo "✅ Costs confirmed: ${{ github.event.inputs.confirm_costs }}"
|
||||
echo "✅ Framework selected: ${{ github.event.inputs.framework }}"
|
||||
echo "✅ Costs confirmed: $INPUT_CONFIRM_COSTS"
|
||||
echo "✅ Framework selected: $INPUT_FRAMEWORK"
|
||||
echo ""
|
||||
echo "To run real tests:"
|
||||
echo "1. Select a framework (autogen, crewai, or all)"
|
||||
|
||||
@@ -26,7 +26,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
+2
-1
@@ -94,7 +94,8 @@ Cargo.lock
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
.windsurf
|
||||
.agent
|
||||
|
||||
src/praisonai/build*.egg-info/
|
||||
src/praisonai/PraisonAI.egg-info/
|
||||
jobs/*
|
||||
*.egg-info
|
||||
@@ -23,14 +23,13 @@
|
||||
|
||||
</div>
|
||||
|
||||
PraisonAI 🦞 — Automate and solve complex challenges with AI agent teams that plan, research, code, and deliver results to Telegram, Discord, and WhatsApp — running 24/7. A low-code, production-ready multi-agent framework with handoffs, guardrails, memory, RAG, and 100+ LLM providers, built around simplicity, customisation, and effective human-agent collaboration.
|
||||
PraisonAI 🦞 — **Hire a 24/7 AI Workforce.** Stop writing boilerplate and start shipping autonomous agents that research, plan, and execute tasks across your apps. From one agent to an entire organization, deployed in 5 lines of code.
|
||||
|
||||
<div align="center">
|
||||
<br>
|
||||
<a href="https://x.com/elonmusk/status/1893870468249141688" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Highlighted_by_Elon_Musk-000000?style=for-the-badge&logo=x&logoColor=white" alt="Highlighted by Elon Musk" />
|
||||
</a>
|
||||
<p><em>"Grok 3 customer support" — <a href="https://x.com/elonmusk/status/1893870468249141688">Elon Musk quoting PraisonAI's tutorial</a></em></p>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
@@ -69,26 +68,6 @@ PraisonAI 🦞 — Automate and solve complex challenges with AI agent teams tha
|
||||
|
||||
---
|
||||
|
||||
> **Quick Paths:**
|
||||
> - 🆕 **New here?** → [Quick Start](#-quick-start) *(1 minute to first agent)*
|
||||
> - 📦 **Installing?** → [Installation](#-installation)
|
||||
> - 🐍 **Python SDK?** → [Python Examples](#-using-python-code)
|
||||
> - 📄 **YAML/No-Code?** → [YAML Examples](#-using-yaml-no-code)
|
||||
> - 🎯 **CLI user?** → [CLI Quick Reference](#-cli-quick-reference)
|
||||
> - 🤝 **Contributing?** → [Contributing](#-contributing)
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 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.
|
||||
|
||||
| Performance Metric | PraisonAI |
|
||||
|--------------------|-----------|
|
||||
| Avg Instantiation Time | **3.77 μs** |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
AI agents solving real-world problems across industries:
|
||||
@@ -104,9 +83,44 @@ AI agents solving real-world problems across industries:
|
||||
|
||||
---
|
||||
|
||||
## Supported Providers
|
||||
## 🚀 Meet your first Agent (Under 1 Minute)
|
||||
|
||||
PraisonAI supports 100+ LLM providers through seamless integration:
|
||||
1. Install the lightweight core SDK:
|
||||
```bash
|
||||
pip install praisonaiagents
|
||||
export OPENAI_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
2. Run your first autonomous agent:
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Give your agent a goal, and watch it work.
|
||||
agent = Agent(instructions="You are a senior data analyst.")
|
||||
agent.start("Analyze the top 3 tech trends of 2026 and format as a markdown table.")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌌 The PraisonAI Ecosystem
|
||||
|
||||
Start simple with the core SDK, or expand to full visual builders and dashboards when you're ready.
|
||||
|
||||
* **Core SDK (`praisonaiagents`)**: For pure Python development. `pip install praisonaiagents`
|
||||
* 💻 **PraisonAI CLI (`praisonai`)**: For terminal-based developers. `pip install praisonai`
|
||||
* 🦞 **Claw Dashboard**: Connect agents directly to Telegram, Slack, or Discord. `pip install "praisonai[claw]"`
|
||||
* 🔗 **Flow Visual Builder**: Drag-and-drop workflow creation. `pip install "praisonai[flow]"`
|
||||
* 🤖 **PraisonAI UI**: Clean chat interface. `pip install "praisonai[ui]"`
|
||||
|
||||
### JavaScript SDK
|
||||
|
||||
```bash
|
||||
npm install praisonai
|
||||
```
|
||||
|
||||
## 🧠 Supported Providers & Features
|
||||
|
||||
Powered by 100+ LLMs (OpenAI, Anthropic, Gemini & local models).
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/OpenAI-412991?style=flat&logo=openai&logoColor=white" alt="OpenAI" />
|
||||
@@ -164,6 +178,14 @@ PraisonAI supports 100+ LLM providers through seamless integration:
|
||||
|
||||
</details>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://x.com/elonmusk/status/1893870468249141688" target="_blank">
|
||||
<img src=".github/images/elon_musk_praisonai.png" alt="Highlighted by Elon Musk" width="600" />
|
||||
</a>
|
||||
<p><em>"Grok 3 customer support" — <a href="https://x.com/elonmusk/status/1893870468249141688">Elon Musk quoting PraisonAI's tutorial</a></em></p>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Why PraisonAI?
|
||||
@@ -205,78 +227,6 @@ PraisonAI supports 100+ LLM providers through seamless integration:
|
||||
</details>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://x.com/elonmusk/status/1893870468249141688" target="_blank">
|
||||
<img src=".github/images/elon_musk_praisonai.png" alt="Highlighted by Elon Musk" width="600" />
|
||||
</a>
|
||||
<p><em>"Grok 3 customer support" — <a href="https://x.com/elonmusk/status/1893870468249141688">Elon Musk quoting PraisonAI's tutorial</a></em></p>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Get started with PraisonAI in under 1 minute:
|
||||
|
||||
```bash
|
||||
# Install
|
||||
pip install praisonaiagents
|
||||
|
||||
# Set API key
|
||||
export OPENAI_API_KEY=your_key_here
|
||||
|
||||
# Create a simple agent
|
||||
python -c "from praisonaiagents import Agent; Agent(instructions='You are a helpful AI assistant').start('Write a haiku about AI')"
|
||||
```
|
||||
|
||||
> **Next Steps:** [Single Agent Example](#1-single-agent) | [Multi Agents](#2-multi-agents) | [Full Docs](https://docs.praison.ai)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Python SDK
|
||||
|
||||
Lightweight package dedicated for coding:
|
||||
|
||||
```bash
|
||||
pip install praisonaiagents
|
||||
```
|
||||
|
||||
For the full framework with CLI support:
|
||||
|
||||
```bash
|
||||
pip install praisonai
|
||||
```
|
||||
|
||||
🦞 **PraisonAI Claw** — full UI with bots, memory, knowledge, and gateway:
|
||||
|
||||
```bash
|
||||
pip install "praisonai[claw]"
|
||||
praisonai claw
|
||||
```
|
||||
|
||||
🔗 **PraisonAI Flow** — Langflow Visual Flow Builder:
|
||||
|
||||
```bash
|
||||
pip install "praisonai[flow]"
|
||||
praisonai flow
|
||||
```
|
||||
|
||||
🤖 **PraisonAI UI** — Clean chat interface:
|
||||
|
||||
```bash
|
||||
pip install "praisonai[ui]"
|
||||
praisonai ui
|
||||
```
|
||||
|
||||
### JavaScript SDK
|
||||
|
||||
```bash
|
||||
npm install praisonai
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
@@ -700,6 +650,7 @@ praisonai agents.yaml
|
||||
| Auto-Save Sessions | [Docs](https://docs.praison.ai/docs/cli/session) | [📖](https://docs.praison.ai/docs/cli/session) |
|
||||
| History in Context | [Docs](https://docs.praison.ai/docs/cli/session) | [📖](https://docs.praison.ai/docs/cli/session) |
|
||||
| Telemetry | [Example](examples/python/telemetry/production-telemetry-example.py) | [📖](https://docs.praison.ai/docs/features/telemetry) |
|
||||
| Langfuse Tracing | [Docs](https://docs.praison.ai/docs/observability/langfuse) | [📖](https://docs.praison.ai/docs/observability/langfuse) |
|
||||
| Project Docs (.praison/docs/) | [Docs](https://docs.praison.ai/docs/cli/docs) | [📖](https://docs.praison.ai/docs/cli/docs) |
|
||||
| AI Commit Messages | [Docs](https://docs.praison.ai/docs/cli/commit) | [📖](https://docs.praison.ai/docs/cli/commit) |
|
||||
| @Mentions in Prompts | [Docs](https://docs.praison.ai/docs/cli/mentions) | [📖](https://docs.praison.ai/docs/cli/mentions) |
|
||||
@@ -774,6 +725,20 @@ const agent = new Agent({ instructions: 'You are a helpful AI assistant' });
|
||||
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.
|
||||
|
||||
| Performance Metric | PraisonAI |
|
||||
|--------------------|-----------|
|
||||
| Avg Instantiation Time | **3.77 μs** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ⭐ Star History
|
||||
@@ -782,6 +747,19 @@ agent.start('Write a movie script about a robot in Mars');
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Langfuse Tracing
|
||||
|
||||
```bash
|
||||
pip install "praisonai[langfuse]"
|
||||
praisonai langfuse
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/images/langfuse.png" alt="PraisonAI Langfuse Tracing" width="800" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Video Tutorials
|
||||
|
||||
Learn PraisonAI through our comprehensive video series:
|
||||
|
||||
@@ -28,15 +28,19 @@ from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepR
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">aclose</a>() -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">agent_id</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">analyze_prompt</a>(prompt: str) -> set</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_history</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_history</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">close</a>() -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>(value)</code>
|
||||
@@ -52,6 +56,7 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_skills_prompt</a>() -> str</code>
|
||||
* <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_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>
|
||||
@@ -201,6 +206,7 @@ Methods:
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback_sync</a>(task_output: TaskOutput) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">get_next_task</a>(context: Dict[str, Any]) -> Optional[str]</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">initialize_memory</a>()</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">set_status</a>(new_status: str) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">store_in_memory</a>(content: str, agent_name: str = None, task_id: str = None)</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">loop</a>(step: Any = None, steps: Optional[List[Any]] = None, over: Optional[str] = None, from_csv: Optional[str] = None, from_file: Optional[str] = None, var_name: str = 'item', parallel: bool = False, max_workers: Optional[int] = None, output_variable: Optional[str] = None) -> Loop</code>
|
||||
@@ -359,6 +365,9 @@ Methods:
|
||||
|
||||
* <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 LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">__call__</a>(task_output) -> Tuple[bool, Union[str, 'TaskOutput']]</code>
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_input</a>(content: str, **kwargs) -> Tuple[bool, str]</code>
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_output</a>(content: str, **kwargs) -> Tuple[bool, str]</code>
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_tool_call</a>(tool_name: str, arguments: Dict[str, Any], **kwargs) -> Tuple[bool, Dict[str, Any]]</code>
|
||||
|
||||
# Planning
|
||||
|
||||
@@ -547,7 +556,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import FlowDisplay, obs, track_workflow
|
||||
from praisonaiagents import FlowDisplay, track_workflow
|
||||
```
|
||||
|
||||
Methods:
|
||||
@@ -733,7 +742,7 @@ Methods:
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonai import AgentApp, AgentOS, CloudProvider, Deploy, DeployConfig, DeployType, PraisonAI, __version__
|
||||
from praisonai import AgentApp, AgentOS, AnthropicManagedAgent, CloudProvider, DB, Deploy, DeployConfig, DeployType, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, PraisonAI, __version__
|
||||
```
|
||||
|
||||
# CLI
|
||||
@@ -855,6 +864,7 @@ Methods:
|
||||
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai github triage <a href="./src/praisonai/praisonai/cli/commands/github.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks list <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks remove <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
@@ -862,6 +872,17 @@ Methods:
|
||||
* <code title="cli">praisonai knowledge index <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge list <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge search <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse config <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse connect <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse init <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse langfuse-start <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse sessions <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse show <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse status <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse stop <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse test <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse traces <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse version <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai loop help <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
|
||||
* <code title="cli">praisonai loop loop-main <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp logs <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
@@ -869,6 +890,16 @@ Methods:
|
||||
* <code title="cli">praisonai lsp start <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp status <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp stop <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed get <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed list <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed managed-callback <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed multi <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed restore <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed resume <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed run <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed save <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed show <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai managed update <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp add <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp auth <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp describe <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
@@ -901,6 +932,10 @@ Methods:
|
||||
* <code title="cli">praisonai plugins list <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins plugins-callback <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai plugins remove <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
|
||||
* <code title="cli">praisonai port check <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
|
||||
* <code title="cli">praisonai port kill <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
|
||||
* <code title="cli">praisonai port list <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
|
||||
* <code title="cli">praisonai port port-callback <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile imports <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile optimize <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile profile-callback <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
|
||||
@@ -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.5.112" \
|
||||
"praisonai>=4.6.5" \
|
||||
"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.5.112" \
|
||||
"praisonai>=4.6.5" \
|
||||
"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.5.112" \
|
||||
"praisonai>=4.6.5" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + ClickHouse Persistence — Real conversation with session resume.
|
||||
|
||||
ClickHouse stores analytics/state; SQLite stores conversation history.
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents clickhouse-connect
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
# ClickHouse running on localhost:8123
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import clickhouse_connect
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
sys.exit("ERROR: OPENAI_API_KEY not set.")
|
||||
|
||||
from praisonai import ManagedAgent, LocalManagedConfig, DB
|
||||
from praisonaiagents import Agent
|
||||
|
||||
CH_HOST = os.getenv("CH_HOST", "localhost")
|
||||
CH_PORT = int(os.getenv("CH_PORT", "8123"))
|
||||
CH_USER = os.getenv("CH_USER", "clickhouse")
|
||||
CH_PASSWORD = os.getenv("CH_PASSWORD", "clickhouse")
|
||||
SQLITE_PATH = os.path.join(tempfile.gettempdir(), "managed_ch_conv.db")
|
||||
print(f"ManagedAgent + ClickHouse Persistence\nCH: {CH_HOST}:{CH_PORT}\nSQLite: {SQLITE_PATH}\n")
|
||||
|
||||
# Setup ClickHouse table
|
||||
client = clickhouse_connect.get_client(host=CH_HOST, port=CH_PORT, username=CH_USER, password=CH_PASSWORD)
|
||||
TABLE = "managed_agent_sessions"
|
||||
client.command(f"""
|
||||
CREATE TABLE IF NOT EXISTS {TABLE} (
|
||||
session_id String, agent_id String,
|
||||
input_tokens UInt64, output_tokens UInt64,
|
||||
state_json String, created_at DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree() ORDER BY (session_id, created_at)
|
||||
""")
|
||||
|
||||
# ── Phase 1: Create agent, teach facts ──
|
||||
print("=== Phase 1: First Session ===")
|
||||
|
||||
db = DB(database_url=SQLITE_PATH)
|
||||
managed = ManagedAgent(
|
||||
provider="local", db=db,
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini", name="ClickHouse Memory Agent",
|
||||
system="You are a helpful assistant. Remember all facts the user tells you.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="User", backend=managed)
|
||||
|
||||
result1 = agent.run("Remember: My favourite movie is Interstellar by Christopher Nolan, 2014. Confirm.")
|
||||
print(f"Agent: {result1[:200]}...")
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
|
||||
result2 = agent.run("Also remember: The Hans Zimmer soundtrack is my favourite film score. Confirm.")
|
||||
print(f"Agent: {result2[:200]}...")
|
||||
|
||||
# Store to ClickHouse
|
||||
session_id = managed.session_id
|
||||
client.insert(TABLE, [[
|
||||
session_id, managed.agent_id or "",
|
||||
managed.total_input_tokens, managed.total_output_tokens,
|
||||
json.dumps({"environment_id": managed.environment_id}),
|
||||
]], column_names=["session_id", "agent_id", "input_tokens", "output_tokens", "state_json"])
|
||||
|
||||
# ── Phase 2: ClickHouse Verification ──
|
||||
print("\n=== Phase 2: ClickHouse Verification ===")
|
||||
rows = client.query(f"SELECT session_id, agent_id FROM {TABLE} WHERE session_id=%(sid)s LIMIT 1", parameters={"sid": session_id})
|
||||
for row in rows.result_rows:
|
||||
print(f" session_id: {row[0]}, agent_id: {row[1]}")
|
||||
|
||||
# ── Phase 3: Destroy instance ──
|
||||
print("\n=== Phase 3: Instance Goes Idle ===")
|
||||
saved_ids = managed.save_ids()
|
||||
print(f"Saved IDs: {saved_ids}")
|
||||
del agent, managed, db
|
||||
print("Agent destroyed.\n")
|
||||
|
||||
# ── Phase 4: Resume — no config needed ──
|
||||
print("=== Phase 4: Resume Session ===")
|
||||
db2 = DB(database_url=SQLITE_PATH)
|
||||
managed2 = ManagedAgent(provider="local", db=db2)
|
||||
managed2.resume_session(session_id)
|
||||
print(f"Resumed session: {managed2.session_id}")
|
||||
|
||||
agent2 = Agent(name="User", backend=managed2)
|
||||
result3 = agent2.run("What is my favourite movie, director, year, and soundtrack composer?")
|
||||
print(f"Agent: {result3[:300]}...")
|
||||
|
||||
# ── Phase 5: Validate ──
|
||||
print("\n=== Phase 5: Validation ===")
|
||||
r = result3.lower()
|
||||
checks = {
|
||||
"Remembers 'Interstellar'": "interstellar" in r,
|
||||
"Remembers 'Nolan'": "nolan" in r,
|
||||
"Remembers '2014'": "2014" in r,
|
||||
"Remembers 'Zimmer'": "zimmer" in r,
|
||||
"Session ID continuity": managed2.session_id == session_id,
|
||||
}
|
||||
all_passed = True
|
||||
for check, ok in checks.items():
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {check}")
|
||||
if not ok:
|
||||
all_passed = False
|
||||
|
||||
client.command(f"ALTER TABLE {TABLE} DELETE WHERE session_id = '{session_id}'")
|
||||
client.close()
|
||||
os.unlink(SQLITE_PATH)
|
||||
print(f"\n{'PASS' if all_passed else 'FAIL'}: ClickHouse ManagedAgent persistence test")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
CockroachDB Serverless Persistence Example.
|
||||
|
||||
CockroachDB Serverless provides distributed SQL with scale-to-zero.
|
||||
Auto-handles serialization retries (error 40001) and SSL.
|
||||
|
||||
Setup:
|
||||
1. Create free CockroachDB Serverless cluster at https://cockroachlabs.cloud
|
||||
2. Copy connection string from dashboard
|
||||
3. Set env var: export COCKROACHDB_URL="postgresql://user:[email protected]:26257/mydb?sslmode=verify-full"
|
||||
4. pip install praisonai[cockroachdb]
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="CockroachDB Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
db={"database_url": os.getenv("COCKROACHDB_URL")},
|
||||
session_id="crdb-demo-session",
|
||||
)
|
||||
|
||||
result = agent.start("What is distributed SQL? Explain in 2 sentences.")
|
||||
print(result)
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + JSON File Persistence — Real conversation with session resume.
|
||||
|
||||
Uses DefaultSessionStore (JSON files on disk). No external database required.
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
sys.exit("ERROR: OPENAI_API_KEY not set.")
|
||||
|
||||
from praisonai import ManagedAgent, LocalManagedConfig
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.session.store import DefaultSessionStore
|
||||
|
||||
SESSION_DIR = os.path.join(tempfile.gettempdir(), "managed_json_sessions")
|
||||
print(f"ManagedAgent + JSON File Persistence\nSession dir: {SESSION_DIR}\n")
|
||||
|
||||
# ── Phase 1: Create agent, teach facts ──
|
||||
print("=== Phase 1: First Session ===")
|
||||
|
||||
store = DefaultSessionStore(session_dir=SESSION_DIR)
|
||||
managed = ManagedAgent(
|
||||
provider="local", session_store=store,
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini", name="JSON Memory Agent",
|
||||
system="You are a helpful assistant. Remember all facts the user tells you.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="User", backend=managed)
|
||||
|
||||
result1 = agent.run("Remember: My favourite city is Barcelona, Spain. I love the Sagrada Familia. Confirm.")
|
||||
print(f"Agent: {result1[:200]}...")
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
|
||||
result2 = agent.run("Also remember: I always stay at Hotel Arts in Barcelona. Confirm.")
|
||||
print(f"Agent: {result2[:200]}...")
|
||||
|
||||
# ── Phase 2: Verify JSON files ──
|
||||
print("\n=== Phase 2: JSON File Verification ===")
|
||||
json_files = [f for f in os.listdir(SESSION_DIR) if f.endswith(".json")]
|
||||
print(f"Files: {json_files}")
|
||||
for fname in json_files:
|
||||
with open(os.path.join(SESSION_DIR, fname), "r") as f:
|
||||
data = json.load(f)
|
||||
print(f" {fname}: {len(data.get('messages', []))} messages")
|
||||
|
||||
# ── Phase 3: Destroy instance ──
|
||||
print("\n=== Phase 3: Instance Goes Idle ===")
|
||||
saved_ids = managed.save_ids()
|
||||
session_id = saved_ids["session_id"]
|
||||
print(f"Saved IDs: {saved_ids}")
|
||||
del agent, managed, store
|
||||
print("Agent destroyed.\n")
|
||||
|
||||
# ── Phase 4: Resume — no config needed ──
|
||||
print("=== Phase 4: Resume Session ===")
|
||||
store2 = DefaultSessionStore(session_dir=SESSION_DIR)
|
||||
managed2 = ManagedAgent(provider="local", session_store=store2)
|
||||
managed2.resume_session(session_id)
|
||||
print(f"Resumed session: {managed2.session_id}")
|
||||
|
||||
agent2 = Agent(name="User", backend=managed2)
|
||||
result3 = agent2.run("What is my favourite city, landmark, and hotel?")
|
||||
print(f"Agent: {result3[:300]}...")
|
||||
|
||||
# ── Phase 5: Validate ──
|
||||
print("\n=== Phase 5: Validation ===")
|
||||
r = result3.lower()
|
||||
checks = {
|
||||
"Remembers 'Barcelona'": "barcelona" in r,
|
||||
"Remembers 'Sagrada Familia'": "sagrada" in r,
|
||||
"Remembers 'Hotel Arts'": "hotel arts" in r or "arts" in r,
|
||||
"Session ID continuity": managed2.session_id == session_id,
|
||||
}
|
||||
all_passed = True
|
||||
for check, ok in checks.items():
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {check}")
|
||||
if not ok:
|
||||
all_passed = False
|
||||
|
||||
shutil.rmtree(SESSION_DIR)
|
||||
print(f"\n{'PASS' if all_passed else 'FAIL'}: JSON File ManagedAgent persistence test")
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + MongoDB Persistence — Real conversation with session resume.
|
||||
|
||||
MongoDB stores state metadata; SQLite stores conversation history.
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents pymongo
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
# MongoDB running on localhost:27017
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import pymongo
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
sys.exit("ERROR: OPENAI_API_KEY not set.")
|
||||
|
||||
from praisonai import ManagedAgent, LocalManagedConfig, DB
|
||||
from praisonaiagents import Agent
|
||||
|
||||
MONGO_URL = os.getenv("MONGO_URL", "mongodb://localhost:27017")
|
||||
SQLITE_PATH = os.path.join(tempfile.gettempdir(), "managed_mongo_conv.db")
|
||||
print(f"ManagedAgent + MongoDB Persistence\nMongoDB: {MONGO_URL}\nSQLite: {SQLITE_PATH}\n")
|
||||
|
||||
# ── Phase 1: Create agent, teach facts ──
|
||||
print("=== Phase 1: First Session ===")
|
||||
|
||||
db = DB(database_url=SQLITE_PATH)
|
||||
managed = ManagedAgent(
|
||||
provider="local", db=db,
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini", name="MongoDB Memory Agent",
|
||||
system="You are a helpful assistant. Remember all facts the user tells you.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="User", backend=managed)
|
||||
|
||||
result1 = agent.run("Remember: My favourite book is 'Dune' by Frank Herbert, published 1965. Confirm.")
|
||||
print(f"Agent: {result1[:200]}...")
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
|
||||
result2 = agent.run("Also remember: 'Dune Messiah' is my second favourite. Confirm.")
|
||||
print(f"Agent: {result2[:200]}...")
|
||||
|
||||
# Store session metadata into MongoDB for verification
|
||||
mongo_client = pymongo.MongoClient(MONGO_URL)
|
||||
mongo_col = mongo_client["praisonai_managed"]["sessions"]
|
||||
state_doc = {
|
||||
"_id": managed.session_id,
|
||||
"agent_id": managed.agent_id,
|
||||
"total_input_tokens": managed.total_input_tokens,
|
||||
}
|
||||
mongo_col.replace_one({"_id": managed.session_id}, state_doc, upsert=True)
|
||||
|
||||
# ── Phase 2: MongoDB Verification ──
|
||||
print("\n=== Phase 2: MongoDB Verification ===")
|
||||
doc = mongo_col.find_one({"_id": managed.session_id})
|
||||
print(f" Session in MongoDB: {doc['_id']}" if doc else " WARNING: not found")
|
||||
|
||||
# ── Phase 3: Destroy instance ──
|
||||
print("\n=== Phase 3: Instance Goes Idle ===")
|
||||
saved_ids = managed.save_ids()
|
||||
session_id = saved_ids["session_id"]
|
||||
print(f"Saved IDs: {saved_ids}")
|
||||
del agent, managed, db
|
||||
print("Agent destroyed.\n")
|
||||
|
||||
# ── Phase 4: Resume — no config needed ──
|
||||
print("=== Phase 4: Resume Session ===")
|
||||
db2 = DB(database_url=SQLITE_PATH)
|
||||
managed2 = ManagedAgent(provider="local", db=db2)
|
||||
managed2.resume_session(session_id)
|
||||
print(f"Resumed session: {managed2.session_id}")
|
||||
|
||||
agent2 = Agent(name="User", backend=managed2)
|
||||
result3 = agent2.run("What is my favourite book, author, year, and second favourite?")
|
||||
print(f"Agent: {result3[:300]}...")
|
||||
|
||||
# ── Phase 5: Validate ──
|
||||
print("\n=== Phase 5: Validation ===")
|
||||
r = result3.lower()
|
||||
checks = {
|
||||
"Remembers 'Dune'": "dune" in r,
|
||||
"Remembers 'Herbert'": "herbert" in r,
|
||||
"Remembers '1965'": "1965" in r,
|
||||
"Remembers 'Messiah'": "messiah" in r,
|
||||
"Session ID continuity": managed2.session_id == session_id,
|
||||
}
|
||||
all_passed = True
|
||||
for check, ok in checks.items():
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {check}")
|
||||
if not ok:
|
||||
all_passed = False
|
||||
|
||||
mongo_col.delete_one({"_id": session_id})
|
||||
mongo_client.close()
|
||||
os.unlink(SQLITE_PATH)
|
||||
print(f"\n{'PASS' if all_passed else 'FAIL'}: MongoDB ManagedAgent persistence test")
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + MySQL Persistence — Real conversation with session resume.
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents mysql-connector-python
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
# MySQL running on localhost:3307
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import mysql.connector
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
sys.exit("ERROR: OPENAI_API_KEY not set.")
|
||||
|
||||
from praisonai import ManagedAgent, LocalManagedConfig, DB
|
||||
from praisonaiagents import Agent
|
||||
|
||||
MYSQL_URL = os.getenv("MYSQL_URL", "mysql://root:praisontest@localhost:3307/praisonai")
|
||||
print(f"ManagedAgent + MySQL Persistence\nDB: {MYSQL_URL}\n")
|
||||
|
||||
# ── Phase 1: Create agent, teach facts ──
|
||||
print("=== Phase 1: First Session ===")
|
||||
|
||||
db = DB(database_url=MYSQL_URL)
|
||||
managed = ManagedAgent(
|
||||
provider="local", db=db,
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini", name="MySQL Memory Agent",
|
||||
system="You are a helpful assistant. Remember all facts the user tells you.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="User", backend=managed)
|
||||
|
||||
result1 = agent.run("Remember: I live in Tokyo, Japan and love ramen from Ichiran. Confirm.")
|
||||
print(f"Agent: {result1[:200]}...")
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
|
||||
result2 = agent.run("Also remember: I work as a machine learning engineer at a robotics company. Confirm.")
|
||||
print(f"Agent: {result2[:200]}...")
|
||||
|
||||
# ── Phase 2: Direct MySQL Verification ──
|
||||
print("\n=== Phase 2: MySQL Verification ===")
|
||||
conn = mysql.connector.connect(host="localhost", port=3307, user="root", password="praisontest", database="praisonai")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT table_name FROM information_schema.tables WHERE table_schema='praisonai' AND table_name LIKE '%message%'")
|
||||
msg_tables = [r[0] for r in cursor.fetchall()]
|
||||
print(f"Message tables: {msg_tables}")
|
||||
for t in msg_tables:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM `{t}`")
|
||||
print(f" {t}: {cursor.fetchone()[0]} rows")
|
||||
conn.close()
|
||||
|
||||
# ── Phase 3: Destroy instance ──
|
||||
print("\n=== Phase 3: Instance Goes Idle ===")
|
||||
saved_ids = managed.save_ids()
|
||||
print(f"Saved IDs: {saved_ids}")
|
||||
del agent, managed, db
|
||||
print("Agent destroyed.\n")
|
||||
|
||||
# ── Phase 4: Resume — no config needed ──
|
||||
print("=== Phase 4: Resume Session ===")
|
||||
db2 = DB(database_url=MYSQL_URL)
|
||||
managed2 = ManagedAgent(provider="local", db=db2)
|
||||
managed2.resume_session(saved_ids["session_id"])
|
||||
print(f"Resumed session: {managed2.session_id}")
|
||||
|
||||
agent2 = Agent(name="User", backend=managed2)
|
||||
result3 = agent2.run("Where do I live, what's my favourite food, and what do I do for work?")
|
||||
print(f"Agent: {result3[:300]}...")
|
||||
|
||||
# ── Phase 5: Validate ──
|
||||
print("\n=== Phase 5: Validation ===")
|
||||
r = result3.lower()
|
||||
checks = {
|
||||
"Remembers 'Tokyo'": "tokyo" in r,
|
||||
"Remembers 'ramen'": "ramen" in r,
|
||||
"Remembers 'Ichiran'": "ichiran" in r,
|
||||
"Remembers 'machine learning'": "machine learning" in r or "ml" in r,
|
||||
"Remembers 'robotics'": "robotic" in r,
|
||||
"Session ID continuity": managed2.session_id == saved_ids["session_id"],
|
||||
}
|
||||
all_passed = True
|
||||
for check, ok in checks.items():
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {check}")
|
||||
if not ok:
|
||||
all_passed = False
|
||||
|
||||
print(f"\n{'PASS' if all_passed else 'FAIL'}: MySQL ManagedAgent persistence test")
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Neon Serverless PostgreSQL Persistence Example.
|
||||
|
||||
Neon auto-idles after inactivity — you only pay when active.
|
||||
The PraisonAI adapter auto-handles cold-start retries and SSL.
|
||||
|
||||
Setup:
|
||||
1. Create free Neon project at https://neon.tech
|
||||
2. Copy connection string from dashboard
|
||||
3. Set env var: export NEON_DATABASE_URL="postgresql://user:[email protected]/dbname?sslmode=require"
|
||||
4. pip install praisonai[neon]
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Option 1: Use NEON_DATABASE_URL env var (recommended)
|
||||
agent = Agent(
|
||||
name="Neon Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
db={"database_url": os.getenv("NEON_DATABASE_URL")},
|
||||
session_id="neon-demo-session",
|
||||
)
|
||||
|
||||
# Option 2: Direct URL
|
||||
# agent = Agent(
|
||||
# name="Neon Agent",
|
||||
# instructions="You are a helpful assistant.",
|
||||
# db={"database_url": "postgresql://user:[email protected]/dbname?sslmode=require"},
|
||||
# session_id="neon-demo-session",
|
||||
# )
|
||||
|
||||
result = agent.start("What is serverless PostgreSQL? Explain in 2 sentences.")
|
||||
print(result)
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + PostgreSQL Persistence — Real conversation with session resume.
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents psycopg2-binary
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
# PostgreSQL running on localhost:5432
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import psycopg2
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
sys.exit("ERROR: OPENAI_API_KEY not set.")
|
||||
|
||||
from praisonai import ManagedAgent, LocalManagedConfig, DB
|
||||
from praisonaiagents import Agent
|
||||
|
||||
PG_URL = os.getenv("PG_URL", "postgresql://postgres:postgres@localhost:5432/postgres")
|
||||
print(f"ManagedAgent + PostgreSQL Persistence\nDB: {PG_URL}\n")
|
||||
|
||||
# ── Phase 1: Create agent, teach facts ──
|
||||
print("=== Phase 1: First Session ===")
|
||||
|
||||
db = DB(database_url=PG_URL)
|
||||
managed = ManagedAgent(
|
||||
provider="local", db=db,
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini", name="PG Memory Agent",
|
||||
system="You are a helpful assistant. Remember all facts the user tells you.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="User", backend=managed)
|
||||
|
||||
result1 = agent.run("Remember: My favourite language is Rust, 3 years experience. Confirm.")
|
||||
print(f"Agent: {result1[:200]}...")
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
|
||||
result2 = agent.run("Also remember: I'm building a web crawler with the reqwest crate. Confirm.")
|
||||
print(f"Agent: {result2[:200]}...")
|
||||
|
||||
# ── Phase 2: Direct PostgreSQL Verification ──
|
||||
print("\n=== Phase 2: PostgreSQL Verification ===")
|
||||
conn = psycopg2.connect(PG_URL)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name LIKE '%message%'
|
||||
""")
|
||||
msg_tables = [r[0] for r in cursor.fetchall()]
|
||||
print(f"Message tables: {msg_tables}")
|
||||
for t in msg_tables:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {t}")
|
||||
print(f" {t}: {cursor.fetchone()[0]} rows")
|
||||
conn.close()
|
||||
|
||||
# ── Phase 3: Destroy instance ──
|
||||
print("\n=== Phase 3: Instance Goes Idle ===")
|
||||
saved_ids = managed.save_ids()
|
||||
print(f"Saved IDs: {saved_ids}")
|
||||
del agent, managed, db
|
||||
print("Agent destroyed.\n")
|
||||
|
||||
# ── Phase 4: Resume — no config needed ──
|
||||
print("=== Phase 4: Resume Session ===")
|
||||
db2 = DB(database_url=PG_URL)
|
||||
managed2 = ManagedAgent(provider="local", db=db2)
|
||||
managed2.resume_session(saved_ids["session_id"])
|
||||
print(f"Resumed session: {managed2.session_id}")
|
||||
|
||||
agent2 = Agent(name="User", backend=managed2)
|
||||
result3 = agent2.run("What is my favourite language, experience, and what am I building?")
|
||||
print(f"Agent: {result3[:300]}...")
|
||||
|
||||
# ── Phase 5: Validate ──
|
||||
print("\n=== Phase 5: Validation ===")
|
||||
r = result3.lower()
|
||||
checks = {
|
||||
"Remembers 'Rust'": "rust" in r,
|
||||
"Remembers '3 years'": "3" in r and "year" in r,
|
||||
"Remembers 'crawler'": "crawler" in r or "crawl" in r,
|
||||
"Remembers 'reqwest'": "reqwest" in r,
|
||||
"Session ID continuity": managed2.session_id == saved_ids["session_id"],
|
||||
}
|
||||
all_passed = True
|
||||
for check, ok in checks.items():
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {check}")
|
||||
if not ok:
|
||||
all_passed = False
|
||||
|
||||
print(f"\n{'PASS' if all_passed else 'FAIL'}: PostgreSQL ManagedAgent persistence test")
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + Redis Persistence — Real conversation with session resume.
|
||||
|
||||
Redis stores state/metadata; SQLite stores conversation history.
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents redis
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
# Redis running on localhost:6379
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import redis as redis_lib
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
sys.exit("ERROR: OPENAI_API_KEY not set.")
|
||||
|
||||
from praisonai import ManagedAgent, LocalManagedConfig, DB
|
||||
from praisonaiagents import Agent
|
||||
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://:myredissecret@localhost:6379/0")
|
||||
SQLITE_PATH = os.path.join(tempfile.gettempdir(), "managed_redis_conv.db")
|
||||
print(f"ManagedAgent + Redis Persistence\nRedis: {REDIS_URL}\nSQLite: {SQLITE_PATH}\n")
|
||||
|
||||
# ── Phase 1: Create agent, teach facts ──
|
||||
print("=== Phase 1: First Session ===")
|
||||
|
||||
db = DB(database_url=SQLITE_PATH, state_url=REDIS_URL)
|
||||
managed = ManagedAgent(
|
||||
provider="local", db=db,
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini", name="Redis Memory Agent",
|
||||
system="You are a helpful assistant. Remember all facts the user tells you.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="User", backend=managed)
|
||||
|
||||
result1 = agent.run("Remember: My cat Luna is a Siamese who loves tuna treats. Confirm.")
|
||||
print(f"Agent: {result1[:200]}...")
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
|
||||
result2 = agent.run("Also remember: Luna was adopted from a shelter in Portland, Oregon. Confirm.")
|
||||
print(f"Agent: {result2[:200]}...")
|
||||
|
||||
# ── Phase 2: Direct Redis Verification ──
|
||||
print("\n=== Phase 2: Redis Verification ===")
|
||||
rc = redis_lib.Redis(host="localhost", port=6379, password="myredissecret", decode_responses=True)
|
||||
all_keys = rc.keys("*")
|
||||
print(f"Total Redis keys: {len(all_keys)}")
|
||||
|
||||
# ── Phase 3: Destroy instance ──
|
||||
print("\n=== Phase 3: Instance Goes Idle ===")
|
||||
saved_ids = managed.save_ids()
|
||||
print(f"Saved IDs: {saved_ids}")
|
||||
del agent, managed, db
|
||||
print("Agent destroyed.\n")
|
||||
|
||||
# ── Phase 4: Resume — no config needed ──
|
||||
print("=== Phase 4: Resume Session ===")
|
||||
db2 = DB(database_url=SQLITE_PATH, state_url=REDIS_URL)
|
||||
managed2 = ManagedAgent(provider="local", db=db2)
|
||||
managed2.resume_session(saved_ids["session_id"])
|
||||
print(f"Resumed session: {managed2.session_id}")
|
||||
|
||||
agent2 = Agent(name="User", backend=managed2)
|
||||
result3 = agent2.run("What is my cat's name, breed, favourite treat, and adoption city?")
|
||||
print(f"Agent: {result3[:300]}...")
|
||||
|
||||
# ── Phase 5: Validate ──
|
||||
print("\n=== Phase 5: Validation ===")
|
||||
r = result3.lower()
|
||||
checks = {
|
||||
"Remembers 'Luna'": "luna" in r,
|
||||
"Remembers 'Siamese'": "siamese" in r,
|
||||
"Remembers 'tuna'": "tuna" in r,
|
||||
"Remembers 'Portland'": "portland" in r,
|
||||
"Session ID continuity": managed2.session_id == saved_ids["session_id"],
|
||||
}
|
||||
all_passed = True
|
||||
for check, ok in checks.items():
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {check}")
|
||||
if not ok:
|
||||
all_passed = False
|
||||
|
||||
os.unlink(SQLITE_PATH)
|
||||
print(f"\n{'PASS' if all_passed else 'FAIL'}: Redis ManagedAgent persistence test")
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ManagedAgent + SQLite Persistence — Real conversation with session resume.
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
sys.exit("ERROR: OPENAI_API_KEY not set.")
|
||||
|
||||
from praisonai import ManagedAgent, LocalManagedConfig, DB
|
||||
from praisonaiagents import Agent
|
||||
|
||||
DB_PATH = os.path.join(tempfile.gettempdir(), "managed_sqlite_test.db")
|
||||
print(f"ManagedAgent + SQLite Persistence\nDB: {DB_PATH}\n")
|
||||
|
||||
# ── Phase 1: Create agent, teach facts ──
|
||||
print("=== Phase 1: First Session ===")
|
||||
|
||||
db = DB(database_url=DB_PATH)
|
||||
managed = ManagedAgent(
|
||||
provider="local", db=db,
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini", name="SQLite Memory Agent",
|
||||
system="You are a helpful assistant. Remember all facts the user tells you.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="User", backend=managed)
|
||||
|
||||
result1 = agent.run("Remember: My dog's name is Biscuit, a golden retriever. Confirm.")
|
||||
print(f"Agent: {result1[:200]}...")
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
|
||||
result2 = agent.run("Also remember: Biscuit's favourite toy is a blue squeaky ball. Confirm.")
|
||||
print(f"Agent: {result2[:200]}...")
|
||||
|
||||
# ── Phase 2: Verify SQLite directly ──
|
||||
print("\n=== Phase 2: SQLite Verification ===")
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = [r[0] for r in cursor.fetchall()]
|
||||
print(f"Tables: {tables}")
|
||||
for t in tables:
|
||||
if "message" in t.lower():
|
||||
cursor.execute(f"SELECT COUNT(*) FROM [{t}]")
|
||||
print(f" {t}: {cursor.fetchone()[0]} rows")
|
||||
conn.close()
|
||||
|
||||
# ── Phase 3: Destroy instance (simulate idle) ──
|
||||
print("\n=== Phase 3: Instance Goes Idle ===")
|
||||
saved_ids = managed.save_ids()
|
||||
print(f"Saved IDs: {saved_ids}")
|
||||
del agent, managed, db
|
||||
print("Agent destroyed.\n")
|
||||
|
||||
# ── Phase 4: Resume — no config needed, restored from DB ──
|
||||
print("=== Phase 4: Resume Session ===")
|
||||
db2 = DB(database_url=DB_PATH)
|
||||
managed2 = ManagedAgent(provider="local", db=db2)
|
||||
managed2.resume_session(saved_ids["session_id"])
|
||||
print(f"Resumed session: {managed2.session_id}")
|
||||
|
||||
agent2 = Agent(name="User", backend=managed2)
|
||||
result3 = agent2.run("What is my dog's name, breed, and favourite toy?")
|
||||
print(f"Agent: {result3[:300]}...")
|
||||
|
||||
# ── Phase 5: Validate ──
|
||||
print("\n=== Phase 5: Validation ===")
|
||||
r = result3.lower()
|
||||
checks = {
|
||||
"Remembers 'Biscuit'": "biscuit" in r,
|
||||
"Remembers 'golden retriever'": "golden retriever" in r,
|
||||
"Remembers toy": "blue" in r or "squeaky" in r,
|
||||
"Session ID continuity": managed2.session_id == saved_ids["session_id"],
|
||||
}
|
||||
all_passed = True
|
||||
for check, ok in checks.items():
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {check}")
|
||||
if not ok:
|
||||
all_passed = False
|
||||
|
||||
os.unlink(DB_PATH)
|
||||
print(f"\n{'PASS' if all_passed else 'FAIL'}: SQLite ManagedAgent persistence test")
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Supabase Direct PostgreSQL Persistence Example.
|
||||
|
||||
Supabase offers two connection modes:
|
||||
1. REST API (via supabase-py) — set SUPABASE_URL + SUPABASE_KEY
|
||||
2. Direct PostgreSQL (via psycopg2) — use Postgres connection string
|
||||
|
||||
This example uses direct PostgreSQL mode with auto-retry for paused projects.
|
||||
|
||||
Setup:
|
||||
1. Create free Supabase project at https://supabase.com
|
||||
2. Go to Settings > Database > Connection string (URI)
|
||||
3. Set env var: export SUPABASE_DATABASE_URL="postgresql://postgres.xxx:[pass]@xxx.pooler.supabase.com:6543/postgres"
|
||||
4. pip install praisonai[postgresql]
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Supabase Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
db={"database_url": os.getenv("SUPABASE_DATABASE_URL")},
|
||||
session_id="supabase-demo-session",
|
||||
)
|
||||
|
||||
result = agent.start("What is Supabase? Explain in 2 sentences.")
|
||||
print(result)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Turso/libSQL Persistence Example.
|
||||
|
||||
Turso provides SQLite-at-the-edge with automatic sync and scale-to-zero.
|
||||
|
||||
Setup:
|
||||
1. Create free Turso database at https://turso.tech
|
||||
2. Run: turso db tokens create <db-name>
|
||||
3. Set env vars:
|
||||
export TURSO_DATABASE_URL="libsql://mydb-user.turso.io"
|
||||
export TURSO_AUTH_TOKEN="eyJ..."
|
||||
4. pip install praisonai[turso]
|
||||
"""
|
||||
|
||||
import os
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Turso Agent",
|
||||
instructions="You are a helpful assistant.",
|
||||
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.")
|
||||
print(result)
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
ClickHouse — Persistence Example
|
||||
|
||||
Demonstrates:
|
||||
- Connecting to ClickHouse
|
||||
- Creating tables for agent state
|
||||
- Writing and reading agent metadata
|
||||
- JSON state storage and retrieval
|
||||
- Session resume after simulated restart
|
||||
|
||||
Requirements:
|
||||
pip install clickhouse-connect
|
||||
Docker: ClickHouse on localhost:8123 (user=clickhouse, password=clickhouse)
|
||||
|
||||
Run:
|
||||
python clickhouse_persistence.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import clickhouse_connect
|
||||
|
||||
CH_HOST = "localhost"
|
||||
CH_PORT = 8123
|
||||
CH_USER = "clickhouse"
|
||||
CH_PASSWORD = "clickhouse"
|
||||
|
||||
print(f"ClickHouse: {CH_HOST}:{CH_PORT}\n")
|
||||
|
||||
client = clickhouse_connect.get_client(
|
||||
host=CH_HOST, port=CH_PORT, username=CH_USER, password=CH_PASSWORD,
|
||||
)
|
||||
|
||||
TABLE_SESSIONS = f"praison_sessions_{uuid.uuid4().hex[:8]}"
|
||||
TABLE_STATE = f"praison_state_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# --- Phase 1: Create tables and insert session data ---
|
||||
print("=== Phase 1: Create Tables & Insert Sessions ===")
|
||||
client.command(f"""
|
||||
CREATE TABLE IF NOT EXISTS {TABLE_SESSIONS} (
|
||||
session_id String,
|
||||
agent_id String,
|
||||
agent_version UInt32,
|
||||
total_input_tokens UInt64,
|
||||
total_output_tokens UInt64,
|
||||
model String,
|
||||
created_at DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree()
|
||||
ORDER BY session_id
|
||||
""")
|
||||
|
||||
sessions = [
|
||||
["session_ch_001", "agent_demo_001", 3, 500, 200, "gpt-4o"],
|
||||
["session_ch_002", "agent_demo_002", 1, 1000, 400, "gpt-4o-mini"],
|
||||
["session_ch_003", "agent_demo_001", 3, 750, 300, "gpt-4o"],
|
||||
]
|
||||
|
||||
client.insert(
|
||||
TABLE_SESSIONS, sessions,
|
||||
column_names=["session_id", "agent_id", "agent_version", "total_input_tokens", "total_output_tokens", "model"],
|
||||
)
|
||||
|
||||
for s in sessions:
|
||||
print(f" Inserted: {s[0]} (agent={s[1]}, tokens_in={s[3]})")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 2: Query and verify ---
|
||||
print("=== Phase 2: Query & Verify ===")
|
||||
result = client.query(f"SELECT * FROM {TABLE_SESSIONS} ORDER BY session_id")
|
||||
print(f" Total sessions: {len(result.result_rows)}")
|
||||
for row in result.result_rows:
|
||||
print(f" {row[0]}: agent={row[1]}, v{row[2]}, tokens_in={row[3]}, model={row[5]}")
|
||||
|
||||
assert len(result.result_rows) == 3
|
||||
assert result.result_rows[0][0] == "session_ch_001"
|
||||
|
||||
# Aggregate query
|
||||
agg = client.query(f"SELECT agent_id, SUM(total_input_tokens) as total_in FROM {TABLE_SESSIONS} GROUP BY agent_id ORDER BY agent_id")
|
||||
print("\n Tokens by agent:")
|
||||
for row in agg.result_rows:
|
||||
print(f" {row[0]}: {row[1]} input tokens")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 3: JSON state storage ---
|
||||
print("=== Phase 3: JSON State Storage ===")
|
||||
client.command(f"""
|
||||
CREATE TABLE IF NOT EXISTS {TABLE_STATE} (
|
||||
session_id String,
|
||||
state_json String,
|
||||
updated_at DateTime DEFAULT now()
|
||||
) ENGINE = ReplacingMergeTree(updated_at)
|
||||
ORDER BY session_id
|
||||
""")
|
||||
|
||||
state = {
|
||||
"agent_id": "agent_demo_001",
|
||||
"agent_version": 3,
|
||||
"environment_id": "env_ch_001",
|
||||
"total_input_tokens": 2000,
|
||||
"total_output_tokens": 800,
|
||||
"compute_instance_id": "flyio_ch_001",
|
||||
"session_history": [
|
||||
{"id": "session_ch_001", "status": "completed"},
|
||||
{"id": "session_ch_003", "status": "idle"},
|
||||
],
|
||||
}
|
||||
|
||||
client.insert(
|
||||
TABLE_STATE,
|
||||
[["session_ch_001", json.dumps(state)]],
|
||||
column_names=["session_id", "state_json"],
|
||||
)
|
||||
print(" Stored JSON state for session_ch_001")
|
||||
|
||||
# Retrieve
|
||||
result = client.query(f"SELECT state_json FROM {TABLE_STATE} WHERE session_id = 'session_ch_001'")
|
||||
recovered = json.loads(result.result_rows[0][0])
|
||||
print(f" Recovered agent_id: {recovered['agent_id']}")
|
||||
print(f" Recovered tokens: in={recovered['total_input_tokens']}, out={recovered['total_output_tokens']}")
|
||||
print(f" Recovered compute: {recovered['compute_instance_id']}")
|
||||
assert recovered["total_input_tokens"] == 2000
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 4: Session Resume ---
|
||||
print("=== Phase 4: Session Resume (Simulating Restart) ===")
|
||||
client.close()
|
||||
|
||||
client2 = clickhouse_connect.get_client(
|
||||
host=CH_HOST, port=CH_PORT, username=CH_USER, password=CH_PASSWORD,
|
||||
)
|
||||
|
||||
result2 = client2.query(f"SELECT state_json FROM {TABLE_STATE} WHERE session_id = 'session_ch_001'")
|
||||
assert len(result2.result_rows) > 0, "State not found after restart!"
|
||||
recovered2 = json.loads(result2.result_rows[0][0])
|
||||
print(f" Recovered after restart: agent={recovered2['agent_id']}, tokens_in={recovered2['total_input_tokens']}")
|
||||
|
||||
# Update state
|
||||
recovered2["total_input_tokens"] += 500
|
||||
client2.insert(
|
||||
TABLE_STATE,
|
||||
[["session_ch_001", json.dumps(recovered2)]],
|
||||
column_names=["session_id", "state_json"],
|
||||
)
|
||||
print(f" Updated tokens_in: {recovered2['total_input_tokens']}") # noqa: F541
|
||||
|
||||
# --- Cleanup ---
|
||||
print("\n=== Cleanup ===")
|
||||
client2.command(f"DROP TABLE IF EXISTS {TABLE_SESSIONS}")
|
||||
client2.command(f"DROP TABLE IF EXISTS {TABLE_STATE}")
|
||||
client2.close()
|
||||
print(" Tables dropped.")
|
||||
|
||||
print("\n✅ ClickHouse Persistence — All tests passed!")
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
JSON File DefaultSessionStore — Full Persistence Example
|
||||
|
||||
Demonstrates:
|
||||
- Creating a DefaultSessionStore (JSON file-backed)
|
||||
- Adding messages and metadata
|
||||
- Verifying data on disk
|
||||
- Session resume after simulated restart
|
||||
- Full managed agent state roundtrip (7 data categories)
|
||||
|
||||
Requirements:
|
||||
pip install praisonaiagents
|
||||
|
||||
Run:
|
||||
python json_file_session_store.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
from praisonaiagents.session.store import DefaultSessionStore
|
||||
|
||||
# --- Setup ---
|
||||
session_dir = os.path.join(tempfile.gettempdir(), f"praison_json_example_{uuid.uuid4().hex[:6]}")
|
||||
os.makedirs(session_dir, exist_ok=True)
|
||||
print(f"Session directory: {session_dir}\n")
|
||||
|
||||
store = DefaultSessionStore(session_dir=session_dir)
|
||||
SESSION_ID = f"json-demo-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# --- Phase 1: Add messages ---
|
||||
print("=== Phase 1: Add Messages ===")
|
||||
messages = [
|
||||
("user", "What is machine learning?"),
|
||||
("assistant", "Machine learning is a subset of AI that enables systems to learn from data."),
|
||||
("user", "Give me an example."),
|
||||
("assistant", "Spam filters use ML to classify emails as spam or not spam."),
|
||||
]
|
||||
|
||||
for role, content in messages:
|
||||
store.add_message(SESSION_ID, role, content)
|
||||
print(f" Added [{role}]: {content[:60]}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 2: Store metadata (simulating managed agent state) ---
|
||||
print("=== Phase 2: Store Metadata ===")
|
||||
session = store.get_session(SESSION_ID)
|
||||
session.metadata = {
|
||||
"agent_id": "json_demo_agent_001",
|
||||
"agent_version": 2,
|
||||
"environment_id": "env_local_json",
|
||||
"total_input_tokens": 450,
|
||||
"total_output_tokens": 180,
|
||||
"compute_instance_id": "local_json_demo",
|
||||
"session_history": [
|
||||
{"id": SESSION_ID, "status": "idle", "title": "ML chat"},
|
||||
],
|
||||
}
|
||||
store._save_session(session)
|
||||
|
||||
print(f" agent_id: {session.metadata['agent_id']}")
|
||||
print(f" tokens: in={session.metadata['total_input_tokens']}, out={session.metadata['total_output_tokens']}")
|
||||
print(f" compute: {session.metadata['compute_instance_id']}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 3: Verify on disk ---
|
||||
print("=== Phase 3: Verify JSON File on Disk ===")
|
||||
json_file = os.path.join(session_dir, f"{SESSION_ID}.json")
|
||||
assert os.path.exists(json_file), f"JSON file not found: {json_file}"
|
||||
|
||||
with open(json_file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f" File: {json_file}")
|
||||
print(f" Session ID: {data['session_id']}")
|
||||
print(f" Messages: {len(data.get('messages', []))}")
|
||||
print(f" Metadata agent_id: {data['metadata']['agent_id']}")
|
||||
print(f" Metadata tokens_in: {data['metadata']['total_input_tokens']}")
|
||||
|
||||
assert data["metadata"]["agent_id"] == "json_demo_agent_001"
|
||||
assert data["metadata"]["total_input_tokens"] == 450
|
||||
assert len(data["messages"]) == 4
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 4: Session Resume ---
|
||||
print("=== Phase 4: Session Resume (Simulating Restart) ===")
|
||||
del store
|
||||
|
||||
store2 = DefaultSessionStore(session_dir=session_dir)
|
||||
|
||||
# Verify session exists
|
||||
assert store2.session_exists(SESSION_ID), "Session not found after restart!"
|
||||
|
||||
# Recover history
|
||||
history = store2.get_chat_history(SESSION_ID)
|
||||
print(f" Recovered messages: {len(history)}")
|
||||
assert len(history) == 4
|
||||
|
||||
# Recover metadata
|
||||
session2 = store2.get_session(SESSION_ID)
|
||||
meta = session2.metadata
|
||||
print(f" Recovered agent_id: {meta['agent_id']}")
|
||||
print(f" Recovered tokens: in={meta['total_input_tokens']}, out={meta['total_output_tokens']}")
|
||||
print(f" Recovered compute: {meta['compute_instance_id']}")
|
||||
print(f" Recovered session_history: {len(meta['session_history'])} entries")
|
||||
|
||||
assert meta["agent_id"] == "json_demo_agent_001"
|
||||
assert meta["total_input_tokens"] == 450
|
||||
assert meta["compute_instance_id"] == "local_json_demo"
|
||||
|
||||
# Continue conversation
|
||||
store2.add_message(SESSION_ID, "user", "What about deep learning?")
|
||||
store2.add_message(SESSION_ID, "assistant", "Deep learning uses neural networks with many layers.")
|
||||
final_history = store2.get_chat_history(SESSION_ID)
|
||||
print(f" Messages after resume: {len(final_history)}")
|
||||
assert len(final_history) == 6
|
||||
|
||||
# --- Cleanup ---
|
||||
print("\n=== Cleanup ===")
|
||||
shutil.rmtree(session_dir)
|
||||
print(" Session directory removed.")
|
||||
|
||||
print("\n✅ JSON File DefaultSessionStore — All tests passed!")
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
MongoDB StateStore — Full Persistence Example
|
||||
|
||||
Demonstrates:
|
||||
- Creating a MongoDB StateStore
|
||||
- Key-value operations (get/set/delete/exists)
|
||||
- Complex state persistence (agent metadata, usage tokens)
|
||||
- Direct MongoDB verification
|
||||
- Session resume after simulated restart
|
||||
|
||||
Requirements:
|
||||
pip install praisonai pymongo
|
||||
Docker: MongoDB on localhost:27017 (no auth)
|
||||
|
||||
Run:
|
||||
python mongodb_state_store.py
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import pymongo
|
||||
from praisonai.persistence.state.mongodb import MongoDBStateStore
|
||||
|
||||
MONGO_URL = "mongodb://localhost:27017"
|
||||
DATABASE = "praisonai_example"
|
||||
COLLECTION = f"state_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
print(f"MongoDB: {MONGO_URL}")
|
||||
print(f"Database: {DATABASE}, Collection: {COLLECTION}\n")
|
||||
|
||||
store = MongoDBStateStore(url=MONGO_URL, database=DATABASE, collection=COLLECTION)
|
||||
|
||||
# --- Phase 1: Basic key-value operations ---
|
||||
print("=== Phase 1: Basic Key-Value Operations ===")
|
||||
store.set("agent_id", "mongo_demo_agent_001")
|
||||
store.set("model", "gpt-4o-mini")
|
||||
store.set("counter", 42)
|
||||
|
||||
print(f" agent_id: {store.get('agent_id')}")
|
||||
print(f" model: {store.get('model')}")
|
||||
print(f" counter: {store.get('counter')}")
|
||||
print(f" exists('agent_id'): {store.exists('agent_id')}")
|
||||
print(f" exists('missing'): {store.exists('missing')}")
|
||||
|
||||
assert store.get("agent_id") == "mongo_demo_agent_001"
|
||||
assert store.get("counter") == 42
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 2: Complex state persistence ---
|
||||
print("=== Phase 2: Complex State Persistence ===")
|
||||
managed_state = {
|
||||
"agent_id": "mongo_demo_agent_001",
|
||||
"agent_version": 4,
|
||||
"environment_id": "env_mongo_001",
|
||||
"total_input_tokens": 800,
|
||||
"total_output_tokens": 300,
|
||||
"compute_instance_id": "e2b_mongo_demo",
|
||||
"session_history": [
|
||||
{"id": "session_m1", "status": "completed"},
|
||||
{"id": "session_m2", "status": "idle"},
|
||||
],
|
||||
}
|
||||
|
||||
store.set("managed_state", managed_state)
|
||||
recovered = store.get("managed_state")
|
||||
|
||||
print(f" agent_id: {recovered['agent_id']}")
|
||||
print(f" agent_version: {recovered['agent_version']}")
|
||||
print(f" total_input_tokens: {recovered['total_input_tokens']}")
|
||||
print(f" compute_instance_id: {recovered['compute_instance_id']}")
|
||||
print(f" session_history count: {len(recovered['session_history'])}")
|
||||
|
||||
assert recovered["agent_id"] == "mongo_demo_agent_001"
|
||||
assert recovered["total_input_tokens"] == 800
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 3: Direct MongoDB Verification ---
|
||||
print("=== Phase 3: Direct MongoDB Verification ===")
|
||||
client = pymongo.MongoClient(MONGO_URL)
|
||||
doc = client[DATABASE][COLLECTION].find_one({"_id": "managed_state"})
|
||||
print(f" Raw doc _id: {doc['_id']}")
|
||||
print(f" Raw doc value.agent_id: {doc['value']['agent_id']}")
|
||||
print(f" Raw doc value.total_input_tokens: {doc['value']['total_input_tokens']}")
|
||||
assert doc["value"]["compute_instance_id"] == "e2b_mongo_demo"
|
||||
client.close()
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 4: Session Resume ---
|
||||
print("=== Phase 4: Session Resume (Simulating Restart) ===")
|
||||
store.close()
|
||||
|
||||
store2 = MongoDBStateStore(url=MONGO_URL, database=DATABASE, collection=COLLECTION)
|
||||
|
||||
recovered2 = store2.get("managed_state")
|
||||
assert recovered2 is not None, "State not found after restart!"
|
||||
print(f" Recovered agent_id: {recovered2['agent_id']}")
|
||||
print(f" Recovered tokens: in={recovered2['total_input_tokens']}, out={recovered2['total_output_tokens']}")
|
||||
print(f" Recovered compute: {recovered2['compute_instance_id']}")
|
||||
|
||||
# Update
|
||||
recovered2["total_input_tokens"] += 400
|
||||
store2.set("managed_state", recovered2)
|
||||
final = store2.get("managed_state")
|
||||
print(f" Updated input_tokens: {final['total_input_tokens']}")
|
||||
assert final["total_input_tokens"] == 1200
|
||||
|
||||
# Delete test
|
||||
store2.delete("counter")
|
||||
assert not store2.exists("counter")
|
||||
print(" Delete verified: counter removed")
|
||||
|
||||
store2.close()
|
||||
|
||||
# --- Cleanup ---
|
||||
print("\n=== Cleanup ===")
|
||||
client = pymongo.MongoClient(MONGO_URL)
|
||||
client[DATABASE].drop_collection(COLLECTION)
|
||||
client.close()
|
||||
print(" Collection dropped.")
|
||||
|
||||
print("\n✅ MongoDB StateStore — All tests passed!")
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
MySQL ConversationStore — Full Persistence Example
|
||||
|
||||
Demonstrates:
|
||||
- Creating a MySQL ConversationStore
|
||||
- Creating sessions, adding messages
|
||||
- Direct SQL verification that data is in MySQL
|
||||
- Session resume after simulated restart
|
||||
|
||||
Requirements:
|
||||
pip install praisonai mysql-connector-python
|
||||
Docker: MySQL on localhost:3307 (user=root, password=praisontest, database=praisonai)
|
||||
|
||||
Run:
|
||||
python mysql_conversation_store.py
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import mysql.connector
|
||||
from praisonai.persistence.conversation.mysql import MySQLConversationStore
|
||||
from praisonai.persistence.conversation.base import ConversationSession, ConversationMessage
|
||||
|
||||
MYSQL_HOST = "localhost"
|
||||
MYSQL_PORT = 3307
|
||||
MYSQL_USER = "root"
|
||||
MYSQL_PASSWORD = "praisontest"
|
||||
MYSQL_DB = "praisonai"
|
||||
TABLE_PREFIX = f"example_{uuid.uuid4().hex[:6]}_"
|
||||
|
||||
print(f"MySQL: {MYSQL_USER}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}")
|
||||
print(f"Table prefix: {TABLE_PREFIX}\n")
|
||||
|
||||
store = MySQLConversationStore(
|
||||
host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER,
|
||||
password=MYSQL_PASSWORD, database=MYSQL_DB, table_prefix=TABLE_PREFIX,
|
||||
)
|
||||
SESSION_ID = f"mysql-demo-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# --- Phase 1: Create session and add messages ---
|
||||
print("=== Phase 1: Create Session & Add Messages ===")
|
||||
session = ConversationSession(
|
||||
session_id=SESSION_ID,
|
||||
agent_id="mysql_demo_agent",
|
||||
name="MySQL Demo",
|
||||
metadata={"agent_version": 7, "model": "gpt-4o-mini"},
|
||||
)
|
||||
store.create_session(session)
|
||||
|
||||
messages = [
|
||||
("user", "What makes MySQL popular?"),
|
||||
("assistant", "MySQL is popular for its speed, reliability, and ease of use in web applications."),
|
||||
("user", "What about scalability?"),
|
||||
("assistant", "MySQL supports replication, sharding, and clustering for horizontal scalability."),
|
||||
]
|
||||
|
||||
for role, content in messages:
|
||||
msg = ConversationMessage(session_id=SESSION_ID, role=role, content=content)
|
||||
store.add_message(SESSION_ID, msg)
|
||||
print(f" Added [{role}]: {content[:60]}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 2: Verify data ---
|
||||
print("=== Phase 2: Verify Data in MySQL ===")
|
||||
retrieved = store.get_session(SESSION_ID)
|
||||
print(f" Session: {retrieved.session_id}")
|
||||
print(f" Agent ID: {retrieved.agent_id}")
|
||||
print(f" Metadata: {retrieved.metadata}")
|
||||
|
||||
stored = store.get_messages(SESSION_ID)
|
||||
print(f" Messages: {len(stored)}")
|
||||
for msg in stored:
|
||||
print(f" [{msg.role}] {msg.content[:70]}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 3: Direct SQL Verification ---
|
||||
print("=== Phase 3: Direct SQL Verification ===")
|
||||
conn = mysql.connector.connect(
|
||||
host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER,
|
||||
password=MYSQL_PASSWORD, database=MYSQL_DB,
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"SELECT COUNT(*) FROM {TABLE_PREFIX}messages WHERE session_id = %s", (SESSION_ID,))
|
||||
count = cur.fetchone()[0]
|
||||
print(f" Raw SQL message count: {count}")
|
||||
assert count == 4, f"Expected 4, got {count}"
|
||||
|
||||
cur.execute(f"SELECT agent_id FROM {TABLE_PREFIX}sessions WHERE session_id = %s", (SESSION_ID,))
|
||||
agent_id = cur.fetchone()[0]
|
||||
print(f" Raw SQL agent_id: {agent_id}")
|
||||
assert agent_id == "mysql_demo_agent"
|
||||
conn.close()
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 4: Session Resume ---
|
||||
print("=== Phase 4: Session Resume (Simulating Restart) ===")
|
||||
store.close()
|
||||
|
||||
store2 = MySQLConversationStore(
|
||||
host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER,
|
||||
password=MYSQL_PASSWORD, database=MYSQL_DB, table_prefix=TABLE_PREFIX,
|
||||
)
|
||||
resumed = store2.get_session(SESSION_ID)
|
||||
assert resumed is not None, "Session not found after restart!"
|
||||
print(f" Resumed session: {resumed.session_id}")
|
||||
print(f" Metadata preserved: {resumed.metadata}")
|
||||
|
||||
msgs = store2.get_messages(SESSION_ID)
|
||||
print(f" Messages recovered: {len(msgs)}")
|
||||
assert len(msgs) == 4
|
||||
|
||||
# Continue
|
||||
store2.add_message(SESSION_ID, ConversationMessage(session_id=SESSION_ID, role="user", content="Thanks!"))
|
||||
store2.add_message(SESSION_ID, ConversationMessage(session_id=SESSION_ID, role="assistant", content="You're welcome!"))
|
||||
final = store2.get_messages(SESSION_ID)
|
||||
print(f" Messages after resume: {len(final)}")
|
||||
assert len(final) == 6
|
||||
|
||||
store2.close()
|
||||
|
||||
# --- Cleanup ---
|
||||
print("\n=== Cleanup ===")
|
||||
conn = mysql.connector.connect(
|
||||
host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER,
|
||||
password=MYSQL_PASSWORD, database=MYSQL_DB,
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"DROP TABLE IF EXISTS {TABLE_PREFIX}messages")
|
||||
cur.execute(f"DROP TABLE IF EXISTS {TABLE_PREFIX}sessions")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(" Tables dropped.")
|
||||
|
||||
print("\n✅ MySQL ConversationStore — All tests passed!")
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
PostgreSQL ConversationStore — Full Persistence Example
|
||||
|
||||
Demonstrates:
|
||||
- Creating a PostgreSQL ConversationStore
|
||||
- Creating sessions, adding messages, storing metadata
|
||||
- Direct SQL verification that data is in PostgreSQL
|
||||
- Session resume after simulated restart
|
||||
|
||||
Requirements:
|
||||
pip install praisonai psycopg2-binary
|
||||
Docker: PostgreSQL on localhost:5432 (user=postgres, password=postgres)
|
||||
|
||||
Run:
|
||||
python postgres_conversation_store.py
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import psycopg2
|
||||
from praisonai.persistence.conversation.postgres import PostgresConversationStore
|
||||
from praisonai.persistence.conversation.base import ConversationSession, ConversationMessage
|
||||
|
||||
PG_URL = "postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
TABLE_PREFIX = f"example_{uuid.uuid4().hex[:6]}_"
|
||||
|
||||
print(f"PostgreSQL URL: {PG_URL}")
|
||||
print(f"Table prefix: {TABLE_PREFIX}\n")
|
||||
|
||||
store = PostgresConversationStore(url=PG_URL, table_prefix=TABLE_PREFIX)
|
||||
SESSION_ID = f"pg-demo-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# --- Phase 1: Create session and add messages ---
|
||||
print("=== Phase 1: Create Session & Add Messages ===")
|
||||
session = ConversationSession(
|
||||
session_id=SESSION_ID,
|
||||
agent_id="pg_demo_agent",
|
||||
name="PostgreSQL Demo",
|
||||
metadata={"agent_version": 5, "model": "gpt-4o"},
|
||||
)
|
||||
store.create_session(session)
|
||||
|
||||
messages = [
|
||||
("user", "What are the benefits of PostgreSQL?"),
|
||||
("assistant", "PostgreSQL offers ACID compliance, extensibility, JSON support, and excellent performance."),
|
||||
("user", "How does it compare to MySQL?"),
|
||||
("assistant", "PostgreSQL has better standards compliance and advanced features, while MySQL is simpler for basic use."),
|
||||
]
|
||||
|
||||
for role, content in messages:
|
||||
msg = ConversationMessage(session_id=SESSION_ID, role=role, content=content)
|
||||
store.add_message(SESSION_ID, msg)
|
||||
print(f" Added [{role}]: {content[:60]}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 2: Verify data in PostgreSQL ---
|
||||
print("=== Phase 2: Verify Data in PostgreSQL ===")
|
||||
retrieved = store.get_session(SESSION_ID)
|
||||
print(f" Session: {retrieved.session_id}")
|
||||
print(f" Agent ID: {retrieved.agent_id}")
|
||||
print(f" Metadata: {retrieved.metadata}")
|
||||
|
||||
stored = store.get_messages(SESSION_ID)
|
||||
print(f" Messages: {len(stored)}")
|
||||
for msg in stored:
|
||||
print(f" [{msg.role}] {msg.content[:70]}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 3: Direct SQL Verification ---
|
||||
print("=== Phase 3: Direct SQL Verification ===")
|
||||
conn = psycopg2.connect(PG_URL)
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"SELECT COUNT(*) FROM public.{TABLE_PREFIX}messages WHERE session_id = %s", (SESSION_ID,))
|
||||
count = cur.fetchone()[0]
|
||||
print(f" Raw SQL message count: {count}")
|
||||
assert count == 4, f"Expected 4, got {count}"
|
||||
|
||||
cur.execute(f"SELECT agent_id FROM public.{TABLE_PREFIX}sessions WHERE session_id = %s", (SESSION_ID,))
|
||||
agent_id = cur.fetchone()[0]
|
||||
print(f" Raw SQL agent_id: {agent_id}")
|
||||
assert agent_id == "pg_demo_agent"
|
||||
conn.close()
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 4: Session Resume ---
|
||||
print("=== Phase 4: Session Resume (Simulating Restart) ===")
|
||||
store.close()
|
||||
|
||||
store2 = PostgresConversationStore(url=PG_URL, table_prefix=TABLE_PREFIX)
|
||||
resumed = store2.get_session(SESSION_ID)
|
||||
assert resumed is not None, "Session not found after restart!"
|
||||
print(f" Resumed session: {resumed.session_id}")
|
||||
print(f" Metadata preserved: {resumed.metadata}")
|
||||
|
||||
msgs = store2.get_messages(SESSION_ID)
|
||||
print(f" Messages recovered: {len(msgs)}")
|
||||
assert len(msgs) == 4
|
||||
|
||||
# Continue
|
||||
store2.add_message(SESSION_ID, ConversationMessage(session_id=SESSION_ID, role="user", content="Thanks!"))
|
||||
store2.add_message(SESSION_ID, ConversationMessage(session_id=SESSION_ID, role="assistant", content="You're welcome!"))
|
||||
final = store2.get_messages(SESSION_ID)
|
||||
print(f" Messages after resume: {len(final)}")
|
||||
assert len(final) == 6
|
||||
|
||||
store2.close()
|
||||
|
||||
# --- Cleanup ---
|
||||
print("\n=== Cleanup ===")
|
||||
conn = psycopg2.connect(PG_URL)
|
||||
conn.autocommit = True
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"DROP TABLE IF EXISTS public.{TABLE_PREFIX}messages CASCADE")
|
||||
cur.execute(f"DROP TABLE IF EXISTS public.{TABLE_PREFIX}sessions CASCADE")
|
||||
conn.close()
|
||||
print(" Tables dropped.")
|
||||
|
||||
print("\n✅ PostgreSQL ConversationStore — All tests passed!")
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Redis StateStore — Full Persistence Example
|
||||
|
||||
Demonstrates:
|
||||
- Creating a Redis StateStore
|
||||
- Key-value operations (get/set/delete/exists)
|
||||
- JSON state persistence (agent metadata, usage tokens)
|
||||
- Hash operations for structured data
|
||||
- Session resume: simulate restart, recover state
|
||||
|
||||
Requirements:
|
||||
pip install praisonai redis
|
||||
Docker: Redis on localhost:6379 (password=myredissecret)
|
||||
|
||||
Run:
|
||||
python redis_state_store.py
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import redis as redis_lib
|
||||
from praisonai.persistence.state.redis import RedisStateStore
|
||||
|
||||
REDIS_HOST = "localhost"
|
||||
REDIS_PORT = 6379
|
||||
REDIS_PASSWORD = "myredissecret"
|
||||
PREFIX = f"example_{uuid.uuid4().hex[:6]}:"
|
||||
|
||||
print(f"Redis: {REDIS_HOST}:{REDIS_PORT}")
|
||||
print(f"Key prefix: {PREFIX}\n")
|
||||
|
||||
store = RedisStateStore(
|
||||
host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, prefix=PREFIX,
|
||||
)
|
||||
|
||||
# --- Phase 1: Basic key-value operations ---
|
||||
print("=== Phase 1: Basic Key-Value Operations ===")
|
||||
store.set("agent_id", "redis_demo_agent_001")
|
||||
store.set("model", "gpt-4o-mini")
|
||||
store.set("counter", "42")
|
||||
|
||||
print(f" agent_id: {store.get('agent_id')}")
|
||||
print(f" model: {store.get('model')}")
|
||||
print(f" counter: {store.get('counter')}")
|
||||
print(f" exists('agent_id'): {store.exists('agent_id')}")
|
||||
print(f" exists('missing'): {store.exists('missing')}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 2: JSON state persistence ---
|
||||
print("=== Phase 2: JSON State Persistence ===")
|
||||
managed_state = {
|
||||
"agent_id": "redis_demo_agent_001",
|
||||
"agent_version": 3,
|
||||
"environment_id": "env_local_001",
|
||||
"total_input_tokens": 1500,
|
||||
"total_output_tokens": 600,
|
||||
"compute_instance_id": "docker_redis_demo",
|
||||
"session_history": [
|
||||
{"id": "session_001", "status": "completed", "title": "Weather chat"},
|
||||
{"id": "session_002", "status": "idle", "title": "Code review"},
|
||||
],
|
||||
}
|
||||
|
||||
store.set_json("managed_state", managed_state)
|
||||
recovered = store.get_json("managed_state")
|
||||
|
||||
print(f" agent_id: {recovered['agent_id']}")
|
||||
print(f" agent_version: {recovered['agent_version']}")
|
||||
print(f" total_input_tokens: {recovered['total_input_tokens']}")
|
||||
print(f" total_output_tokens: {recovered['total_output_tokens']}")
|
||||
print(f" compute_instance_id: {recovered['compute_instance_id']}")
|
||||
print(f" session_history count: {len(recovered['session_history'])}")
|
||||
|
||||
assert recovered["agent_id"] == "redis_demo_agent_001"
|
||||
assert recovered["total_input_tokens"] == 1500
|
||||
assert recovered["compute_instance_id"] == "docker_redis_demo"
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 3: Hash operations ---
|
||||
print("=== Phase 3: Hash Operations ===")
|
||||
store.hset("agent_meta", "version", "5")
|
||||
store.hset("agent_meta", "env_id", "env_abc")
|
||||
store.hset("agent_meta", "model", "gpt-4o")
|
||||
|
||||
print(f" version: {store.hget('agent_meta', 'version')}")
|
||||
all_meta = store.hgetall("agent_meta")
|
||||
print(f" all fields: {all_meta}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 4: Session Resume (Simulating Restart) ---
|
||||
print("=== Phase 4: Session Resume (Simulating Restart) ===")
|
||||
store.close()
|
||||
|
||||
# New store instance (simulating process restart)
|
||||
store2 = RedisStateStore(
|
||||
host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, prefix=PREFIX,
|
||||
)
|
||||
|
||||
# Recover state
|
||||
recovered2 = store2.get_json("managed_state")
|
||||
assert recovered2 is not None, "State not found after restart!"
|
||||
print(f" Recovered agent_id: {recovered2['agent_id']}")
|
||||
print(f" Recovered tokens: in={recovered2['total_input_tokens']}, out={recovered2['total_output_tokens']}")
|
||||
print(f" Recovered compute: {recovered2['compute_instance_id']}")
|
||||
print(f" Recovered sessions: {len(recovered2['session_history'])}")
|
||||
|
||||
# Update state (simulate more work)
|
||||
recovered2["total_input_tokens"] += 500
|
||||
recovered2["total_output_tokens"] += 200
|
||||
store2.set_json("managed_state", recovered2)
|
||||
|
||||
final = store2.get_json("managed_state")
|
||||
print(f" Updated tokens: in={final['total_input_tokens']}, out={final['total_output_tokens']}")
|
||||
assert final["total_input_tokens"] == 2000
|
||||
assert final["total_output_tokens"] == 800
|
||||
|
||||
store2.close()
|
||||
|
||||
# --- Cleanup ---
|
||||
print("\n=== Cleanup ===")
|
||||
r = redis_lib.Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD)
|
||||
for key in r.keys(f"{PREFIX}*"):
|
||||
r.delete(key)
|
||||
r.close()
|
||||
print(" Keys deleted.")
|
||||
|
||||
print("\n✅ Redis StateStore — All tests passed!")
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
SQLite ConversationStore — Full Persistence Example
|
||||
|
||||
Demonstrates:
|
||||
- Creating a SQLite ConversationStore
|
||||
- Creating sessions and adding messages
|
||||
- Verifying data is stored in the SQLite file
|
||||
- Session resume: destroy agent, restore from DB, verify state
|
||||
|
||||
Requirements:
|
||||
pip install praisonai
|
||||
|
||||
Run:
|
||||
python sqlite_conversation_store.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import tempfile
|
||||
from praisonai.persistence.conversation.sqlite import SQLiteConversationStore
|
||||
from praisonai.persistence.conversation.base import ConversationSession, ConversationMessage
|
||||
|
||||
# --- Setup ---
|
||||
db_path = os.path.join(tempfile.gettempdir(), "praison_sqlite_example.db")
|
||||
print(f"SQLite DB path: {db_path}\n")
|
||||
|
||||
store = SQLiteConversationStore(path=db_path)
|
||||
|
||||
SESSION_ID = "sqlite-demo-session-001"
|
||||
|
||||
# --- Phase 1: Create session and add messages ---
|
||||
print("=== Phase 1: Create Session & Add Messages ===")
|
||||
session = ConversationSession(
|
||||
session_id=SESSION_ID,
|
||||
agent_id="sqlite_demo_agent",
|
||||
name="SQLite Demo Session",
|
||||
metadata={"agent_version": 3, "environment": "local"},
|
||||
)
|
||||
store.create_session(session)
|
||||
|
||||
messages = [
|
||||
("user", "Hello! What is the capital of France?"),
|
||||
("assistant", "The capital of France is Paris."),
|
||||
("user", "What about Germany?"),
|
||||
("assistant", "The capital of Germany is Berlin."),
|
||||
]
|
||||
|
||||
for role, content in messages:
|
||||
msg = ConversationMessage(session_id=SESSION_ID, role=role, content=content)
|
||||
store.add_message(SESSION_ID, msg)
|
||||
print(f" Added [{role}]: {content[:50]}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 2: Verify data in DB ---
|
||||
print("=== Phase 2: Verify Data in SQLite ===")
|
||||
retrieved_session = store.get_session(SESSION_ID)
|
||||
print(f" Session ID: {retrieved_session.session_id}")
|
||||
print(f" Agent ID: {retrieved_session.agent_id}")
|
||||
print(f" Metadata: {retrieved_session.metadata}")
|
||||
|
||||
stored_messages = store.get_messages(SESSION_ID)
|
||||
print(f" Messages stored: {len(stored_messages)}")
|
||||
for msg in stored_messages:
|
||||
print(f" [{msg.role}] {msg.content[:60]}")
|
||||
|
||||
print()
|
||||
|
||||
# --- Phase 3: Session Resume ---
|
||||
print("=== Phase 3: Session Resume (Simulating Restart) ===")
|
||||
store.close()
|
||||
|
||||
# Reopen store from same file (simulating process restart)
|
||||
store2 = SQLiteConversationStore(path=db_path)
|
||||
|
||||
# Verify session still exists
|
||||
resumed_session = store2.get_session(SESSION_ID)
|
||||
assert resumed_session is not None, "Session not found after restart!"
|
||||
print(f" Resumed session: {resumed_session.session_id}")
|
||||
print(f" Agent ID: {resumed_session.agent_id}")
|
||||
print(f" Metadata preserved: {resumed_session.metadata}")
|
||||
|
||||
# Verify messages
|
||||
resumed_messages = store2.get_messages(SESSION_ID)
|
||||
print(f" Messages recovered: {len(resumed_messages)}")
|
||||
assert len(resumed_messages) == 4, f"Expected 4 messages, got {len(resumed_messages)}"
|
||||
|
||||
# Continue the conversation
|
||||
new_msg = ConversationMessage(session_id=SESSION_ID, role="user", content="What about Spain?")
|
||||
store2.add_message(SESSION_ID, new_msg)
|
||||
new_msg2 = ConversationMessage(session_id=SESSION_ID, role="assistant", content="The capital of Spain is Madrid.")
|
||||
store2.add_message(SESSION_ID, new_msg2)
|
||||
|
||||
final_messages = store2.get_messages(SESSION_ID)
|
||||
print(f" Messages after resume: {len(final_messages)}")
|
||||
assert len(final_messages) == 6
|
||||
|
||||
store2.close()
|
||||
|
||||
# --- Phase 4: Direct SQLite verification ---
|
||||
print("\n=== Phase 4: Direct SQLite Verification ===")
|
||||
conn = sqlite3.connect(db_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT COUNT(*) FROM praison_messages WHERE session_id = ?", (SESSION_ID,))
|
||||
count = cur.fetchone()[0]
|
||||
print(f" Raw SQL message count: {count}")
|
||||
assert count == 6
|
||||
conn.close()
|
||||
|
||||
# Cleanup
|
||||
os.remove(db_path)
|
||||
print("\n✅ SQLite ConversationStore — All tests passed!")
|
||||
@@ -43,14 +43,34 @@ app = FastAPI(
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
# Add CORS middleware with secure configuration
|
||||
cors_origins = os.getenv("API_CORS_ORIGINS", "").split(",")
|
||||
cors_origins = [origin.strip() for origin in cors_origins if origin.strip() and origin.strip() != "*"]
|
||||
|
||||
# Default secure origins if none specified
|
||||
if not cors_origins:
|
||||
# Secure defaults for different environments
|
||||
if os.getenv("ENVIRONMENT") == "production":
|
||||
# In production, require explicit configuration
|
||||
cors_origins = []
|
||||
else:
|
||||
# Development defaults - restrict to local origins
|
||||
cors_origins = [
|
||||
"http://localhost:3000", # Development frontend
|
||||
"http://localhost:8000", # Local development
|
||||
"http://127.0.0.1:3000", # Local development
|
||||
"http://127.0.0.1:8000", # Local development
|
||||
]
|
||||
|
||||
# Only add CORS middleware if origins are specified
|
||||
if cors_origins:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["Authorization", "Content-Type", "Origin", "Accept"],
|
||||
)
|
||||
|
||||
# Create directories for storing reports
|
||||
REPORTS_DIR = Path("generated_reports")
|
||||
|
||||
@@ -14,14 +14,17 @@ import subprocess
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_cli(cmd: str, cwd: str = None) -> tuple[int, str, str]:
|
||||
"""Run a CLI command and return (exit_code, stdout, stderr)."""
|
||||
# Use shell=False with shlex.split for safer execution
|
||||
args = shlex.split(cmd)
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
shell=True,
|
||||
args,
|
||||
shell=False, # Use shell=False for security
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from praisonai import Agent, ManagedAgent
|
||||
|
||||
# Zero config — defaults: name="Agent", model="claude-haiku-4-5"
|
||||
managed = ManagedAgent()
|
||||
agent = Agent(name="coder", backend=managed)
|
||||
result = agent.start("Say hello")
|
||||
|
||||
print(f"Agent ID: {managed.agent_id}")
|
||||
print(f"Version: {managed.agent_version}")
|
||||
@@ -0,0 +1,17 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# Environment is created automatically — just specify packages/networking in config
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Coding Assistant",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful coding assistant. Write clean, well-documented code.",
|
||||
networking={"type": "unrestricted"},
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="coder", backend=managed)
|
||||
result = agent.start("Say hello")
|
||||
|
||||
print(f"Agent ID: {managed.agent_id}")
|
||||
print(f"Environment ID: {managed.environment_id}")
|
||||
@@ -0,0 +1,18 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# Agent, environment, and session are all created automatically on first use
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Coding Assistant",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful coding assistant. Write clean, well-documented code.",
|
||||
session_title="Quickstart session",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="coder", backend=managed)
|
||||
result = agent.start("Say hello")
|
||||
|
||||
print(f"Agent ID: {managed.agent_id}")
|
||||
print(f"Environment ID: {managed.environment_id}")
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
@@ -0,0 +1,18 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Coding Assistant",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful coding assistant. Write clean, well-documented code.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="coder", backend=managed)
|
||||
|
||||
result = agent.start(
|
||||
"Create a Python script that generates the first 20 Fibonacci numbers and saves them to fibonacci.txt",
|
||||
stream=True,
|
||||
)
|
||||
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,27 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# Select specific tools: only bash, read, write — everything else disabled
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Bash Only Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant that can only use bash commands.",
|
||||
tools=[
|
||||
{
|
||||
"type": "agent_toolset_20260401",
|
||||
"default_config": {"enabled": False},
|
||||
"configs": [
|
||||
{"name": "bash", "enabled": True},
|
||||
{"name": "read", "enabled": True},
|
||||
{"name": "write", "enabled": True},
|
||||
],
|
||||
},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="bash-agent", backend=managed)
|
||||
result = agent.start("List the current directory contents and show system info using uname -a")
|
||||
|
||||
print(result)
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,25 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# Disable specific tools: web access disabled, everything else stays on
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="No Web Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a coding assistant. You cannot access the web.",
|
||||
tools=[
|
||||
{
|
||||
"type": "agent_toolset_20260401",
|
||||
"configs": [
|
||||
{"name": "web_fetch", "enabled": False},
|
||||
{"name": "web_search", "enabled": False},
|
||||
],
|
||||
},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="no-web-agent", backend=managed)
|
||||
result = agent.start("Write a hello world Python script and run it")
|
||||
|
||||
print(result)
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,41 @@
|
||||
import json
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
|
||||
def handle_weather(tool_name, tool_input):
|
||||
"""Custom tool callback — PraisonAI calls this when the agent uses get_weather."""
|
||||
location = tool_input.get("location", "Unknown")
|
||||
print(f"\n[Custom tool call: {tool_name}]")
|
||||
print(f"[Input: {json.dumps(tool_input)}]")
|
||||
return f"Weather in {location}: 15°C, partly cloudy, humidity 72%"
|
||||
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Weather Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a weather assistant. Use the get_weather tool to check weather.",
|
||||
tools=[
|
||||
{"type": "agent_toolset_20260401"},
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "get_weather",
|
||||
"description": "Get current weather for a location",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string", "description": "City name"},
|
||||
},
|
||||
"required": ["location"],
|
||||
},
|
||||
},
|
||||
],
|
||||
),
|
||||
on_custom_tool=handle_weather,
|
||||
)
|
||||
|
||||
agent = Agent(name="weather-agent", backend=managed)
|
||||
result = agent.start("What is the weather in London?")
|
||||
|
||||
print(result)
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,22 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="My Agent v1",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="updatable", backend=managed)
|
||||
|
||||
# First call creates the agent
|
||||
result = agent.start("Say hello briefly")
|
||||
print(f"Created: {managed.agent_id}, version: {managed.agent_version}")
|
||||
|
||||
# Update the agent's system prompt (no need to recreate)
|
||||
managed.update_agent(
|
||||
name="My Agent v2",
|
||||
system="You are a senior Python developer. Write production-quality code.",
|
||||
)
|
||||
print(f"Updated: {managed.agent_id}, version: {managed.agent_version}")
|
||||
@@ -0,0 +1,23 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Session Demo Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="session-demo", backend=managed)
|
||||
|
||||
# Run a task to create agent + session
|
||||
agent.start("Say hello briefly")
|
||||
|
||||
# List sessions for this agent
|
||||
sessions = managed.list_sessions()
|
||||
print(f"Total sessions: {len(sessions)}")
|
||||
for s in sessions[:5]:
|
||||
print(f" ID: {s['id']}")
|
||||
print(f" Status: {s['status']}")
|
||||
print(f" Title: {s['title']}")
|
||||
print()
|
||||
@@ -0,0 +1,17 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Research Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a research assistant. Search the web for information.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="researcher", backend=managed)
|
||||
result = agent.start(
|
||||
"Search the web for the latest Python 3.13 features and summarize them in 3 bullet points"
|
||||
)
|
||||
|
||||
print(result)
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,25 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Multi Turn Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful coding assistant.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="multi-turn", backend=managed)
|
||||
print(f"Session ID: {managed.session_id}")
|
||||
|
||||
# Multi-turn: each call reuses the same session — agent remembers context
|
||||
messages = [
|
||||
"Create a file called greeting.py that prints 'Hello World'",
|
||||
"Now modify greeting.py to accept a name as a command line argument",
|
||||
"Run greeting.py with the argument 'Claude'",
|
||||
]
|
||||
|
||||
for i, message in enumerate(messages):
|
||||
print(f"\n--- Turn {i + 1}: {message} ---\n")
|
||||
result = agent.start(message)
|
||||
print(result)
|
||||
print("\nAgent finished turn.")
|
||||
@@ -0,0 +1,22 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# Just add packages to the config — environment is created automatically
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Data Science Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a data science assistant.",
|
||||
packages={"pip": ["pandas", "numpy"]},
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="data-scientist", backend=managed)
|
||||
|
||||
print(f"Environment ID: {managed.environment_id}")
|
||||
|
||||
result = agent.start(
|
||||
"Create a Python script that generates random sales data with pandas and saves a summary to sales_summary.txt"
|
||||
)
|
||||
|
||||
print(result)
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,20 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Interruptable Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful coding assistant.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="interruptable", backend=managed)
|
||||
|
||||
# Start a task
|
||||
result = agent.start("Write a Python script that prints numbers 1 to 5")
|
||||
print(result)
|
||||
|
||||
# Interrupt the agent mid-work
|
||||
print("\n[Sending interrupt...]")
|
||||
managed.interrupt()
|
||||
print("Agent stopped.")
|
||||
@@ -0,0 +1,28 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Usage Tracker Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="tracker", backend=managed)
|
||||
result = agent.start("Write a one-line Python script that prints the current date and run it")
|
||||
|
||||
print(result)
|
||||
print("\nAgent finished.")
|
||||
|
||||
# Usage tracked automatically
|
||||
print("\n--- Usage Report ---")
|
||||
print(f"Input tokens: {managed.total_input_tokens}")
|
||||
print(f"Output tokens: {managed.total_output_tokens}")
|
||||
|
||||
# Or retrieve detailed session info from the API
|
||||
info = managed.retrieve_session()
|
||||
print(f"\nSession ID: {info.get('id')}")
|
||||
print(f"Status: {info.get('status')}")
|
||||
if "usage" in info:
|
||||
print(f"API Input tokens: {info['usage']['input_tokens']}")
|
||||
print(f"API Output tokens: {info['usage']['output_tokens']}")
|
||||
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
import pathlib
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
IDS_FILE = pathlib.Path("managed_ids.json")
|
||||
|
||||
if IDS_FILE.exists():
|
||||
saved = json.loads(IDS_FILE.read_text())
|
||||
print(f"Resuming session: {saved['session_id']}\n")
|
||||
|
||||
managed = ManagedAgent()
|
||||
managed.resume_session(saved["session_id"])
|
||||
|
||||
agent = Agent(name="coder", backend=managed)
|
||||
result = agent.start("What is my favourite number?", stream=True)
|
||||
|
||||
else:
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Persistent Coder",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful coding assistant.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="coder", backend=managed)
|
||||
result = agent.start("Remember this: my favourite number is 42.", stream=True)
|
||||
|
||||
ids = managed.save_ids()
|
||||
IDS_FILE.write_text(json.dumps(ids, indent=2))
|
||||
|
||||
print(f"\nSaved IDs to {IDS_FILE}")
|
||||
print(f" agent_id : {managed.agent_id}")
|
||||
print(f" environment_id: {managed.environment_id}")
|
||||
print(f" session_id : {managed.session_id}")
|
||||
print("\nRun this script again to resume the session.")
|
||||
@@ -0,0 +1,21 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Session Demo",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="demo", backend=managed)
|
||||
|
||||
agent.start("Say hello and confirm you are ready.", stream=True)
|
||||
|
||||
print("\n--- IDs ---")
|
||||
print(f"agent_id : {managed.agent_id}")
|
||||
print(f"environment_id: {managed.environment_id}")
|
||||
print(f"session_id : {managed.session_id}")
|
||||
print(f"chat_history : {len(agent.chat_history)} messages")
|
||||
|
||||
ids = managed.save_ids()
|
||||
print(f"\nsave_ids(): {ids}")
|
||||
@@ -0,0 +1,22 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# Multiple package managers — pip + npm installed before agent starts
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Full Stack Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a full stack developer.",
|
||||
packages={
|
||||
"pip": ["pandas", "numpy", "scikit-learn"],
|
||||
"npm": ["express"],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="fullstack", backend=managed)
|
||||
result = agent.start(
|
||||
"Verify pandas and express are installed: run 'python3 -c \"import pandas; print(pandas.__version__)\"' and 'node -e \"console.log(require.resolve('express'))\"'",
|
||||
stream=True,
|
||||
)
|
||||
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,24 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# Limited networking: restrict container to specific hosts only
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Restricted Network Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant with restricted network access.",
|
||||
networking={
|
||||
"type": "limited",
|
||||
"allowed_hosts": ["api.github.com"],
|
||||
"allow_mcp_servers": False,
|
||||
"allow_package_managers": True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="restricted", backend=managed)
|
||||
result = agent.start(
|
||||
"Try to fetch https://api.github.com and report what you get. Then try https://example.com — it should fail.",
|
||||
stream=True,
|
||||
)
|
||||
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,43 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# Environment management: list, retrieve, archive, delete
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Env Management Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant.",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="env-mgmt", backend=managed)
|
||||
|
||||
# First call creates agent + environment + session
|
||||
agent.start("Say hello briefly")
|
||||
|
||||
# Access the underlying Anthropic client for environment management
|
||||
client = managed._get_client()
|
||||
env_id = managed.environment_id
|
||||
print(f"Environment ID: {env_id}")
|
||||
|
||||
# List all environments
|
||||
environments = client.beta.environments.list()
|
||||
print(f"\nTotal environments: {len(environments.data)}")
|
||||
for env in environments.data[:5]:
|
||||
print(f" {env.id} | {env.name} | {getattr(env, 'status', 'active')}")
|
||||
|
||||
# Retrieve a specific environment
|
||||
env = client.beta.environments.retrieve(env_id)
|
||||
print(f"\nRetrieved: {env.id} | {env.name}")
|
||||
|
||||
# Archive (read-only, existing sessions continue)
|
||||
archived = client.beta.environments.archive(env_id)
|
||||
print(f"Archived: {archived.id} | status: {getattr(archived, 'status', 'archived')}")
|
||||
|
||||
# Delete (only if no sessions reference it — may fail if sessions exist)
|
||||
try:
|
||||
client.beta.environments.delete(env_id)
|
||||
print(f"Deleted: {env_id}")
|
||||
except Exception as e:
|
||||
print(f"Delete skipped (expected if sessions exist): {e}")
|
||||
|
||||
print("\nEnvironment management complete.")
|
||||
@@ -0,0 +1,35 @@
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# MCP servers: configure agent with remote MCP tool servers
|
||||
# Note: Anthropic's managed agents only support URL-based (SSE) MCP servers
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="MCP Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant with access to MCP servers.",
|
||||
tools=[
|
||||
{"type": "agent_toolset_20260401"},
|
||||
{"type": "mcp_toolset", "mcp_server_name": "deepwiki"},
|
||||
],
|
||||
mcp_servers=[
|
||||
{
|
||||
"type": "url",
|
||||
"url": "https://mcp.deepwiki.com/sse",
|
||||
"name": "deepwiki",
|
||||
},
|
||||
],
|
||||
networking={
|
||||
"type": "limited",
|
||||
"allow_mcp_servers": True,
|
||||
"allow_package_managers": True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="mcp-agent", backend=managed)
|
||||
result = agent.start(
|
||||
"Use the deepwiki MCP to read the wiki page for the anthropics/anthropic-cookbook github repo and give a one sentence summary",
|
||||
stream=True,
|
||||
)
|
||||
|
||||
print("\nAgent finished.")
|
||||
@@ -0,0 +1,340 @@
|
||||
import json
|
||||
import pathlib
|
||||
from praisonai import Agent, ManagedAgent, ManagedConfig
|
||||
|
||||
# 1. Create an agent
|
||||
|
||||
managed = ManagedAgent()
|
||||
agent = Agent(name="teacher", backend=managed)
|
||||
result = agent.start("Say hello briefly", stream=True)
|
||||
|
||||
print(f"[1] Agent created: {managed.agent_id} (v{managed.agent_version})")
|
||||
|
||||
# 2. Update the agent
|
||||
|
||||
managed.update_agent(
|
||||
name="Teaching Agent v2",
|
||||
system="You are a senior Python developer. Write clean, production-quality code.",
|
||||
)
|
||||
|
||||
print(f"[2] Agent updated: Teaching Agent v2 (v{managed.agent_version})")
|
||||
|
||||
# 3-4. Environment + Session are created automatically (already done in step 1)
|
||||
|
||||
print(f"[3] Environment created: {managed.environment_id}")
|
||||
print(f"[4] Session created: {managed.session_id}")
|
||||
|
||||
# 5. Stream a response
|
||||
|
||||
print("\n[5] Streaming response...")
|
||||
|
||||
result = agent.start("Write a Python script that prints 'Hello from Managed Agents!' and run it", stream=True)
|
||||
|
||||
# 6. Multi-turn conversation (same session remembers context)
|
||||
|
||||
print("\n[6] Multi-turn: sending follow-up...")
|
||||
|
||||
result = agent.start("Now modify that script to accept a name argument and greet that person", stream=True)
|
||||
|
||||
# 7. Track usage
|
||||
|
||||
info = managed.retrieve_session()
|
||||
print("\n[7] Usage report:")
|
||||
if info.get("usage"):
|
||||
print(f" Input tokens: {info['usage']['input_tokens']}")
|
||||
print(f" Output tokens: {info['usage']['output_tokens']}")
|
||||
else:
|
||||
print(f" Input tokens: {managed.total_input_tokens}")
|
||||
print(f" Output tokens: {managed.total_output_tokens}")
|
||||
|
||||
# 8. List sessions
|
||||
|
||||
sessions = managed.list_sessions()
|
||||
print(f"\n[8] Total sessions: {len(sessions)}")
|
||||
for s in sessions[:3]:
|
||||
print(f" {s['id']} | {s['status']} | {s['title']}")
|
||||
|
||||
# 9. Selective tools (only bash + read + write)
|
||||
|
||||
bash_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Bash Only Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You can only use bash, read, and write tools.",
|
||||
tools=[
|
||||
{
|
||||
"type": "agent_toolset_20260401",
|
||||
"default_config": {"enabled": False},
|
||||
"configs": [
|
||||
{"name": "bash", "enabled": True},
|
||||
{"name": "read", "enabled": True},
|
||||
{"name": "write", "enabled": True},
|
||||
],
|
||||
},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
bash_agent = Agent(name="bash-only", backend=bash_managed)
|
||||
|
||||
print("\n[9] Bash-only agent streaming...")
|
||||
result = bash_agent.start("Show the current date and Python version using bash", stream=True)
|
||||
|
||||
# 10. Disable specific tools (web disabled, everything else on)
|
||||
|
||||
no_web_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="No Web Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a coding assistant. You cannot access the web.",
|
||||
tools=[
|
||||
{
|
||||
"type": "agent_toolset_20260401",
|
||||
"configs": [
|
||||
{"name": "web_fetch", "enabled": False},
|
||||
{"name": "web_search", "enabled": False},
|
||||
],
|
||||
},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
no_web_agent = Agent(name="no-web", backend=no_web_managed)
|
||||
|
||||
print("\n[10] No-web agent streaming...")
|
||||
result = no_web_agent.start("Write a Python one-liner that calculates 2**100 and print the result", stream=True)
|
||||
|
||||
# 11. Custom tools (you define the tool, PraisonAI calls your callback)
|
||||
|
||||
|
||||
def handle_weather(tool_name, tool_input):
|
||||
print(f"\n [Custom tool: {tool_name} | Input: {json.dumps(tool_input)}]")
|
||||
return "Tokyo: 22°C, sunny, humidity 55%"
|
||||
|
||||
|
||||
custom_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Weather Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a weather assistant. Use the get_weather tool to check weather.",
|
||||
tools=[
|
||||
{"type": "agent_toolset_20260401"},
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "get_weather",
|
||||
"description": "Get current weather for a location",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string", "description": "City name"},
|
||||
},
|
||||
"required": ["location"],
|
||||
},
|
||||
},
|
||||
],
|
||||
),
|
||||
on_custom_tool=handle_weather,
|
||||
)
|
||||
|
||||
custom_agent = Agent(name="weather", backend=custom_managed)
|
||||
|
||||
print("\n[11] Custom tool agent streaming...")
|
||||
result = custom_agent.start("What is the weather in Tokyo?", stream=True)
|
||||
|
||||
# 12. Web search agent
|
||||
|
||||
search_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Search Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a research assistant. Search the web and summarize.",
|
||||
),
|
||||
)
|
||||
|
||||
search_agent = Agent(name="searcher", backend=search_managed)
|
||||
|
||||
print("\n[12] Web search agent streaming...")
|
||||
result = search_agent.start("Search the web for Python 3.13 new features and give me 3 bullet points", stream=True)
|
||||
|
||||
# 13. Environment with pre-installed packages
|
||||
|
||||
data_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Data Science Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a data science assistant.",
|
||||
packages={"pip": ["pandas", "numpy"]},
|
||||
),
|
||||
)
|
||||
|
||||
data_agent = Agent(name="data-scientist", backend=data_managed)
|
||||
|
||||
print("\n[13] Data science environment streaming...")
|
||||
result = data_agent.start("Use pandas to create a small DataFrame with 3 rows of sample data and print it", stream=True)
|
||||
|
||||
# 14. Interrupt a session
|
||||
|
||||
interrupt_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Interruptable Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful coding assistant.",
|
||||
),
|
||||
)
|
||||
|
||||
interrupt_agent = Agent(name="interruptable", backend=interrupt_managed)
|
||||
|
||||
print("\n[14] Interrupt demo...")
|
||||
result = interrupt_agent.start("Write a Python script that prints numbers 1 to 10", stream=True)
|
||||
interrupt_managed.interrupt()
|
||||
print(" [Interrupt sent]")
|
||||
|
||||
# 15. Session resume — save IDs, create a fresh ManagedAgent, resume, and verify context
|
||||
|
||||
print("\n[15] Session resume demo...")
|
||||
|
||||
# Tell the original agent a memorable fact
|
||||
result = agent.start("Remember this: my favourite number is 42", stream=True)
|
||||
|
||||
# Save IDs to disk
|
||||
ids = managed.save_ids()
|
||||
ids_file = pathlib.Path("managed_ids.json")
|
||||
ids_file.write_text(json.dumps(ids, indent=2))
|
||||
print(f" Saved IDs to {ids_file}: {ids}")
|
||||
|
||||
# Create a completely new ManagedAgent and resume the saved session
|
||||
resume_managed = ManagedAgent()
|
||||
resume_managed.resume_session(ids["session_id"])
|
||||
resume_agent = Agent(name="resumed", backend=resume_managed)
|
||||
|
||||
# Ask about the fact — proves the session memory was preserved
|
||||
result = resume_agent.start("What is my favourite number?", stream=True)
|
||||
print(f" Resumed session: {resume_managed.session_id}")
|
||||
|
||||
# Clean up
|
||||
ids_file.unlink(missing_ok=True)
|
||||
|
||||
# 16. Multi-package managers — pip + npm installed before agent starts
|
||||
|
||||
print("\n[16] Multi-package managers...")
|
||||
|
||||
multi_pkg_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Full Stack Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a full stack developer.",
|
||||
packages={
|
||||
"pip": ["pandas", "numpy", "scikit-learn"],
|
||||
"npm": ["express"],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
multi_pkg_agent = Agent(name="fullstack", backend=multi_pkg_managed)
|
||||
result = multi_pkg_agent.start(
|
||||
"Verify pandas and express are installed: run python3 -c 'import pandas; print(pandas.__version__)' and node -e 'console.log(require.resolve(\"express\"))'",
|
||||
stream=True,
|
||||
)
|
||||
|
||||
# 17. Limited networking — restrict container to specific hosts
|
||||
|
||||
print("\n[17] Limited networking...")
|
||||
|
||||
limited_net_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="Restricted Network Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant with restricted network access.",
|
||||
networking={
|
||||
"type": "limited",
|
||||
"allowed_hosts": ["api.github.com"],
|
||||
"allow_mcp_servers": False,
|
||||
"allow_package_managers": True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
limited_net_agent = Agent(name="restricted", backend=limited_net_managed)
|
||||
result = limited_net_agent.start("Fetch https://api.github.com and report the status", stream=True)
|
||||
|
||||
# 18. Environment management — list, retrieve, archive
|
||||
|
||||
print("\n[18] Environment management...")
|
||||
|
||||
env_client = managed._get_client()
|
||||
|
||||
# List all environments
|
||||
environments = env_client.beta.environments.list()
|
||||
print(f" Total environments: {len(environments.data)}")
|
||||
for env in environments.data[:3]:
|
||||
print(f" {env.id} | {env.name}")
|
||||
|
||||
# Retrieve a specific environment
|
||||
env = env_client.beta.environments.retrieve(managed.environment_id)
|
||||
print(f" Retrieved: {env.id} | {env.name}")
|
||||
|
||||
# 19. MCP servers — configure agent with remote MCP tool servers
|
||||
|
||||
print("\n[19] MCP servers...")
|
||||
|
||||
mcp_managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
name="MCP Agent",
|
||||
model="claude-haiku-4-5",
|
||||
system="You are a helpful assistant with access to MCP servers.",
|
||||
tools=[
|
||||
{"type": "agent_toolset_20260401"},
|
||||
{"type": "mcp_toolset", "mcp_server_name": "deepwiki"},
|
||||
],
|
||||
mcp_servers=[
|
||||
{
|
||||
"type": "url",
|
||||
"url": "https://mcp.deepwiki.com/sse",
|
||||
"name": "deepwiki",
|
||||
},
|
||||
],
|
||||
networking={
|
||||
"type": "limited",
|
||||
"allow_mcp_servers": True,
|
||||
"allow_package_managers": True,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
mcp_agent = Agent(name="mcp-agent", backend=mcp_managed)
|
||||
result = mcp_agent.start("Use the deepwiki MCP to read the wiki page for the anthropics/anthropic-cookbook github repo and give a one sentence summary", stream=True)
|
||||
|
||||
# Final usage summary
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("FINAL USAGE SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
all_backends = [
|
||||
("Teaching Agent v2", managed),
|
||||
("Bash Only Agent", bash_managed),
|
||||
("No Web Agent", no_web_managed),
|
||||
("Weather Agent", custom_managed),
|
||||
("Search Agent", search_managed),
|
||||
("Data Science Agent", data_managed),
|
||||
("Interruptable Agent", interrupt_managed),
|
||||
("Resumed Session", resume_managed),
|
||||
("Full Stack Agent", multi_pkg_managed),
|
||||
("Restricted Network", limited_net_managed),
|
||||
("MCP Agent", mcp_managed),
|
||||
]
|
||||
|
||||
total_input = 0
|
||||
total_output = 0
|
||||
|
||||
for name, backend in all_backends:
|
||||
info = backend.retrieve_session()
|
||||
usage = info.get("usage", {})
|
||||
inp = usage.get("input_tokens", backend.total_input_tokens)
|
||||
out = usage.get("output_tokens", backend.total_output_tokens)
|
||||
total_input += inp
|
||||
total_output += out
|
||||
print(f" {name:30s} | in: {inp:6d} | out: {out:6d}")
|
||||
|
||||
print(f" {'TOTAL':30s} | in: {total_input:6d} | out: {total_output:6d}")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,5 @@
|
||||
from praisonai import Agent, ManagedAgent
|
||||
|
||||
agent = Agent(name="teacher", backend=ManagedAgent())
|
||||
result = agent.start("Write a Python script that prints 'Hello from Managed Agents!' and run it")
|
||||
print(result)
|
||||
@@ -0,0 +1,117 @@
|
||||
"""All compute providers — comprehensive test across Local, Docker, E2B, and Modal.
|
||||
|
||||
This example mirrors the Anthropic app.py but uses the local provider with
|
||||
various compute backends instead of Anthropic's managed infrastructure.
|
||||
|
||||
Requires:
|
||||
- Docker running locally
|
||||
- E2B_API_KEY set
|
||||
- modal CLI configured (modal token set)
|
||||
"""
|
||||
import asyncio
|
||||
from praisonai import Agent, ManagedAgent, LocalManagedConfig
|
||||
|
||||
|
||||
async def test_provider(name, compute, extra_provision_kwargs=None):
|
||||
"""Test a single compute provider end-to-end."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" PROVIDER: {name}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
managed = ManagedAgent(
|
||||
provider="local",
|
||||
compute=compute,
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini",
|
||||
system="You are a helpful assistant. Be concise.",
|
||||
name=f"{name}Agent",
|
||||
),
|
||||
)
|
||||
agent = Agent(name=f"{name}-test", backend=managed)
|
||||
|
||||
# 1. Provision
|
||||
print(f"\n [1] Provisioning {name}...")
|
||||
provision_kwargs = extra_provision_kwargs or {}
|
||||
info = await managed.provision_compute(**provision_kwargs)
|
||||
print(f" Instance: {info.instance_id}")
|
||||
print(f" Status: {info.status}")
|
||||
|
||||
# 2. Execute command
|
||||
print(f"\n [2] Execute in {name}...")
|
||||
result = await managed.execute_in_compute("python3 -c 'print(42 * 13)'")
|
||||
stdout = result["stdout"].strip()
|
||||
assert "546" in stdout, f"Expected 546, got: {stdout}"
|
||||
print(f" 42 * 13 = {stdout} ✓")
|
||||
|
||||
# 3. Execute echo
|
||||
result = await managed.execute_in_compute(f"echo 'Hello from {name}'")
|
||||
print(f" Echo: {result['stdout'].strip()}")
|
||||
|
||||
# 4. Agent LLM
|
||||
print(f"\n [3] Agent LLM via {name}...")
|
||||
llm_result = agent.start("What is 9 * 8? Just the number.", stream=True)
|
||||
print(f" LLM result: {llm_result}")
|
||||
|
||||
# 5. Multi-turn
|
||||
print("\n [4] Multi-turn...")
|
||||
llm_result = agent.start("Add 10 to that. Just the number.", stream=True)
|
||||
print(f" Follow-up: {llm_result}")
|
||||
|
||||
# 6. Usage
|
||||
session = managed.retrieve_session()
|
||||
usage = session["usage"]
|
||||
print(f"\n [5] Usage: in={usage['input_tokens']}, out={usage['output_tokens']}")
|
||||
|
||||
# 7. Shutdown
|
||||
print(f"\n [6] Shutdown {name}...")
|
||||
await managed.shutdown_compute()
|
||||
print(" Done ✓")
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"input_tokens": usage["input_tokens"],
|
||||
"output_tokens": usage["output_tokens"],
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
results = []
|
||||
|
||||
# Docker
|
||||
try:
|
||||
r = await test_provider("Docker", "docker", {"image": "python:3.12-slim"})
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f"\n Docker FAILED: {e}")
|
||||
|
||||
# E2B
|
||||
try:
|
||||
r = await test_provider("E2B", "e2b", {"idle_timeout_s": 120})
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f"\n E2B FAILED: {e}")
|
||||
|
||||
# Modal
|
||||
try:
|
||||
r = await test_provider("Modal", "modal", {"idle_timeout_s": 120})
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f"\n Modal FAILED: {e}")
|
||||
|
||||
# Summary
|
||||
print(f"\n\n{'='*60}")
|
||||
print(" SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
total_in = 0
|
||||
total_out = 0
|
||||
for r in results:
|
||||
total_in += r["input_tokens"]
|
||||
total_out += r["output_tokens"]
|
||||
print(f" {r['name']:15s} | in: {r['input_tokens']:6d} | out: {r['output_tokens']:6d}")
|
||||
print(f" {'TOTAL':15s} | in: {total_in:6d} | out: {total_out:6d}")
|
||||
print(f"{'='*60}")
|
||||
print(f" {len(results)}/{3} providers passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Daytona compute provider — run agent tools inside a Daytona cloud sandbox.
|
||||
|
||||
Requires: DAYTONA_API_KEY environment variable set.
|
||||
Organization must have a default region configured in Daytona Dashboard.
|
||||
Install: pip install daytona-sdk
|
||||
"""
|
||||
import asyncio
|
||||
from praisonai import Agent, ManagedAgent, LocalManagedConfig
|
||||
|
||||
# ── 1. Create agent with Daytona compute ──
|
||||
managed = ManagedAgent(
|
||||
provider="local",
|
||||
compute="daytona",
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini",
|
||||
system="You are a helpful coding assistant. Be concise.",
|
||||
name="DaytonaAgent",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="daytona-agent", backend=managed)
|
||||
|
||||
print("[1] Agent created with Daytona compute")
|
||||
print(f" Compute: {managed.compute_provider.provider_name}")
|
||||
|
||||
# ── 2. Provision Daytona sandbox ──
|
||||
print("\n[2] Provisioning Daytona sandbox...")
|
||||
info = asyncio.run(managed.provision_compute(idle_timeout_s=120))
|
||||
print(f" Instance: {info.instance_id}")
|
||||
print(f" Status: {info.status}")
|
||||
|
||||
# ── 3. Execute commands ──
|
||||
print("\n[3] Executing commands in Daytona...")
|
||||
result = asyncio.run(managed.execute_in_compute("python3 -c 'import sys; print(sys.version)'"))
|
||||
print(f" Python version: {result['stdout'].strip()}")
|
||||
print(f" Exit code: {result['exit_code']}")
|
||||
|
||||
result = asyncio.run(managed.execute_in_compute("echo 'Hello from Daytona!'"))
|
||||
print(f" Echo: {result['stdout'].strip()}")
|
||||
|
||||
# ── 4. Agent LLM execution ──
|
||||
print("\n[4] Agent LLM execution...")
|
||||
result = agent.start("What is 11 * 13? Just the number.", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# ── 5. Usage ──
|
||||
info = managed.retrieve_session()
|
||||
print(f"\n[5] Usage: in={info['usage']['input_tokens']}, out={info['usage']['output_tokens']}")
|
||||
|
||||
# ── 6. Shutdown ──
|
||||
print("\n[6] Shutting down Daytona sandbox...")
|
||||
asyncio.run(managed.shutdown_compute())
|
||||
print(" Shutdown complete.")
|
||||
|
||||
print("\nDone!")
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Docker compute provider — run agent tools inside a Docker container.
|
||||
|
||||
Requires: Docker running locally.
|
||||
"""
|
||||
import asyncio
|
||||
from praisonai import Agent, ManagedAgent, LocalManagedConfig
|
||||
|
||||
# ── 1. Basic agent with Docker compute ──
|
||||
managed = ManagedAgent(
|
||||
provider="local",
|
||||
compute="docker",
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini",
|
||||
system="You are a helpful coding assistant. Be concise.",
|
||||
name="DockerAgent",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="docker-agent", backend=managed)
|
||||
|
||||
print("[1] Agent created with Docker compute")
|
||||
print(f" Agent ID: {managed.agent_id or '(lazy — created on first call)'}")
|
||||
print(f" Compute: {managed.compute_provider.provider_name}")
|
||||
|
||||
# ── 2. Provision Docker container ──
|
||||
print("\n[2] Provisioning Docker container...")
|
||||
info = asyncio.run(managed.provision_compute(image="python:3.12-slim"))
|
||||
print(f" Instance: {info.instance_id}")
|
||||
print(f" Status: {info.status}")
|
||||
|
||||
# ── 3. Execute commands inside the container ──
|
||||
print("\n[3] Executing commands in Docker...")
|
||||
result = asyncio.run(managed.execute_in_compute("python3 -c 'import sys; print(sys.version)'"))
|
||||
print(f" Python version: {result['stdout'].strip()}")
|
||||
print(f" Exit code: {result['exit_code']}")
|
||||
|
||||
result = asyncio.run(managed.execute_in_compute("echo 'Hello from Docker!'"))
|
||||
print(f" Echo: {result['stdout'].strip()}")
|
||||
|
||||
# ── 4. Install packages in the container ──
|
||||
print("\n[4] Installing packages...")
|
||||
result = asyncio.run(managed.execute_in_compute("pip install requests -q"))
|
||||
print(f" pip exit code: {result['exit_code']}")
|
||||
|
||||
result = asyncio.run(managed.execute_in_compute("python3 -c 'import requests; print(requests.__version__)'"))
|
||||
print(f" requests version: {result['stdout'].strip()}")
|
||||
|
||||
# ── 5. Use agent with LLM (runs locally, compute is for tool sandboxing) ──
|
||||
print("\n[5] Agent LLM execution...")
|
||||
result = agent.start("What is 15 * 23? Just the number.", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# ── 6. Multi-turn ──
|
||||
print("\n[6] Multi-turn...")
|
||||
result = agent.start("Double that number.", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# ── 7. Usage tracking ──
|
||||
info = managed.retrieve_session()
|
||||
print(f"\n[7] Usage: in={info['usage']['input_tokens']}, out={info['usage']['output_tokens']}")
|
||||
|
||||
# ── 8. Shutdown Docker container ──
|
||||
print("\n[8] Shutting down Docker container...")
|
||||
asyncio.run(managed.shutdown_compute())
|
||||
print(" Shutdown complete.")
|
||||
|
||||
print("\nDone!")
|
||||
@@ -0,0 +1,66 @@
|
||||
"""E2B compute provider — run agent tools inside an E2B cloud sandbox.
|
||||
|
||||
Requires: E2B_API_KEY environment variable set.
|
||||
Install: pip install e2b
|
||||
"""
|
||||
import asyncio
|
||||
from praisonai import Agent, ManagedAgent, LocalManagedConfig
|
||||
|
||||
# ── 1. Create agent with E2B compute ──
|
||||
managed = ManagedAgent(
|
||||
provider="local",
|
||||
compute="e2b",
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini",
|
||||
system="You are a helpful coding assistant. Be concise.",
|
||||
name="E2BAgent",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="e2b-agent", backend=managed)
|
||||
|
||||
print("[1] Agent created with E2B compute")
|
||||
print(f" Compute: {managed.compute_provider.provider_name}")
|
||||
|
||||
# ── 2. Provision E2B sandbox ──
|
||||
print("\n[2] Provisioning E2B sandbox...")
|
||||
info = asyncio.run(managed.provision_compute(idle_timeout_s=120))
|
||||
print(f" Instance: {info.instance_id}")
|
||||
print(f" Status: {info.status}")
|
||||
|
||||
# ── 3. Execute commands in the sandbox ──
|
||||
print("\n[3] Executing commands in E2B...")
|
||||
result = asyncio.run(managed.execute_in_compute("python3 -c 'import sys; print(sys.version)'"))
|
||||
print(f" Python version: {result['stdout'].strip()}")
|
||||
print(f" Exit code: {result['exit_code']}")
|
||||
|
||||
result = asyncio.run(managed.execute_in_compute("echo 'Hello from E2B!'"))
|
||||
print(f" Echo: {result['stdout'].strip()}")
|
||||
|
||||
# ── 4. Run a Python script ──
|
||||
print("\n[4] Running Python script...")
|
||||
result = asyncio.run(managed.execute_in_compute(
|
||||
"python3 -c 'for i in range(5): print(f\"Line {i}\")'"
|
||||
))
|
||||
print(f" Output:\n{result['stdout']}")
|
||||
|
||||
# ── 5. Agent LLM execution ──
|
||||
print("[5] Agent LLM execution...")
|
||||
result = agent.start("What is the square root of 144? Just the number.", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# ── 6. Multi-turn ──
|
||||
print("\n[6] Multi-turn...")
|
||||
result = agent.start("Now cube that number.", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# ── 7. Usage ──
|
||||
info = managed.retrieve_session()
|
||||
print(f"\n[7] Usage: in={info['usage']['input_tokens']}, out={info['usage']['output_tokens']}")
|
||||
|
||||
# ── 8. Shutdown ──
|
||||
print("\n[8] Shutting down E2B sandbox...")
|
||||
asyncio.run(managed.shutdown_compute())
|
||||
print(" Shutdown complete.")
|
||||
|
||||
print("\nDone!")
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Local provider — advanced features: update agent, custom tools, session resume, packages."""
|
||||
from praisonai import Agent, ManagedAgent, LocalManagedConfig
|
||||
|
||||
# ── 1. Create agent ──
|
||||
managed = ManagedAgent(
|
||||
provider="local",
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini",
|
||||
system="You are a helpful math tutor.",
|
||||
name="MathTutor",
|
||||
),
|
||||
)
|
||||
agent = Agent(name="tutor", backend=managed)
|
||||
result = agent.start("What is 17 * 23?", stream=True)
|
||||
print(f"[1] Created agent: {managed.agent_id} (v{managed.agent_version})")
|
||||
|
||||
# ── 2. Update agent ──
|
||||
managed.update_agent(
|
||||
name="Senior Math Tutor",
|
||||
system="You are a senior math tutor. Always show your work step by step.",
|
||||
)
|
||||
print(f"[2] Updated to v{managed.agent_version}")
|
||||
result = agent.start("What is 123 + 456?", stream=True)
|
||||
|
||||
# ── 3. Custom tool ──
|
||||
def handle_calculator(tool_name, tool_input):
|
||||
expr = tool_input.get("expression", "0")
|
||||
try:
|
||||
val = eval(expr, {"__builtins__": {}})
|
||||
except Exception:
|
||||
val = "error"
|
||||
print(f" [Calculator: {expr} = {val}]")
|
||||
return str(val)
|
||||
|
||||
custom_managed = ManagedAgent(
|
||||
provider="local",
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini",
|
||||
system="You are an assistant with a calculator tool. Use it for math.",
|
||||
name="CalcAgent",
|
||||
tools=[
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "calculator",
|
||||
"description": "Evaluate a math expression",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {"type": "string", "description": "Math expression"},
|
||||
},
|
||||
"required": ["expression"],
|
||||
},
|
||||
},
|
||||
],
|
||||
),
|
||||
on_custom_tool=handle_calculator,
|
||||
)
|
||||
custom_agent = Agent(name="calc", backend=custom_managed)
|
||||
print("\n[3] Custom tool agent...")
|
||||
result = custom_agent.start("Use the calculator to compute 99 * 77", stream=True)
|
||||
|
||||
# ── 4. Session resume ──
|
||||
print("\n[4] Session resume...")
|
||||
result = agent.start("Remember: my lucky number is 7", stream=True)
|
||||
ids = managed.save_ids()
|
||||
print(f" Saved IDs: {ids}")
|
||||
|
||||
resume_managed = ManagedAgent(provider="local", config=LocalManagedConfig(model="gpt-4o-mini"))
|
||||
resume_managed.restore_ids(ids)
|
||||
resume_agent = Agent(name="resumed", backend=resume_managed)
|
||||
result = resume_agent.start("What is my lucky number?", stream=True)
|
||||
print(f" Resumed session: {resume_managed.session_id}")
|
||||
|
||||
# ── 5. Interrupt ──
|
||||
print("\n[5] Interrupt demo...")
|
||||
int_managed = ManagedAgent(
|
||||
provider="local",
|
||||
config=LocalManagedConfig(model="gpt-4o-mini", system="Be helpful.", name="IntAgent"),
|
||||
)
|
||||
int_agent = Agent(name="interruptable", backend=int_managed)
|
||||
result = int_agent.start("Count from 1 to 5", stream=True)
|
||||
int_managed.interrupt()
|
||||
print(" [Interrupt sent]")
|
||||
|
||||
# ── 6. Reset session ──
|
||||
print("\n[6] Reset session...")
|
||||
managed.reset_session()
|
||||
result = agent.start("What was my lucky number?", stream=True)
|
||||
print(" (new session — context lost, expected)")
|
||||
|
||||
# ── Usage summary ──
|
||||
print("\n" + "=" * 50)
|
||||
info = managed.retrieve_session()
|
||||
print(f"Usage: in={info['usage']['input_tokens']}, out={info['usage']['output_tokens']}")
|
||||
print("=" * 50)
|
||||
print("Done!")
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Local provider — basic managed agent with gpt-4o-mini.
|
||||
|
||||
No external infrastructure needed. Runs the agent loop locally.
|
||||
"""
|
||||
from praisonai import Agent, ManagedAgent, LocalManagedConfig
|
||||
|
||||
# Create a local managed agent (auto-detects local when no ANTHROPIC_API_KEY)
|
||||
managed = ManagedAgent(
|
||||
provider="local",
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini",
|
||||
system="You are a helpful assistant. Be concise.",
|
||||
name="LocalAgent",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="local-basic", backend=managed)
|
||||
|
||||
# 1. Basic execution
|
||||
print("[1] Basic execution...")
|
||||
result = agent.start("What is the capital of France? One word.", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# 2. Agent metadata
|
||||
print(f"\n[2] Agent ID: {managed.agent_id}")
|
||||
print(f" Version: {managed.agent_version}")
|
||||
print(f" Env ID: {managed.environment_id}")
|
||||
print(f" Session: {managed.session_id}")
|
||||
|
||||
# 3. Multi-turn (same session keeps context)
|
||||
print("\n[3] Multi-turn...")
|
||||
result = agent.start("What country is that city in?", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# 4. Usage tracking
|
||||
info = managed.retrieve_session()
|
||||
print(f"\n[4] Usage: in={info['usage']['input_tokens']}, out={info['usage']['output_tokens']}")
|
||||
|
||||
# 5. List sessions
|
||||
sessions = managed.list_sessions()
|
||||
print(f"\n[5] Sessions: {len(sessions)}")
|
||||
for s in sessions:
|
||||
print(f" {s['id']} | {s['status']}")
|
||||
|
||||
print("\nDone!")
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Modal compute provider — run agent tools inside a Modal cloud sandbox.
|
||||
|
||||
Requires: modal CLI configured (modal token set) or MODAL_TOKEN_ID + MODAL_TOKEN_SECRET.
|
||||
Install: pip install modal
|
||||
"""
|
||||
import asyncio
|
||||
from praisonai import Agent, ManagedAgent, LocalManagedConfig
|
||||
|
||||
# ── 1. Create agent with Modal compute ──
|
||||
managed = ManagedAgent(
|
||||
provider="local",
|
||||
compute="modal",
|
||||
config=LocalManagedConfig(
|
||||
model="gpt-4o-mini",
|
||||
system="You are a helpful coding assistant. Be concise.",
|
||||
name="ModalAgent",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(name="modal-agent", backend=managed)
|
||||
|
||||
print("[1] Agent created with Modal compute")
|
||||
print(f" Compute: {managed.compute_provider.provider_name}")
|
||||
|
||||
# ── 2. Provision Modal sandbox ──
|
||||
print("\n[2] Provisioning Modal sandbox...")
|
||||
info = asyncio.run(managed.provision_compute(idle_timeout_s=120))
|
||||
print(f" Instance: {info.instance_id}")
|
||||
print(f" Status: {info.status}")
|
||||
|
||||
# ── 3. Execute commands in the sandbox ──
|
||||
print("\n[3] Executing commands in Modal...")
|
||||
result = asyncio.run(managed.execute_in_compute("python3 -c 'import sys; print(sys.version)'"))
|
||||
print(f" Python version: {result['stdout'].strip()}")
|
||||
print(f" Exit code: {result['exit_code']}")
|
||||
|
||||
result = asyncio.run(managed.execute_in_compute("echo 'Hello from Modal!'"))
|
||||
print(f" Echo: {result['stdout'].strip()}")
|
||||
|
||||
# ── 4. Run a computation ──
|
||||
print("\n[4] Running computation...")
|
||||
result = asyncio.run(managed.execute_in_compute(
|
||||
"python3 -c 'print(sum(range(1, 101)))'"
|
||||
))
|
||||
print(f" Sum 1..100 = {result['stdout'].strip()}")
|
||||
|
||||
# ── 5. Agent LLM execution ──
|
||||
print("\n[5] Agent LLM execution...")
|
||||
result = agent.start("What is 7 factorial? Just the number.", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# ── 6. Multi-turn ──
|
||||
print("\n[6] Multi-turn...")
|
||||
result = agent.start("Is that number even or odd?", stream=True)
|
||||
print(f" Result: {result}")
|
||||
|
||||
# ── 7. Usage ──
|
||||
info = managed.retrieve_session()
|
||||
print(f"\n[7] Usage: in={info['usage']['input_tokens']}, out={info['usage']['output_tokens']}")
|
||||
|
||||
# ── 8. Shutdown ──
|
||||
print("\n[8] Shutting down Modal sandbox...")
|
||||
asyncio.run(managed.shutdown_compute())
|
||||
print(" Shutdown complete.")
|
||||
|
||||
print("\nDone!")
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Managed Agent Backend Example — Real end-to-end test.
|
||||
|
||||
Demonstrates using Anthropic's Managed Agents API as an execution backend
|
||||
for a PraisonAI Agent. The agent runs in Anthropic's managed infrastructure.
|
||||
|
||||
Prerequisites:
|
||||
export ANTHROPIC_API_KEY="your-key"
|
||||
pip install 'anthropic>=0.94.0' praisonaiagents praisonai
|
||||
|
||||
Usage:
|
||||
python examples/python/managed_agent_example.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
# ── Setup logging so we can see what the integration does ──
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
)
|
||||
|
||||
# ── Guard: API key must be set ──
|
||||
if not os.getenv("ANTHROPIC_API_KEY"):
|
||||
print("ERROR: ANTHROPIC_API_KEY not set. Export it first.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def example_1_standalone():
|
||||
"""Example 1: Use ManagedAgentIntegration directly (no PraisonAI Agent)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 1: Standalone ManagedAgentIntegration")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonai.integrations.managed_agents import ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
model="claude-sonnet-4-6",
|
||||
name="Standalone Test Agent",
|
||||
system="You are a concise assistant. Answer in one sentence.",
|
||||
),
|
||||
)
|
||||
|
||||
result = managed._execute_sync("What is 2 + 2? Answer in one sentence.")
|
||||
print(f"\nResult: {result}")
|
||||
print(f"Tokens: in={managed.total_input_tokens}, out={managed.total_output_tokens}")
|
||||
assert result and len(result) > 0, "Expected non-empty response"
|
||||
print("PASS: Standalone execution")
|
||||
return result
|
||||
|
||||
|
||||
def example_2_with_agent():
|
||||
"""Example 2: Use ManagedAgentIntegration as a PraisonAI Agent backend."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 2: PraisonAI Agent with managed backend")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonai.integrations.managed_agents import ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
model="claude-sonnet-4-6",
|
||||
name="PraisonAI Backend Agent",
|
||||
system="You are a helpful coding assistant. Be concise.",
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="managed-coder",
|
||||
instructions="You are a helpful coding assistant.",
|
||||
backend=managed,
|
||||
)
|
||||
|
||||
result = agent.start("Write a Python one-liner that prints the first 10 Fibonacci numbers.")
|
||||
print(f"\nResult: {result}")
|
||||
assert result and len(result) > 0, "Expected non-empty response from agent.start()"
|
||||
print("PASS: Agent backend execution")
|
||||
return result
|
||||
|
||||
|
||||
def example_3_with_tools():
|
||||
"""Example 3: Managed agent with built-in tools (bash, file ops)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 3: Managed agent with built-in tools")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonai.integrations.managed_agents import ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
model="claude-sonnet-4-6",
|
||||
name="Tool-Using Agent",
|
||||
system="You are a coding agent with access to bash and file tools.",
|
||||
tools=[{"type": "agent_toolset_20260401"}],
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="tool-agent",
|
||||
instructions="You are a coding agent.",
|
||||
backend=managed,
|
||||
)
|
||||
|
||||
result = agent.start(
|
||||
"Use bash to run: echo 'Hello from managed agent!' && python3 -c 'print(sum(range(10)))'"
|
||||
)
|
||||
print(f"\nResult: {result}")
|
||||
assert result and len(result) > 0, "Expected non-empty response"
|
||||
print("PASS: Tool-using agent execution")
|
||||
return result
|
||||
|
||||
|
||||
def example_4_with_packages():
|
||||
"""Example 4: Managed agent with custom packages installed."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 4: Managed agent with pip packages")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonai.integrations.managed_agents import ManagedAgent, ManagedConfig
|
||||
|
||||
managed = ManagedAgent(
|
||||
config=ManagedConfig(
|
||||
model="claude-sonnet-4-6",
|
||||
name="Data Agent",
|
||||
system="You are a data analysis agent. Use pandas when helpful.",
|
||||
packages={"pip": ["pandas", "numpy"]},
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="data-agent",
|
||||
instructions="You are a data analysis agent.",
|
||||
backend=managed,
|
||||
)
|
||||
|
||||
result = agent.start(
|
||||
"Use bash to run: python3 -c \"import pandas as pd; print(pd.__version__)\""
|
||||
)
|
||||
print(f"\nResult: {result}")
|
||||
assert result and len(result) > 0, "Expected non-empty response"
|
||||
print("PASS: Package installation agent")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Managed Agent Backend — Real End-to-End Tests")
|
||||
print("Using Anthropic Managed Agents API (beta)")
|
||||
|
||||
examples = [
|
||||
("standalone", example_1_standalone),
|
||||
("agent_backend", example_2_with_agent),
|
||||
("with_tools", example_3_with_tools),
|
||||
("with_packages", example_4_with_packages),
|
||||
]
|
||||
results = {}
|
||||
|
||||
for name, fn in examples:
|
||||
try:
|
||||
results[name] = fn()
|
||||
except Exception as e:
|
||||
print(f"FAIL: {name}: {e}")
|
||||
results[name] = None
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
passed = sum(1 for v in results.values() if v is not None)
|
||||
total = len(results)
|
||||
for name, result in results.items():
|
||||
status = "PASS" if result else "FAIL"
|
||||
print(f" {status}: {name}")
|
||||
print(f"\n{passed}/{total} examples passed")
|
||||
@@ -308,7 +308,7 @@ def manage_state_versioning() -> Dict[str, Any]:
|
||||
versions[version_id] = {
|
||||
"config": current_config.copy(),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"hash": hashlib.md5(json.dumps(current_config, sort_keys=True).encode()).hexdigest()
|
||||
"hash": hashlib.sha256(json.dumps(current_config, sort_keys=True).encode()).hexdigest()
|
||||
}
|
||||
|
||||
# Make changes
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
import subprocess
|
||||
import shlex
|
||||
import os
|
||||
|
||||
def run_terminal_command(command: str):
|
||||
@@ -7,7 +8,9 @@ def run_terminal_command(command: str):
|
||||
Run a terminal command and return its output.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
# Use shell=False with shlex.split for safer execution
|
||||
args = shlex.split(command)
|
||||
result = subprocess.run(args, shell=False, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
print(f"Command output: {result}")
|
||||
return {"stdout": result.stdout, "stderr": result.stderr}
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
||||
@@ -2,6 +2,7 @@ from praisonaiagents import Agent, Task, AgentTeam, error_logs
|
||||
import json, os
|
||||
from e2b_code_interpreter import Sandbox
|
||||
import subprocess
|
||||
import shlex
|
||||
|
||||
def code_interpret(code: str):
|
||||
"""
|
||||
@@ -40,7 +41,9 @@ def run_terminal_command(command: str):
|
||||
Run a terminal command and return its output.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
# Use shell=False with shlex.split for safer execution
|
||||
args = shlex.split(command)
|
||||
result = subprocess.run(args, shell=False, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
print(f"Command output: {result}")
|
||||
return {"stdout": result.stdout, "stderr": result.stderr}
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Terminal-Bench 2.0 Integration with PraisonAI
|
||||
|
||||
This directory contains examples for integrating PraisonAI Agents with **Terminal-Bench 2.0** via the **Harbor framework** for AI agent benchmarking.
|
||||
|
||||
## Overview
|
||||
|
||||
[Terminal-Bench 2.0](https://tbench.ai) is a Stanford/Laude Institute benchmark that has become the gold standard for evaluating AI coding agents in real terminal environments. The [Harbor framework](https://harborframework.com) is the official evaluation harness that abstracts container lifecycle, parallelization, and cloud providers.
|
||||
|
||||
## Integration Types
|
||||
|
||||
### 1. External Agent (Direct Agent Class)
|
||||
- **File**: `praisonai_external_agent.py`
|
||||
- **Purpose**: External agent that uses direct `Agent()` class instantiation
|
||||
- **Usage**: Run with `--agent-import-path` flag
|
||||
- **Approach**: Uses `praisonaiagents.Agent` directly with `execute_command` tool
|
||||
|
||||
### 2. Wrapper Agent (CLI-Based)
|
||||
- **File**: `praisonai_wrapper_agent.py`
|
||||
- **Purpose**: Uses `praisonai "TASK"` CLI pattern instead of direct Agent class
|
||||
- **Usage**: Run with `--agent-import-path examples.terminal_bench.praisonai_wrapper_agent:PraisonAIWrapperAgent`
|
||||
- **Approach**: Installs `praisonai` CLI inside container and runs tasks via subprocess
|
||||
- **Best for**: Users who prefer CLI-style interface matching standard `praisonai` usage
|
||||
|
||||
### 3. Installed Agent (Production)
|
||||
- **File**: `praisonai_installed_agent.py`
|
||||
- **Purpose**: Production-ready agent installed inside Harbor containers
|
||||
- **Usage**: Integrates with Harbor's leaderboard system
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install Harbor framework
|
||||
pip install harbor
|
||||
|
||||
# Install PraisonAI with shell tools
|
||||
pip install praisonaiagents[tools]
|
||||
```
|
||||
|
||||
### Running External Agent (Direct Agent Class)
|
||||
|
||||
```bash
|
||||
# Test with oracle agent first
|
||||
harbor run -d terminal-bench/terminal-bench-2 -a oracle
|
||||
|
||||
# Run PraisonAI external agent (uses direct Agent() class)
|
||||
harbor run -d terminal-bench/terminal-bench-2 \
|
||||
--agent-import-path examples.terminal_bench.praisonai_external_agent:PraisonAIExternalAgent \
|
||||
--model openai/gpt-4o \
|
||||
--ae OPENAI_API_KEY=$OPENAI_API_KEY \
|
||||
-n 4
|
||||
```
|
||||
|
||||
### Running Wrapper Agent (CLI-Based)
|
||||
|
||||
```bash
|
||||
# Run PraisonAI wrapper agent (uses `praisonai "TASK"` CLI pattern)
|
||||
harbor run -d terminal-bench/terminal-bench-2 \
|
||||
--agent-import-path examples.terminal_bench.praisonai_wrapper_agent:PraisonAIWrapperAgent \
|
||||
--model openai/gpt-4o \
|
||||
--ae OPENAI_API_KEY=$OPENAI_API_KEY \
|
||||
-n 4
|
||||
```
|
||||
|
||||
### Running on Cloud (Daytona/E2B/Modal)
|
||||
|
||||
```bash
|
||||
harbor run -d terminal-bench/terminal-bench-2 \
|
||||
--agent-import-path examples.terminal_bench.praisonai_external_agent:PraisonAIExternalAgent \
|
||||
--model openai/gpt-4o \
|
||||
--env daytona -n 32 \
|
||||
--ae OPENAI_API_KEY=$OPENAI_API_KEY
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Shell Execution Bridge**: Wraps Harbor's `BaseEnvironment.exec()` as PraisonAI tool
|
||||
- **Auto-Approval**: Bypasses `@require_approval` for container-isolated execution
|
||||
- **Token Tracking**: Populates Harbor's `AgentContext` with usage metrics
|
||||
- **API Key Forwarding**: Supports environment variable injection
|
||||
- **Multi-Agent Support**: Can run `AgentTeam` and `AgentFlow` workflows
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Harbor Framework │
|
||||
│ (Terminal-Bench 2.0 Evaluation) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ BaseEnvironment.exec() ←→ bash_tool │
|
||||
├─────────────────────────────────────────┤
|
||||
│ PraisonAI Agent │
|
||||
│ • Uses execute_command tool │
|
||||
│ • Auto-approval for container safety │
|
||||
│ • Token tracking and metrics │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `README.md` - This documentation
|
||||
- `praisonai_external_agent.py` - External agent (direct Agent class)
|
||||
- `praisonai_wrapper_agent.py` - Wrapper agent (CLI-based approach)
|
||||
- `praisonai_installed_agent.py` - Installed agent implementation
|
||||
- `multi_agent_example.py` - Multi-agent team example
|
||||
- `job.yaml` - Example Harbor job configuration
|
||||
- `test_basic.py` - Basic smoke tests
|
||||
- `test_integration.py` - Integration tests
|
||||
- `test_real_agentic.py` - Real agentic tests
|
||||
|
||||
## Terminal-Bench 2.0 Tasks
|
||||
|
||||
The benchmark includes 89 carefully curated tasks covering:
|
||||
- Compiling code
|
||||
- Training models
|
||||
- Setting up servers
|
||||
- System administration
|
||||
- File manipulation
|
||||
- Package management
|
||||
|
||||
Each task provides:
|
||||
- English instruction
|
||||
- Docker container environment
|
||||
- Test script (writes reward to `/logs/verifier/reward.txt`)
|
||||
- Reference oracle solution
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Test changes with oracle agent first: `harbor run -d terminal-bench/terminal-bench-2 -a oracle`
|
||||
2. Run real agentic tests to ensure end-to-end functionality
|
||||
3. Follow PraisonAI's AGENTS.md architecture guidelines
|
||||
4. Add both unit tests and integration tests
|
||||
|
||||
## Resources
|
||||
|
||||
- [Terminal-Bench 2.0 Announcement](https://www.tbench.ai/news/announcement-2-0)
|
||||
- [Harbor Framework Docs](https://www.harborframework.com/docs)
|
||||
- [Terminal-Bench Leaderboard](https://tbench.ai/leaderboard)
|
||||
- [Harbor GitHub Repo](https://github.com/laude-institute/harbor)
|
||||
@@ -0,0 +1,83 @@
|
||||
# Harbor Job Configuration for PraisonAI on Terminal-Bench 2.0
|
||||
#
|
||||
# Usage:
|
||||
# harbor run -c examples/terminal_bench/job.yaml
|
||||
#
|
||||
# This configuration runs PraisonAI external agent on Terminal-Bench 2.0 tasks
|
||||
# with 8 concurrent trials using GPT-4o.
|
||||
|
||||
# Dataset: Terminal-Bench 2.0 (89 carefully curated terminal tasks)
|
||||
dataset: terminal-bench/terminal-bench-2
|
||||
|
||||
# Agent configuration
|
||||
agent:
|
||||
# Use import path for external agent
|
||||
import_path: examples.terminal_bench.praisonai_external_agent:PraisonAIExternalAgent
|
||||
|
||||
# Model configuration
|
||||
model_name: openai/gpt-4o
|
||||
|
||||
# Environment variables (API keys)
|
||||
env:
|
||||
OPENAI_API_KEY: "${OPENAI_API_KEY}"
|
||||
# Optional: Add other API keys if using different models
|
||||
# ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}"
|
||||
# GEMINI_API_KEY: "${GEMINI_API_KEY}"
|
||||
|
||||
# Execution configuration
|
||||
n_concurrent: 8 # Run 8 trials in parallel
|
||||
n_attempts: 1 # Single attempt per task (benchmark standard)
|
||||
|
||||
# Optional: Environment configuration for cloud execution
|
||||
# environment:
|
||||
# provider: daytona # Options: local, daytona, e2b, modal, runloop, gke
|
||||
# n_concurrent: 32 # Higher concurrency on cloud
|
||||
|
||||
# Optional: Filter to specific tasks for testing
|
||||
# task_filter:
|
||||
# task_names: ["compile_simple_c", "install_python_package", "create_directory"]
|
||||
|
||||
# Optional: Advanced configuration
|
||||
# timeout_sec: 600 # 10 minute timeout per task
|
||||
# save_logs: true # Save execution logs
|
||||
# save_artifacts: true # Save container artifacts
|
||||
|
||||
---
|
||||
|
||||
# Alternative configuration using wrapper agent (CLI-based approach)
|
||||
# Uses `praisonai "TASK"` pattern instead of direct Agent class
|
||||
# Uncomment to use the wrapper approach:
|
||||
|
||||
# dataset: terminal-bench/terminal-bench-2
|
||||
#
|
||||
# agent:
|
||||
# import_path: examples.terminal_bench.praisonai_wrapper_agent:PraisonAIWrapperAgent
|
||||
# model_name: openai/gpt-4o
|
||||
#
|
||||
# n_concurrent: 8
|
||||
# n_attempts: 1
|
||||
#
|
||||
# env:
|
||||
# OPENAI_API_KEY: "${OPENAI_API_KEY}"
|
||||
|
||||
---
|
||||
|
||||
# Alternative configuration using installed agent (requires Harbor integration)
|
||||
# Uncomment this section once PraisonAI is integrated into Harbor's codebase
|
||||
|
||||
# dataset: terminal-bench/terminal-bench-2
|
||||
#
|
||||
# agent:
|
||||
# name: praisonai
|
||||
# model_name: openai/gpt-4o
|
||||
# flags:
|
||||
# max_turns: 30
|
||||
# verbose: false
|
||||
# memory: false
|
||||
# auto_approval: true
|
||||
#
|
||||
# n_concurrent: 8
|
||||
# n_attempts: 1
|
||||
#
|
||||
# env:
|
||||
# OPENAI_API_KEY: "${OPENAI_API_KEY}"
|
||||
@@ -0,0 +1,22 @@
|
||||
# Harbor Job Configuration for PraisonAI - 5 Task Test
|
||||
# Run with: harbor run -c examples/terminal_bench/job_test_5.yaml -n 1
|
||||
|
||||
dataset: terminal-bench/terminal-bench-2
|
||||
|
||||
agent:
|
||||
import_path: examples.terminal_bench.praisonai_external_agent:PraisonAIExternalAgent
|
||||
model_name: openai/gpt-4o-mini
|
||||
env:
|
||||
OPENAI_API_KEY: "${OPENAI_API_KEY}"
|
||||
|
||||
n_concurrent: 1
|
||||
n_attempts: 1
|
||||
|
||||
# Filter to 5 specific simple tasks
|
||||
task_filter:
|
||||
task_names:
|
||||
- "hello-world"
|
||||
- "fix-permissions"
|
||||
- "create-bucket"
|
||||
- "csv-to-parquet"
|
||||
- "simple-web-scraper"
|
||||
@@ -0,0 +1,22 @@
|
||||
# Harbor Job Configuration for PraisonAI Wrapper - 5 Task Test
|
||||
# Run with: harbor run -c examples/terminal_bench/job_test_5_wrapper.yaml -n 1
|
||||
|
||||
dataset: terminal-bench/terminal-bench-2
|
||||
|
||||
agent:
|
||||
import_path: examples.terminal_bench.praisonai_wrapper_agent:PraisonAIWrapperAgent
|
||||
model_name: openai/gpt-4o-mini
|
||||
env:
|
||||
OPENAI_API_KEY: "${OPENAI_API_KEY}"
|
||||
|
||||
n_concurrent: 1
|
||||
n_attempts: 1
|
||||
|
||||
# Filter to 5 specific simple tasks
|
||||
task_filter:
|
||||
task_names:
|
||||
- "hello-world"
|
||||
- "fix-permissions"
|
||||
- "create-bucket"
|
||||
- "csv-to-parquet"
|
||||
- "simple-web-scraper"
|
||||
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Multi-Agent PraisonAI Integration for Terminal-Bench 2.0
|
||||
|
||||
This example shows how to use PraisonAI's AgentTeam and AgentFlow
|
||||
capabilities with Terminal-Bench 2.0 for complex multi-step tasks.
|
||||
|
||||
The multi-agent approach is useful for Terminal-Bench tasks that benefit from:
|
||||
- Task planning and decomposition
|
||||
- Specialized roles (planner, executor, verifier)
|
||||
- Error correction and refinement
|
||||
- Complex workflows with multiple phases
|
||||
|
||||
Usage:
|
||||
harbor run -d terminal-bench/terminal-bench-2 \
|
||||
--agent-import-path examples.terminal_bench.multi_agent_example:MultiAgentPraisonAI \
|
||||
--model openai/gpt-4o
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
try:
|
||||
from harbor.agents.base import BaseAgent
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
from harbor.models.agent.context import AgentContext
|
||||
except ImportError as e:
|
||||
raise ImportError(f"Harbor framework required: {e}") from e
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent, AgentTeam, Task
|
||||
from praisonaiagents.approval import get_approval_registry, AutoApproveBackend
|
||||
except ImportError as e:
|
||||
raise ImportError(f"PraisonAI required: {e}") from e
|
||||
|
||||
|
||||
class MultiAgentPraisonAI(BaseAgent):
|
||||
"""Multi-agent PraisonAI system for complex Terminal-Bench tasks."""
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "praisonai-multi"
|
||||
|
||||
def version(self) -> str | None:
|
||||
try:
|
||||
import praisonaiagents
|
||||
return getattr(praisonaiagents, "__version__", None)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
async def setup(self, environment: BaseEnvironment) -> None:
|
||||
"""Setup phase - no container setup needed for external agent."""
|
||||
pass
|
||||
|
||||
async def run(
|
||||
self,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
context: AgentContext,
|
||||
) -> None:
|
||||
"""
|
||||
Run multi-agent PraisonAI team on the Terminal-Bench task.
|
||||
|
||||
Uses a three-agent approach:
|
||||
1. Planner: Analyzes task and creates execution plan
|
||||
2. Executor: Implements the plan using shell commands
|
||||
3. Verifier: Tests and validates the solution
|
||||
"""
|
||||
|
||||
# Set auto-approval for container safety
|
||||
registry = get_approval_registry()
|
||||
original_backend = registry.get_backend()
|
||||
registry.set_backend(AutoApproveBackend(), agent_name="multi-agent-planner")
|
||||
|
||||
try:
|
||||
# Create bash tool that wraps Harbor environment
|
||||
async def bash_tool(command: str) -> str:
|
||||
"""Execute bash command in Harbor environment."""
|
||||
if not command.strip():
|
||||
return "Error: Empty command"
|
||||
|
||||
try:
|
||||
result = await environment.exec(command=command, timeout_sec=30)
|
||||
output_parts = []
|
||||
if result.stdout:
|
||||
output_parts.append(result.stdout.strip())
|
||||
if result.stderr:
|
||||
output_parts.append(f"[stderr]: {result.stderr.strip()}")
|
||||
if result.return_code != 0:
|
||||
output_parts.append(f"[exit_code]: {result.return_code}")
|
||||
return "\n".join(output_parts) if output_parts else "(no output)"
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
# Create specialized agents
|
||||
planner = Agent(
|
||||
name="planner",
|
||||
instructions=(
|
||||
"You are an expert task planner for terminal/coding tasks. "
|
||||
"Analyze the given instruction and create a detailed, step-by-step plan. "
|
||||
"Break down complex tasks into smaller, manageable steps. "
|
||||
"Consider potential issues and edge cases. "
|
||||
"Output your plan as a numbered list of specific actions."
|
||||
),
|
||||
llm=self.model_name or "openai/gpt-4o",
|
||||
)
|
||||
|
||||
executor = Agent(
|
||||
name="executor",
|
||||
instructions=(
|
||||
"You are an expert terminal executor. "
|
||||
"Follow the provided plan step-by-step using bash commands. "
|
||||
"Use the bash_tool to execute commands safely. "
|
||||
"Be precise and check each step before proceeding. "
|
||||
"If a step fails, try alternative approaches."
|
||||
),
|
||||
tools=[bash_tool],
|
||||
llm=self.model_name or "openai/gpt-4o",
|
||||
)
|
||||
|
||||
verifier = Agent(
|
||||
name="verifier",
|
||||
instructions=(
|
||||
"You are a solution verifier and tester. "
|
||||
"Test the completed solution to ensure it works correctly. "
|
||||
"Run appropriate tests and checks using bash commands. "
|
||||
"Report whether the solution meets the requirements. "
|
||||
"If issues are found, suggest specific fixes."
|
||||
),
|
||||
tools=[bash_tool],
|
||||
llm=self.model_name or "openai/gpt-4o",
|
||||
)
|
||||
|
||||
print(f"🚀 Multi-Agent PraisonAI starting: {instruction[:100]}...")
|
||||
|
||||
# Phase 1: Planning
|
||||
print("📋 Phase 1: Task Planning")
|
||||
plan = await planner.astart(f"Create a detailed plan for: {instruction}")
|
||||
print(f"Plan created: {len(plan.split('.')) if plan else 0} steps")
|
||||
|
||||
# Phase 2: Execution
|
||||
print("⚡ Phase 2: Task Execution")
|
||||
execution_prompt = f"Execute this plan step by step:\n\nOriginal task: {instruction}\n\nPlan:\n{plan}"
|
||||
execution_result = await executor.astart(execution_prompt)
|
||||
print("Execution completed")
|
||||
|
||||
# Phase 3: Verification
|
||||
print("✅ Phase 3: Solution Verification")
|
||||
verification_prompt = f"Verify this solution works correctly:\n\nOriginal task: {instruction}\n\nSolution: {execution_result}\n\nRun tests to confirm it works."
|
||||
verification_result = await verifier.astart(verification_prompt)
|
||||
print("Verification completed")
|
||||
|
||||
# Combine results
|
||||
final_result = {
|
||||
"plan": plan,
|
||||
"execution": execution_result,
|
||||
"verification": verification_result,
|
||||
}
|
||||
|
||||
print("✅ Multi-Agent PraisonAI completed task")
|
||||
|
||||
# Populate context
|
||||
self._populate_context([planner, executor, verifier], context, final_result)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Multi-Agent PraisonAI failed: {str(e)}")
|
||||
context.metadata = {"error": str(e)}
|
||||
raise
|
||||
finally:
|
||||
# Restore original approval backend to avoid global state pollution
|
||||
if original_backend:
|
||||
registry.set_backend(original_backend)
|
||||
else:
|
||||
registry.remove_backend(agent_name="multi-agent-planner")
|
||||
|
||||
def _populate_context(self, agents: list, context: AgentContext, result: Dict[str, Any]) -> None:
|
||||
"""Populate Harbor context with multi-agent metrics."""
|
||||
try:
|
||||
# Aggregate token usage from all agents
|
||||
total_input_tokens = 0
|
||||
total_output_tokens = 0
|
||||
total_cost = 0.0
|
||||
|
||||
for agent in agents:
|
||||
# Use agent's actual metrics properties
|
||||
total_input_tokens += getattr(agent, '_total_tokens_in', 0)
|
||||
total_output_tokens += getattr(agent, '_total_tokens_out', 0)
|
||||
total_cost += agent.total_cost or 0.0
|
||||
|
||||
context.n_input_tokens = total_input_tokens if total_input_tokens > 0 else None
|
||||
context.n_output_tokens = total_output_tokens if total_output_tokens > 0 else None
|
||||
context.cost_usd = total_cost if total_cost > 0 else None
|
||||
|
||||
context.metadata = {
|
||||
"framework": "praisonai-multi",
|
||||
"agent_type": "multi-agent-team",
|
||||
"agents": [agent.name for agent in agents],
|
||||
"model": self.model_name or "openai/gpt-4o",
|
||||
"phases": ["planning", "execution", "verification"],
|
||||
"tools_used": ["bash_tool"],
|
||||
"result_summary": str(result.get("verification", ""))[:200],
|
||||
"version": self.version(),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
context.metadata = {"context_error": str(e)}
|
||||
|
||||
|
||||
# Alternative implementation using AgentTeam (more structured)
|
||||
class AgentTeamPraisonAI(BaseAgent):
|
||||
"""AgentTeam-based implementation for Terminal-Bench tasks."""
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "praisonai-team"
|
||||
|
||||
def version(self) -> str | None:
|
||||
try:
|
||||
import praisonaiagents
|
||||
return getattr(praisonaiagents, "__version__", None)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
async def setup(self, environment: BaseEnvironment) -> None:
|
||||
pass
|
||||
|
||||
async def run(
|
||||
self,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
context: AgentContext,
|
||||
) -> None:
|
||||
"""Run structured AgentTeam workflow."""
|
||||
|
||||
registry = get_approval_registry()
|
||||
original_backend = registry.get_backend()
|
||||
registry.set_backend(AutoApproveBackend(), agent_name="agent-team")
|
||||
|
||||
try:
|
||||
# Create bash tool
|
||||
async def bash_tool(command: str) -> str:
|
||||
result = await environment.exec(command=command, timeout_sec=30)
|
||||
output_parts = []
|
||||
if result.stdout:
|
||||
output_parts.append(result.stdout.strip())
|
||||
if result.stderr:
|
||||
output_parts.append(f"[stderr]: {result.stderr.strip()}")
|
||||
if result.return_code != 0:
|
||||
output_parts.append(f"[exit_code]: {result.return_code}")
|
||||
return "\n".join(output_parts) if output_parts else "(no output)"
|
||||
|
||||
# Create agents
|
||||
planner = Agent(
|
||||
name="planner",
|
||||
instructions="Create detailed execution plans for terminal tasks",
|
||||
llm=self.model_name or "openai/gpt-4o"
|
||||
)
|
||||
|
||||
executor = Agent(
|
||||
name="executor",
|
||||
instructions="Execute terminal commands based on plans",
|
||||
tools=[bash_tool],
|
||||
llm=self.model_name or "openai/gpt-4o"
|
||||
)
|
||||
|
||||
# Create tasks
|
||||
plan_task = Task(
|
||||
name="plan",
|
||||
description=f"Create plan for: {instruction}",
|
||||
agent=planner
|
||||
)
|
||||
|
||||
execute_task = Task(
|
||||
name="execute",
|
||||
description=f"Execute plan for: {instruction}",
|
||||
agent=executor,
|
||||
dependencies=[plan_task] # Execute after planning
|
||||
)
|
||||
|
||||
# Create and run team
|
||||
team = AgentTeam(
|
||||
agents=[planner, executor],
|
||||
tasks=[plan_task, execute_task],
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
print(f"🚀 AgentTeam starting: {instruction[:100]}...")
|
||||
result = await team.astart(instruction)
|
||||
print("✅ AgentTeam completed")
|
||||
|
||||
# Populate context
|
||||
context.metadata = {
|
||||
"framework": "praisonai-team",
|
||||
"workflow": "sequential",
|
||||
"agents_count": len(team.agents),
|
||||
"tasks_count": len(team.tasks),
|
||||
"result": str(result)[:200] if result else None,
|
||||
}
|
||||
|
||||
finally:
|
||||
# Restore original approval backend to avoid global state pollution
|
||||
if original_backend:
|
||||
registry.set_backend(original_backend)
|
||||
else:
|
||||
registry.remove_backend(agent_name="agent-team")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Multi-Agent PraisonAI for Terminal-Bench 2.0")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print("Available agent implementations:")
|
||||
print("1. MultiAgentPraisonAI - Custom multi-agent workflow")
|
||||
print("2. AgentTeamPraisonAI - Structured AgentTeam workflow")
|
||||
print()
|
||||
print("Usage examples:")
|
||||
print()
|
||||
print("# Multi-agent custom workflow")
|
||||
print("harbor run -d terminal-bench/terminal-bench-2 \\")
|
||||
print(" --agent-import-path examples.terminal_bench.multi_agent_example:MultiAgentPraisonAI \\")
|
||||
print(" --model openai/gpt-4o")
|
||||
print()
|
||||
print("# AgentTeam structured workflow")
|
||||
print("harbor run -d terminal-bench/terminal-bench-2 \\")
|
||||
print(" --agent-import-path examples.terminal_bench.multi_agent_example:AgentTeamPraisonAI \\")
|
||||
print(" --model openai/gpt-4o")
|
||||
print()
|
||||
print("Benefits of multi-agent approach:")
|
||||
print("- Task decomposition and planning")
|
||||
print("- Specialized roles and expertise")
|
||||
print("- Error detection and correction")
|
||||
print("- Higher success rates on complex tasks")
|
||||
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
PraisonAI External Agent for Terminal-Bench 2.0 (Harbor)
|
||||
|
||||
An external agent that bridges PraisonAI's Agent class with Harbor's
|
||||
BaseEnvironment interface. The agent uses Harbor's exec() as a bash tool.
|
||||
|
||||
Usage:
|
||||
harbor run -d terminal-bench/terminal-bench-2 \
|
||||
--agent-import-path examples.terminal_bench.praisonai_external_agent:PraisonAIExternalAgent \
|
||||
--model openai/gpt-4o \
|
||||
--ae OPENAI_API_KEY=$OPENAI_API_KEY
|
||||
|
||||
Architecture:
|
||||
Harbor Container ←→ BaseEnvironment.exec() ←→ bash_tool ←→ PraisonAI Agent
|
||||
|
||||
Dependencies:
|
||||
pip install harbor praisonaiagents
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from harbor.agents.base import BaseAgent
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
from harbor.models.agent.context import AgentContext
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
f"Harbor framework not installed: {e}\n"
|
||||
"Install with: pip install harbor"
|
||||
) from e
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.approval import get_approval_registry, AutoApproveBackend
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
f"PraisonAI not installed: {e}\n"
|
||||
"Install with: pip install praisonaiagents"
|
||||
) from e
|
||||
|
||||
|
||||
class PraisonAIExternalAgent(BaseAgent):
|
||||
"""External PraisonAI agent that drives a Harbor container environment."""
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "praisonai"
|
||||
|
||||
def version(self) -> str | None:
|
||||
try:
|
||||
import praisonaiagents
|
||||
return (
|
||||
getattr(praisonaiagents, "__version__", None)
|
||||
or getattr(praisonaiagents, "version", None)
|
||||
or "unknown"
|
||||
)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
async def setup(self, environment: BaseEnvironment) -> None:
|
||||
"""Setup phase - external agent needs no container setup."""
|
||||
pass
|
||||
|
||||
async def run(
|
||||
self,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
context: AgentContext,
|
||||
) -> None:
|
||||
"""
|
||||
Run the PraisonAI agent on the given instruction using Harbor's environment.
|
||||
|
||||
This method bridges Harbor's BaseEnvironment.exec() to PraisonAI's tool system.
|
||||
"""
|
||||
|
||||
# Inject API keys from Harbor's --ae env vars into host os.environ
|
||||
# so litellm can pick them up (--ae only sets them inside Docker, not the host)
|
||||
agent_env = getattr(context, 'env', {}) or {}
|
||||
for key, val in agent_env.items():
|
||||
if key not in os.environ and val:
|
||||
os.environ[key] = val
|
||||
print(f"[ENV] Set {key} from Harbor agent env")
|
||||
|
||||
# Set auto-approval for container-isolated execution
|
||||
# Harbor's container provides isolation, so we can safely auto-approve shell commands
|
||||
registry = get_approval_registry()
|
||||
original_backend = registry.get_backend()
|
||||
registry.set_backend(AutoApproveBackend(), agent_name="terminal-agent")
|
||||
|
||||
try:
|
||||
# Create bash tool that wraps Harbor's environment.exec()
|
||||
async def bash_tool(command: str) -> str:
|
||||
"""Execute a bash command in the Harbor sandboxed environment."""
|
||||
if not command.strip():
|
||||
return "Error: Empty command provided"
|
||||
|
||||
print(f"[CMD] {command[:200]}")
|
||||
try:
|
||||
# Execute command in Harbor's container
|
||||
result = await environment.exec(command=command, timeout_sec=300)
|
||||
|
||||
# Format output similar to PraisonAI's execute_command tool
|
||||
output_parts = []
|
||||
if result.stdout:
|
||||
output_parts.append(result.stdout.strip())
|
||||
if result.stderr:
|
||||
output_parts.append(f"[stderr]: {result.stderr.strip()}")
|
||||
if result.return_code != 0:
|
||||
output_parts.append(f"[exit_code]: {result.return_code}")
|
||||
|
||||
output = "\n".join(output_parts) if output_parts else "(no output)"
|
||||
print(f"[OUT] {output[:300]}")
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERR] {str(e)}")
|
||||
return f"Error executing command: {str(e)}"
|
||||
|
||||
# Create PraisonAI agent with the bash tool
|
||||
agent = Agent(
|
||||
name="terminal-agent",
|
||||
instructions=(
|
||||
"You are an expert terminal agent. You solve coding, debugging, data analysis, security, "
|
||||
"and system administration tasks using the bash_tool to run shell commands."
|
||||
"\n\nCRITICAL RULES:"
|
||||
"\n1. START by exploring: ls /app/ && cat /app/*.py /app/*.sh /app/*.txt 2>/dev/null | head -200"
|
||||
" Read ALL task files to understand what is needed before doing anything."
|
||||
"\n2. Find and run the test IMMEDIATELY to see what is expected:"
|
||||
" find /app -name 'test_*.py' | head -3 && cd /app && python3 -m pytest -v 2>&1 | tail -40 || true"
|
||||
" The test error trace tells you EXACTLY what output/behavior is required."
|
||||
"\n3. ITERATE: implement a solution, run the test, read the error, refine. Repeat until it passes."
|
||||
" Never give up after one attempt — try multiple approaches if needed."
|
||||
"\n4. For writing files with special characters use Python:"
|
||||
" python3 -c \"with open('/app/file','w') as f: f.write('content')\""
|
||||
" or heredoc: cat > /app/file << 'EOF'\\n...content...\\nEOF"
|
||||
"\n5. Read ALL error messages — they tell you exactly what to fix next."
|
||||
"\n6. NEVER just describe your plan. ALWAYS immediately run bash_tool commands."
|
||||
" Keep calling bash_tool until the test PASSES or you have exhausted all approaches."
|
||||
),
|
||||
tools=[bash_tool],
|
||||
llm=self.model_name or "openai/gpt-4o",
|
||||
)
|
||||
|
||||
# Execute the agent with outer loop to handle premature stopping
|
||||
print(f"🚀 PraisonAI Agent starting task: {instruction[:100]}...")
|
||||
result = await agent.achat(instruction)
|
||||
for _iter in range(19):
|
||||
result_str = str(result)
|
||||
# Stop if test passed
|
||||
if any(sig in result_str.lower() for sig in [
|
||||
" passed", "passed ", "test passed", "all tests pass", "1 passed"
|
||||
]):
|
||||
break
|
||||
result = await agent.achat(
|
||||
"The task is NOT complete yet — keep working. Run bash_tool commands now. "
|
||||
"If you haven't already: (1) read all files in /app/, "
|
||||
"(2) run the test to see the exact failure, "
|
||||
"(3) implement a fix, (4) run the test again. "
|
||||
"Repeat until the test passes. What is your next bash_tool command?"
|
||||
)
|
||||
print(f"✅ PraisonAI Agent completed task")
|
||||
|
||||
# Populate Harbor context with metadata
|
||||
self._populate_context(agent, context, result)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ PraisonAI Agent failed: {str(e)}")
|
||||
context.metadata = {"error": str(e)}
|
||||
raise
|
||||
finally:
|
||||
# Restore original approval backend to avoid global state pollution
|
||||
if original_backend:
|
||||
registry.set_backend(original_backend)
|
||||
else:
|
||||
registry.remove_backend(agent_name="terminal-agent")
|
||||
|
||||
def _populate_context(self, agent: Agent, context: AgentContext, result: Any) -> None:
|
||||
"""
|
||||
Populate Harbor's AgentContext with metrics from PraisonAI agent execution.
|
||||
|
||||
Harbor tracks: n_input_tokens, n_output_tokens, cost_usd, metadata
|
||||
"""
|
||||
try:
|
||||
# Extract token usage and cost from agent
|
||||
try:
|
||||
summary = agent.cost_summary() if callable(getattr(agent, 'cost_summary', None)) else None
|
||||
if isinstance(summary, dict):
|
||||
context.n_input_tokens = summary.get('tokens_in')
|
||||
context.n_output_tokens = summary.get('tokens_out')
|
||||
context.cost_usd = summary.get('cost')
|
||||
else:
|
||||
context.n_input_tokens = getattr(agent, '_total_tokens_in', 0)
|
||||
context.n_output_tokens = getattr(agent, '_total_tokens_out', 0)
|
||||
context.cost_usd = getattr(agent, 'total_cost', None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Store result summary and agent info
|
||||
context.metadata = {
|
||||
"agent_name": agent.name,
|
||||
"model": getattr(agent, 'llm', 'unknown'),
|
||||
"final_response": str(result)[:500] if result else None,
|
||||
"tools_used": ["bash_tool"],
|
||||
"framework": "praisonai",
|
||||
"version": self.version(),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Don't fail the whole run if context population fails
|
||||
context.metadata = {"context_error": str(e)}
|
||||
|
||||
|
||||
# Example usage for testing
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI External Agent for Terminal-Bench 2.0")
|
||||
print("Usage: harbor run -d terminal-bench/terminal-bench-2 \\")
|
||||
print(" --agent-import-path examples.terminal_bench.praisonai_external_agent:PraisonAIExternalAgent \\")
|
||||
print(" --model openai/gpt-4o")
|
||||
print()
|
||||
print("Dependencies:")
|
||||
print(" pip install harbor praisonaiagents")
|
||||
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
PraisonAI Installed Agent for Terminal-Bench 2.0 (Harbor)
|
||||
|
||||
Production-ready agent that installs PraisonAI inside Harbor's container
|
||||
and runs it headlessly. This follows the same pattern as other agents
|
||||
in Harbor (codex, gemini-cli, claude-code).
|
||||
|
||||
Usage:
|
||||
# First, this would need to be integrated into Harbor's codebase as:
|
||||
# src/harbor/agents/installed/praisonai.py
|
||||
|
||||
harbor run -d terminal-bench/terminal-bench-2 -a praisonai --model openai/gpt-4o
|
||||
|
||||
Architecture:
|
||||
Harbor Container → praisonaiagents installed inside → execute_command tool
|
||||
|
||||
For now, this serves as a reference implementation that could be contributed
|
||||
to the Harbor project via PR.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import json
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
try:
|
||||
# These imports would work if this file was in Harbor's codebase
|
||||
from harbor.agents.installed.base import BaseInstalledAgent, with_prompt_template, CliFlag
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
from harbor.models.agent.context import AgentContext
|
||||
from harbor.models.trial.paths import EnvironmentPaths
|
||||
except ImportError:
|
||||
# Fallback for development/testing outside Harbor
|
||||
print("Note: This is a reference implementation for Harbor integration")
|
||||
print("To actually use this, it needs to be integrated into Harbor's codebase")
|
||||
|
||||
# Mock classes for development
|
||||
class BaseInstalledAgent:
|
||||
pass
|
||||
|
||||
def with_prompt_template(func):
|
||||
return func
|
||||
|
||||
class CliFlag:
|
||||
def __init__(self, name, cli, type, default):
|
||||
pass
|
||||
|
||||
class BaseEnvironment:
|
||||
async def exec(self, **kwargs):
|
||||
pass
|
||||
|
||||
class AgentContext:
|
||||
pass
|
||||
|
||||
|
||||
class PraisonAIInstalledAgent(BaseInstalledAgent):
|
||||
"""
|
||||
PraisonAI agent installed inside Harbor's container environment.
|
||||
|
||||
This is a production-ready integration that follows Harbor's established
|
||||
patterns for installed agents like Codex CLI, Gemini CLI, etc.
|
||||
"""
|
||||
|
||||
# ATIF trajectory support - would be implemented once PraisonAI has trajectory format
|
||||
SUPPORTS_ATIF: bool = False
|
||||
|
||||
# Command-line configuration flags
|
||||
CLI_FLAGS = [
|
||||
CliFlag("max_turns", cli="--max-turns", type="int", default=30),
|
||||
CliFlag("verbose", cli="--verbose", type="bool", default=False),
|
||||
CliFlag("memory", cli="--memory", type="bool", default=False),
|
||||
CliFlag("auto_approval", cli="--auto-approval", type="bool", default=True),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "praisonai"
|
||||
|
||||
def get_version_command(self) -> str | None:
|
||||
"""Command to get PraisonAI version inside the container."""
|
||||
return "python3 -c 'import praisonaiagents; print(praisonaiagents.__version__)'"
|
||||
|
||||
async def install(self, environment: BaseEnvironment) -> None:
|
||||
"""
|
||||
Install PraisonAI and dependencies inside the Harbor container.
|
||||
|
||||
This runs as root first for system packages, then as agent user
|
||||
for Python packages.
|
||||
"""
|
||||
# System packages (as root)
|
||||
await self.exec_as_root(
|
||||
environment,
|
||||
command="apt-get update && apt-get install -y python3 python3-pip curl git",
|
||||
env={"DEBIAN_FRONTEND": "noninteractive"},
|
||||
)
|
||||
|
||||
# Install PraisonAI (as agent user)
|
||||
version_spec = f"=={self._version}" if self._version else ""
|
||||
install_cmd = f"pip install praisonaiagents{version_spec} --quiet"
|
||||
|
||||
await self.exec_as_agent(
|
||||
environment,
|
||||
command=(
|
||||
f"{install_cmd} && "
|
||||
"python3 -c 'import praisonaiagents; print(\"✅ praisonaiagents installed:\", praisonaiagents.__version__)'"
|
||||
),
|
||||
)
|
||||
|
||||
# Create headless runner script
|
||||
runner_script = self._build_runner_script()
|
||||
await self.exec_as_agent(
|
||||
environment,
|
||||
command=f"cat > /tmp/praisonai_runner.py << 'SCRIPT_EOF'\n{runner_script}\nSCRIPT_EOF",
|
||||
)
|
||||
|
||||
print("✅ PraisonAI installation completed")
|
||||
|
||||
def _build_runner_script(self) -> str:
|
||||
"""
|
||||
Build the Python runner script that will execute inside the container.
|
||||
|
||||
This script handles:
|
||||
- Setting up auto-approval for container safety
|
||||
- Creating agent with shell tools
|
||||
- Executing the instruction
|
||||
- Outputting results in JSON format
|
||||
"""
|
||||
return '''#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Headless Runner for Harbor Terminal-Bench 2.0
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({"error": "No instruction provided"}))
|
||||
sys.exit(1)
|
||||
|
||||
instruction = sys.argv[1]
|
||||
model = sys.argv[2] if len(sys.argv) > 2 else "openai/gpt-4o"
|
||||
max_turns = int(sys.argv[3]) if len(sys.argv) > 3 else 30
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import execute_command
|
||||
from praisonaiagents.approval import get_approval_registry, AutoApproveBackend
|
||||
|
||||
# Set auto-approval for container-isolated execution
|
||||
get_approval_registry().set_backend(AutoApproveBackend())
|
||||
|
||||
# Create terminal agent with shell execution capabilities
|
||||
agent = Agent(
|
||||
name="terminal-agent",
|
||||
instructions=(
|
||||
"You are an expert terminal agent working on coding and system administration tasks. "
|
||||
"Use the execute_command tool to run shell commands safely. "
|
||||
"Be precise, verify your work, and complete the task step by step. "
|
||||
"Always test your solution to ensure it works correctly."
|
||||
),
|
||||
tools=[execute_command],
|
||||
llm=model,
|
||||
)
|
||||
|
||||
# Execute the instruction
|
||||
result = agent.start(instruction)
|
||||
|
||||
# Extract metrics
|
||||
metrics = {
|
||||
"result": str(result) if result else None,
|
||||
"agent_name": agent.name,
|
||||
"model": model,
|
||||
"tools_used": ["execute_command"],
|
||||
}
|
||||
|
||||
# Add token usage if available
|
||||
if hasattr(agent, '_usage'):
|
||||
usage = agent._usage
|
||||
if usage:
|
||||
metrics.update({
|
||||
"input_tokens": getattr(usage, 'input_tokens', None),
|
||||
"output_tokens": getattr(usage, 'output_tokens', None),
|
||||
})
|
||||
|
||||
# Add cost if available
|
||||
if hasattr(agent, '_cost'):
|
||||
metrics["cost_usd"] = agent._cost
|
||||
|
||||
print(json.dumps(metrics))
|
||||
|
||||
except Exception as e:
|
||||
error_result = {
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
}
|
||||
print(json.dumps(error_result))
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
@with_prompt_template
|
||||
async def run(
|
||||
self,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
context: AgentContext,
|
||||
) -> None:
|
||||
"""
|
||||
Run the PraisonAI agent on the given instruction.
|
||||
|
||||
This executes the headless runner script with the instruction,
|
||||
similar to how other installed agents work in Harbor.
|
||||
"""
|
||||
model = self.model_name or "openai/gpt-4o"
|
||||
max_turns = getattr(self, 'max_turns', 30)
|
||||
|
||||
# Build command to run PraisonAI
|
||||
cmd_args = [
|
||||
"python3", "/tmp/praisonai_runner.py",
|
||||
shlex.quote(instruction),
|
||||
shlex.quote(model),
|
||||
str(max_turns),
|
||||
]
|
||||
|
||||
command = " ".join(cmd_args)
|
||||
|
||||
try:
|
||||
# Execute the agent
|
||||
await self.exec_as_agent(
|
||||
environment,
|
||||
command=command,
|
||||
env=self._get_environment_vars(),
|
||||
)
|
||||
except Exception as e:
|
||||
# Store error in context for Harbor's reporting
|
||||
context.metadata = {"execution_error": str(e)}
|
||||
raise
|
||||
|
||||
def _get_environment_vars(self) -> Dict[str, str]:
|
||||
"""Get environment variables to pass to the agent execution."""
|
||||
env_vars = {}
|
||||
|
||||
# Common API keys that should be forwarded
|
||||
api_keys = [
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"GOOGLE_API_KEY",
|
||||
"AZURE_OPENAI_API_KEY",
|
||||
"GROQ_API_KEY",
|
||||
]
|
||||
|
||||
for key in api_keys:
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
env_vars[key] = value
|
||||
|
||||
return env_vars
|
||||
|
||||
def populate_context_post_run(self, context: AgentContext) -> None:
|
||||
"""
|
||||
Parse the agent's output and populate Harbor's AgentContext.
|
||||
|
||||
This extracts token counts, costs, and other metrics from the
|
||||
JSON output of the headless runner script.
|
||||
"""
|
||||
try:
|
||||
# Parse the last stdout output for JSON metrics
|
||||
# In Harbor's model, the last execution output should contain our JSON
|
||||
last_output = getattr(context, '_last_stdout', None)
|
||||
|
||||
if last_output:
|
||||
try:
|
||||
metrics = json.loads(last_output.strip())
|
||||
|
||||
# Extract metrics with safe defaults
|
||||
context.n_input_tokens = metrics.get('input_tokens')
|
||||
context.n_output_tokens = metrics.get('output_tokens')
|
||||
context.cost_usd = metrics.get('cost_usd')
|
||||
|
||||
# Store additional metadata
|
||||
context.metadata = {
|
||||
"framework": "praisonai",
|
||||
"agent_type": "installed",
|
||||
"agent_name": metrics.get('agent_name', 'terminal-agent'),
|
||||
"model": metrics.get('model'),
|
||||
"tools_used": metrics.get('tools_used', []),
|
||||
"version": self.get_version() if hasattr(self, 'get_version') else None,
|
||||
}
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
# If JSON parsing fails, store basic metadata
|
||||
context.metadata = {
|
||||
"framework": "praisonai",
|
||||
"agent_type": "installed",
|
||||
"parse_error": str(e),
|
||||
"raw_output": str(last_output)[:200] if last_output else None,
|
||||
}
|
||||
else:
|
||||
# No output to parse
|
||||
context.metadata = {
|
||||
"framework": "praisonai",
|
||||
"agent_type": "installed",
|
||||
"status": "no_output",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
context.metadata = {"context_population_error": str(e)}
|
||||
|
||||
|
||||
# Factory function for Harbor integration
|
||||
def create_praisonai_agent(**kwargs) -> PraisonAIInstalledAgent:
|
||||
"""Factory function to create PraisonAI installed agent."""
|
||||
return PraisonAIInstalledAgent(**kwargs)
|
||||
|
||||
|
||||
# Example usage and testing
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Installed Agent for Terminal-Bench 2.0")
|
||||
print()
|
||||
print("This is a reference implementation for Harbor integration.")
|
||||
print("To use this in production:")
|
||||
print()
|
||||
print("1. Add this file to Harbor's codebase as:")
|
||||
print(" src/harbor/agents/installed/praisonai.py")
|
||||
print()
|
||||
print("2. Register in Harbor's agent factory:")
|
||||
print(" - Add 'PRAISONAI = \"praisonai\"' to AgentName enum")
|
||||
print(" - Add PraisonAI to _AGENTS list and _AGENT_MAP")
|
||||
print()
|
||||
print("3. Run with Harbor:")
|
||||
print(" harbor run -d terminal-bench/terminal-bench-2 -a praisonai --model openai/gpt-4o")
|
||||
print()
|
||||
print("Dependencies:")
|
||||
print(" pip install harbor praisonaiagents")
|
||||
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
PraisonAI Wrapper Agent for Terminal-Bench 2.0 (Harbor)
|
||||
|
||||
An external agent that uses the PraisonAI CLI wrapper instead of
|
||||
direct Agent class instantiation. This provides a higher-level interface
|
||||
that matches the standard `praisonai "TASK"` CLI usage pattern.
|
||||
|
||||
Usage:
|
||||
harbor run -d terminal-bench/terminal-bench-2 \
|
||||
--agent-import-path examples.terminal_bench.praisonai_wrapper_agent:PraisonAIWrapperAgent \
|
||||
--model openai/gpt-4o \
|
||||
--ae OPENAI_API_KEY=$OPENAI_API_KEY \
|
||||
-n 4
|
||||
|
||||
Architecture:
|
||||
Harbor Container ←→ BaseEnvironment.exec() ←→ praisonai CLI subprocess
|
||||
|
||||
Dependencies:
|
||||
pip install harbor praisonai
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import shlex
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# These imports require Harbor to be installed
|
||||
try:
|
||||
from harbor.agents.base import BaseAgent
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
from harbor.models.agent.context import AgentContext
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
f"Harbor framework not installed: {e}\n"
|
||||
"Install with: pip install harbor"
|
||||
) from e
|
||||
|
||||
|
||||
class PraisonAIWrapperAgent(BaseAgent):
|
||||
"""
|
||||
PraisonAI wrapper agent that uses the CLI/subprocess approach.
|
||||
|
||||
This agent installs PraisonAI inside the Harbor container and runs
|
||||
tasks using the `praisonai "TASK"` pattern, matching standard CLI usage.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "praisonai-wrapper"
|
||||
|
||||
def version(self) -> str | None:
|
||||
"""Get PraisonAI version from container."""
|
||||
try:
|
||||
import praisonai
|
||||
return getattr(praisonai, "__version__", None)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
async def setup(self, environment: BaseEnvironment) -> None:
|
||||
"""
|
||||
Setup phase - install PraisonAI wrapper inside the container.
|
||||
"""
|
||||
print("🔧 Installing PraisonAI wrapper inside container...")
|
||||
|
||||
# Install system dependencies
|
||||
await self._exec_as_root(
|
||||
environment,
|
||||
command="apt-get update && apt-get install -y python3 python3-pip curl git",
|
||||
env={"DEBIAN_FRONTEND": "noninteractive"},
|
||||
)
|
||||
|
||||
# Install PraisonAI wrapper
|
||||
version_spec = f"=={self._version}" if getattr(self, '_version', None) else ""
|
||||
install_cmd = f"pip install praisonai{version_spec} --quiet"
|
||||
|
||||
await self._exec_as_agent(
|
||||
environment,
|
||||
command=(
|
||||
f"{install_cmd} && "
|
||||
"python3 -c 'import praisonai; print(\"✅ praisonai installed:\", praisonai.__version__)'"
|
||||
),
|
||||
)
|
||||
|
||||
print("✅ PraisonAI wrapper setup completed")
|
||||
|
||||
async def run(
|
||||
self,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
context: AgentContext,
|
||||
) -> None:
|
||||
"""
|
||||
Run the PraisonAI wrapper agent on the given instruction.
|
||||
|
||||
Uses the `praisonai "TASK"` CLI pattern inside the Harbor container.
|
||||
"""
|
||||
model = self.model_name or "openai/gpt-4o"
|
||||
|
||||
# Build the praisonai CLI command
|
||||
# Format: praisonai "TASK" --model MODEL
|
||||
cmd_parts = [
|
||||
"praisonai",
|
||||
shlex.quote(instruction),
|
||||
"--model", shlex.quote(model),
|
||||
]
|
||||
|
||||
# Add API key to environment
|
||||
env_vars = self._get_environment_vars()
|
||||
|
||||
command = " ".join(cmd_parts)
|
||||
|
||||
try:
|
||||
print(f"🚀 PraisonAI wrapper starting task: {instruction[:100]}...")
|
||||
|
||||
# Execute praisonai CLI inside the container
|
||||
result = await environment.exec(
|
||||
command=command,
|
||||
timeout_sec=600, # 10 minute timeout for complex tasks
|
||||
env=env_vars,
|
||||
)
|
||||
|
||||
print(f"✅ PraisonAI wrapper completed task")
|
||||
|
||||
# Populate Harbor context with results
|
||||
self._populate_context(result, context, instruction)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ PraisonAI wrapper failed: {str(e)}")
|
||||
context.metadata = {"error": str(e)}
|
||||
raise
|
||||
|
||||
def _populate_context(
|
||||
self,
|
||||
result: Any,
|
||||
context: AgentContext,
|
||||
instruction: str
|
||||
) -> None:
|
||||
"""
|
||||
Populate Harbor's AgentContext with execution results.
|
||||
"""
|
||||
try:
|
||||
# Extract output from result
|
||||
stdout = getattr(result, 'stdout', '') or ''
|
||||
stderr = getattr(result, 'stderr', '') or ''
|
||||
return_code = getattr(result, 'return_code', 0)
|
||||
|
||||
# Store result summary
|
||||
context.metadata = {
|
||||
"agent_name": "praisonai-wrapper",
|
||||
"model": self.model_name or "openai/gpt-4o",
|
||||
"framework": "praisonai",
|
||||
"wrapper_type": "cli",
|
||||
"instruction_preview": instruction[:200],
|
||||
"return_code": return_code,
|
||||
"stderr_preview": stderr[:500] if stderr else None,
|
||||
}
|
||||
|
||||
# Try to parse any JSON output for metrics
|
||||
try:
|
||||
# Look for JSON in the output
|
||||
lines = stdout.strip().split('\n')
|
||||
for line in reversed(lines):
|
||||
if line.strip().startswith('{'):
|
||||
metrics = json.loads(line.strip())
|
||||
if isinstance(metrics, dict):
|
||||
context.n_input_tokens = metrics.get('input_tokens')
|
||||
context.n_output_tokens = metrics.get('output_tokens')
|
||||
context.cost_usd = metrics.get('cost_usd')
|
||||
context.metadata['parsed_metrics'] = metrics
|
||||
break
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass # No JSON metrics found
|
||||
|
||||
# Store final response (last non-empty line or full stdout)
|
||||
final_lines = [l for l in stdout.split('\n') if l.strip()]
|
||||
context.metadata['final_response'] = final_lines[-1][:500] if final_lines else stdout[:500]
|
||||
|
||||
except Exception as e:
|
||||
context.metadata = {"context_error": str(e)}
|
||||
|
||||
def _get_environment_vars(self) -> Dict[str, str]:
|
||||
"""Get environment variables to pass to the agent execution."""
|
||||
env_vars = {}
|
||||
|
||||
# Forward API keys
|
||||
api_keys = [
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"GOOGLE_API_KEY",
|
||||
"AZURE_OPENAI_API_KEY",
|
||||
"GROQ_API_KEY",
|
||||
]
|
||||
|
||||
for key in api_keys:
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
env_vars[key] = value
|
||||
|
||||
# Set LOGLEVEL to reduce noise
|
||||
env_vars["LOGLEVEL"] = "WARNING"
|
||||
|
||||
return env_vars
|
||||
|
||||
async def _exec_as_root(self, environment: BaseEnvironment, command: str, env: Optional[Dict] = None) -> Any:
|
||||
"""Execute command as root in the container."""
|
||||
if hasattr(environment, 'exec_as_root'):
|
||||
return await environment.exec_as_root(command=command, env=env or {})
|
||||
else:
|
||||
# Fallback: use sudo
|
||||
return await environment.exec(command=f"sudo {command}", env=env or {})
|
||||
|
||||
async def _exec_as_agent(self, environment: BaseEnvironment, command: str) -> Any:
|
||||
"""Execute command as the agent user in the container."""
|
||||
return await environment.exec(command=command)
|
||||
|
||||
|
||||
# Example usage for testing
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Wrapper Agent for Terminal-Bench 2.0")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print("This agent uses the PraisonAI CLI wrapper approach:")
|
||||
print(' praisonai "TASK" --model MODEL')
|
||||
print()
|
||||
print("Usage with Harbor:")
|
||||
print(" harbor run -d terminal-bench/terminal-bench-2 \\")
|
||||
print(" --agent-import-path examples.terminal_bench.praisonai_wrapper_agent:PraisonAIWrapperAgent \\")
|
||||
print(" --model openai/gpt-4o")
|
||||
print()
|
||||
print("Dependencies:")
|
||||
print(" pip install harbor praisonai")
|
||||
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent Comparison Test - Run 5 tasks with both wrapper and direct agent
|
||||
|
||||
This test compares:
|
||||
1. Direct Agent approach (praisonai_external_agent pattern)
|
||||
2. CLI Wrapper approach (praisonai_wrapper_agent pattern)
|
||||
|
||||
Usage:
|
||||
python test_agent_comparison.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
# Add paths
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'praisonai-agents'))
|
||||
|
||||
# 5 Test Tasks (simple to moderately complex)
|
||||
TEST_TASKS = [
|
||||
{
|
||||
"name": "list_directory",
|
||||
"instruction": "List all files in the current directory and count how many Python files exist",
|
||||
"expected_artifacts": ["file count", ".py count"]
|
||||
},
|
||||
{
|
||||
"name": "create_structure",
|
||||
"instruction": "Create a directory called 'test_workspace', create a file inside it named 'readme.txt' with content 'Hello World', then verify it exists",
|
||||
"expected_artifacts": ["test_workspace/", "readme.txt"]
|
||||
},
|
||||
{
|
||||
"name": "file_operations",
|
||||
"instruction": "Create a file called 'numbers.txt' with numbers 1-10 each on a new line, then display the first 3 lines",
|
||||
"expected_artifacts": ["numbers.txt"]
|
||||
},
|
||||
{
|
||||
"name": "system_info",
|
||||
"instruction": "Show the current working directory, current user, and Python version",
|
||||
"expected_artifacts": ["pwd output", "whoami output"]
|
||||
},
|
||||
{
|
||||
"name": "text_processing",
|
||||
"instruction": "Create a file 'words.txt' with 5 words (apple, banana, cherry, date, elderberry), then count the total characters in the file",
|
||||
"expected_artifacts": ["words.txt", "character count"]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class DirectAgentRunner:
|
||||
"""Runs tasks using direct Agent class approach."""
|
||||
|
||||
def __init__(self):
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import execute_command
|
||||
from praisonaiagents.approval import get_approval_registry, AutoApproveBackend
|
||||
|
||||
self.Agent = Agent
|
||||
self.execute_command = execute_command
|
||||
self.registry = get_approval_registry()
|
||||
self.registry.set_backend(AutoApproveBackend())
|
||||
self.results = []
|
||||
|
||||
def run_task(self, task: dict) -> dict:
|
||||
"""Run a single task and return metrics."""
|
||||
print(f"\n🎯 Direct Agent - Running: {task['name']}")
|
||||
print(f" Instruction: {task['instruction'][:60]}...")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
agent = self.Agent(
|
||||
name=f"direct-{task['name']}",
|
||||
instructions="You are a terminal task agent. Use execute_command tool to complete tasks.",
|
||||
tools=[self.execute_command],
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
result = agent.start(task['instruction'])
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# Get cost if available
|
||||
cost = getattr(agent, 'total_cost', 0) or 0
|
||||
|
||||
print(f" ✅ Completed in {elapsed:.2f}s")
|
||||
print(f" 💰 Cost: ${cost:.6f}")
|
||||
|
||||
return {
|
||||
"task": task['name'],
|
||||
"success": True,
|
||||
"time": elapsed,
|
||||
"cost": cost,
|
||||
"result_preview": str(result)[:100] if result else "No output",
|
||||
"error": None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
elapsed = time.time() - start_time
|
||||
print(f" ❌ Failed in {elapsed:.2f}s: {e}")
|
||||
return {
|
||||
"task": task['name'],
|
||||
"success": False,
|
||||
"time": elapsed,
|
||||
"cost": 0,
|
||||
"result_preview": None,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
class WrapperAgentRunner:
|
||||
"""Runs tasks using CLI wrapper approach via subprocess."""
|
||||
|
||||
def __init__(self):
|
||||
self.results = []
|
||||
|
||||
def run_task(self, task: dict) -> dict:
|
||||
"""Run a single task using praisonai CLI via subprocess."""
|
||||
print(f"\n🎯 Wrapper Agent - Running: {task['name']}")
|
||||
print(f" Instruction: {task['instruction'][:60]}...")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
import shlex
|
||||
|
||||
# Build the praisonai CLI command
|
||||
# Format: praisonai "TASK" --model MODEL
|
||||
cmd = [
|
||||
"praisonai",
|
||||
task['instruction'],
|
||||
"--model", "gpt-4o-mini"
|
||||
]
|
||||
|
||||
# Run the command
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300, # 5 minute timeout
|
||||
env={**os.environ, "LOGLEVEL": "WARNING"}
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f" ✅ Completed in {elapsed:.2f}s")
|
||||
return {
|
||||
"task": task['name'],
|
||||
"success": True,
|
||||
"time": elapsed,
|
||||
"cost": 0, # Cost tracking not available in CLI mode
|
||||
"result_preview": result.stdout[:100] if result.stdout else "No output",
|
||||
"error": None
|
||||
}
|
||||
else:
|
||||
print(f" ❌ Failed with exit code {result.returncode} in {elapsed:.2f}s")
|
||||
return {
|
||||
"task": task['name'],
|
||||
"success": False,
|
||||
"time": elapsed,
|
||||
"cost": 0,
|
||||
"result_preview": None,
|
||||
"error": f"Exit code {result.returncode}: {result.stderr[:200]}"
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
elapsed = time.time() - start_time
|
||||
print(f" ⏱️ Timeout after {elapsed:.2f}s")
|
||||
return {
|
||||
"task": task['name'],
|
||||
"success": False,
|
||||
"time": elapsed,
|
||||
"cost": 0,
|
||||
"result_preview": None,
|
||||
"error": "Timeout after 300s"
|
||||
}
|
||||
|
||||
except FileNotFoundError:
|
||||
elapsed = time.time() - start_time
|
||||
print(f" ⚠️ praisonai CLI not found")
|
||||
return {
|
||||
"task": task['name'],
|
||||
"success": False,
|
||||
"time": elapsed,
|
||||
"cost": 0,
|
||||
"result_preview": None,
|
||||
"error": "praisonai CLI not installed (pip install praisonai)"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
elapsed = time.time() - start_time
|
||||
print(f" ❌ Failed in {elapsed:.2f}s: {e}")
|
||||
return {
|
||||
"task": task['name'],
|
||||
"success": False,
|
||||
"time": elapsed,
|
||||
"cost": 0,
|
||||
"result_preview": None,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def compare_results(direct_results: list, wrapper_results: list):
|
||||
"""Compare results from both approaches."""
|
||||
print("\n" + "=" * 70)
|
||||
print("📊 COMPARISON RESULTS")
|
||||
print("=" * 70)
|
||||
|
||||
# Calculate metrics
|
||||
direct_success = sum(1 for r in direct_results if r['success'])
|
||||
wrapper_success = sum(1 for r in wrapper_results if r['success'])
|
||||
|
||||
direct_total_time = sum(r['time'] for r in direct_results)
|
||||
wrapper_total_time = sum(r['time'] for r in wrapper_results)
|
||||
|
||||
direct_total_cost = sum(r['cost'] for r in direct_results)
|
||||
|
||||
print(f"\n✅ Success Rate:")
|
||||
print(f" Direct Agent: {direct_success}/5 ({direct_success*20}%)")
|
||||
print(f" Wrapper Agent: {wrapper_success}/5 ({wrapper_success*20}%)")
|
||||
|
||||
print(f"\n⏱️ Total Time:")
|
||||
print(f" Direct Agent: {direct_total_time:.2f}s (avg: {direct_total_time/5:.2f}s per task)")
|
||||
print(f" Wrapper Agent: {wrapper_total_time:.2f}s (avg: {wrapper_total_time/5:.2f}s per task)")
|
||||
|
||||
print(f"\n💰 Total Cost (Direct Agent only):")
|
||||
print(f" ${direct_total_cost:.6f} (avg: ${direct_total_cost/5:.6f} per task)")
|
||||
|
||||
print(f"\n📋 Detailed Results:")
|
||||
print(f" {'Task':<20} {'Direct':<12} {'Wrapper':<12} {'Winner':<10}")
|
||||
print(f" {'-'*56}")
|
||||
|
||||
for d, w in zip(direct_results, wrapper_results):
|
||||
d_status = "✅" if d['success'] else "❌"
|
||||
w_status = "✅" if w['success'] else "❌"
|
||||
|
||||
if d['success'] and w['success']:
|
||||
winner = "Direct" if d['time'] < w['time'] else "Wrapper"
|
||||
elif d['success']:
|
||||
winner = "Direct"
|
||||
elif w['success']:
|
||||
winner = "Wrapper"
|
||||
else:
|
||||
winner = "None"
|
||||
|
||||
print(f" {d['task']:<20} {d_status} {d['time']:>6.2f}s {w_status} {w['time']:>6.2f}s {winner}")
|
||||
|
||||
# Overall winner
|
||||
print(f"\n🏆 OVERALL WINNER:")
|
||||
if direct_success > wrapper_success:
|
||||
print(f" 🥇 Direct Agent (higher success rate)")
|
||||
elif wrapper_success > direct_success:
|
||||
print(f" 🥇 Wrapper Agent (higher success rate)")
|
||||
elif direct_success == wrapper_success == 5:
|
||||
if direct_total_time < wrapper_total_time:
|
||||
print(f" 🥇 Direct Agent (faster, same success rate)")
|
||||
else:
|
||||
print(f" 🥇 Wrapper Agent (faster, same success rate)")
|
||||
else:
|
||||
print(f" ⚠️ Mixed results - both had failures")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("🔬 AGENT COMPARISON TEST - 5 Tasks")
|
||||
print("=" * 70)
|
||||
print(f"\nTesting {len(TEST_TASKS)} tasks with both approaches...")
|
||||
print("Model: gpt-4o-mini")
|
||||
print(f"Start time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# Clean up any previous test artifacts
|
||||
print("\n🧹 Cleaning up previous test artifacts...")
|
||||
cleanup_commands = [
|
||||
"rm -rf test_workspace numbers.txt words.txt readme.txt 2>/dev/null; true"
|
||||
]
|
||||
for cmd in cleanup_commands:
|
||||
os.system(cmd)
|
||||
|
||||
# Run with Direct Agent
|
||||
print("\n" + "=" * 70)
|
||||
print("🤖 RUNNING WITH DIRECT AGENT (Agent class)")
|
||||
print("=" * 70)
|
||||
|
||||
direct_runner = DirectAgentRunner()
|
||||
direct_results = []
|
||||
|
||||
for task in TEST_TASKS:
|
||||
result = direct_runner.run_task(task)
|
||||
direct_results.append(result)
|
||||
|
||||
# Clean up between runs
|
||||
print("\n🧹 Cleaning up before wrapper test...")
|
||||
for cmd in cleanup_commands:
|
||||
os.system(cmd)
|
||||
|
||||
# Run with Wrapper Agent
|
||||
print("\n" + "=" * 70)
|
||||
print("📦 RUNNING WITH WRAPPER AGENT (CLI approach)")
|
||||
print("=" * 70)
|
||||
|
||||
wrapper_runner = WrapperAgentRunner()
|
||||
wrapper_results = []
|
||||
|
||||
for task in TEST_TASKS:
|
||||
result = wrapper_runner.run_task(task)
|
||||
wrapper_results.append(result)
|
||||
|
||||
# Compare results
|
||||
compare_results(direct_results, wrapper_results)
|
||||
|
||||
# Final cleanup
|
||||
print("\n🧹 Final cleanup...")
|
||||
for cmd in cleanup_commands:
|
||||
os.system(cmd)
|
||||
|
||||
print(f"\n✅ Comparison complete! End time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
return direct_results, wrapper_results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
direct_results, wrapper_results = main()
|
||||
|
||||
# Exit with error if both failed completely
|
||||
direct_success = sum(1 for r in direct_results if r['success'])
|
||||
wrapper_success = sum(1 for r in wrapper_results if r['success'])
|
||||
|
||||
if direct_success == 0 and wrapper_success == 0:
|
||||
print("\n❌ Both approaches failed completely!")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("\n✅ At least one approach succeeded on some tasks")
|
||||
sys.exit(0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ Test interrupted by user")
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Fatal error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Basic smoke tests for Terminal-Bench Integration
|
||||
|
||||
Tests basic functionality without requiring LLM calls or external dependencies.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the package to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'praisonai-agents'))
|
||||
|
||||
def test_imports():
|
||||
"""Test that all required modules can be imported."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
print("✅ Agent imported successfully")
|
||||
|
||||
from praisonaiagents.tools import execute_command
|
||||
print("✅ execute_command tool imported successfully")
|
||||
|
||||
from praisonaiagents.approval import get_approval_registry, AutoApproveBackend
|
||||
print("✅ Approval system imported successfully")
|
||||
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Import failed: {e}")
|
||||
return False
|
||||
|
||||
def test_agent_creation():
|
||||
"""Test that Agent can be created without issues."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Create agent without calling LLM
|
||||
agent = Agent(
|
||||
name='test-agent',
|
||||
instructions='Test agent for terminal tasks'
|
||||
)
|
||||
|
||||
assert agent.name == 'test-agent'
|
||||
print("✅ Agent creation successful")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Agent creation failed: {e}")
|
||||
return False
|
||||
|
||||
def test_approval_system():
|
||||
"""Test approval system configuration."""
|
||||
try:
|
||||
from praisonaiagents.approval import get_approval_registry, AutoApproveBackend
|
||||
|
||||
registry = get_approval_registry()
|
||||
registry.set_backend(AutoApproveBackend())
|
||||
|
||||
print("✅ Approval system configuration successful")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Approval system test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_external_agent_import():
|
||||
"""Test that our external agent can be imported."""
|
||||
try:
|
||||
# Mock Harbor imports for testing
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
sys.modules['harbor'] = MagicMock()
|
||||
sys.modules['harbor.agents'] = MagicMock()
|
||||
sys.modules['harbor.agents.base'] = MagicMock()
|
||||
sys.modules['harbor.environments'] = MagicMock()
|
||||
sys.modules['harbor.environments.base'] = MagicMock()
|
||||
sys.modules['harbor.models'] = MagicMock()
|
||||
sys.modules['harbor.models.agent'] = MagicMock()
|
||||
sys.modules['harbor.models.agent.context'] = MagicMock()
|
||||
|
||||
# Now try to import our agent
|
||||
import praisonai_external_agent
|
||||
print("✅ External agent import successful")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ External agent import failed: {e}")
|
||||
return False
|
||||
|
||||
def test_multi_agent_import():
|
||||
"""Test that multi-agent example can be imported."""
|
||||
try:
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Mock Harbor for testing
|
||||
sys.modules['harbor'] = MagicMock()
|
||||
sys.modules['harbor.agents'] = MagicMock()
|
||||
sys.modules['harbor.agents.base'] = MagicMock()
|
||||
sys.modules['harbor.environments'] = MagicMock()
|
||||
sys.modules['harbor.environments.base'] = MagicMock()
|
||||
sys.modules['harbor.models'] = MagicMock()
|
||||
sys.modules['harbor.models.agent'] = MagicMock()
|
||||
sys.modules['harbor.models.agent.context'] = MagicMock()
|
||||
|
||||
import multi_agent_example
|
||||
print("✅ Multi-agent example import successful")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Multi-agent example import failed: {e}")
|
||||
return False
|
||||
|
||||
def test_wrapper_agent_import():
|
||||
"""Test that wrapper agent can be imported."""
|
||||
try:
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Mock Harbor for testing
|
||||
sys.modules['harbor'] = MagicMock()
|
||||
sys.modules['harbor.agents'] = MagicMock()
|
||||
sys.modules['harbor.agents.base'] = MagicMock()
|
||||
sys.modules['harbor.environments'] = MagicMock()
|
||||
sys.modules['harbor.environments.base'] = MagicMock()
|
||||
sys.modules['harbor.models'] = MagicMock()
|
||||
sys.modules['harbor.models.agent'] = MagicMock()
|
||||
sys.modules['harbor.models.agent.context'] = MagicMock()
|
||||
|
||||
import praisonai_wrapper_agent
|
||||
print("✅ Wrapper agent import successful (CLI-based approach)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Wrapper agent import failed: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Terminal-Bench Integration - Basic Tests")
|
||||
print("=" * 55)
|
||||
|
||||
tests = [
|
||||
("Basic Imports", test_imports),
|
||||
("Agent Creation", test_agent_creation),
|
||||
("Approval System", test_approval_system),
|
||||
("External Agent Import", test_external_agent_import),
|
||||
("Multi-Agent Import", test_multi_agent_import),
|
||||
("Wrapper Agent Import", test_wrapper_agent_import),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
for test_name, test_func in tests:
|
||||
print(f"\n🧪 Testing {test_name}...")
|
||||
try:
|
||||
if test_func():
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f"❌ Test {test_name} crashed: {e}")
|
||||
|
||||
print("\n" + "=" * 55)
|
||||
print(f"Results: {passed}/{total} tests passed")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 ALL BASIC TESTS PASSED!")
|
||||
print("The Terminal-Bench integration components are properly structured.")
|
||||
else:
|
||||
print("❌ Some tests failed - check output above")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Tests for PraisonAI Terminal-Bench 2.0 Integration
|
||||
|
||||
This module contains both unit tests and integration tests for the
|
||||
PraisonAI Harbor integration.
|
||||
|
||||
Run tests with:
|
||||
python -m pytest examples/terminal_bench/test_integration.py -v
|
||||
|
||||
Requirements:
|
||||
pip install pytest praisonaiagents
|
||||
# Harbor is optional for unit tests, required for integration tests
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestPraisonAIExternalAgent:
|
||||
"""Test the external agent implementation."""
|
||||
|
||||
def test_agent_metadata(self):
|
||||
"""Test agent name and version reporting."""
|
||||
try:
|
||||
from .praisonai_external_agent import PraisonAIExternalAgent
|
||||
except ImportError:
|
||||
pytest.skip("Harbor not installed - skipping Harbor-specific tests")
|
||||
|
||||
agent = PraisonAIExternalAgent()
|
||||
assert agent.name() == "praisonai"
|
||||
|
||||
# Version should be None if praisonaiagents not installed, or actual version
|
||||
version = agent.version()
|
||||
assert version is None or isinstance(version, str)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup(self):
|
||||
"""Test agent setup phase."""
|
||||
try:
|
||||
from .praisonai_external_agent import PraisonAIExternalAgent
|
||||
except ImportError:
|
||||
pytest.skip("Harbor not installed")
|
||||
|
||||
agent = PraisonAIExternalAgent()
|
||||
mock_env = Mock()
|
||||
|
||||
# Setup should complete without error (external agent needs no setup)
|
||||
await agent.setup(mock_env)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_tool_execution(self):
|
||||
"""Test the bash tool wrapper around Harbor's exec()."""
|
||||
try:
|
||||
from .praisonai_external_agent import PraisonAIExternalAgent
|
||||
except ImportError:
|
||||
pytest.skip("Harbor not installed")
|
||||
|
||||
# Mock Harbor environment
|
||||
mock_env = Mock()
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "Hello, World!"
|
||||
mock_result.stderr = ""
|
||||
mock_result.return_code = 0
|
||||
mock_env.exec = AsyncMock(return_value=mock_result)
|
||||
|
||||
# Mock agent context
|
||||
mock_context = Mock()
|
||||
mock_context.metadata = {}
|
||||
|
||||
agent = PraisonAIExternalAgent()
|
||||
|
||||
# Mock PraisonAI Agent to avoid LLM calls in tests
|
||||
with patch('praisonaiagents.Agent') as mock_agent_class:
|
||||
mock_agent_instance = Mock()
|
||||
mock_agent_instance.start.return_value = "Task completed successfully"
|
||||
mock_agent_instance.name = "terminal-agent"
|
||||
mock_agent_class.return_value = mock_agent_instance
|
||||
|
||||
# Mock approval backend
|
||||
with patch('praisonaiagents.approval.set_approval_backend'):
|
||||
await agent.run("echo 'Hello, World!'", mock_env, mock_context)
|
||||
|
||||
# Verify agent was created with correct parameters
|
||||
mock_agent_class.assert_called_once()
|
||||
args, kwargs = mock_agent_class.call_args
|
||||
|
||||
assert kwargs['name'] == 'terminal-agent'
|
||||
assert 'tools' in kwargs
|
||||
assert len(kwargs['tools']) == 1 # bash_tool
|
||||
assert kwargs['verbose'] is False
|
||||
assert kwargs['memory'] is False
|
||||
|
||||
def test_context_population(self):
|
||||
"""Test that agent context is properly populated."""
|
||||
try:
|
||||
from .praisonai_external_agent import PraisonAIExternalAgent
|
||||
except ImportError:
|
||||
pytest.skip("Harbor not installed")
|
||||
|
||||
agent_impl = PraisonAIExternalAgent()
|
||||
|
||||
# Mock agent with usage data
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "test-agent"
|
||||
mock_agent.llm = "gpt-4o"
|
||||
mock_agent._usage = Mock()
|
||||
mock_agent._usage.input_tokens = 100
|
||||
mock_agent._usage.output_tokens = 50
|
||||
mock_agent._cost = 0.01
|
||||
|
||||
mock_context = Mock()
|
||||
mock_context.metadata = {}
|
||||
|
||||
# Test context population
|
||||
agent_impl._populate_context(mock_agent, mock_context, "Test result")
|
||||
|
||||
assert mock_context.n_input_tokens == 100
|
||||
assert mock_context.n_output_tokens == 50
|
||||
assert mock_context.cost_usd == 0.01
|
||||
assert mock_context.metadata['agent_name'] == 'test-agent'
|
||||
assert mock_context.metadata['model'] == 'gpt-4o'
|
||||
assert mock_context.metadata['framework'] == 'praisonai'
|
||||
|
||||
|
||||
class TestPraisonAIInstalledAgent:
|
||||
"""Test the installed agent implementation."""
|
||||
|
||||
def test_agent_configuration(self):
|
||||
"""Test agent CLI flags and configuration."""
|
||||
try:
|
||||
from .praisonai_installed_agent import PraisonAIInstalledAgent
|
||||
except ImportError:
|
||||
pytest.skip("Harbor not installed")
|
||||
|
||||
agent = PraisonAIInstalledAgent()
|
||||
assert agent.name() == "praisonai"
|
||||
assert agent.SUPPORTS_ATIF is False # Until trajectory format is implemented
|
||||
|
||||
# Check CLI flags
|
||||
flag_names = [flag.name if hasattr(flag, 'name') else str(flag) for flag in agent.CLI_FLAGS]
|
||||
expected_flags = ['max_turns', 'verbose', 'memory', 'auto_approval']
|
||||
for expected in expected_flags:
|
||||
assert any(expected in flag_name for flag_name in flag_names)
|
||||
|
||||
def test_version_command(self):
|
||||
"""Test version detection command."""
|
||||
try:
|
||||
from .praisonai_installed_agent import PraisonAIInstalledAgent
|
||||
except ImportError:
|
||||
pytest.skip("Harbor not installed")
|
||||
|
||||
agent = PraisonAIInstalledAgent()
|
||||
version_cmd = agent.get_version_command()
|
||||
|
||||
assert version_cmd is not None
|
||||
assert "praisonaiagents" in version_cmd
|
||||
assert "__version__" in version_cmd
|
||||
|
||||
def test_runner_script_generation(self):
|
||||
"""Test that the headless runner script is properly generated."""
|
||||
try:
|
||||
from .praisonai_installed_agent import PraisonAIInstalledAgent
|
||||
except ImportError:
|
||||
pytest.skip("Harbor not installed")
|
||||
|
||||
agent = PraisonAIInstalledAgent()
|
||||
script = agent._build_runner_script()
|
||||
|
||||
assert "import praisonaiagents" in script
|
||||
assert "Agent" in script
|
||||
assert "execute_command" in script
|
||||
assert "AutoApproveBackend" in script
|
||||
assert "json.dumps" in script
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests that require both PraisonAI and Harbor."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_praisonai_agent_real(self):
|
||||
"""
|
||||
Real agentic test - agent must call LLM end-to-end.
|
||||
|
||||
This is a MANDATORY test per AGENTS.md §9.4.
|
||||
Agent MUST call agent.start() with real prompt and produce LLM output.
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
except ImportError:
|
||||
pytest.skip("PraisonAI not installed")
|
||||
|
||||
# Create real agent that will call LLM
|
||||
agent = Agent(
|
||||
name="test-terminal-agent",
|
||||
instructions="You are a helpful terminal assistant"
|
||||
)
|
||||
|
||||
# Real agentic test - agent must call LLM and produce text response
|
||||
result = agent.start("Say hello in one sentence and mention you can help with terminal tasks")
|
||||
|
||||
# Verify we got actual LLM output
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
# Print output for manual verification
|
||||
print("✅ Real agentic test result:")
|
||||
print(result)
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_tool_real_execution(self):
|
||||
"""Test bash tool with real command execution (if safe)."""
|
||||
# This would test real bash execution in a safe environment
|
||||
# For now, we'll mock it to avoid system changes
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "PraisonAI Terminal Test\n"
|
||||
mock_result.stderr = ""
|
||||
mock_result.return_code = 0
|
||||
|
||||
# Test that our bash tool wrapper works correctly
|
||||
async def mock_exec(command, timeout_sec=30):
|
||||
assert "echo" in command # Ensure we're testing echo command
|
||||
return mock_result
|
||||
|
||||
# This simulates Harbor's BaseEnvironment.exec()
|
||||
result = await mock_exec("echo 'PraisonAI Terminal Test'")
|
||||
|
||||
assert result.stdout.strip() == "PraisonAI Terminal Test"
|
||||
assert result.return_code == 0
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_auto_approval_setup(self):
|
||||
"""Test that auto-approval backend works correctly."""
|
||||
try:
|
||||
from praisonaiagents.approval import set_approval_backend, AutoApproveBackend
|
||||
except ImportError:
|
||||
pytest.skip("PraisonAI approval system not available")
|
||||
|
||||
# Test setting and restoring approval backend
|
||||
original = set_approval_backend(AutoApproveBackend())
|
||||
new_backend = set_approval_backend(original)
|
||||
|
||||
assert isinstance(new_backend, AutoApproveBackend)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Allow running tests directly
|
||||
import sys
|
||||
|
||||
print("PraisonAI Terminal-Bench 2.0 Integration Tests")
|
||||
print("=" * 50)
|
||||
|
||||
# Check dependencies
|
||||
try:
|
||||
import praisonaiagents
|
||||
print(f"✅ PraisonAI version: {praisonaiagents.__version__}")
|
||||
except ImportError:
|
||||
print("❌ PraisonAI not installed: pip install praisonaiagents")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import harbor
|
||||
print("✅ Harbor framework available")
|
||||
except ImportError:
|
||||
print("⚠️ Harbor not installed: pip install harbor")
|
||||
print(" (Some tests will be skipped)")
|
||||
|
||||
print()
|
||||
print("Run tests with: python -m pytest examples/terminal_bench/test_integration.py -v")
|
||||
print("Run real agentic test: python -m pytest examples/terminal_bench/test_integration.py::TestIntegration::test_praisonai_agent_real -v -s")
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Real Agentic Test for Terminal-Bench Integration
|
||||
|
||||
This is a MANDATORY test per AGENTS.md §9.4.
|
||||
The agent MUST call agent.start() with a real prompt and produce LLM output.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the package to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'praisonai-agents'))
|
||||
|
||||
def test_real_agentic():
|
||||
"""Real agentic test - agent must call LLM end-to-end."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
except ImportError as e:
|
||||
print(f"❌ Cannot import praisonaiagents: {e}")
|
||||
print("Make sure you're running from the correct directory")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Create agent that will call LLM
|
||||
agent = Agent(
|
||||
name='terminal-bench-test-agent',
|
||||
instructions='You are a helpful assistant that can help with terminal and coding tasks'
|
||||
)
|
||||
|
||||
# MUST call agent.start() with real prompt - this calls the LLM
|
||||
print("🚀 Starting real agentic test (calling LLM)...")
|
||||
result = agent.start('Say hello and briefly mention you can help with terminal tasks. Keep it to one sentence.')
|
||||
|
||||
# Verify we got actual LLM output
|
||||
assert result is not None, "Agent returned None"
|
||||
assert isinstance(result, str), f"Agent returned {type(result)}, expected str"
|
||||
assert len(result) > 0, "Agent returned empty string"
|
||||
|
||||
print("✅ Real agentic test PASSED!")
|
||||
print(f"📊 Result length: {len(result)} characters")
|
||||
print(f"🤖 Agent response: {result}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Real agentic test FAILED: {e}")
|
||||
return False
|
||||
|
||||
def test_shell_tools():
|
||||
"""Test that shell tools are available and importable."""
|
||||
try:
|
||||
from praisonaiagents.tools import execute_command
|
||||
print("✅ Shell tools imported successfully")
|
||||
|
||||
# Test tool metadata
|
||||
print(f"📋 execute_command available: {callable(execute_command)}")
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Cannot import shell tools: {e}")
|
||||
return False
|
||||
|
||||
def test_approval_system():
|
||||
"""Test that approval system works for auto-approval."""
|
||||
try:
|
||||
from praisonaiagents.approval import get_approval_registry, AutoApproveBackend
|
||||
|
||||
# Test setting auto-approval backend
|
||||
registry = get_approval_registry()
|
||||
registry.set_backend(AutoApproveBackend())
|
||||
|
||||
print("✅ Approval system works correctly")
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Cannot import approval system: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Terminal-Bench Integration - Real Agentic Test")
|
||||
print("=" * 60)
|
||||
|
||||
all_passed = True
|
||||
|
||||
# Test 1: Shell tools
|
||||
print("\n🧪 Test 1: Shell Tools Import")
|
||||
if not test_shell_tools():
|
||||
all_passed = False
|
||||
|
||||
# Test 2: Approval system
|
||||
print("\n🧪 Test 2: Approval System")
|
||||
if not test_approval_system():
|
||||
all_passed = False
|
||||
|
||||
# Test 3: Real agentic test (MANDATORY)
|
||||
print("\n🧪 Test 3: Real Agentic Test (MANDATORY)")
|
||||
if not test_real_agentic():
|
||||
all_passed = False
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if all_passed:
|
||||
print("🎉 ALL TESTS PASSED - Terminal-Bench integration is ready!")
|
||||
else:
|
||||
print("❌ Some tests failed - check output above")
|
||||
sys.exit(1)
|
||||
Loaded 100 of 427 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user