Compare commits

...
Author SHA1 Message Date
MervinPraison 81dfb6a555 Release v4.6.3 2026-04-14 06:19:12 +01:00
MervinPraison bc52cd67fd docs: auto-update api.md [skip ci] 2026-04-14 05:19:09 +00:00
MervinPraison d4b563987f feat(agent, task): add concurrency limiter and message sink protocols
Implemented a concurrency registry to control concurrent task execution
for agents, allowing users to set limits per agent name. Also added message
sink protocols for managing sequenced task execution messages, enabling
replay of agent outputs and tool calls.

New files include:
- agent/concurrency.py
- task/message_sink.py

Updated the task module to include the new protocols in exports.

Total changes across 13 files with 1099 insertions.
2026-04-14 06:18:36 +01:00
MervinPraison 4c68e61438 docs: auto-update feature parity trackers [skip ci] 2026-04-13 16:08:27 +00:00
MervinPraison e42a75d5d7 Release v4.6.2 2026-04-13 17:07:52 +01:00
MervinPraison 6693a7552e docs: auto-update api.md [skip ci] 2026-04-12 04:16:27 +00:00
MervinPraison 135412c338 Release v4.6.1 2026-04-12 05:16:03 +01:00
MervinPraison 0c37498118 feat(managed-agents): add examples for agent management features
Added multiple examples demonstrating the creation and management of agents,
environments, sessions, and custom tools in the managed agents architecture.
This comprehensive set of examples now includes functionalities such as
tool selection, disabling tools, and updating agent configurations, providing
users with clear guidance on utilizing the framework effectively.
2026-04-11 13:20:11 +01:00
MervinPraison be72b83521 docs: auto-update api.md [skip ci] 2026-04-11 07:58:30 +00:00
MervinPraison b868e20f61 Release v4.5.149 2026-04-11 08:58:10 +01:00
MervinPraison 83a1e95f20 docs: auto-update feature parity trackers [skip ci] 2026-04-11 07:23:10 +00:00
MervinPraison 33cc2a9614 Release v4.5.148 2026-04-11 08:22:46 +01:00
MervinPraison d4a8b4766d docs: auto-update feature parity trackers [skip ci] 2026-04-11 05:17:26 +00:00
MervinPraison 7a5635ff3d Release v4.5.147 2026-04-11 06:16:52 +01:00
MervinPraison 5f7f155ea7 Release v4.5.146 2026-04-11 06:03:55 +01:00
MervinPraison ab62317ea9 docs: auto-update feature parity trackers [skip ci] 2026-04-11 04:49:51 +00:00
Mervin Praison 1eafb07846 Merge pull request #1359 from MervinPraison/claude/issue-1358-20260410-1627
feat: add External Managed Agent Backends support
2026-04-11 05:49:27 +01:00
praisonai-triage-agent[bot]andMervin Praison 40e40174db feat: implement backend delegation in Agent execution paths
- Add backend delegation logic to run(), start(), and chat() methods
- Delegate to external managed agent backend when available
- Fallback to local execution when no backend is configured
- Handle both streaming and non-streaming execution modes
- Add comprehensive test coverage for delegation functionality
- Fix remaining P1 architectural issue from PR reviews

This completes the hybrid execution feature for external managed agent backends.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-10 21:53:14 +00:00
Mervin Praison 0156893f56 Merge pull request #1357 from MervinPraison/claude/issue-1356-20260410-1554
feat: Add --metrics-json flag for structured CLI cost output
2026-04-10 22:47:16 +01:00
MervinPraison 6ea089db11 security: add persist-credentials: false to all actions/checkout steps
Fixes ArtiPACKED vulnerability (GHSA-3959-6v5q-45q2) where GitHub tokens
were being persisted in .git/config and could leak through artifacts.

- Added persist-credentials: false to 25+ workflow files
- Fixed duplicate with blocks in claude.yml and build-image.yml
- Fixed .github/actions/claude-issue-triage-action/action.yml
2026-04-10 22:39:59 +01:00
praisonai-triage-agent[bot]andMervin Praison a2edba05c8 fix: resolve critical issues in --metrics-json implementation
Addresses three critical issues identified in code review:

1. CRITICAL: Fix exception path to always emit JSON to stdout
   - Previously only printed warning to stderr on exception
   - Now emits fallback JSON to maintain tooling reliability

2. HIGH: Capture final_agent metrics when --final-agent used
   - Previously only captured original agent metrics
   - Now intelligently selects final_agent when available

3. MEDIUM: Move json import to module top per PEP 8
   - Moved import json from inline to module level
   - Removed redundant inline import sys

Fixes ensure --metrics-json flag maintains reliability contract
for downstream tooling like Terminal-Bench.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-10 21:37:41 +00:00
praisonai-triage-agent[bot]andClaude fb6d8ed427 fix: resolve critical runtime issues in managed agent integration
- Add missing asyncio import to prevent NameError
- Fix asyncio.wait_for with async generator using deadline-based timeout
- Add exception chaining to preserve original traceback context
- Ensures stream() method now works without TypeError

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-04-10 21:27:45 +00:00
copilot-swe-agent[bot]andMervinPraison 635ab502a1 fix: address Gemini review issues in managed_agents.py
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/a7e69a3b-9573-454e-b592-791b5a4be16f

Co-authored-by: MervinPraison <[email protected]>
2026-04-10 17:20:04 +00:00
praisonai-triage-agent[bot]andMervin Praison 56c4f7d626 fix: resolve critical issues in managed agent backend integration
- Fix authentication header: use x-api-key instead of Authorization Bearer
- Fix session cache bug: store session_id instead of session_key
- Fix SSE parsing: use readline() for proper line-by-line processing
- Add timeout enforcement to async operations
- Persist API key when loaded from environment variables
- Make AnthropicManagedProvider inherit from ManagedBackendProtocol
- Add regression tests for session caching and API key persistence

Addresses P0 issues identified by reviewers (Greptile, CodeRabbit, Copilot, Gemini)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Mervin Praison <[email protected]>
2026-04-10 17:19:34 +00:00
praisonai-triage-agent[bot]andMervinPraison dfcb1c8392 feat: add External Managed Agent Backends support
- Implement ManagedAgentIntegration class in praisonai/integrations/
- Add support for Anthropic Managed Agents API
- Add backend parameter to Agent class for hybrid execution
- Zero new dependencies (uses existing aiohttp)
- Follow existing BaseCLIIntegration pattern
- Include comprehensive tests

Fixes #1358

Co-authored-by: MervinPraison <[email protected]>
2026-04-10 16:35:19 +00:00
copilot-swe-agent[bot]andMervinPraison fc75ed8671 fix: log metrics extraction errors to stderr and fix spelling
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/a2c02b62-61e2-4573-b757-860fece00f3d

Co-authored-by: MervinPraison <[email protected]>
2026-04-10 16:13:55 +00:00
copilot-swe-agent[bot]andMervinPraison 86acdf59b7 fix: correct token key names, add error handling, and add unit tests for --metrics-json
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/a2c02b62-61e2-4573-b757-860fece00f3d

Co-authored-by: MervinPraison <[email protected]>
2026-04-10 16:12:57 +00:00
praisonai-triage-agent[bot]andMervinPraison 70085486a3 feat: Add --metrics-json flag for structured CLI cost output
- Add --metrics-json command-line flag to enable JSON cost output
- Output structured metrics including cost_usd, tokens_in, tokens_out, model, request_count
- Enables programmatic cost tracking for Terminal-Bench and other tools
- Uses existing MetricsHandler for data extraction
- Fixes CLI cost tracking gap identified in issue #1356

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: MervinPraison <[email protected]>
2026-04-10 16:00:28 +00:00
MervinPraison e788d6f355 Release v4.5.145 2026-04-10 16:54:10 +01:00
MervinPraison b93f6489cc Release v4.5.144 2026-04-10 16:46:31 +01:00
MervinPraison 4a838852b1 feat(terminal_bench): add wrapper agent and improve documentation
Added `praisonai_wrapper_agent.py` for a CLI-based agent approach.
Updated `README.md` to include integration details for both external
and wrapper agents, and added examples for usage. Enhanced `job.yaml`
with configuration options for the wrapper agent. Minor updates
to existing files to improve clarity and maintainability.
2026-04-10 15:58:29 +01:00
MervinPraison 40fde37e1e Release v4.5.143 2026-04-10 14:40:32 +01:00
MervinPraison 462b7eb809 fix(cli): update git commit user information handling
Added support for custom git author information from environment variables
in the CLI. Updated version to 1.5.143 in project files. Adjusted
git commit commands to include the author if provided.
2026-04-10 14:40:00 +01:00
PraisonAI f25c8c2e88 feat(docs): update README with Langfuse tracing instructions
Added tracing instructions for Langfuse and updated the default user
name and email settings in CheckpointConfig and FileSnapshot to fetch
from environment variables. Bumped version to 1.5.142.
2026-04-10 13:49:22 +01:00
PraisonAI 2ab04d2160 feat(README, checkpoint): add Langfuse tracing and user configs
- Added Langfuse tracing documentation to README.
- Introduced user_name and user_email parameters in CheckpointService and FileSnapshot classes for custom Git configuration.
- Updated relevant methods to utilize new parameters for better user identification in commits.
- Added a new image for Langfuse tracing under .github/images.
2026-04-10 13:39:19 +01:00
PraisonAI 7e0536ab63 Release v4.5.140 2026-04-10 13:03:59 +01:00
MervinPraison 184194ea7a docs: auto-update api.md [skip ci] 2026-04-10 11:47:38 +00:00
PraisonAI 0f689671f6 refactor: harden input validation and execution security 2026-04-10 12:46:56 +01:00
PraisonAI 0af82c8cfc Update security audit workflow 2026-04-10 12:18:46 +01:00
PraisonAI 17dce2cbe0 update 2026-04-10 12:18:46 +01:00
MervinPraison d4c4872478 docs: auto-update feature parity trackers [skip ci] 2026-04-10 11:13:09 +00:00
Mervin Praison a67ad5fe2e Merge pull request #1354 from MervinPraison/claude/issue-1353-20260410-0933
feat: address top 3 architectural gaps - async safety, guardrails, and memory pipeline
2026-04-10 12:12:52 +01:00
praisonai-triage-agent[bot]andMervin Praison 13811df460 fix: resolve P0 critical bugs identified by code reviewers
- Add __enter__/__exit__ context manager support to AsyncSafeState for backward compatibility
- Add chat_history property setter to prevent AttributeError on assignments
- Fix validate_output to call _llm_validate instead of non-existent validate method
- Update all _memory references to _memory_instance in AsyncMemoryMixin
- Replace deprecated asyncio.get_event_loop() with get_running_loop()

Fixes critical runtime errors that would break core Agent functionality.
All changes maintain backward compatibility and follow AGENTS.md principles.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-10 11:11:36 +00:00
praisonai-triage-agent[bot]andMervin Praison 70116adba3 fix: resolve critical P0 architectural issues
- Add __enter__/__exit__ context manager support to AsyncSafeState for backward compatibility
- Add chat_history property setter to prevent AttributeError on assignments
- Fix memory attribute references throughout AsyncMemoryMixin (_memory → _memory_instance)
- Fix LLMGuardrail.validate_output to use _llm_validate instead of non-existent validate method
- Move GuardrailChain implementation out of protocols.py for cleaner separation
- Update deprecated asyncio.get_event_loop() to asyncio.get_running_loop()

All changes maintain backward compatibility and align with AGENTS.md principles.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-10 11:08:10 +00:00
PraisonAI e7feea03f2 fix: CodeRabbit trigger typo - 'summarize' not 'summary' 2026-04-10 11:57:32 +01:00
MervinPraison f3653b9759 docs: auto-update api.md [skip ci] 2026-04-10 10:45:55 +00:00
Mervin Praison 64d7b96a99 Merge pull request #1355 from MervinPraison/praisonai/issue-1350
Praisonai/issue 1350
2026-04-10 11:45:37 +01:00
PraisonAI 2cdc08e61d feat(workflows): update install.sh sync process and enhance security audit docs
- Changed repository reference for install.sh from praisonai-package to MervinPraison/PraisonAI.
- Improved sync mechanism to only copy files if there are changes, ensuring the latest version is served.
- Added references to official resources and documentation in the security audit workflow.
- Included guidelines for creating security advisories and reporting vulnerabilities.
2026-04-10 11:36:58 +01:00
Mervin Praison 0e9a67e3ee Merge pull request #1352 from MervinPraison/claude/issue-1351-20260410-0632
feat: integrate PraisonAI with Terminal-Bench 2.0 via Harbor framework
2026-04-10 11:21:17 +01:00
praisonai-triage-agent[bot]andMervin Praison e9444030ab fix: resolve critical Terminal-Bench integration issues
- Fix missing os/Dict imports causing NameError in installed agent
- Replace sync agent.start() with async agent.astart() to prevent event loop blocking
- Fix broken approval API from set_approval_backend to get_approval_registry().set_backend()
- Use correct agent metrics (cost_summary, _total_tokens_*) instead of non-existent _usage/_cost
- Add agent-scoped approval backends with proper cleanup to prevent global state pollution
- Implement populate_context_post_run JSON parsing for Harbor metrics integration
- Fix markdown lint issue by adding language tag to fenced code block

Addresses P0/P1 issues identified by CodeRabbit, Greptile, Copilot reviewers.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-10 10:14:33 +00:00
PraisonAI 961f5046c9 Release v4.5.139 2026-04-10 10:47:30 +01:00
praisonai-triage-agent[bot]andMervinPraison 0c663e6b7c feat: address top 3 architectural gaps - async safety, guardrails, and memory pipeline
- Gap 1: Add AsyncSafeState dual-lock abstraction for async/sync agent state protection
- Gap 2: Add GuardrailProtocol with fail-closed error handling for production safety
- Gap 3: Add AsyncMemoryMixin for async-safe memory operations without event loop blocking

Fixes core async/concurrency issues, improves guardrails safety model, and enables
proper async memory operations as outlined in issue analysis.

Co-authored-by: MervinPraison <[email protected]>
2026-04-10 09:40:43 +00:00
PraisonAI 00c6f77b28 Release v4.5.137 2026-04-10 08:02:13 +01:00
praisonai-triage-agent[bot]andMervinPraison 1248e7791c feat: integrate PraisonAI with Terminal-Bench 2.0 via Harbor framework
Add comprehensive integration between PraisonAI Agents and Terminal-Bench 2.0
(the gold standard for AI coding agent evaluation) via Harbor framework.

Features:
- External agent (proof-of-concept) and installed agent (production)
- Shell execution bridge: Harbor BaseEnvironment.exec() → PraisonAI tools
- Auto-approval for container-isolated execution (bypasses @require_approval)
- Token tracking and cost metrics integration with Harbor's AgentContext
- Multi-agent support: AgentTeam and AgentFlow workflow examples
- Cloud execution ready: Daytona, E2B, Modal, Runloop, GKE support

Files added:
- examples/terminal_bench/README.md - Complete documentation and usage
- examples/terminal_bench/praisonai_external_agent.py - Phase 1 implementation
- examples/terminal_bench/praisonai_installed_agent.py - Phase 2 production
- examples/terminal_bench/multi_agent_example.py - Multi-agent workflows
- examples/terminal_bench/job.yaml - Harbor job configuration
- examples/terminal_bench/test_*.py - Comprehensive test suite

Architecture:
- Follows AGENTS.md guidelines: examples/ placement, no Core SDK changes
- Uses existing execute_command tool, no functionality duplication
- Optional Harbor dependency, clean separation of concerns
- Enables PraisonAI to appear on Terminal-Bench 2.0 leaderboard

Closes #1351

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: MervinPraison <[email protected]>
2026-04-10 06:40:52 +00:00
PraisonAI 3414e55b1b Release v4.5.136 2026-04-09 19:37:40 +01:00
PraisonAI 6e363e470a feat: add debug logging after successful branch checkout in github_create_branch 2026-04-09 18:28:41 +01:00
PraisonAI dd0e02c056 PraisonAI Automated Fix 2026-04-09 18:26:51 +01:00
MervinPraison 365f75040f PraisonAI Automated Fix 2026-04-09 16:26:04 +01:00
MervinPraison 88f364c34d Release v4.5.134 2026-04-09 16:22:31 +01:00
MervinPraison 4ac0d883cf Release v4.5.133 2026-04-09 15:57:42 +01:00
MervinPraison 78af42457c feat(github): emulate claude code native capabilities via planner, native tools, and uv pipeline 2026-04-09 15:56:10 +01:00
MervinPraison 0accebb2e3 refactor: harden sqlite identifier validation 2026-04-09 15:48:35 +01:00
MervinPraison 757559f67f fix(cli/github): refine tracker logs and include task context headers for live UX consistency 2026-04-09 14:44:17 +01:00
MervinPraison 43a5d097dd feat(cli/github): migrate to TraceSinkProtocol for native tracking observability 2026-04-09 14:40:17 +01:00
MervinPraison 18b4914de2 fix(ci): strictly bind execute_command argument signatures to avoid LLM schema confusion 2026-04-09 14:33:58 +01:00
MervinPraison 4675577a3f Release v4.5.132 2026-04-09 14:30:34 +01:00
MervinPraison 426ecfc90a fix(ci): patch agent prompt to prevent cwd hallucination with execute_command 2026-04-09 14:29:40 +01:00
MervinPraison c9b8685d27 ci: temporarily install from source to test new github triage thread debouncing 2026-04-09 14:25:11 +01:00
MervinPraison 7ea4b9ad13 ci: add PRAISONAI_AUTO_APPROVE to bypass interactive prompt in github action 2026-04-09 14:21:06 +01:00
MervinPraison defa325754 feat(release): improve version release process
Enhance `bump_and_release.py` and `release.py` scripts to add specific
release files instead of all changes. Introduced a check for
uncommitted changes with informative error messages. This reduces the
risk of accidental changes during the release process.
2026-04-09 14:13:54 +01:00
MervinPraison 64d048c897 ci: revert PyPI bypass hack. Testing off v4.5.131 PyPI release 2026-04-09 14:08:49 +01:00
MervinPraison 2806f47093 Release v4.5.131 2026-04-09 14:06:29 +01:00
MervinPraison 8db093245e fix(cli): use native sys.argv passthrough for internal workflow execution instead of hallucinated module 2026-04-09 13:23:22 +01:00
MervinPraison 78523f8911 fix(cli): remove unsupported is_async kwarg from EventBus subscribe 2026-04-09 13:19:59 +01:00
MervinPraison 51b8761303 ci: install dependencies locally to prevent PyPI publish lag on feature test 2026-04-09 13:17:25 +01:00
MervinPraison c8144d6532 fix(cli): resolve broken import for Github Tracker Sink 2026-04-09 13:14:59 +01:00
MervinPraison 9327462a89 fix(cli): register core github route to bypass LLM generation 2026-04-09 13:11:58 +01:00
MervinPraison 24f1945c70 docs: auto-update api.md [skip ci] 2026-04-09 12:08:10 +00:00
MervinPraison 9ac9dc5aed Release v4.5.130 2026-04-09 13:07:52 +01:00
MervinPraison fde17acdc8 fix(github-actions): update issue triage workflow for clarity
Refactor instructions for issue analysts and software engineers to
enhance clarity. Details include requiring issue number discovery via
local environment and simplifying commit and PR messaging to remove
explicit issue number references.
2026-04-09 12:52:32 +01:00
MervinPraison 5ed5f1a6a9 Auto: Revert trigger and add PraisonAI Native Issue Triage 2026-04-09 12:39:13 +01:00
MervinPraison cc1853f0dc Release v4.5.129 2026-04-09 12:31:56 +01:00
MervinPraison 147aa701dd refactor(yaml): update memory configuration in agents_workflow.yaml
Changed 'memory_config' to 'memory' and updated key to 'backend' for clarity.
2026-04-09 12:16:58 +01:00
MervinPraison 6496455c45 fix(testing): resolve gc shutdown test tracebacks and workflow typos 2026-04-09 12:03:15 +01:00
Mervin Praison 239660715a Merge pull request #1333 from MervinPraison/claude/issue-1329-20260409-0920
feat: integrate PraisonAI PR Reviewer in CI/CD pipeline
2026-04-09 11:43:12 +01:00
MervinPraison 8907022b3a fix(review-chain): resolve merge conflicts on review-chain.md 2026-04-09 11:42:58 +01:00
MervinPraison bb0d1968ad docs: auto-update api.md [skip ci] 2026-04-09 10:42:06 +00:00
Mervin Praison 2e7ffe7338 Merge pull request #1335 from MervinPraison/claude/issue-1330-20260409-0928
fix: resolve three critical architectural gaps in error handling and resource management
2026-04-09 11:41:47 +01:00
MervinPraison a2b44dc100 fix(reviewer): install praisonai instead of praisonaiagents for CLI 2026-04-09 11:40:58 +01:00
MervinPraison ca4ee2e42b fix(reviewer): resolve critical PR functionality and security issues
- Convert invalid yaml dict structure to list in steps configuration
- Fix hallucinated run_shell_command to execute_command
- Resolve command injection via pipe to gh pr comment
- Add infinite bot loop guards to workflow triggers
- Add proper workflow dispatch checkout fallback
- Ensure PRAISONAI_ prefix is properly documented for GitHub Action secrets
- Synchronize correct behavior to setup template
- Move PR Review setup guide to examples/yaml as per repo hygiene standards
2026-04-09 11:37:18 +01:00
Mervin Praison 40f8d0d62c Merge pull request #1334 from MervinPraison/claude/issue-1331-20260409-0930
feat: Recipe and Gateway Integration with Inbox & Stepper Pattern
2026-04-09 11:36:27 +01:00
MervinPraison f74012e374 feat(gateway): implement Inbox & Stepper pattern and complete Recipe Bot adapter 2026-04-09 11:36:06 +01:00
copilot-swe-agent[bot]andMervinPraison 1145252406 fix: remaining RecipeBotAdapter issues - achat kwargs bug and docstring
- Fix achat: run_in_executor doesn't accept **kwargs directly; use
  functools.partial to bind prompt+kwargs before passing to executor
- Add copy_context_to_callable to propagate contextvars (trace emitters)
  when running sync chat in executor, with graceful fallback + debug log
- Update class docstring to accurately reflect the API:
  - Remove false claim of .chat(message: str) (now prompt: str, **kwargs)
  - Remove false streaming support claim (stream_emitter not wired up)

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/3de69c66-b614-4a2e-bf97-a5c0a10b69ba

Co-authored-by: MervinPraison <[email protected]>
2026-04-09 10:33:31 +00:00
MervinPraison f74ee5d619 feat(gateway): implement Inbox & Stepper pattern and Recipe Bot adapter
- Add asyncio queue inbox to GatewaySession for concurrency
- Intercept and enqueue messages while session is executing
- Resolve GatewaySession stepper background worker thread processing
- Implement RecipeBotAdapter to bridge declarative recipes to conversational agents
- Register support for --recipe flag in bot start commands
- Refactor 'praisonai serve recipe' to boot WebSocketGateway instead
2026-04-09 11:25:32 +01:00
MervinPraison fe30293e85 fix(agents): resolve 8 critical P0/P1 issues mapped from automated reviews
- Fix event loop blocking in _safe_sleep() by separating async variant
- Resolve server registry ID mismatch using self.agent_id
- Add fallback_output logic backward compatibility in structured executor
- Update memory EventBus emit to publish
- Use correct memory method compute_quality_score instead of _calculate...
- Enforce resilient resource teardown with isolated try catch blocks
- Add defensive cache checks in approval tests
2026-04-09 11:19:56 +01:00
praisonai-triage-agent[bot]andMervin Praison 6865574255 fix: Resolve critical RecipeBotAdapter issues for AgentProtocol compliance
- Fix AgentProtocol compliance: chat(prompt: str, **kwargs) and achat() method
- Fix deprecated asyncio.get_event_loop() -> asyncio.get_running_loop()
- Fix config corruption by separating config from input parameters
- Implement session isolation with per-session context variables
- Improve error handling with contextual logging and user-safe messages
- Fix falsy output handling (0, False, '', {}) treated as valid
- Use UUID for stable session IDs instead of memory address
- Add proper exception logging for recipe metadata failures

Addresses all critical issues raised by CodeRabbit, Qodo, Greptile, and Copilot reviewers.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-09 10:11:28 +00:00
MervinPraison b4e3a8a84a Release v4.5.128 2026-04-09 10:59:32 +01:00
MervinPraison 0889f800c1 feat: manually missing workflow file 2026-04-09 10:40:54 +01:00
praisonai-triage-agent[bot]andMervinPraison e84d2b18da feat: implement structured error handling and agent lifecycle management
Addresses the three critical architectural gaps identified in issue #1330:

**Issue 1: Silent Error Swallowing**
- Add MemoryResult and SearchResult structured types for memory operations
- Replace silent empty string returns with explicit success/failure/fallback status
- Wire _emit_memory_event to EventBus instead of no-op implementation
- Convert error_logs from unbounded list to ring buffer (maxlen=1000)

**Issue 2: Agent Resource Lifecycle Gap**
- Add Agent.close() and Agent.aclose() methods for proper cleanup
- Implement context manager support (__enter__/__exit__, __aenter__/__aexit__)
- Add _cleanup_server_registrations() to remove entries from global dicts
- Add destructor __del__ to ensure cleanup happens
- Replace blocking time.sleep() with async-safe _safe_sleep() method

**Issue 3: Workflow Error Propagation**
- Create StepResult, StepError, and WorkflowResult structured types
- Add ErrorStrategy enum (stop/skip/retry/fallback) for configurable error handling
- Implement StructuredWorkflowExecutor with proper retry logic
- Distinguish between recoverable errors (retry/skip) and fatal errors (halt)

**Architecture Alignment:**
- Follow protocol-driven design with new result protocols
- Maintain backward compatibility with legacy string returns
- Add lazy imports to prevent performance regressions
- Enable proper observability and debugging in production deployments

Co-authored-by: MervinPraison <[email protected]>

🤖 Generated with [Claude Code](https://claude.ai/code)
2026-04-09 09:37:49 +00:00
praisonai-triage-agent[bot]andMervinPraison dfbc68f92f feat: Recipe and Gateway Integration with Inbox & Stepper Pattern
- Implement Inbox & Stepper Pattern in WebSocket Gateway:
  * Add inbox queue and execution state to GatewaySession
  * Queue messages when session is executing to prevent concurrency issues
  * Send user-friendly acknowledgments for queued messages
  * Background task processes queued messages sequentially

- Create RecipeBotAdapter for Recipe-to-Agent bridge:
  * Implements Agent interface by wrapping Recipe execution
  * Supports .chat() method and streaming via .stream_emitter
  * Enables Human-in-The-Loop via context injection
  * Maintains conversation state through Recipe variables

- Integrate recipe support into CLI commands:
  * Add --recipe flag to bot commands (telegram, discord, slack)
  * Extend 'praisonai serve recipe' with --gateway mode for WebSocket serving
  * Maintain backward compatibility with existing recipe serving
  * Enable commands like: praisonai bot telegram --recipe support-team

Resolves #1331

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: MervinPraison <[email protected]>
2026-04-09 09:37:15 +00:00
praisonai-triage-agent[bot]andMervinPraison 4b806f8eca feat: integrate PraisonAI PR Reviewer in CI/CD pipeline
- Add multi-agent PR review configuration (.github/praisonai-reviewer.yaml)
- Create GitHub Actions workflow template (examples/yaml/praisonai-pr-review.yml.template)
- Update review chain documentation to include PraisonAI
- Add comprehensive setup guide (PRAISONAI_PR_REVIEWER_SETUP.md)

Implements agent-centric PR review with specialized teams:
- Security Reviewer: vulnerability and security analysis
- Performance Reviewer: bottleneck and efficiency analysis
- Maintainability Reviewer: code quality and best practices
- Lead Reviewer: synthesis and comprehensive feedback

Follows Protocol-Driven Core design with zero-code YAML configuration.
Integrates seamlessly with existing review chain (CodeRabbit/Qodo → Gemini/PraisonAI → Copilot → Claude).

Fixes #1329

Co-authored-by: MervinPraison <[email protected]>
2026-04-09 09:24:59 +00:00
MervinPraison f00763937b Release v4.5.126 2026-04-09 07:34:38 +01:00
MervinPraison d0523d5b64 Release v4.5.125 2026-04-09 07:05:42 +01:00
dd41a3c426 fix(cors): Patch 3 follow-up bugs from CWE-942 CORS hardening (#1323)
* security: Fix CORS Configuration Vulnerabilities (CWE-942) - Replace wildcard origins

- Replace allow_origins=['*'] with secure environment-based configurations
- Update browser server to use BROWSER_CORS_ORIGINS env var with localhost defaults
- Update jobs server to use JOBS_CORS_ORIGINS env var with localhost defaults
- Update example API to use API_CORS_ORIGINS env var with localhost defaults
- Remove wildcard defaults from ServerConfig, GatewayConfig, and AppConfig
- Add secure localhost defaults to MCP server transport
- Update tests and documentation to reflect secure defaults
- Restrict CORS methods and headers to essential ones only

Fixes #1321 - Addresses 3 instances of CWE-942 vulnerabilities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>

* fix: Address 3 CORS bugs found in review - chrome-extension regex, http_stream headers, server.py methods/headers

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/b76a2075-da18-4ad5-82e8-79fc09457529

Co-authored-by: MervinPraison <[email protected]>

* security: Fix remaining CORS vulnerabilities (CWE-942)

- Add environment-based CORS defaults (empty for production)
- Validate env vars to reject wildcard origins (*)
- Add missing Idempotency-Key header to jobs CORS
- Unify WebSocket and HTTP CORS origin validation
- Make http_stream transport environment-aware

Fixes all critical security issues identified by code reviewers:
- Production no longer allows localhost by default
- Environment variables cannot bypass security with *
- WebSocket uses same origin validation as CORS middleware
- Jobs API properly supports Idempotency-Key header

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-04-09 07:00:47 +01:00
MervinPraison e87cc650dc docs: auto-update feature parity trackers [skip ci] 2026-04-09 05:57:14 +00:00
3ac10ab851 fix: broken test and in-function shlex imports from CWE-78 remediation (#1322)
* security: Fix Shell Injection Vulnerabilities (CWE-78) - 15 files fixed

- Replace shell=True with shell=False across subprocess calls
- Add shlex.split() for safe argument parsing
- Update async subprocess calls to use create_subprocess_exec
- Fix vulnerabilities in core, CLI, tools, examples, and tests
- Maintain existing command validation and functionality
- Eliminate 29 shell injection attack vectors

Security improvements:
* Core command execution (execute_command.py)
* Safe shell interface (safe_shell.py)
* Sandbox execution (sandbox_executor.py)
* Workflow and action execution
* Memory hooks and documentation tools
* Example code and test utilities

All functionality preserved while securing against CWE-78 injection attacks.

Fixes #1320

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>

* fix: move import shlex to module level, fix broken test_command_failure

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/7af9d997-bcd4-48a1-b0fe-5c11679ea3a9

Co-authored-by: MervinPraison <[email protected]>

* fix: address reviewer feedback - shell API contract, cross-platform shlex, exception handling, env validation

- Fix shell=True API contract violation: explicitly reject shell=True with clear error message
- Add cross-platform compatibility for shlex.split() using posix=(os.name == 'posix')
- Add proper exception handling for malformed command syntax (ValueError from shlex.split)
- Add environment variable validation to filter dangerous vars (LD_PRELOAD, PYTHONPATH, etc.)
- Update docstring to indicate shell=True is deprecated for security reasons

Addresses issues raised by Gemini, Qodo, CodeRabbit, and Copilot reviewers.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-04-09 06:56:55 +01:00
MervinPraison 2ecc383e6e Release v4.5.124 2026-04-08 22:25:33 +01:00
MervinPraison 35761ea88e refactor(actions, workflows): standardize input variable usage in YAML files
Updated action and workflow files to use consistent input variable references.
This includes changes to environment variable handling for better clarity
and maintainability, enhancing code quality and reducing potential errors.
2026-04-08 22:23:54 +01:00
MervinPraison e122d7d227 Release v4.5.123 2026-04-08 22:05:05 +01:00
2284535f65 fix(security): Replace insecure MD5 with SHA-256, disable debug mode (29 fixes) (#1319)
* fix(security): replace insecure MD5 with SHA-256 and disable debug mode

Replace all 29 instances of hashlib.md5() with hashlib.sha256() across
22 files. MD5 is cryptographically broken (CWE-328) and should not be
used for hashing, even for non-security purposes like cache keys and
content fingerprints.

Also disable debug=True in api.py (CWE-489) to prevent information
leakage in production deployments.

Vulnerabilities fixed:
- 28x CWE-328: Use of Weak Hash (MD5 → SHA-256)
- 1x CWE-489: Active Debug Code (debug=True → debug=False)

Detected by semgrep (p/owasp-top-ten, p/python).
Verified by re-scan: 66 findings → 37 remaining (29 fixed).

Remaining findings (require manual review):
- 14x shell injection (subprocess with user input)
- 10x subprocess shell=True
- 3x wildcard CORS
- 5x tainted env args in subprocess

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address review feedback on MD5→SHA-256 migration

Fixes raised by Gemini Code Assist, Qodo, and CodeRabbit:

1. templates/security.py: algorithm="md5" now raises ValueError
   instead of silently returning SHA-256 digest

2. recipe/security.py: CycloneDX serialNumber now uses uuid5()
   for RFC 4122 compliant UUID format (was 64-char hexdigest)

3. agent/agent.py: backward-compatible session ID — checks for
   legacy MD5-based session file on disk before using new SHA-256 ID

4. snapshot/snapshot.py: backward-compatible shadow repo path —
   adopts legacy MD5-based directory if it exists on disk

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Michael Winczuk <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-08 21:50:42 +01:00
MervinPraison eb2cb40b30 fix: correct context payload for github-script in pull request checks 2026-04-08 21:44:07 +01:00
MervinPraison 459688d32d Release v4.5.122 2026-04-08 21:43:08 +01:00
MervinPraison b7f3e55669 ci: fix claude-code-action checkout for fork PRs by tricking origin 2026-04-08 21:41:16 +01:00
MervinPraison 1cd48fde4e trigger: Run claude test without skip ci 2026-04-08 21:34:26 +01:00
MervinPraison 584d85f305 docs: auto-update feature parity trackers [skip ci] 2026-04-08 20:28:01 +00:00
MervinPraison 42807f1740 Release v4.5.121 2026-04-08 21:27:43 +01:00
MervinPraison 0d40fa93aa Release v4.5.120 2026-04-08 19:36:40 +01:00
MervinPraison 75439026e7 docs: auto-update feature parity trackers [skip ci] 2026-04-08 18:33:22 +00:00
MervinPraison 616532d4ed Release v4.5.119 2026-04-08 19:32:52 +01:00
MervinPraison 6693274671 Release v4.5.118 2026-04-08 19:23:26 +01:00
MervinPraison 142db6c5d5 refactor(tests): simplify and enhance test implementations
Updated unit and integration tests for better readability and maintainability.
Replaced repetitive subprocess calls with a dedicated `run_doctor` function in
the doctor CLI tests, and improved error handling in the handoff error tests.
Removed unnecessary test cases and cleaned up assertions for clarity.
Total changes: 57 insertions, 151 deletions across 10 files.
2026-04-08 19:11:26 +01:00
a2d915f6ab feat: implement protocol-driven memory/knowledge adapter registries (#1310)
* feat: implement protocol-driven memory/knowledge adapter registries

Replace hardcoded backend imports with adapter registry pattern to fix
Core SDK Gap #3 architecture violations. This enables clean protocol-driven
backend resolution while preserving backward compatibility.

Memory System Changes:
- Created memory/adapters/factories.py with lazy-loading factory functions
- Registered mem0, chroma, mongodb adapter factories in registry
- Added memory/registry_demo.py demonstrating protocol-driven approach
- Eliminates 146+ hardcoded _check_* functions and self.use_* flags

Knowledge System Changes:
- Created knowledge/adapters/factories.py with lazy-loading factory functions
- Registered mem0, chroma, mongodb, sqlite adapter factories in registry
- Added knowledge/registry_demo.py demonstrating protocol-driven approach
- Eliminates hardcoded 'import chromadb' and 'from markitdown import MarkItDown'

Registry Infrastructure:
- Updated memory/adapters/__init__.py to register factory functions
- Updated knowledge/adapters/__init__.py to register factory functions
- All heavy dependencies now lazy-loaded via factory pattern
- Core adapters (sqlite, in_memory) work without heavy dependencies
- Heavy adapters gracefully fallback when dependencies missing

Benefits:
- Protocol-driven core: No heavy imports until actually needed
- Performance-first: < 200ms import time maintained
- Extensible: Third-party backends can register without core edits
- Backward compatible: All existing APIs preserved via adapter delegation

Closes #1306

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>

* fix: address critical bugs in protocol-driven adapter implementation

- Fix SearchResult.results vs .items AttributeError in ChromaKnowledgeAdapter
- Fix AddResult error field usage - change to message field (5 instances)
- Fix adapter registry fallback mechanism for missing dependencies
- Fix datetime.utcnow() deprecation warnings

Co-authored-by: Mervin Praison <[email protected]>

* fix: address critical bugs in protocol-driven adapter implementation

- Fix ChromaKnowledgeAdapter.search() to properly handle user_id/agent_id/run_id/filters scoping
- Fix ChromaKnowledgeAdapter.delete_all() to recreate collection after full delete to keep adapter usable
- Fix LegacyMemoryAdapter to use stable UUIDs instead of unstable id(text) for memory identifiers
- Fix memory logging to avoid exposing PII/secrets by removing text[:100] content from INFO logs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

* refactor: integrate protocol-driven adapter registry into core Memory and Knowledge classes

Remove DRY violations by eliminating demo and refactored files:
- Remove memory_refactored.py, registry_demo.py files
- Integrate registry logic directly into memory.py and knowledge.py

Protocol-driven Memory integration:
- Replace hardcoded backend checks with get_memory_adapter() calls
- Update store/search methods to delegate to adapters first
- Add provider-specific config filtering for proper adapter instantiation
- Maintain backward compatibility with SQLite fallback

Protocol-driven Knowledge integration:
- Replace hardcoded chromadb imports with get_knowledge_adapter() calls
- Update memory property to use adapter registry with fallbacks
- Remove heavy ChromaDB client creation from config property

This follows AGENTS.md principles:
- Core SDK uses protocol-driven design with adapters
- Heavy implementations accessed via lazy-loading registry
- Maintains full backward compatibility
- Eliminates hardcoded dependency imports from core

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-04-08 17:13:25 +01:00
e40269d194 feat: implement LLM provider adapter pattern to eliminate scattered provider branching (#1307)
* feat: implement LLM provider adapter pattern to eliminate scattered provider branching

- Add comprehensive LLMProviderAdapter protocol with provider-specific hooks
- Implement OllamaAdapter, AnthropicAdapter, GeminiAdapter with specific behaviors
- Add centralized _detect_provider() method replacing scattered _is_X_provider() calls
- Refactor key methods to use adapter pattern:
  - _supports_streaming_tools() -> adapter.supports_streaming_with_tools()
  - _supports_prompt_caching() -> adapter.supports_prompt_caching()
  - _handle_ollama_sequential_logic() -> adapter.should_summarize_tools()
- Fix duplicate LLMProviderProtocol definitions in protocols.py
- Enable extensible provider support without core code edits

Addresses issue #1305 - Core SDK Gap #2: Provider-specific dispatch

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>

* fix: resolve all critical issues in LLM provider adapter pattern

- Fix provider detection false positives by removing substring matching
- Preserve backward compatibility by keeping LLMProviderProtocol name
- Add explicit tool_call_id parameter to format_tool_result_message
- Fix prompt caching regression to allow fallback to model capabilities
- Move provider defaults from core to adapter pattern
- Remove hot-path provider detection in favor of adapter delegation

Addresses all issues identified by Qodo, CodeRabbit, and Gemini reviewers.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 17:13:16 +01:00
d2ce315ce1 fix: patch import crash, Pydantic V2 deprecations, and broken fallback in unified config schema (#1301)
* feat: implement unified CLI/YAML/Python configuration schema

Resolves architectural gap #2 by implementing a single source of truth
for configuration validation, CLI generation, and field mapping across
all three interface modes.

Core Changes:
- Add ConfigSchemaProtocol and related protocols to core SDK
- Implement UnifiedRAGSchema with Pydantic validation
- Create fallback implementation for environments without Pydantic
- Update config_loader.py to use unified schema
- Add comprehensive test suite

Key Features:
- Schema-driven CLI flag generation
- Guaranteed CLI/YAML/Python mapping consistency
- Single documented precedence chain (CLI → ENV → YAML → defaults)
- Strong validation with user-friendly error messages
- Zero breaking changes to existing interfaces

Architecture Compliance:
✅ Core SDK: Lightweight protocols only
✅ Wrapper Layer: Heavy Pydantic implementations
✅ Backward Compatible: Existing interfaces still work
✅ 3-way feature surface: CLI/YAML/Python parity maintained

Fixes #1300

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>

* fix: resolve import NameError, Pydantic V2 deprecations, and fallback crash in unified schema

- Add List to typing imports in config_loader.py (was causing NameError at import)
- Remove unused `field` import from dataclasses in config_loader.py
- Route config_loader.py through schema_provider.py for proper Pydantic fallback
- Guard UnifiedRAGSchema class with `if BaseModel is not None` to prevent TypeError
  when Pydantic is unavailable (Field=None was called as function, crashing module import)
- Replace deprecated Pydantic V1 @validator with V2 @field_validator + @classmethod
- Replace deprecated .dict() with .model_dump() (Pydantic V2)
- Replace deprecated __fields__ with model_fields (Pydantic V2)
- Replace deprecated field_info.type_ with field_info.annotation (Pydantic V2)
- Handle PydanticUndefinedType for default values in model_fields iteration

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/c17eb200-5e14-4332-95b0-b0d09f989a7b

Co-authored-by: MervinPraison <[email protected]>

* fix: address all critical issues found in code review

- Fix Pydantic V1/V2 compatibility (model_fields vs __fields__, model_dump vs dict)
- Fix import-time crash when Pydantic missing (conditional class definition)
- Implement schema-derived CLI flags with fallback mappings
- Correct precedence chain order to ENV > CLI > YAML > defaults
- Add unknown field validation for both Pydantic and fallback providers
- Maintain backward compatibility for get_config_schema() API
- Add source attribution to validation errors (shows CLI/ENV/YAML origin)
- Fix provider selector bypass (use schema_provider import)
- Handle legacy nested YAML sections for backward compatibility
- Fix boolean CLI flags (use store_true instead of type=bool)
- Remove CLI defaults to preserve precedence chain

Co-authored-by: Mervin Praison <[email protected]>

* fix: resolve field import shadowing in fallback schema

- Rename loop variable 'field' to 'field_name' to avoid shadowing dataclasses.field import
- Fixes Ruff F402 error and ensures proper import usage
- Maintains all existing functionality while improving code clarity

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

* fix: resolve field import shadowing in fallback schema

- Fix field import shadowing in fallback_schema.py (renamed loop variable field -> fld)
- Add try/catch fallback layer for cross-package imports from praisonaiagents
- Correct precedence chain order in fallback schema (ENV → CLI → YAML → defaults)
- Fix validation to allow omitted fields with defaults (collection field)
- Add comprehensive import compatibility for environments without praisonaiagents
- Fix CliMapping dataclass structure with proper field ordering

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <[email protected]>

* fix: correct precedence chain order and resolve critical import issues

- Fix precedence chain to CLI > ENV > YAML > defaults (was ENV > CLI)
- Remove field import shadowing in fallback_schema.py
- Add Pydantic V1/V2 compatibility handling
- Ensure import safety when praisonaiagents not available
- Update precedence documentation in all modules
- All tests passing: 14 CLI flags generated, validation working, backward compatibility maintained

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-04-08 17:13:07 +01:00
MervinPraison f069930d67 docs(README): update supported providers and features section
Added detailed list of supported providers and features with examples.
This improves clarity and accessibility for users exploring integrations.
2026-04-08 17:10:55 +01:00
MervinPraison b58a85a669 docs(README): update use cases section with additional examples
Replaced the old use cases section with a more detailed table outlining specific applications of AI agents across various industries, enhancing clarity and usability for readers.
2026-04-08 16:34:49 +01:00
Mervin Praison 6d60068661 Fix memory search extraction and retrieve fallbacks (#1318) 2026-04-08 16:31:19 +01:00
Mervin Praison 5fa2a7fc47 Merge pull request #1316 from MervinPraison/claude/issue-1313-20260408-1245
test: add unit tests for check_bot_security + fix dead code and misleading messages
2026-04-08 15:30:13 +01:00
copilot-swe-agent[bot]andMervinPraison fbdcb92ff2 test: add unit tests for check_bot_security covering SKIP/PASS/WARN/FAIL paths
Co-authored-by: MervinPraison <[email protected]>
2026-04-08 13:46:50 +00:00
praisonai-triage-agent[bot]andMervin Praison 18da39991a fix: resolve bot security check issues from code review
- Remove unreachable dead code (issues list never populated)
- Fix misleading warning message to distinguish channel vs global warnings
- Improve exception handling: ValueError for config errors (FAIL) vs runtime errors (ERROR)
- Make bot checks accessible via CLI by:
  - Converting to registry-based @register_check decorators
  - Adding bot_checks to register_all_checks()
  - Adding 'bots' subcommand to CLI actions and category mapping

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 13:10:23 +00:00
praisonai-triage-agent[bot]andpraisonai-triage-agent[bot] 1c32d811f7 feat: add security doctor check for bot DM policies and safe defaults
- Add check_bot_security() function to bot_checks.py doctor module
- Validates allowlist/blocklist configuration across channels
- Warns about overly permissive group policies (respond_all vs mention_only)
- Checks for PRAISONAI_GATEWAY_SECRET to ensure pairing persistence
- Provides actionable remediation guidance for secure configurations
- Integrates with existing doctor framework for 'praisonai doctor --category bots'

This implements the 'doctor-style check' component requested in issue #1313
for auditing risky bot security configurations before production deployment.

Fixes #1313

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
2026-04-08 12:51:42 +00:00
Mervin Praison 27de2635eb feat: implement unified LLM protocol dispatch to eliminate dual execution paths
feat: implement unified LLM protocol dispatch to eliminate dual execu…
2026-04-08 13:16:43 +01:00
Mervin Praison d6bdc7e0de feat: eliminate async/sync execution duplication in core agent
feat: eliminate async/sync execution duplication in core agent
2026-04-08 13:12:34 +01:00
MervinPraison 92c6a13cac docs(README): restore Performance section and enhance details
Reintroduced the Performance section to highlight PraisonAI's speed,
including specifics on agent instantiation time for improved clarity.
2026-04-08 13:04:06 +01:00
MervinPraison 5882eb0ffc docs(README): update layout and enhance visibility of Elon Musk highlight
Moved the highlighted section from the middle to the bottom of the README for better visibility. Adjusted image and quote formatting to maintain clarity and consistency.
2026-04-08 12:59:21 +01:00
MervinPraison 7bcba30b14 docs(README): update installation instructions and enhance layout
Improved installation instructions for SDK, CLI, Dashboard, and Builders
with example commands. Added new visual elements for better engagement.
Removed redundant elements to streamline content.
2026-04-08 12:57:28 +01:00
MervinPraison ce3c3c8d59 feat(docs): update README for clarity and new messaging
Revise main description to emphasize hiring autonomous AI agents.
Condense sections for quick paths and performance, improving the overall flow.
Remove outdated use cases and provide a more engaging introduction to PraisonAI.
2026-04-08 12:54:09 +01:00
praisonai-triage-agent[bot]andMervin Praison 4dd917e303 fix: resolve protocol issues in DRY execution helpers
- Add Awaitable import and update TaskExecutorProtocol return type for async compatibility
- Add error handling for missing executor agents with clear remediation message
- Fix variable substitution to include team-level variables for async/sync consistency
- Remove unused Union import (already done in previous commit)

Addresses valid reviewer feedback from CodeRabbit, Qodo, Copilot, and Gemini.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 11:15:23 +00:00
praisonai-triage-agent[bot]andMervin Praison 16c08ba682 fix: address all valid reviewer feedback on protocol-driven architecture
- Remove duplicate adapters.py file to eliminate multiple sources of truth
- Replace runtime class mutation with composition-based unified dispatch for safety
- Fix missing tool execution parameters in unified sync mode
- Preserve hooks and post-processing flow in unified paths to maintain observability
- Fix duplicate verbose kwarg crash in LiteLLMAdapter
- Fix protocol violations in sync wrappers (no asyncio.run in library internals)
- Fix message extraction logic to prevent prompt duplication
- Make OpenAIAdapter sync delegate to async as required by protocol
- Improve test suite with proper sync/async parity and performance regression tests
- Add comprehensive real agentic test as required by AGENTS.md

All critical issues from reviewers addressed while maintaining backward compatibility.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 10:55:46 +00:00
MervinPraison 9b7f458b35 Release v4.5.117 2026-04-08 11:52:17 +01:00
MervinPraison 83f7babd89 docs: auto-update feature parity trackers [skip ci] 2026-04-08 10:51:07 +00:00
MervinPraison 0d65b5aaf9 feat(plugins): add entry point discovery support for plugins
Implemented automatic discovery and loading of protocol-driven plugins
via pip in the `PluginManager`. The new method `discover_entry_points`
fetches entry points from the `praisonai.plugins` group, loads callable
plugins, and registers them. Updated version number to 1.5.117.
2026-04-08 11:50:49 +01:00
praisonai-triage-agent[bot]andMervin Praison ab90f57c8e fix: use ExecutionContext/TaskResult protocols in DRY helpers
- Remove unused Union import from protocols.py
- Update _build_execution_context() to return ExecutionContext dataclass
- Update _process_task_result() to accept ExecutionContext and return TaskResult
- Fix execute_task/aexecute_task to use attribute access on ExecutionContext
- Remove trailing whitespace in protocols.py

Addresses reviewer feedback from Qodo, Gemini, CodeRabbit, and Copilot
regarding protocol usage consistency and type safety.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 10:47:10 +00:00
praisonai-triage-agent[bot]andClaude 6fbf238628 feat: implement unified LLM protocol dispatch to eliminate dual execution paths
This commit addresses Issue #1304 by consolidating the dual execution paths
in the LLM layer (LiteLLM vs OpenAI) into a single async-first protocol-driven
architecture, eliminating DRY violations and maintenance burden.

Key changes:
- Added UnifiedLLMDispatcher with provider adapter pattern
- Created LiteLLMAdapter and OpenAIAdapter for protocol conformance
- Enhanced agent chat logic with opt-in unified dispatch
- Maintained full backward compatibility (existing code unchanged)
- Established protocol-driven foundation for future provider additions

Benefits:
✅ Single async chat completion implementation
✅ Sync methods as thin wrappers around async
✅ Protocol-driven provider selection
✅ Elimination of dual execution paths
✅ All existing functionality preserved

Files added:
- praisonaiagents/llm/unified_adapters.py (protocol adapters)
- praisonaiagents/agent/unified_chat_mixin.py (unified implementation)
- test_unified_llm.py (verification tests)

Files modified:
- praisonaiagents/llm/__init__.py (updated exports)
- praisonaiagents/agent/chat_mixin.py (added unified dispatch option)

The unified dispatch is opt-in via _use_unified_llm_dispatch flag, ensuring
zero impact on existing deployments while enabling clean migration path.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-04-08 10:33:02 +00:00
Mervin Praison 22e1e0bdb0 Merge pull request #1303 from MervinPraison/claude/issue-1302-20260408-0931
fix: review feedback on protocol-driven architecture — bugs, DRY, tests
2026-04-08 11:19:14 +01:00
praisonai-triage-agent[bot]andMervin Praison cc5683c5c4 fix: address all valid reviewer feedback on protocol-driven architecture
- Fix registry locking issues: hold lock only for lookup, not construction
- Fix hardcoded provider detection: replace with true registration API
- Add canonical naming aliases per AGENTS.md: add_*/has_* functions
- Fix knowledge adapter defaults: use actual available adapters
- Fix asyncio.run() documentation: remove problematic streaming guidance
- Fix memory leak: defensive copy in get_all_memories()
- Rename LLMProviderAdapter -> LLMProviderProtocol per conventions

Addresses feedback from Qodo, CodeRabbit, Gemini reviewers.
Maintains backward compatibility via wrapper functions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Mervin Praison <[email protected]>
2026-04-08 09:56:54 +00:00
copilot-swe-agent[bot]andMervinPraison d4e82d3909 fix: address review feedback on protocol-driven architecture
- Fix GeminiAdapter tool names to match GEMINI_INTERNAL_TOOLS constants
- Fix OllamaAdapter threshold to match LLM.OLLAMA_SUMMARY_ITERATION_THRESHOLD = 1
- Extract generic AdapterRegistry[T] base class to eliminate DRY violation
- Add debug logging to registry exception handling (no longer silent)
- Move InMemoryAdapter from __init__.py to its own in_memory_adapter.py module
- Add 38 unit tests covering all new adapter and registry code paths"

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/0fc360e2-23ac-43a1-8a56-50dd4686feb1

Co-authored-by: MervinPraison <[email protected]>
2026-04-08 09:52:41 +00:00
praisonai-triage-agent[bot]andMervinPraison 35146cd1c8 demo: add concrete LLM provider adapters to demonstrate protocol approach
Shows how to replace scattered provider dispatch in llm.py with clean adapters:

- DefaultAdapter: Base implementation with sensible fallbacks
- OllamaAdapter: Replaces OLLAMA_SUMMARY_ITERATION_THRESHOLD logic
- AnthropicAdapter: Handles Claude prompt caching capabilities
- GeminiAdapter: Handles gemini_internal_tools formatting
- get_provider_adapter(): Replaces model.startswith() branching

This demonstrates the systematic approach for Gap 2 - extracting the 40+
provider branches from core into clean, testable adapters.

Co-authored-by: MervinPraison <[email protected]>
2026-04-08 09:38:04 +00:00
praisonai-triage-agent[bot]andMervinPraison 3ad181950c feat: implement protocol-driven architecture for LLM and memory adapters
This addresses architectural gaps #1-3 by implementing:

1. LLMProviderAdapter protocol to replace scattered provider dispatch
2. UnifiedLLMProtocol to consolidate dual execution paths
3. Memory and Knowledge adapter registries for protocol-driven backends

Changes:
- Add LLMProviderAdapter and UnifiedLLMProtocol to llm/protocols.py
- Create memory/adapters/registry.py with thread-safe adapter registration
- Create knowledge/adapters/registry.py with thread-safe adapter registration
- Register core adapters (sqlite, in_memory) in core SDK
- Heavy implementations (chromadb, mem0) to be moved to wrapper

Follows AGENTS.md protocol-driven core principle. Backward compatible.

Co-authored-by: MervinPraison <[email protected]>
2026-04-08 09:36:45 +00:00
praisonai-triage-agent[bot]andpraisonai-triage-agent[bot] 78d07eb09f feat: eliminate async/sync execution duplication in core agent
- Add TaskExecutorProtocol, ExecutionContext, and TaskResult to core protocols
- Create unified DRY helpers: _build_execution_context() and _process_task_result()
- Refactor aexecute_task() and execute_task() to use DRY helpers
- Reduce code duplication by ~150 lines (56% reduction)
- Maintain 100% backward compatibility
- Follow AGENTS.md protocol-driven architecture

Resolves #1299

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
2026-04-08 09:27:50 +00:00
MervinPraison 09970cd348 docs: auto-update feature parity trackers [skip ci] 2026-04-08 08:10:23 +00:00
Mervin Praison 387b81d324 Merge pull request #1297 from MervinPraison/claude/issue-1288-20260408-0723
fix: resolve three architectural gaps (DRY, error protocol, streaming parity)
2026-04-08 09:10:07 +01:00
Mervin Praison ae1cf98fc9 Merge pull request #1294 from MervinPraison/claude/issue-1289-20260408-0723
fix: Critical async safety issues (Gap 2 from #1289)
2026-04-08 09:01:01 +01:00
MervinPraison 52354bf3f4 docs: auto-update feature parity trackers [skip ci] 2026-04-08 08:00:01 +00:00
Mervin Praison 5bd317178a Merge pull request #1295 from MervinPraison/claude/issue-1290-20260408-0723
feat: unify Handoff and SubagentDelegator into single execution engine
2026-04-08 08:59:42 +01:00
Mervin Praison 94bc2ce611 Merge pull request #1296 from MervinPraison/claude/issue-1293-20260408-0723
feat: Implement PraisonAI-Plugins system with entry_points discovery
2026-04-08 08:57:43 +01:00
praisonai-triage-agent[bot]andMervin Praison 55287d4cc3 fix: address reviewer feedback - backward compatibility and bugs
- Add missing BudgetExceededError import in chat_mixin.py
- Fix budget_type from 'tokens' to 'cost' for USD values
- Add backward compatibility constructor for BudgetExceededError
- Add legacy attributes (agent_name, total_cost, max_budget) for compatibility
- Fix HandoffTimeoutError constructor call in handoff.py
- Add backward compatibility aliases for handoff errors
- Fix streaming precedence logic to handle explicit false values
- Add explicit BudgetExceededError re-raise to prevent generic exception swallowing

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 07:56:55 +00:00
praisonai-triage-agent[bot]andMervin Praison a8b72b046f fix: address all code review issues in plugin entry_points discovery
- Fix entry points never discovered in main flow (plugins.enable now calls discover_entry_points)
- Fix fragile entry_points fallback using robust try/except pattern from ToolRegistry
- Fix non-deterministic tests by mocking importlib.metadata properly
- Fix overbroad import monkeypatch to only affect importlib.metadata
- Add thread safety with proper _lock usage in register() and discover_entry_points()
- Add positive test case for successful entry point loading and registration

All reviewer feedback from Qodo, Gemini, CodeRabbit addressed.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 07:50:20 +00:00
praisonai-triage-agent[bot]andMervin Praison 69fce71ad5 fix: address critical bugs and architectural issues in handoff unification
- Fix HandoffConfig.max_concurrent default from 3 to 5 per issue requirements
- Fix deadlock bug: parallel_handoffs() now treats max_concurrent<=0 as unlimited
- Fix BaseException handling to prevent non-HandoffResult objects in return list
- Export parallel_handoffs in package __init__.py for public API access
- Remove broken documentation URL from deprecation warning
- Wire allow_parallel flag to control parallel_handoffs() behavior
- Fix unused variable warning by prefixing with underscore

These changes address all critical bugs and API inconsistencies identified
by code reviewers while maintaining backward compatibility.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 07:47:43 +00:00
praisonai-triage-agent[bot]andMervin Praison e4e2657f17 fix: resolve critical async safety issues identified by code reviewers
- Fix threading.Lock used with async with in Process class (issue #1289)
  * Replace sync lock creation with lazy async lock initialization
  * Prevents runtime AttributeError in aworkflow method
- Improve thread safety in AsyncPostgres lazy lock initialization
  * Add double-checked locking pattern to prevent race conditions
  * Ensures only one asyncio.Lock instance created across coroutines
- Remove redundant asyncio import in async_postgres.py
- Improve exception chaining for better error diagnostics

Addresses critical issues raised by Gemini, Qodo, CodeRabbit reviewers.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 07:45:43 +00:00
praisonai-triage-agent[bot]andClaude 56443bf3f8 feat: fix three architectural gaps identified in issue #1288
1. DRY violation in AgentTeam execute_task/aexecute_task
   - Extracted shared helpers: _prepare_task_prompt(), _execute_with_agent_sync/async()
   - Fixed missing stream=self.stream parameter in async path
   - Eliminated ~120 lines of duplicated logic

2. Structured exception hierarchy with ErrorContextProtocol
   - Created praisonaiagents/errors.py with protocol-driven error handling
   - Added error categories: tool, llm, budget, validation, network, handoff
   - Migrated existing ad-hoc errors to structured hierarchy
   - Updated lazy imports for backward compatibility

3. Streaming feature parity for YAML/CLI
   - Enhanced YAML schema to support streaming: configuration block
   - Added CLI flags: --stream and --stream-metrics
   - Extended agents_generator.py to wire streaming through OutputConfig
   - Achieves complete CLI + YAML + Python feature parity

Fixes violations of stated pillars: DRY, protocol-driven core, 3-way feature parity.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-04-08 07:35:57 +00:00
praisonai-triage-agent[bot]andMervin Praison 258ab1cd20 feat(plugins): add entry_points discovery to PluginManager
- Add discover_entry_points() method to PluginManager for automatic plugin discovery
- Support pip-installed plugins via 'praisonaiagents.plugins' entry_points group
- Compatible with Python 3.8+ using importlib.metadata
- Graceful error handling for missing dependencies
- Added comprehensive unit tests for entry_points discovery
- Enables external plugin packages like PraisonAI-Plugins to be auto-discovered

Fixes #1293

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 07:31:37 +00:00
MervinPraison 63248a3a46 ci: add hooks and guardrails to plugin description in claude.yml 2026-04-08 08:29:45 +01:00
praisonai-triage-agent[bot]andMervin Praison 331b47013e feat: unify Handoff and SubagentDelegator into single execution engine
- Extend HandoffConfig with allow_parallel field for parallel execution
- Add parallel_handoffs() function providing SubagentDelegator's parallel capabilities
- Add deprecation warning to SubagentDelegator directing users to handoff system
- Maintains backward compatibility while consolidating overlapping functionality

Fixes #1290

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 07:28:46 +00:00
MervinPraison 162c10c16f ci: expressly differentiate Tools vs Plugins in claude routing 2026-04-08 08:28:02 +01:00
MervinPraisonandMervin Praison cdefa98c09 fix: critical async safety issues - replace sync constructor asyncio.Lock, add postgres init race protection, fix state store save races
Addresses Gap 2 from issue #1289:
1. Replace asyncio.Lock() with threading.Lock() in Process.__init__() - asyncio.Lock in sync constructor fails outside event loop
2. Add async lock protection to async postgres store init() to prevent race conditions creating duplicate connection pools
3. Fix memory state store _maybe_save() race condition where multiple threads could trigger overlapping saves
4. Verified existing thread safety in global error_logs, callbacks already properly protected with locks
5. Verified MemoryMixin already has proper thread safety with _history_lock

These changes ensure multi-agent safety by eliminating race conditions and thread safety issues that could corrupt state or cause crashes when running multiple agents concurrently.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 07:27:22 +00:00
MervinPraison 5b2d924121 ci: add PraisonAI-Plugins to routing instructions in claude workflow 2026-04-08 08:26:46 +01:00
MervinPraison 9fcf1b4acd ci: rewrite Claude workflow STEP 3-ALT to enforce git -C and proper auth headers 2026-04-08 08:23:03 +01:00
MervinPraison 7bec5453d0 test(langfuse_sink): update agent span handling and method assertions
Refactor tests for `LangfuseSinkEmit` to use agent spans with a consistent
identifier format. Update assertions to reflect changes in method
validations for agent classes in `TestApiMdGenerator`. Adjusted several
mock object usages for improved clarity and consistency across tests.
2026-04-08 07:53:49 +01:00
MervinPraison 7c87046753 fix(ci): fix cross-repo auth and identity for Claude Action
Root cause of issue #1283 failure:
1. git config was local, not --global, so identity failed in cloned repos
2. External repos (PraisonAIDocs) were cloned with bare https (no auth)
3. Claude tried to push to main instead of a feature branch

Fix:
- Use --global git config for identity across all repos
- Add STEP 3-ALT with explicit cross-repo workflow using gh repo clone
- gh repo clone auto-configures auth token for push
- Add PraisonAI-Plugins and PraisonAIDocs to routing table
- Explicit instructions to never use bare git clone https://
2026-04-08 07:46:58 +01:00
MervinPraison 81ea4d7706 ci: allow praisonai-triage-agent bot for claude code action 2026-04-08 07:44:38 +01:00
MervinPraison f1d53a409b ci: add cross-repo git configuration to claude workflows 2026-04-08 07:26:54 +01:00
Mervin Praison 384bb06210 Merge pull request #1285 from MervinPraison/claude/pr-1284-20260407-2042
PR #1284: Changes from Claude
2026-04-08 06:54:57 +01:00
MervinPraison 2dd1a80fde docs: auto-update feature parity trackers [skip ci] 2026-04-08 05:48:30 +00:00
Mervin Praison d4a1b6e64b Merge pull request #1286 from MervinPraison/claude/issue-1280-20260407-2043
feat: add missing LLM and retrieval provider protocols
2026-04-08 06:48:13 +01:00
MervinPraisonandClaude 09e85a24d2 security: fix critical path traversal and sandbox escape vulnerabilities
- Resolve merge conflicts with main branch
- Fix getattr sandbox inconsistency by removing getattr from blocked AST calls
- Combine dangerous patterns from both branches for comprehensive protection
- Allow _safe_getattr implementation to work properly in sandbox mode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <[email protected]>
2026-04-08 05:44:55 +00:00
MervinPraisonandMervin Praison 5990367b3d fix: address code review issues in LLM and RAG protocols
- Fix ModelNotAvailableError to properly pass provider/model to base exception
- Fix RateLimitError type annotation (Optional[str] instead of str = None)
- Fix ContextLengthExceededError to accept and pass provider/model parameters
- Add async aretrieve method to RetrievalStrategyProtocol for async I/O support
- Add missing newline at end of protocols.py file

Addresses review feedback from code reviewers.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-08 05:43:57 +00:00
MervinPraison 01a20e307c refactor(tests): reorganize test agent and tool files into fixtures
Moved test agents and tools to a dedicated fixtures directory. Updated test scripts to reference the new file paths. Removed obsolete test agent and tool files. Improved instructions for running tests.
2026-04-08 06:36:46 +01:00
MervinPraison ed3a689f8c Release v4.5.115 2026-04-07 21:55:08 +01:00
MervinPraison 7313b64544 fix(ci): add Core vs Wrapper vs Tools repo routing to claude prompts
Explicitly tells Claude to clone and open PRs in MervinPraison/PraisonAI-Tools
when a request is for an external tool, integration, or community extension.
2026-04-07 21:50:59 +01:00
MervinPraisonandMervin Praison dacb1dc42c feat: add missing LLM and retrieval provider protocols
Add LLMProviderProtocol, ModelCapabilitiesProtocol, LLMRateLimiterProtocol,
and LLMFailoverProtocol to enable extensibility without vendor lock-in.

Add RetrievalStrategyProtocol to make retrieval strategies pluggable
instead of hardcoded enum values.

Addresses Gap #3 from architectural analysis - missing provider protocols.
Follows AGENTS.md protocol-driven architecture principles.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-07 20:48:55 +00:00
MervinPraisonandMervin Praison 74241c1459 security: fix critical path traversal and sandbox escape vulnerabilities
- Fix path traversal in TypeScript YAML parser: use proper path segment check instead of substring, ensure consistent validation/access paths
- Add missing getattr to Python sandbox blocking to prevent attribute access bypasses
- Enhance command sanitization: add missing pipe, semicolon, newline patterns
- Improve path validation: use robust Path.relative_to() instead of string prefix matching
- Fix type hints and remove unused imports in agent tools

Addresses security issues identified by code reviewers.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-04-07 20:46:16 +00:00
MervinPraison b1048da6a5 refactor: harden input validation and bump to 1.5.115/4.5.115 2026-04-07 21:45:23 +01:00
MervinPraison ffc8cb1bbc fix(ci): DRY consolidation — add issue-triage job for external users in claude.yml
- Added lightweight 'issue-triage' job that runs on ALL issues:opened events
- External users get: welcome comment + automatic label triage (enhancement/bug/question/etc)
- Owner/collaborator issues get: claude label auto-added → triggers code-fix job
- Removed 'allowed_non_write_users' (deprecated in claude-code-action@beta)
- claude-response job now skips 'opened' events (handled by triage job)
- Eliminates need for separate auto-issue-comment.yml intermediary (DRY)
2026-04-07 21:45:23 +01:00
MervinPraison eceaa96141 docs: auto-update feature parity trackers [skip ci] 2026-04-07 20:38:36 +00:00
Mervin Praison e632aadd42 Merge pull request #1284 from MervinPraison/fix/agent-init-type-hints
Fix/agent init type hints
2026-04-07 21:38:17 +01:00
MervinPraison bbcc505693 Release v4.5.114 2026-04-07 21:37:51 +01:00
MervinPraison cc0544a4e3 feat(context): enhance agent ledger and monitor functionality
- Improved `get_agent_ledger` method in `ledger.py` by adding validation
  for `agent_id` to raise a `ValueError` if it is empty.
- Updated `_get_output_path` in `monitor.py` to include detailed path
  sanitization steps and added checks to prevent path traversal attacks.
- Refined agent name sanitation and fallback techniques to ensure unique
  naming.
- Incremented version number to `1.5.114` in `pyproject.toml` and
  `uv.lock`.
2026-04-07 21:23:57 +01:00
Mervin Praison b8455205f6 Merge pull request #1278 from MervinPraison/fix/agent-init-type-hints
fix(types): widen type hints for agent init
2026-04-07 11:48:37 +01:00
MervinPraison a10af4e839 docs: auto-update feature parity trackers [skip ci] 2026-04-07 10:46:37 +00:00
Mervin Praison ff37cbcf6f Merge pull request #1279 from MervinPraison/claude/issue-1261-20260405-0507
Revert broken async sleep helpers in execution_mixin.py
2026-04-07 11:46:15 +01:00
copilot-swe-agent[bot]andMervinPraison a42c6d325e fix(types): restore type hints that have runtime backing, narrow those that don't
After rebase, commit 72e477f added runtime support for:
- context str/dict -> restore to type hint
- autonomy str -> restore to type hint
- approval dict -> restore to type hint
- learn str -> restore to type hint

templates str remains removed (resolve() with no presets returns None).
Updated all docstrings to document actual supported types including new forms.

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/e371c8e1-a816-4fb2-8eb8-a18242daa2de

Co-authored-by: MervinPraison <[email protected]>
2026-04-05 05:24:10 +00:00
copilot-swe-agent[bot]andMervinPraison 724e4af963 fix(types): address reviewer feedback on agent init type hints
- Narrow context hint: remove str/Dict (init has no handling for those)
- Narrow autonomy hint: remove str (no string-level preset handling)
- Narrow templates hint: remove str (resolve() returns None with no presets)
- Narrow approval hint: remove Dict (init only handles bool/str/ApprovalConfig/Protocol)
- Narrow learn hint: remove str; replace silent passthrough with a logged warning
- Update docstrings for output/execution/templates/learn to reflect actual supported types

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/e371c8e1-a816-4fb2-8eb8-a18242daa2de

Co-authored-by: MervinPraison <[email protected]>
2026-04-05 05:21:29 +00:00
MervinPraison 7cc9718c41 Fix missing Mount import in MCP server launch
- Add missing 'from starlette.routing import Mount' import
- Mount was used on line 1046 but not imported
- Fixes NameError when launching agents in MCP mode

🤖 Generated with Claude Code

Co-authored-by: MervinPraison <[email protected]>
2026-04-05 05:21:23 +00:00
MervinPraisonandMervin Praison 72e477f4a3 fix: implement runtime support for widened type hints in Agent init
- Add string preset handling in _init_autonomy() for autonomy='full_auto' etc.
- Add str/dict support in context_manager property for context='summarize' etc.
- Add dict-to-ApprovalConfig conversion for approval={'backend': ..., 'all_tools': True}
- Add string mode handling in learn parameter for learn='agentic' etc.
- Fix AUTONOMY_PRESETS to use 'level' field (not 'mode') per AutonomyConfig

Resolves type hint/implementation mismatches identified by code reviewers.
All new type hints now have proper runtime support.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-05 05:20:44 +00:00
copilot-swe-agent[bot]andMervinPraison ecfd858269 Remove broken _smart_sleep/_async_safe_sleep helpers; restore time.sleep() in sync methods
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/4e986fa2-76e0-466f-a027-82bf3291d31a

Co-authored-by: MervinPraison <[email protected]>
2026-04-05 05:18:48 +00:00
Mervin Praison 92c6690ab2 Merge pull request #1277 from MervinPraison/claude/issue-1276-20260404-1738
fix: resolve critical architecture gaps for multi-agent safety
2026-04-05 06:15:27 +01:00
MervinPraisonandMervin Praison efb92fdd2e Fix blocking time.sleep() calls in async code paths
Replace 6 blocking time.sleep() calls with async-safe alternatives to prevent event loop blocking in multi-agent workflows.

- Added _smart_sleep() utility that detects async context
- Added _async_safe_sleep() utility for async-safe sleeping
- Replaced blocking calls at lines 956, 959, 988, 997, 1056, 1061
- Maintains backward compatibility for sync contexts
- No new dependencies added
- Follows AGENTS.md principles: async-safe by default and performance-first

Fixes #1261

Co-authored-by: Mervin Praison <[email protected]>
2026-04-05 05:10:34 +00:00
MervinPraison 9c2f14e042 fix(types): widen type hints for agent init 2026-04-05 06:09:32 +01:00
MervinPraisonandClaude 8b0567262d fix: address review issues - tool factory performance and memory learn method
- Optimize tool factory: cache classes but create fresh instances (prevents import overhead while maintaining multi-agent safety)
- Fix memory core learn method: add missing return statement
- Address CodeRabbit feedback on unreachable code patterns

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-04-05 05:08:56 +00:00
MervinPraison d80bff2d9a Release v4.5.113 2026-04-05 05:56:45 +01:00
MervinPraison 3e29936e86 docs: auto-update api.md [skip ci] 2026-04-05 04:35:58 +00:00
34c6423dbc feat: Langfuse Observability Integration — CLI, LangfuseSink Adapter & Docker Hosting (#1271)
* feat: Add Langfuse observability integration

- Implement LangfuseSink adapter following TraceSinkProtocol
- Add complete praisonai langfuse CLI command group
- Support local (Docker) and remote Langfuse instances
- Add langfuse>=3.0.0 as optional dependency
- Maps ActionEvents to Langfuse trace/span/event API
- Thread-safe with lazy imports following AGENTS.md guidelines
- Zero changes to core SDK (praisonaiagents)

Closes #1270

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

* fix: Address review feedback — fix bugs, add tests, improve code quality

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/178e99c1-305d-436d-8385-6e9f0642fec2

Co-authored-by: MervinPraison <[email protected]>

* fix: Critical Langfuse integration fixes

- Update LangfuseSink to use Langfuse v3+ API (start_observation vs trace/span/event)
- Fix CLI port parsing for Docker Compose JSON format (Publishers is array not string)
- Fix config->test workflow: load ~/.praisonai/langfuse.env in test command
- Improve agent/tool span collision safety with unique keys
- Add proper UUID-based tool invocation tracking

Co-authored-by: MervinPraison <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-04-05 05:35:41 +01:00
MervinPraisonandMervin Praison 6b4e1b3590 fix: remove global shared state from observability hooks
- Remove _global_hooks, enable_observability(), disable_observability(), and get_hooks()
- Observability is now strictly per-agent/per-pipeline to comply with AGENTS.md Rule 4.5
- Agent class already correctly uses per-instance ObservabilityHooks() when observe=True
- Eliminates shared mutable state between agents for multi-agent safety

Addresses critical architecture gap identified in PR #1277 final review.

Co-authored-by: Mervin Praison <[email protected]>
2026-04-05 04:31:54 +00:00
MervinPraison 7eb01489fd docs: auto-update feature parity trackers [skip ci] 2026-04-05 04:24:45 +00:00
731e222a53 fix(ci): integrate Claude final review into auto-pr-comment workflow (#1272)
* fix(ci): integrate Claude final review directly into auto-pr-comment workflow to bypass action_required approval gate

* fix(ci): address review issues in auto-pr-comment workflow

- Fix template injection: use env vars + process.env instead of inline ${{ }} in JS
- Add timeout-minutes: 15 to polling jobs (prevent runaway Actions minutes)
- Add issues: write permission to all Claude-trigger jobs
- Restore claude-after-gemini job lost when chain-claude-after-copilot.yml was deleted
- DRY: extract polling constants and trigger login list to workflow-level env vars

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/0ff67a7f-85be-4e2e-a16f-f3f48ebfefb5

Co-authored-by: MervinPraison <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-04-05 05:24:28 +01:00
df50822ff6 fix: remove global logging monkey-patch from memory.py (#1268)
* fix: remove global logging monkey-patch from memory.py

Remove logging.Logger.trace monkey-patch that violated protocol-driven
architecture by modifying global state at import time. The trace
functionality remains available locally within the memory module.

This addresses one architectural violation from issue #1262 while
maintaining backward compatibility and keeping changes minimal.

Co-authored-by: Mervin Praison <[email protected]>

* fix: replace logger.trace() with logger.log(TRACE_LEVEL) after removing monkey-patch

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/56f044b6-1424-4985-b09b-afe047dadd8b

Co-authored-by: MervinPraison <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-04-05 05:24:26 +01:00
copilot-swe-agent[bot]andMervinPraison 7480c4389a docs: improve comment for _sanitize_metadata in store_short_term_async
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/5f375fcb-adf2-43cf-9f54-a664458ef3d7

Co-authored-by: MervinPraison <[email protected]>
2026-04-04 18:05:49 +00:00
copilot-swe-agent[bot]andMervinPraison 494cd8b44a fix: address review issues - fix task_prompt dead code, workflow_timeout enforcement, and memory async correctness
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/5f375fcb-adf2-43cf-9f54-a664458ef3d7

Co-authored-by: MervinPraison <[email protected]>
2026-04-04 18:05:02 +00:00
MervinPraisonandMervin Praison 2e733e16d7 fix: resolve critical architecture gaps for multi-agent safety
Fixes 3 critical architecture gaps identified in issue #1276:

**Issue 1: Global Shared Mutable State**
- Replace tool singleton cache with factory pattern for per-agent isolation
- Use _state_lock for atomic task status operations
- Add thread safety to observability hooks

**Issue 2: Unsafe Process Orchestration**
- Fix context accumulation bug in task descriptions
- Add bounded retry counter with configurable max_retries
- Add workflow timeout support for production safety

**Issue 3: Async/Sync Boundary Violations**
- Add async memory operations using asyncio.to_thread()
- Prevent event loop blocking in memory auto-promotion

Changes maintain protocol-driven design with no new dependencies.
All fixes are backward compatible and follow AGENTS.md guidelines.

Resolves #1276

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Mervin Praison <[email protected]>
2026-04-04 17:46:13 +00:00
MervinPraison 97141c75bd docs: auto-update feature parity trackers [skip ci] 2026-04-04 07:38:01 +00:00
c660495911 fix: address critical architecture gaps vs core philosophy (#1274)
* fix: address critical architecture gaps vs core philosophy

Fixes 3 critical architecture violations:

Gap 1: YAML Feature Parity - Added missing features
- Added autonomy, guardrails, approval support to YAML configuration
- Updated agents_generator.py to extract and pass these configs to PraisonAgent
- Added CLI field mapping (--trust -> approval, --guardrail -> guardrails)

Gap 2: Agent Constructor Parameter Cleanup - Improved API
- Replaced Optional[Any] type annotations with proper Union types
- Added forward references for better IDE support and type checking

Gap 3: Database Implementation Configuration - Made constants configurable
- Added tool_output_limit field to OutputConfig (default 16000)
- Made hardcoded DEFAULT_TOOL_OUTPUT_LIMIT configurable via OutputConfig
- Updated agent.tool_output_limit usage in tool_execution.py

All changes maintain backward compatibility and follow AGENTS.md guidelines.
Tested: Basic agent creation, YAML parsing, configurable limits work correctly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

* docs: update _merge_cli_config docstring to list new agent-level fields

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/908b1ad0-b9c1-4fda-a330-bba68180b90c

Co-authored-by: MervinPraison <[email protected]>

* fix: address all critical issues identified by code reviewers

- Fix type annotations in Agent.__init__ to include missing str, dict types
- Add missing TYPE_CHECKING imports for forward references
- Fix YAML approval dict handling to prevent runtime crashes
- Add safe fallback for ToolExecutionMixin.tool_output_limit
- Forward missing CLI flags (autonomy, guardrail, approval) to YAML config
- Add validation to OutputConfig.tool_output_limit
- Fix unused variable warning in field mappings loop

Resolves issues found by CodeRabbitAI, Qodo, Gemini, and Copilot reviewers.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-04-04 08:37:43 +01:00
MervinPraison 0cb35a4fe3 fix(security): patch path traversal and tar slip vulnerabilities in recipe registry
- Add _safe_extractall() to reject absolute paths, '..' traversal, and path escape in tar archives
- Validate name/version from manifest BEFORE any filesystem operation in LocalRegistry.publish
- Add path confinement check ensuring resolved recipe_dir stays under recipes_path
- Validate URL path params in RegistryServer publish endpoint (early 400 rejection)
- Replace vulnerable tar.extractall() calls in both LocalRegistry and HttpRegistry
- Bump version to 4.5.113
2026-04-03 08:15:18 +01:00
MervinPraison 3622b1a3d9 docs: auto-update feature parity trackers [skip ci] 2026-04-02 21:56:14 +00:00
20d418e1de Fix: Thread-safe global singletons for multi-agent safety (#1264)
* fix: Add thread-safe locks to global singletons for multi-agent safety

- Added threading.Lock protection to 10+ global singleton instances
- Fixed race conditions in OpenAI client creation (_global_client_lock)
- Protected error_logs, callback registries with locks in main.py
- Made telemetry, trace emitter, circuit breaker thread-safe
- Converted eager singletons to lazy initialization (health_monitor, circuit_breaker)
- Fixed approval registry with lazy __getattr__ pattern
- Added locks to schedule store and hook registry

Resolves thread-safety violations per AGENTS.md section 4.5:
'No shared mutable global state between agents'

Co-authored-by: Mervin Praison <[email protected]>

* fix: Complete thread safety for remaining singleton race conditions

- OpenAI client: Move return statement inside lock block (get_openai_client)
- Schedule tools: Add missing threading import to prevent NameError
- Telemetry: Protect enable_telemetry() with instance lock

Addresses critical race conditions identified in code review.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-04-02 22:55:58 +01:00
375 changed files with 37800 additions and 2151 deletions

No files matched your search

+57 -28
View File
@@ -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
+7 -5
View File
@@ -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

+72
View File
@@ -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".
+116
View File
@@ -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]
+3
View File
@@ -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.
+397 -14
View File
@@ -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
+11 -3
View File
@@ -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
+2
View File
@@ -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'
})
+6 -3
View File
@@ -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
View File
@@ -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:*)
+4
View File
@@ -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 }}
+4
View File
@@ -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
+72
View File
@@ -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
+2 -1
View File
@@ -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:
+2
View File
@@ -22,6 +22,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
+46 -20
View File
@@ -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
+13 -4
View File
@@ -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
+2 -1
View File
@@ -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:
+8
View File
@@ -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
+4
View File
@@ -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
+16 -2
View File
@@ -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
+20 -8
View File
@@ -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)"
+2 -1
View File
@@ -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:
+1 -1
View File
@@ -94,7 +94,7 @@ Cargo.lock
**/*.rs.bk
*.pdb
.windsurf
.agent
src/praisonai/build*.egg-info/
src/praisonai/PraisonAI.egg-info/
jobs/*
+74 -96
View File
@@ -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:
+37 -2
View File
@@ -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>
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.5.112" \
"praisonai>=4.6.3" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.5.112" \
"praisonai>=4.6.3" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.5.112" \
"praisonai>=4.6.3" \
"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!")
+123
View File
@@ -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!")
+129
View File
@@ -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.")
+340
View File
@@ -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)
+5
View File
@@ -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!")
+180
View File
@@ -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
+4 -1
View File
@@ -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:
+4 -1
View File
@@ -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:
+140
View File
@@ -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)
+83
View File
@@ -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}"
+22
View File
@@ -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)
+170
View File
@@ -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)
+274
View File
@@ -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 375 files, more files were not shown because too many files have changed in this diff. Show more