Compare commits

...
Author SHA1 Message Date
MervinPraison fe87b9514e feat: add deep profiling benchmarks for Agent initialization
- deep_profile.py: cProfile analysis for 5000 agent creations
- deep_profile_bench.py: Combined timing and profiling for 1000 agents
- Helps identify performance bottlenecks in Agent.__init__
2026-04-02 19:49:16 +01:00
MervinPraison fec0b5fa32 feat: add @lru_cache to get_defaults_config for performance
- Cache defaults config to avoid repeated file I/O
- Add performance test files for benchmarking
- Further optimizes Agent initialization performance
2026-04-02 19:17:03 +01:00
MervinPraison c8a3881dda feat: JIT hydration optimization for Agent initialization
- Store raw params in __init__ for lazy config resolution
- Update OUTPUT, EXECUTION, CACHING, MEMORY, KNOWLEDGE, PLANNING, REFLECTION, WEB to use raw params
- Reduce eager config resolution overhead
- Benchmark shows 30µs initialization (down from previous baseline)
- Add smoke test for JIT hydration
- Phase 1 of incremental optimization plan

Fixes #performance
2026-04-02 18:51:31 +01:00
MervinPraison bfe3d94bad Release v4.5.112 2026-04-02 10:36:06 +01:00
7ae6c6d19c fix(sandbox): update sandlock integration to use real API surface (#1259)
* fix(sandbox): update sandlock integration to use real API surface

- Replace sandlock.is_available() with landlock_abi_version() >= 6 check
- Remove non-existent TimeoutError and SecurityViolationError exception handling
- Remove invalid max_cpu_time_seconds parameter from Policy constructor
- Update sandbox.run() to only pass cmd and timeout (remove env/cwd)
- Update tests to use correct API mocks

Fixes #1258

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

* fix(sandbox): fix ABI version threshold, remove dead except block, unused var, and fix test mocks

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/ee8122be-e5f3-47dd-bcb6-c0994df15169

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

* fix(sandbox): implement proper sandlock Result status mapping and improve test reliability

- Fix exit_code status mapping: Check result.success and exit_code before returning SandboxStatus.COMPLETED
- Update test mocks to return proper Result objects with success/exit_code instead of raising exceptions
- Add integration test that exercises real sandlock API if available
- Address reviewer feedback from CodeRabbit, Qodo, Gemini, and Copilot

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

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-02 10:34:25 +01:00
MervinPraison cc962c96d4 fix(workflows): robustly match copilot bot string to correctly chain claude 2026-04-02 10:29:06 +01:00
MervinPraison d5a55aaf4b Release v4.5.111 2026-04-02 07:47:12 +01:00
MervinPraison a02a7209a9 Allow external users to trigger Claude Code 2026-04-02 07:31:57 +01:00
MervinPraison 0e55b36056 refactor(tools): replace list_directory with list_files and tidy code
Updated the tools in multiple example scripts to use `list_files` instead of `list_directory`. Removed unnecessary global variables and functions in `shell_tools.py` for better clarity and performance. Updated the `approval` flag in agent configurations to `False` for consistency across examples.
2026-04-01 21:01:39 +01:00
MervinPraison d89e467257 feat(docs): add image and quote from Elon Musk to README
Added a new image highlighting PraisonAI shared by Elon Musk, along with a quote for better engagement and visibility.
2026-04-01 20:46:14 +01:00
MervinPraison 1e2a5ef12d chore(workflows): update email configuration across workflows
Updated the user email in multiple GitHub workflows to a no-reply address for better privacy. This change ensures consistency across the repository's automation scripts.

fix(examples): update web configuration in agent examples

Modified web configuration in `agent_centric_api.py` examples to utilize `WebConfig`. This enhances clarity and control over the web search functionality.

test(unit): skip integration test without API key

Added a condition to skip the `test_circuit_breaker_real_agentic` if the OpenAI API key is not set. This prevents unnecessary test failures in the absence of required credentials.
2026-04-01 20:38:47 +01:00
MervinPraison de069c2cbf Release v4.5.110 2026-04-01 20:26:06 +01:00
MervinPraison ff599795f8 test: Fix evaluator class name in live tests 2026-04-01 20:01:42 +01:00
9164555a7a fix: address code review feedback for mixin framework (#1256)
* fix: address code review feedback for mixin framework

- Add ChatMixin, ExecutionMixin, MemoryMixin exports to __init__.py
- Remove unused imports from memory_mixin.py (threading)
- Align achat() method signature with chat() parameters
- Ensure consistent API between sync/async variants

Addresses reviewer feedback while maintaining backward compatibility.

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

* fix: forward new achat() params to _achat_impl and implement them

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/03002903-d3fc-46e9-8ad7-4b1fafc70a4e

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]>
Co-authored-by: MervinPraison <[email protected]>
2026-04-01 19:58:24 +01:00
b26c44ad90 feat: Complete Agent God Class Decomposition with Unified Mixin Framework (#1257)
* feat: Complete Agent God Class Decomposition with Unified Mixin Framework

- Complete mixin framework: ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin, ExecutionMixin, MemoryMixin
- Created missing ChatHandlerMixin for chat coordination & response processing
- Created missing SessionManagerMixin for session lifecycle & state management
- Updated Agent class inheritance to follow specification order
- Maintain 100% backward API compatibility with all 34+ __init__ parameters
- Performance: Import time 59.20ms (target <200ms achieved)
- Zero breaking changes - all existing public methods preserved
- Agent class reduced from ~8,915 to 4,341 lines through proper decomposition

Resolves #1254

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

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

* fix: correct time import bug, remove raw error leakage in ChatHandlerMixin

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/1cacd729-25d5-42df-a628-782502d1b3f1

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

* fix: handle None agent names in mixins to prevent log/session ID issues

- Fix ChatHandlerMixin to use proper None check for agent names
- Fix SessionManagerMixin to sanitize session IDs and prevent None prefixes
- Ensure consistent 'Agent' fallback across both mixins for nameless agents
- Address reviewer feedback from qodo-code-review and gemini-code-assist

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]>
Co-authored-by: MervinPraison <[email protected]>
2026-04-01 19:56:50 +01:00
4ee7d298c8 feat(sandbox): add SandlockSandbox for kernel-level isolation (#1255)
* feat(sandbox): add SandlockSandbox for kernel-level isolation

Implements SandlockSandbox that provides kernel-level code execution
isolation using Landlock + seccomp-bpf, addressing security gaps in
the current subprocess-based sandbox.

Security improvements:
- Landlock filesystem allowlisting (blocks /etc/passwd, SSH keys, etc.)
- seccomp-bpf syscall filtering
- Network domain/port control
- Resource limits with ~5ms overhead
- No Docker or root required

Features:
- Implements existing SandboxProtocol for drop-in compatibility
- Optional dependency with graceful fallback to SubprocessSandbox
- Comprehensive test suite with security violation testing
- Example and documentation included

Resolves: #1253

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

* fix(sandbox): address review findings in SandlockSandbox

- Security: replace lstrip('/') with _safe_sandbox_path() using os.path.realpath()
  to block path-traversal in write_file/read_file/list_files; tighten check to
  reject paths resolving exactly to the sandbox root (directory writes)
- Correctness: replace deprecated asyncio.get_event_loop() with get_running_loop()
- DRY: extract shared execution logic into _run_sandlocked() private helper
- Dead code: remove unused self._sandbox attribute and code_file variables
- Simplify: is_available uses self._sandlock directly (import enforced in __init__)
- Tests: fix unreliable builtins.__import__ patch; update asyncio mocking to
  get_running_loop; add path-traversal tests; fix SecurityViolationError test to
  use distinct exception classes

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/c46e5ff2-ff2d-4bc7-9bcc-5426981ef322

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

* chore: remove accidentally committed egg-info from PR

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/c46e5ff2-ff2d-4bc7-9bcc-5426981ef322

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]>
Co-authored-by: MervinPraison <[email protected]>
2026-04-01 19:47:27 +01:00
MervinPraison 1ae86076a9 fix(ci): strictly bind claude assistant invocation to explicit @claude mentions in issue comments 2026-04-01 19:38:27 +01:00
MervinPraison 7def9a68fa chore(ci): consolidate claude workflow entirely into claude.yml and disable all competing agents 2026-04-01 19:31:13 +01:00
dependabot[bot] 99cdf47b09 chore(deps): bump lodash (#1229)
Bumps the npm_and_yarn group with 1 update in the /src/praisonai directory: [lodash](https://github.com/lodash/lodash).


Updates `lodash` from 4.17.21 to 4.17.23
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 19:15:23 +01:00
7bb1ca97f5 fix: add defensive wrappers for mem0 MongoDB vector store compatibility (#1251)
- Add _safe_mem0_search() wrappers in Memory and Mem0Adapter classes
- Catch TypeError: 'unexpected keyword argument vectors' gracefully
- Return empty results instead of crashing on MongoDB incompatibility
- Add informative warnings referencing upstream mem0 bug #3185
- Recommend Qdrant/Chroma as alternative vector store backends
- Maintain full backward compatibility for working configurations

Addresses upstream mem0 issue: https://github.com/mem0ai/mem0/issues/3185
Fixes: mem0 MongoDB vector store broken — signature mismatch + Atlas-only requirement

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

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-04-01 19:13:05 +01:00
9ba435f147 feat: enhance evaluation framework with EvalSuite, ComparisonEval, and SafetyEval (#1246)
* feat: enhance evaluation framework with EvalSuite, ComparisonEval, and SafetyEval

- Add EvalSuite orchestrator for running multiple evaluations with aggregated results
- Add ComparisonEval for side-by-side A/B testing of agent outputs across criteria
- Add SafetyEval for detecting harmful/biased/inappropriate content across 6 categories
- Update eval/__init__.py with new exports and lazy loading
- Add comprehensive unit tests for all new components (150+ test cases)
- Follow protocol-driven architecture with zero performance impact
- Maintain backward compatibility with existing eval framework

Addresses #1245

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

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

* fix(eval): resolve critical issues and add real agentic tests

- Add missing _make_llm_call method to BaseLLMGrader for ComparisonGrader and SafetyGrader
- Fix after_run() calls to pass required result parameter
- Fix winner parsing logic to prevent 'Agent B' misclassification as 'agent_a'
- Fix safety score parsing to prevent LLM override of worse category scores
- Fix EvalSuite fail_fast behavior to properly re-raise RuntimeError
- Add support for AccuracyResult avg_score in EvalSuite scoring
- Fix Python type hints compatibility (tuple[str,str] → tuple)
- Add comprehensive real agentic integration tests that call agent.start()

Addresses all critical issues identified in code reviews and fulfills
AGENTS.md Section 9.4 requirement for real agentic tests.

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

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
2026-04-01 19:11:52 +01:00
MervinPraison 8582562e45 docs: auto-update api.md [skip ci] 2026-04-01 18:09:14 +00:00
Mervin Praison 76c1248e38 Merge pull request #1252 from MervinPraison/claude/issue-1250-20260401-1630
feat: move multi-backend storage adapters to wrapper following AGENTS.md
2026-04-01 19:08:58 +01:00
copilot-swe-agent[bot]andMervinPraison 4ca5867075 fix: address security and correctness issues in storage adapters
- PostgreSQL: validate table name against safe identifier regex to
  prevent SQL injection via user-controlled table parameter
- MongoDB: escape regex metacharacters in list_keys prefix to prevent
  ReDoS / unexpected pattern matching
- MongoDB: use $setOnInsert for created_at so it is preserved on
  update (was being overwritten on every save)
- DynamoDB: replace raw string FilterExpression with proper Boto3
  Attr().begins_with() condition in list_keys
- DynamoDB: remove redundant boto3 import in _create_table (already
  imported in _get_table)

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/c012c607-294a-4cef-864d-91a2bccca2dd

Co-authored-by: MervinPraison <[email protected]>
2026-04-01 17:37:40 +00:00
MervinPraison ce7d9815aa fix: implement proper lazy loading for storage adapters
- Remove eager module-level imports of heavy backends (Redis/MongoDB/PostgreSQL/DynamoDB)
- Implement lazy loading in get_backend() function using getattr() only when needed
- Update __all__ exports to only include lightweight backends
- Add module-level __getattr__ for backward compatibility
- Follow praisonaiagents/db pattern for proper separation of core vs wrapper

Fixes critical architectural violation where heavy wrapper dependencies
were being imported at module level, breaking the lazy loading design.

Core SDK imports now work without wrapper package (< 50ms import time).

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

Co-authored-by: Mervin Praison <[email protected]>
2026-04-01 17:27:48 +00:00
MervinPraison 6a531ac1da feat(ci): unify claude evaluation prompts to process ALL bot reviewers 2026-04-01 18:15:06 +01:00
MervinPraison cc6116b888 feat(ci): enhance claude-after-gemini prompt for strict issue fixing 2026-04-01 18:13:22 +01:00
MervinPraison d933eebeaf feat(ci): auto-trigger Claude after Gemini code assist 2026-04-01 18:08:45 +01:00
MervinPraison 2c683af1cd fix(ci): trigger review chain on triage-agent PRs 2026-04-01 18:03:39 +01:00
MervinPraison a1b6989840 docs(workflows): codify manager-agent philosophy for issue triage 2026-04-01 17:48:15 +01:00
MervinPraisonandMervin Praison 98a878673a feat: move multi-backend storage adapters to wrapper following AGENTS.md
- Move Redis, MongoDB, PostgreSQL, and DynamoDB storage adapters from core SDK to wrapper
- Implement lazy proxy pattern following praisonaiagents.db model
- Add optional dependencies [redis], [mongodb], [postgresql], [dynamodb] to wrapper
- Core SDK now protocol-driven only, heavy implementations in wrapper
- Resolves architectural violations from PRs #1241, #1247, #1248

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

Co-authored-by: Mervin Praison <[email protected]>
2026-04-01 16:39:08 +00:00
MervinPraison cec46053ab fix(ci): wire gh_token for workflow cascading and allowlist triage bot 2026-04-01 17:12:25 +01:00
MervinPraison ad7445c528 feat(ci): mandate AGENTS.md reading in gemini code modification prompts 2026-04-01 11:56:10 +01:00
MervinPraison 3e2909b926 feat(ci): mandate PR creation and AGENTS.md reading in claude-code-action prompt 2026-04-01 11:54:54 +01:00
MervinPraison ad32470de4 fix(ci): wire claude pro oauth token into triage workflows 2026-04-01 10:38:28 +01:00
MervinPraison 5dd9bdb96f chore: migrate github actions to use dedicated github app for rate limit bypass 2026-04-01 10:06:45 +01:00
MervinPraison 29b0a6f02e docs: auto-update feature parity trackers [skip ci] 2026-04-01 08:34:46 +00:00
MervinPraison 7a9788c311 fix(qa): resolve missing ephemeral contextmanager and test_mixin_inheritance following god class decomposition 2026-04-01 09:34:29 +01:00
MervinPraison 055d9f5d88 chore: add id-token permission to claude-issue-triage workflow and document OIDC requirements in review-chain instructions 2026-04-01 09:34:29 +01:00
MervinPraison b27ae3a79a feat: extract agent god class into clean chat, execution, memory, and tool mixins 2026-04-01 09:34:29 +01:00
Mervin Praison d0a4796742 Merge pull request #1239 from Sundiver1/fix/pin-axios-supply-chain
Fix: pin axios to 1.8.4 to prevent supply chain compromise
2026-04-01 09:09:38 +01:00
Chris Braley a3ce0f69e3 fix: pin axios to 1.8.4 to avoid supply chain compromise
Compromised versions 1.14.1 and 0.30.4 were removed from npm registry
but semver ranges (^1.7.9) could resolve to malicious versions if
they're re-released or exist in alternate registries.

Pinning to exact version 1.8.4 prevents this attack vector entirely.
2026-04-01 01:55:33 -05:00
Mervin Praison 19929dd9de Merge pull request #1238 from MervinPraison/fix/gha-claude-oidc-token
fix(gha): Add id-token write permission and switch to GH_TOKEN for Claude actions
2026-04-01 07:36:29 +01:00
MervinPraison 36c2e76ca4 fix(gha): Add id-token write permission and switch to GH_TOKEN for Claude actions
Fixes OIDC token fetch failure and 401 Bad Credentials errors in claude.yml and claude-issue-triage.yml.
2026-04-01 07:36:24 +01:00
Mervin Praison a0e4a765ff Merge pull request #1237 from MervinPraison/fix/dynamic-interactive-runtime
feat: Dynamically load InteractiveRuntime for ACP/LSP in YAML workflows
2026-04-01 07:26:38 +01:00
MervinPraison 18a3d26620 feat: Dynamically load InteractiveRuntime for ACP/LSP in YAML workflows 2026-04-01 07:26:33 +01:00
Mervin Praison cbec55cf44 fix: Update valid inputs for claude-code-action in issue triage (#1236)
fix: Update valid inputs for claude-code-action in issue triage
2026-04-01 07:22:32 +01:00
MervinPraison f96eebb5e8 fix: Update valid inputs for claude-code-action in issue triage
The base claude-code-action does not accept 'prompt_file' or 'install_github_mcp'.
This routes the prompt properly through 'direct_prompt' using GitHub step outputs.
2026-04-01 07:22:19 +01:00
Mervin Praison 6fa8eebab1 fix: Cleanup dead acp_enabled and lsp_enabled code from YAML workflow generator (#1234)
fix: Cleanup dead acp_enabled and lsp_enabled code from YAML workflow generator
2026-04-01 07:15:46 +01:00
MervinPraison 9702a2a99c fix: Cleanup dead acp_enabled and lsp_enabled code from YAML workflow generator
Closes #1233
Removes dead code path to ensure alignment with AgentTeam __init__ signature.
2026-04-01 07:15:21 +01:00
MervinPraison 969c21f617 Release v4.5.109 2026-04-01 07:03:13 +01:00
MervinPraison 8b5b67852a docs(README): fix list formatting for TAVILY_API_KEY instruction
Adjusted the formatting of the TAVILY_API_KEY instruction for consistency.
2026-03-31 23:30:30 +01:00
MervinPraison 019ecea1a4 docs(README): add environment variable setup instruction
Added a line to specify the export command for setting the TAVILY_API_KEY. This addition improves clarity for users configuring the environment.
2026-03-31 23:30:03 +01:00
MervinPraison fab53bf095 Release v4.5.108 2026-03-31 23:25:49 +01:00
MervinPraison d6bcd522ed docs: rename AgentClaw to PraisonAI Claw in README 2026-03-31 23:14:09 +01:00
MervinPraison 8769a3bdb8 Release v4.5.107 2026-03-31 23:01:40 +01:00
MervinPraison bfda2ff1db Merge branch 'main' of https://github.com/MervinPraison/PraisonAI 2026-03-31 23:00:00 +01:00
MervinPraison 0a7d4187ad feat(docs): add PraisonAI UI installation instructions and details
Updated README.md to include a new section for the PraisonAI UI,
providing installation instructions and a brief description.
Bumped version numbers in pyproject.toml and lock files to
reflect changes and dependency updates.
2026-03-31 22:59:52 +01:00
MervinPraison b0222c4fae docs: auto-update api.md [skip ci] 2026-03-31 21:55:00 +00:00
MervinPraison 1c901be114 Release v4.5.106 2026-03-31 22:54:43 +01:00
MervinPraison 534594103d Release v4.5.105 2026-03-31 22:34:12 +01:00
MervinPraison 17d7ddd0f7 Release v4.5.104 2026-03-31 22:11:57 +01:00
MervinPraison c546d2c2c9 chore(.github): update agentflow.gif with larger file size for better quality 2026-03-31 22:06:10 +01:00
MervinPraison 13267e5325 Release v4.5.103 2026-03-31 22:04:13 +01:00
MervinPraison f188ae1ff9 Release v4.5.102 2026-03-31 21:37:29 +01:00
MervinPraison 17ca3f8f48 Release v4.5.101 2026-03-31 21:16:53 +01:00
MervinPraison ed76058630 Release v4.5.100 2026-03-31 19:31:04 +01:00
MervinPraison 9b50b4069f feat(PraisonAI): update version and enhance agent memory functionality
Updated package version to 1.5.99 and introduced new memory handling
features in PraisonAI components. Added configuration for session
tracking in memory save. Included additional tests for agent and task
components to ensure correct functionality and integration.
2026-03-31 19:12:12 +01:00
MervinPraison 9ad41a8ec5 Release v4.5.98 2026-03-31 18:54:52 +01:00
MervinPraison 3f77e93840 Release v4.5.96 2026-03-31 18:46:49 +01:00
MervinPraison 06eb6b197c Release v4.5.97 2026-03-31 16:23:39 +01:00
MervinPraison d476379f45 chore: bump praisonaiagents to 1.5.95 2026-03-31 16:23:06 +01:00
MervinPraison d1034adc19 refactor: harden input validation and access controls 2026-03-31 16:20:54 +01:00
MervinPraison ee8d0cf4c3 chore(migration): remove outdated MIGRATION.md file
Deleted the deprecated migration guide as it is no longer relevant.
2026-03-31 16:15:17 +01:00
MervinPraison acad479b19 chore(images): move assets to .github folder and update paths
Relocated image assets from the public directory to the .github/images directory. Updated references in README files to reflect the new asset locations. Added a new GIF asset for showcasing command execution. Total of 14 files changed with 16 insertions and 8 deletions.
2026-03-31 16:13:35 +01:00
MervinPraison 1eb22d74e9 Release v4.5.96 2026-03-31 15:47:09 +01:00
MervinPraison 381c7448ea Release v4.5.95 2026-03-31 15:39:40 +01:00
MervinPraison 13c984d1aa Release v4.5.94 2026-03-31 15:21:59 +01:00
MervinPraison b04513493e docs: auto-update api.md [skip ci] 2026-03-31 13:52:11 +00:00
MervinPraison 3a209d7c36 Release v4.5.93 2026-03-31 14:51:48 +01:00
MervinPraison eb15d66e96 ci: add architecture validation guardrails to Claude agent prompt
- STEP 0: Set git identity to MervinPraison for all commits
- STEP 2: Mandatory bloat validation checklist before writing code
- Checks: protocol-driven design, no heavy impls in core SDK, no duplication,
  proper Agent inheritance, lazy imports, no agent.py growth, no name collisions
- If checks fail: close issue with explanation instead of creating bad PRs
2026-03-31 13:18:22 +01:00
MervinPraison 54ddbaf298 ci: use PAT_TOKEN for claude issue triage so comments appear as MervinPraison 2026-03-31 12:55:03 +01:00
MervinPraison 18b7dc1687 ci: use PAT_TOKEN for claude action so PRs open as MervinPraison 2026-03-31 12:41:08 +01:00
MervinPraison 10869d7570 ci: fix duplicate claude action runs on issue labeled events 2026-03-31 12:38:45 +01:00
MervinPraison c796f75b41 update 2026-03-31 12:24:09 +01:00
MervinPraison 79cda2604b Release v4.5.90 2026-03-31 11:07:00 +01:00
MervinPraison 7659748515 chore: bump praisonaiagents to 1.5.91 2026-03-31 11:04:38 +01:00
MervinPraison aa092c2c3f chore: bump praisonaiagents to 1.5.90 2026-03-31 11:00:34 +01:00
MervinPraison dc69cbc121 refactor: harden input validation and query parameterization
- Strengthen attribute access guards in code execution sandbox
- Parameterize SQL IN clauses in data layer
- Add URL scheme validation for API base URLs
- Use plain string matching instead of regex for tool search
2026-03-31 10:59:47 +01:00
MervinPraison 60121929d0 docs: auto-update api.md [skip ci] 2026-03-31 09:15:54 +00:00
Mervin Praison 6646dd1a4c refactor: SDK Architecture - Centralize Logging and Retry Logic (#1212)
Merges centralized logging (_logging.py, 152 files migrated) and resilience utility (utils/resilience.py). Also fixes CI/CD workflows and test fixtures.

Closes #1131 #1132
2026-03-31 10:15:32 +01:00
MervinPraison ba3a3c3ee0 fix(ci): optimize CI/CD workflows and harden test fixtures
- Remove unittest.yml (517-line debug file, always failed exit 5)
- Fix test-core.yml: deprecated codecov 'file:' -> 'files:' input
- Fix test-optimized.yml: move secrets from inline shell expressions
  to env: blocks to resolve linter context access warnings
- Fix test_serve_integration.py: replace 6x hardcoded /Users/praison paths
  with dynamic pathlib resolution (portable on any CI runner)
- Fix test_tracker_complex_tasks.py: DEFAULT_TOOLS -> AUTONOMY_DEFAULT_TOOLS
  (symbol was renamed in tracker.py, causing ImportError in CI)

Resolves GitHub Actions lint IDs:
7c7e1668 1d881864 aa30b182 a28bb9f7 ca2c8cc1 132d4548
a2735061 d3729ccb 333ae3f5 f3aca47c 7353e2c3 6d3216de
2026-03-31 09:51:45 +01:00
MervinPraison b965bb1011 Release v4.5.89 2026-03-31 09:45:58 +01:00
MervinPraison 4d13ab4af7 fix(logging): improve logger level settings across multiple modules
Updated logger level settings to use consistent DEBUG and WARNING levels
based on the verbosity of the application. This enhances logging clarity
and ensures that third-party loggers are set to WARNING by default.
Adjusted imports to maintain consistent logging behavior across various
modules for better maintainability. Additionally, bumped version in
pyproject.toml to 1.5.89.
2026-03-31 09:45:22 +01:00
MervinPraison 84c4d221a2 refactor: centralize logging and retry logic (#1131, #1132) 2026-03-31 08:59:15 +01:00
MervinPraison 64ae40e369 docs: auto-update feature parity trackers [skip ci] 2026-03-31 07:02:09 +00:00
Mervin Praison 0f5849b734 Merge pull request #1184 from MervinPraison/claude/remaining-thread-safety-fixes
Tool registry thread safety - MERGEABLE, covers gap not addressed by #1175/#1176. Unit tests: 4473 passed. Integration: 294 passed.
2026-03-31 08:01:58 +01:00
Mervin Praison 3a2a2f8592 Merge pull request #1185 from MervinPraison/claude/fix-agent-type-hints-20260330
Type hints PR - MERGEABLE, low risk, improves IDE support. Unit tests: 4473 passed. Integration: 294 passed.
2026-03-31 08:01:49 +01:00
MervinPraison 3193d19a5c Release v4.5.88 2026-03-31 07:16:42 +01:00
MervinPraison 3468f7743f fix(pyproject.toml, uv.lock): update version to 1.5.88
Bumped the version number for praisonaiagents from 1.5.87 to 1.5.88 in both pyproject.toml and uv.lock files to reflect the latest changes.
2026-03-31 07:16:08 +01:00
MervinPraison a5dc5fae0e feat(examples): add social media cross-posting and video generation examples
- Introduced a new agent for cross-posting on multiple social media platforms with required environment variables.
- Added a video generation example using HeyGen and ElevenLabs, demonstrating autonomous video production.
- Created YAML workflow for sequential multi-agent processing in video generation.
- Updated several test files to accommodate new features and examples.
2026-03-31 07:12:52 +01:00
MervinPraison ffd78f0eb6 ci: increase core test timeout to 20min (was timing out at 10min) 2026-03-31 05:53:05 +01:00
MervinPraison 727967f459 docs: auto-update api.md [skip ci] 2026-03-31 04:42:26 +00:00
MervinPraison 9f45a221e6 fix: critical post-merge fixes for agent.py, sandbox, circuit breaker, and test infrastructure
## Critical Fixes:
- **agent.py (IndentationError)**: Removed 137 lines of orphaned MCP code (lines 8849-8983)
  that caused an IndentationError on import, completely breaking the SDK. This was
  dead code from a bad merge — partially commented out code with live import statements.

- **python_tools.py (Sandbox)**: Fixed subprocess sandbox that was too restrictive:
  - Added missing builtins: getattr, ValueError, TypeError, KeyError, etc.
  - Added __build_class__ for function/class definition support
  - Fixed exec() to use shared globals so defined functions can see each other
  - Added _safe_getattr wrapper in subprocess for defense-in-depth dunder blocking

- **circuit_breaker.py**: Fixed reset_all() to clear registry entries (not just reset state)

## Test Fixes:
- test_circuit_breaker.py: Fixed success_threshold mismatch, added setup/teardown cleanup
- test_tools_reliability.py: Fixed psutil mock injection for lazy-imported modules
- test_python_tools_sandbox.py: Updated security test expectations for sandbox mode
- Added __init__.py files to 13 test subdirectories for pytest module resolution

## Metrics:
- agent.py: 9053 → 8916 lines (-137 lines of dead code)
- Import time: 67.5ms (well under 200ms target)
- Unit tests: 4473 passed, 50 skipped (7 pre-existing failures)
2026-03-31 05:42:04 +01:00
MervinPraison 3924bcea86 docs: auto-update api.md [skip ci] 2026-03-30 23:06:51 +00:00
github-actions[bot]andMervin Praison 8dc3136af8 fix: resolve async/concurrency safety violations in core SDK (#1206)
- Fix asyncio.run() from async context in context/aggregator.py using ThreadPoolExecutor
- Add async version of Gemini research polling using asyncio.sleep() instead of time.sleep()
- Replace deprecated asyncio.get_event_loop() with asyncio.get_running_loop() in async contexts
- Update core modules: agent/, approval/, context/, memory/, tools/, llm/
- Maintain backward compatibility with proper sync-to-async error handling
- Ensure multi-agent workflows no longer block event loop during polling

Resolves #1162

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:06:22 +01:00
github-actions[bot]andMervin Praison 0e80a58ccd refactor: decompose monolithic agent.py and memory.py into focused modules (#1204)
- Split agent.py (8920 lines) into 3 focused mixins:
  - tool_execution.py - Tool calling and execution logic
  - chat_handler.py - Chat and conversation management
  - session_manager.py - Session persistence and state management

- Split memory.py (2023 lines) into 4 focused modules:
  - core.py - Core Memory class and quality scoring
  - storage.py - Storage and persistence functionality
  - search.py - Search and retrieval operations
  - utils.py - Shared utility functions

- Maintained 100% backward compatibility via mixin inheritance
- Updated __init__.py files with lazy loading for new modules
- Follows protocol-driven architecture from AGENTS.md

Resolves #1139

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:06:14 +01:00
github-actions[bot]andMervin Praison 751f9b6f77 feat: implement protocol-driven architecture for core SDK (#1202)
- Add HttpLauncherProtocol and McpLauncherProtocol in agent/protocols.py
- Refactor Agent.launch() to delegate to protocol-specific launchers
- Create memory/adapters/ directory with SqliteMemoryAdapter
- Extract MongoDBMemory to knowledge/adapters/mongodb_adapter.py
- Update Knowledge class to use lazy-loaded MongoDB adapter
- Maintain full backward compatibility while following AGENTS.md guidelines
- Implement lazy imports to avoid heavy dependencies in core SDK
- Fix architectural violations: HTTP server, memory factory, MongoDB in core

Resolves core SDK violating protocol-driven architecture (god object)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:06:09 +01:00
github-actions[bot]andMervin Praison cec725f7ee feat: implement circuit breaker pattern for external service calls (#1201)
- Add lightweight CircuitBreaker class with open/half-open/closed states
- Integrate with existing retry utilities in tools/retry.py
- Add health monitoring and graceful degradation support
- Provide integration helpers for LLM, Memory, and MCP systems
- Include comprehensive tests with real agentic test
- Maintain backward compatibility and zero performance regression
- Follow protocol-driven architecture from AGENTS.md

Fixes #1141

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:06:04 +01:00
MervinPraison 4804a4d7ee docs: auto-update api.md [skip ci] 2026-03-30 23:06:02 +00:00
github-actions[bot]andMervin Praison 8fa99cea83 feat: add unified configuration resolver to address scattered env var reads (#1196)
- Create centralized config/resolver.py with protocol-driven design
- Add ConfigRegistry for tracking all environment variables
- Implement caching and type conversion for performance
- Add validation to warn about unknown/deprecated vars
- Extend 'praison config' CLI with env discovery and validation
- Maintain 100% backward compatibility with existing os.getenv() usage
- Follow AGENTS.md principles: protocol-driven, no performance impact

Fixes #1140 - 317+ scattered env var reads now have unified entry point

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:05:52 +01:00
github-actions[bot]andMervin Praison 08c40f17d8 fix: Critical thread-safety gaps in multi-agent concurrent execution (#1193)
- Add global server state lock (_agents_server_lock) to protect FastAPI endpoint registration
- Add task ID assignment lock (_task_id_lock) to prevent race conditions in parallel execution
- Add state mutation lock (_state_lock) to protect AgentTeam shared variables and _state
- Thread-safe protection for all critical sections in server startup and state management

Fixes #1152

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:05:44 +01:00
MervinPraison 6120623e21 docs: auto-update feature parity trackers [skip ci] 2026-03-30 23:05:34 +00:00
github-actions[bot]andMervin Praison 9b365e6594 fix: replace silent exception swallowing with structured logging (#1207)
Fixes issue #1128 by systematically replacing problematic exception
handling patterns that were masking real errors and making debugging
extremely difficult.

Changes:
- Replace bare 'except:' clauses with 'except Exception:' to avoid
  catching SystemExit/KeyboardInterrupt
- Add structured debug logging for all previously silent exceptions
- Maintain backward compatibility and zero performance impact
- Focus on core SDK modules: memory, tools, LLM, telemetry

Key areas fixed:
- Memory operations: Now log failed tracing and cleanup operations
- Telemetry: Debug logging for PostHog failures and queue errors
- LLM client: Debug logging for cost calculation and display failures
- Tools: Debug logging for browser checks and capability detection
- Code execution: Debug logging for temp file cleanup failures

All exception handling now provides context for debugging while
maintaining the fail-safe behavior required for production use.

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:05:22 +01:00
github-actions[bot]andMervin Praison 804cacbda4 fix: eliminate duplicated approval logic in sync/async tool execution paths (#1194)
- Extract shared `_resolve_approval_decision()` method to consolidate ~50-60 lines of duplicated code
- Maintain backward compatibility and zero performance regression
- Handle sync vs async execution paths correctly with `is_async` parameter
- Improve code maintainability by following DRY principle from AGENTS.md
- Fix properly handles both dangerous and non-dangerous tools in async context

Resolves issue #1147

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:05:18 +01:00
github-actions[bot]andMervin Praison 4fe4c27516 feat: implement unified deprecation management system (#1192)
- Create centralized @deprecated decorator with version tracking
- Add warn_deprecated_param() for consistent parameter warnings
- Add MIGRATION.md with comprehensive deprecation guide
- Update 7 key files to use unified deprecation system
- Add version-based removal timeline (deprecated in 1.0.0, removed in 2.0.0)
- Ensure backward compatibility with zero breaking changes
- Add expiration detection for future CI integration

Fixes #1135

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:05:13 +01:00
github-actions[bot]andMervin Praison 1af11908de fix: enhance thread safety in multi-agent scenarios (#1191)
- Add thread-safe caching for tools module lazy loading with double-check pattern
- Enable SQLite WAL mode for better concurrent write safety in storage backend
- Add async lock infrastructure for process workflow shared state protection
- Maintain backward compatibility and zero performance regression

Addresses issue #1163: Core SDK global mutable state without thread safety

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:05:08 +01:00
c634969263 fix: enhance centralized logging with structured logging support (#1187)
* fix: enhance centralized logging with structured logging support

- Enhanced existing _logging.py with get_logger() utility for consistent naming
- Added StructuredFormatter for JSON logging in production environments
- Enforced 'praisonaiagents.<module>' naming convention automatically
- Added optional extra_data parameter for structured logging context
- Exposed logging utilities in __init__.py for easy import
- Updated example modules to demonstrate new patterns
- Added comprehensive LOGGING.md documentation
- Maintained 100% backward compatibility with existing code
- Zero performance impact for existing logging patterns

Addresses #1132: Consolidates 53+ scattered logger instances with unified configuration

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

* fix: address review issues in centralized logging enhancements

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/10430b96-89ed-4e3b-98bf-8e68fd28366e

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-03-31 00:05:04 +01:00
MervinPraison 80b10518dc docs: auto-update feature parity trackers [skip ci] 2026-03-30 23:04:53 +00:00
github-actions[bot]andMervin Praison 259705e230 Add YAML config parity with CLI flags for trust, tool-timeout, planning-tools, ACP, LSP (#1203)
Resolves #1146 by implementing the missing YAML configuration options:

- trust: Agent-level trust configuration (maps to --trust CLI flag)
- tool_timeout: Agent-level timeout setting (maps to --tool-timeout CLI flag)
- planning_tools: Agent-level planning tools (maps to --planning-tools CLI flag)
- acp: Global ACP tools configuration (maps to --acp CLI flag)
- lsp: Global LSP tools configuration (maps to --lsp CLI flag)

Changes:
- Enhanced AgentsGenerator to accept cli_config parameter for CLI overrides
- Added _merge_cli_config method to merge CLI settings with YAML config
- Updated all AgentsGenerator instantiation points to pass CLI configuration
- Added _extract_cli_config_for_yaml helper method to extract relevant CLI flags
- Maintained full backward compatibility with existing YAML files

YAML Schema additions support both agent-level and global configuration:

```yaml
agents:
  researcher:
    trust: true
    tool_timeout: 120
    planning_tools: [web_search]

config:
  acp: true
  lsp: true
```

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:04:42 +01:00
github-actions[bot]andMervin Praison dd41c1ba01 fix: secure python_tools.py exec() with subprocess sandbox (#1199)
- Add code_sandbox_mode parameter to ExecutionConfig (default: 'sandbox')
- Implement _execute_code_sandboxed() using subprocess isolation
- Maintain backward compatibility with 'direct' mode (legacy)
- Subprocess sandbox runs in clean environment (no env vars, files, network)
- Same security validations: AST parsing + text pattern blocking
- Zero performance regression: lazy imports, same API
- All existing security tests pass
- Safe by default: new installs use sandboxed mode

Resolves security vulnerability where exec() ran in same process
with access to filesystem, network, and environment variables.

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:04:38 +01:00
github-actions[bot]andMervin Praison e32cccf62a feat: expand LLM integration test coverage for specialized agents (#1197)
- Add comprehensive @pytest.mark.live tests for 11 specialized agents:
  CodeAgent, VisionAgent, AudioAgent, VideoAgent, OCRAgent, RealtimeAgent,
  DeepResearchAgent, ImageAgent, RouterAgent, ContextAgent, EmbeddingAgent
- Add multi-agent workflow integration tests:
  Sequential, hierarchical, and parallel process types with real LLM calls
- Add tool usage integration tests: web search, Python execution, multi-tool
- Add memory integration tests: persistence across sessions, search, long-term
- Add knowledge/RAG integration tests: document processing, citations, synthesis
- Add MCP integration tests: server connection, tools, streaming
- Add error recovery tests: LLM timeouts, tool failures, graceful degradation

All tests follow existing safety infrastructure with PRAISONAI_LIVE_TESTS=1
gating and @pytest.mark.live markers. Addresses missing real agentic test
coverage identified in issue #1136.

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-31 00:04:33 +01:00
de668f1912 fix: implement token tracking in RouterAgent for cost observability (#1190)
* fix: implement token tracking in RouterAgent for cost observability

- Add TokenUsage dataclass with structured token consumption data
- Enhance LLM.get_response() with optional return_token_usage parameter
- Update RouterAgent to track tokens and costs per model selection
- Emit token usage via existing trace infrastructure for observability
- Maintain full backward compatibility with zero performance regression

Resolves issue #1138 - RouterAgent now provides complete cost visibility
for multi-agent routing scenarios, addressing the TODO at line 234.

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

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

* fix: correct critical bugs in _prepare_return_value token tracking

- Move _prepare_return_value closure to before the try block in
  get_response() so it's defined before use
- Fix infinite recursion: both branches called themselves instead
  of returning the text value
- Revert _prepare_return_value usage from 7 out-of-scope methods
  (_process_stream_delta, get_response_async, _extract_from_responses_output,
  _stream_responses_api, _stream_responses_api_async, response, aresponse)
  where it was a NameError and would corrupt return tuple structure

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/1b069906-c514-4c5f-8c93-34c345db5fd0

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-03-31 00:04:28 +01:00
cfe8277638 feat: implement missing P0 parity features for TypeScript SDK (#1188)
* feat: implement missing P0 parity features for TypeScript SDK

- Add BudgetExceededError class for budget limit handling
- Add get_registry() function for Python SDK parity
- Add get_tool() function to get single tool by name
- Add register_tool() function for tool registration
- Add validate_tool() function for tool validation
- Update PARITY.md to reflect completed P0 features (0 remaining)
- Reduce total gap count from 23 to 18

Resolves critical P0 parity gaps identified in issue #1127

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

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

* fix: align BudgetExceededError with Python SDK, add camelCase aliases, fix validate_tool undefined bug, add tests

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/9ee9003d-3264-405d-bd0c-f839753cf227

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-03-31 00:04:24 +01:00
a8a47c7b7f Security: Replace unsafe eval()/exec() with safe expression evaluators (#1186)
* security: Replace unsafe eval()/exec() with safe expression evaluators

- Fix ai_generator.py: Replace eval() with safe AST-based calculator
- Fix job_workflow.py: Replace eval() with safe AST evaluation for conditions
- Fix hybrid_workflow.py: Replace eval() with safe AST evaluation for conditions
- singlestore_vector.py: Already fixed (uses json.loads + ast.literal_eval)
- python_tools.py: Already has comprehensive security measures

All fixes maintain backward compatibility while eliminating arbitrary code
execution vulnerabilities. Implements defense-in-depth with AST validation
and operator-based evaluation instead of eval().

Fixes #1157

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

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

* fix: address review findings in _safe_eval_ast implementation

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/557089b9-dfe4-460b-95db-f80857c916c6

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-03-31 00:04:19 +01:00
MervinPraison 8a803888ea fix: use GITHUB_TOKEN for copilot, SECRETS_ADMIN_PAT for cross-repo docs 2026-03-30 22:31:50 +01:00
MervinPraison e33710e63a fix: make docs parity checkout non-fatal when GH_TOKEN lacks repo access
GH_TOKEN PAT may not have access to PraisonAIDocs. Workflow now
gracefully skips docs-related steps if checkout fails.
2026-03-30 21:35:01 +01:00
MervinPraison 7f2976c1ff fix: use GH_TOKEN (existing secret) instead of non-existent PAT_TOKEN
PAT_TOKEN secret doesn't exist. GH_TOKEN is the actual PAT secret
that's configured in the repo.
2026-03-30 21:33:06 +01:00
MervinPraison 0c8ff4b1ee fix: use PAT_TOKEN for docs parity tracker checkout 2026-03-30 21:31:12 +01:00
github-actions[bot]andMervin Praison d143cce2f9 fix: Add comprehensive type hints to Agent class public methods
Fixes identified issues from PR #1179:
- Add type hints to chat(), run(), start(), execute_tool(), execute() methods
- Fix ephemeral() return type to Generator[None, None, None] for context manager
- Add Optional[str] return types for methods that can return None (chat, run, execute)
- Add type hints to all property getters and setters
- Add type annotations to **kwargs parameters

This resolves type checker warnings and improves IDE autocompletion support.

Co-authored-by: Mervin Praison <[email protected]>
2026-03-30 20:30:52 +00:00
MervinPraison 1260ede088 Release v4.5.87 2026-03-30 21:30:44 +01:00
MervinPraison 2a463d6b39 docs: auto-update feature parity trackers [skip ci] 2026-03-30 20:29:55 +00:00
MervinPraison 10068583bb fix: add missing pyyaml dep for parity tracker, use PAT_TOKEN fallback for docs checkout
Feature Parity Tracker failed: ModuleNotFoundError: No module named 'yaml'
Docs Parity Tracker failed: Input required and not supplied: token
2026-03-30 21:29:36 +01:00
github-actions[bot]andMervin Praison 9bb149a244 fix: improve thread safety in tool registry
- Use thread-safe registry.unregister() in remove_tool()
- Use thread-safe registry.list_tools() in list_tools()
- Add double-checked locking to get_registry() singleton
- Prevents race conditions in multi-agent deployments

Related to #1167 and closed PR #1182

Co-authored-by: Mervin Praison <[email protected]>
2026-03-30 20:28:22 +00:00
MervinPraison beb3e0a644 ci: use MervinPraison as commit author in all CI workflows
Previously github-actions[bot] appeared as co-author on auto-commits,
polluting the contributor graph.
2026-03-30 21:28:13 +01:00
6a90cc07ee fix: Async correctness - avoid asyncio.run() from sync context reachable by async callers (#1176)
* fix: async correctness in tool approval - prevent RuntimeError in async workflows

* Add event loop detection in _check_tool_approval_sync()
* Use thread pool fallback when already in async context
* Prevents 'asyncio.run() cannot be called from a running event loop' crash
* Maintains backward compatibility with sync usage
* Follows existing pattern from registry.py and backends.py

Fixes #1165

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

* fix: address reviewer feedback on async correctness fix

- Add concurrent.futures import to top of agent.py (per Gemini review)
- Create shared approval/utils.py to eliminate code duplication (per Qodo/Coderabbit)
- Fix timeout configuration to respect agent's _approval_timeout semantics (per all reviewers)
- Replace ThreadPoolExecutor context manager with proper shutdown handling (per Coderabbit)
- Remove redundant loop.is_running() check (per Copilot)
- Consolidate async-to-sync bridging logic across approval system

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-30 21:23:23 +01:00
github-actions[bot] ffb9bd157e docs: auto-update api.md [skip ci] 2026-03-30 20:23:08 +00:00
dd3ccb8b5f fix: SQLite memory/storage layer thread safety for concurrent multi-agent access (#1178)
* fix: implement thread-safe SQLite connections in Memory class

- Add threading.local() for per-thread SQLite connections
- Enable check_same_thread=False for cross-thread access
- Enable WAL mode (PRAGMA journal_mode=WAL) for concurrent read/write
- Replace direct sqlite3.connect() calls with thread-safe getters
- Remove connection.close() calls to enable connection reuse
- Add close_connections() method for proper cleanup

This fixes database corruption and "database is locked" errors in
multi-agent deployments by following the same thread-safe pattern
used in SQLiteBackend class.

Addresses issue #1169: SQLite memory/storage layer unsafe for concurrent
multi-agent access. Tested with 5 concurrent threads performing 20
operations each - 100% success rate.

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

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

* fix: enhance SQLite concurrency safety with write locks and connection management

- Add write lock serialization for all SQLite write operations to prevent database lock contention
- Implement proper connection cleanup across threads using connection registry
- Add timeout and busy_timeout configuration for better lock contention handling
- Verify WAL mode enablement with logging for debugging
- Add context manager support and __del__ cleanup for resource management
- Improve error handling with specific error logging instead of silent failures
- Maintain backward compatibility while fixing thread safety issues

Fixes all reviewer issues from Qodo, CodeRabbit, and Copilot feedback.

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

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-30 21:22:50 +01:00
github-actions[bot] da8786520d docs: auto-update api.md [skip ci] 2026-03-30 20:22:23 +00:00
ed02fc660f fix: Memory subsystem thread safety - synchronized import caches and FileMemory (#1177)
* fix: resolve memory subsystem thread safety race conditions

- Replace unsafe module-level import caches in memory.py with thread-safe implementation using threading.Lock and double-check locking
- Add threading.RLock to FileMemory class to protect all in-memory data structures from concurrent access
- Protect critical operations: add/get/delete operations on _short_term, _long_term, _entities, _summaries lists/dicts
- Use snapshot pattern for search operations to prevent "list changed size during iteration" errors
- Maintain full API compatibility while eliminating TOCTOU race conditions
- Supports multi-agent + async workflows without data corruption or crashes

Fixes #1166

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

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

* fix: complete thread safety implementation for memory subsystem

- Fix broken test: update test_graph_memory.py to use new _module_cache instead of deprecated _chromadb_cache
- Remove unused importlib import from memory.py
- Complete FileMemory lock coverage: add thread-safe protection to all remaining methods:
  * get_all_memories, export, import_data: protect iteration and data access
  * save_session, resume_session: protect memory snapshots and restoration
  * compress: careful lock handling - protect data access but release during LLM calls
  * get_stats: protect memory structure access
  * handle_command: protect entity dictionary iteration

All methods now use consistent self._lock protection while avoiding deadlocks
and ensuring optimal performance during slow operations like LLM calls.

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-30 21:22:12 +01:00
3c45e6f474 fix: Agent thread safety - eager lock initialization and LRU cache bounds (#1175)
* fix: implement thread safety for Agent class

- Replace lazy lock initialization with eager initialization for chat_history and cache locks
- Add thread-safe methods for chat_history operations: _add_to_chat_history(), _get_chat_history_length(), _truncate_chat_history()
- Implement proper LRU cache eviction using OrderedDict with _cache_put() and _cache_get() methods
- Add snapshot_lock to protect undo/redo stacks from TOCTOU race conditions
- Update critical chat_history and cache operations to use thread-safe methods
- Enforce _max_cache_size limits with LRU eviction instead of simple size checks

Fixes race conditions in multi-agent concurrent workflows where:
- chat_history entries could be silently dropped during tool execution
- cache dictionaries could have interleaved reads/writes
- snapshot/redo stacks had TOCTOU vulnerabilities in pop() operations
- unbounded cache growth in long-running agents

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

* fix: resolve thread safety issues in Agent class

- Move OrderedDict import to module level for better performance
- Add module-level logger to fix undefined logger errors in undo/redo
- Fix critical TOCTOU race in duplicate message check with atomic method
- Initialize snapshot lock unconditionally to prevent AttributeError
- Protect diff() method snapshot stack reads with lock

Addresses reviewer feedback from Qodo, CodeRabbit, and Copilot.

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

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-30 21:22:05 +01:00
9b85a22b4c fix: implement lazy loading for tools imports to improve package startup time (#1172)
* fix: implement lazy loading for tools imports to improve package startup time

- Remove eager imports of tools modules from __init__.py (lines 63-66, 72)
- Add tools imports to _LAZY_IMPORTS mapping for proper lazy loading
- Create embedding proxy to resolve function vs subpackage conflict
- Maintain 100% backward compatibility with existing imports
- Improve startup performance by loading tools only when accessed

Fixes #1168

This addresses the core SDK eagerly importing heavy modules issue by:
1. Moving from .tools.* imports to lazy loading system
2. Using existing _LAZY_IMPORTS infrastructure
3. Resolving embedding function import conflict via proxy
4. Zero breaking changes to public API

Performance improvement: Base import reduced from heavy module loading
to lightweight protocol-driven imports following AGENTS.md principles.

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

* fix: address lazy loading implementation issues

- Remove eager config import, move to lazy loading in _custom_handler
- Remove heavy modules from fallback_modules to prevent imports on typos
- Add proper tools/config/memory/workflows module access via _custom_handler
- Fix _EmbeddingProxy introspection with __wrapped__ and __signature__ properties
- Remove dead code in _custom_handler for embedding/embeddings
- Clean up duplicate comments and self-referential error messages
- Maintain full backward compatibility while improving startup performance

Fixes all issues identified in code review by Copilot, CodeRabbit, and Qodo.

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

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

* fix: address lazy loading implementation issues

- Fix _get_embedding_func to avoid overwriting embedding proxy
- Clean up dead code comments
- Update comments for clarity
- All validation tests pass: pa.tools, pa.config, embedding introspection

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

Co-Authored-By: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-03-30 21:21:57 +01:00
MervinPraison 9a79f2a15d fix(pyproject.toml, uv.lock): update version to 1.5.87
Bump version for praisonaiagents to reflect new release.
2026-03-30 21:20:32 +01:00
MervinPraison 11e449bf43 fix: use PAT_TOKEN for @copilot mentions so they post as MervinPraison
Copilot ignores @copilot mentions from github-actions[bot].
Switching to PAT_TOKEN makes comments appear from a real user.
2026-03-30 21:19:21 +01:00
MervinPraison d9581f39f8 fix: ensure Copilot only triggers AFTER CodeRabbit/Qodo/Gemini complete
Bot PR chain: trigger CodeRabbit + Qodo + Gemini → they post reviews →
copilot-after-coderabbit/qodo fires → Copilot reviews → Claude chains.

Copilot is NEVER triggered directly for bot PRs anymore. It waits
for upstream reviewers to finish first.
2026-03-30 21:09:32 +01:00
MervinPraison 88806ce469 fix: trigger review chain for bot-authored PRs
CodeRabbit and Qodo skip bot-authored PRs by default, breaking the
entire review chain (no reviews → no Copilot → no Claude).

Added bot-pr-review-chain job that fires on PR opened by
github-actions[bot] and directly triggers @coderabbitai, /review,
and @copilot.
2026-03-30 21:06:29 +01:00
MervinPraison 468dffa99d ci: add workflow_dispatch to test-core for manual testing 2026-03-30 19:08:49 +01:00
MervinPraison c1947b9e4b fix: set PYTHONUTF8=1 for Windows CI to handle emoji encoding 2026-03-30 19:04:48 +01:00
MervinPraison 21a3774b63 fix: remove remaining WorkflowStep from Windows CI tests 2026-03-30 19:00:49 +01:00
MervinPraison 2c9b4a5c34 fix: add missing output/models.py and editor.py to git
Root .gitignore line 22 has 'output' which was excluding the
praisonaiagents/output/ module from git. This caused Windows CI
ModuleNotFoundError: No module named 'praisonaiagents.output.models'

Added gitignore exemption and force-added the missing files.
2026-03-30 18:57:05 +01:00
MervinPraison bc0a2a3765 ci: deduplicate and optimize test workflows
DELETED (duplicates):
- unittest.yml: 517-line debug file, always failed (exit 5)
- coverage.yml: merged into test-core.yml

OPTIMIZED:
- test-core.yml: stripped 200+ debug lines, added path filtering
- python-package.yml: moved to schedule-only (weekly multi-version)
- test-extended.yml: moved to schedule-only (benchmarks)
- test-optimized.yml: added path filtering
- test-windows.yml: added path filtering, fixed broken WorkflowStep import

Before: 7 workflows per push (~16 min)
After: 3 workflows per push (~5 min) + path-filtered
2026-03-30 18:52:39 +01:00
MervinPraison b6c10e4040 ci: fix Copilot bot username in chain-claude workflow
Was checking for 'copilot-swe-agent' but actual PR reviewer
posts as 'copilot-pull-request-reviewer[bot]'. This is why
Claude's final review was never triggered after Copilot.
2026-03-30 18:33:40 +01:00
MervinPraison f5220973f8 ci: fix duplicate Claude runs on issue creation
Root cause: issues created with '--label claude' triggered both
'issues:opened' and 'issues:labeled' events in claude.yml.
Fix: remove 'opened' from claude.yml triggers (only fire on label).
Also: auto-issue-comment.yml skips if 'claude' label already exists.
2026-03-30 18:07:43 +01:00
MervinPraison a924a55245 ci: add direct_prompt to Claude workflow for auto PR creation
When triggered via label, Claude now gets explicit instructions to
use gh pr create instead of just providing a link.
2026-03-30 17:59:13 +01:00
MervinPraison dcd072dc0c docs: remove CLAUDE.md — AGENTS.md is sufficient 2026-03-30 17:58:45 +01:00
MervinPraison 8173350f79 docs: add CLAUDE.md with explicit auto-PR-creation instructions
Claude was only providing PR links instead of creating them.
CLAUDE.md is auto-read by claude-code-action and instructs Claude
to always use 'gh pr create' to create PRs automatically.
2026-03-30 17:58:14 +01:00
MervinPraison 5b9a046d4b ci: add label_trigger to Claude action so 'claude' label fires it
The anthropics/claude-code-action needs explicit label_trigger input
to recognize label events. Also simplified bot filter logic.
2026-03-30 17:35:13 +01:00
MervinPraison 27aafddaa4 ci: fix Claude trigger — use label instead of @claude comment
Root cause: anthropics/claude-code-action ignores @claude from bots.
Fix: add 'claude' label on issue open, which triggers via issues:labeled event.
2026-03-30 17:33:28 +01:00
MervinPraison 4a0b34ea20 ci: add fallback trigger — Copilot fires after Coderabbit OR Qodo
Root cause: PR #1170 had no Coderabbit review, so Copilot/Claude
were never triggered. Now fires after either reviewer, with dedup
check to prevent double @copilot comments.
2026-03-30 17:31:16 +01:00
MervinPraison 19f5a16766 ci: sequential PR review chain + Claude fixes reviewer issues
Pipeline sequence:
  PR opened → Qodo + Coderabbit (auto)
  Coderabbit done → @copilot (reads their reviews)
  Copilot done → @claude (reads ALL reviews, fixes valid issues, pushes to branch)

Key improvements:
- Copilot waits for Coderabbit before reviewing
- Claude reads ALL reviewer feedback and fixes valid issues in-place
- 3-phase Claude prompt: Review → Fix → Verdict
2026-03-30 17:26:20 +01:00
MervinPraison 20cb053181 ci: make Claude default for issues, Gemini commented out for future
Pipeline flow:
  Issue → @claude (analyzes + creates PR)
  PR → Qodo/Coderabbit (auto) + @copilot
  Copilot done → @claude (final architecture review)
  ✅ Ready to merge

Gemini issue pipeline kept commented out for future dual-agent use.
2026-03-30 17:09:42 +01:00
MervinPraison 41ce1de079 ci: disable gemini_debug — output overflow caused GitHub Actions memory error 2026-03-30 17:02:15 +01:00
MervinPraison 36920ff8fe ci: fix Gemini config — settings_json→settings, remove invalid telemetry keys, use 2.5-flash with debug 2026-03-30 16:44:30 +01:00
MervinPraison e428393e0a ci: switch to official google-github-actions/run-gemini-cli@v0
- Replaces custom local action with official action
- Adds GH_TOKEN env for gh CLI auth inside Gemini sandbox
- Uses gemini-3.1-flash-lite-preview model (highest free quota)
2026-03-30 16:36:35 +01:00
MervinPraison 95fd857212 ci: switch to gemini-3.1-flash-lite-preview model 2026-03-30 16:32:01 +01:00
MervinPraison c260519bf1 ci: switch Gemini to flash model (250 RPD free tier vs Pro's 100 RPD)
Adds model input to gemini action, defaults to gemini-2.5-flash.
Prevents daily quota exhaustion on free tier.
2026-03-30 16:21:15 +01:00
MervinPraison 2921103ddd ci: fix '@gemini triage' exclusion — reword to 'analysis' to avoid filter 2026-03-30 15:56:10 +01:00
MervinPraison 73800f3509 ci: fix broken pipeline — @gemini for issues, correct event types for chain
Fixes:
- auto-issue-comment: @copilot → @gemini (Copilot doesn't respond on issues)
- chain-claude: listen for pull_request_review (how Copilot actually responds)
  + issue_comment from gemini-code-assist[bot] for issue chain
- gemini-issue-review: allow CONTRIBUTOR association (github-actions bot)
2026-03-30 15:18:18 +01:00
MervinPraison 7371ed7048 feat(docs): enhance README and AGENTS documentation with YAML examples
Added detailed sections in README for using YAML with agents, including
two comprehensive examples illustrating agent collaboration and custom
tools. Updated AGENTS.md to reflect CLI and YAML parity, and emphasized
the requirement for real agentic tests in the implementation checklist.
2026-03-27 16:18:06 +00:00
MervinPraison 129174257f docs: update AGENTS.md module sizes to match current codebase 2026-03-27 11:50:47 +00:00
Mervin Praison efcdc42fbb fix: replace unsafe eval() with safe alternatives and lazy-load Rich imports (#1160)
fix: replace unsafe eval() with safe alternatives and lazy-load Rich …
2026-03-27 11:49:00 +00:00
MervinPraison df79c5acb9 ci: event-driven review pipeline — chain Claude after Copilot finishes
- auto-pr-comment.yml: posts @copilot immediately on PR open
- auto-issue-comment.yml: posts @copilot immediately on issue open
- chain-claude-after-copilot.yml: watches for copilot-swe-agent comment,
  then posts @claude as final reviewer (event-driven, no sleep delays)
- Saves ~16 min of wasted runner time per PR
2026-03-27 11:46:34 +00:00
MervinPraison d9da7cc59c ci: expand Copilot prompts — thorough review since it's free 2026-03-27 11:35:38 +00:00
MervinPraison 11d9f290c7 ci: coordinated review pipeline — staggered timing, role specialization, remove infinite loop
- auto-pr-comment.yml: Copilot at T+6min (bloat guard), Claude at T+17min (final reviewer)
- auto-issue-comment.yml: Copilot triages first, Claude implements fix after
- claude.yml: remove post-review-comment job (caused infinite review cycles)
- Each bot has specialized role prompt instead of generic review
2026-03-27 11:34:27 +00:00
MervinPraison f796d12908 ci: improve auto-comment prompts — structured, focused, reference AGENTS.md 2026-03-27 11:18:34 +00:00
MervinPraison 9c4adcee89 ci: use official CLAUDE_CODE_OAUTH_TOKEN secret name 2026-03-27 11:12:18 +00:00
MervinPraison 1d0ffed030 ci: fix expired GH_TOKEN → GITHUB_TOKEN for auto-comments and claude review 2026-03-27 11:02:11 +00:00
copilot-swe-agent[bot]andMervinPraison 3be6df4de8 fix: harden legacy metadata fallback validation in SingleStore parser
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/066a220b-7a22-4676-afe6-f2675b6ff910

Co-authored-by: MervinPraison <[email protected]>
2026-03-27 10:58:16 +00:00
MervinPraison b65de3bf06 ci: add @copilot review comment on PR open 2026-03-27 10:57:23 +00:00
copilot-swe-agent[bot]andMervinPraison f949814af8 fix: make SingleStore metadata parsing safe and backward-compatible
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/066a220b-7a22-4676-afe6-f2675b6ff910

Co-authored-by: MervinPraison <[email protected]>
2026-03-27 10:57:04 +00:00
Claude 7449ccac76 fix: replace unsafe eval() with safe alternatives and lazy-load Rich imports (#1157, #1154)
Security (#1157): Replace eval() with json.loads() in singlestore_vector.py,
AST-whitelisted evaluator in ai_generator.py, and AST node validation in
job_workflow.py/hybrid_workflow.py to prevent arbitrary code execution.

Performance (#1154): Convert eager module-level Rich imports to lazy
function-level imports in main.py, agents.py, knowledge.py, video_agent.py,
and image_agent.py — Rich is no longer loaded on `import praisonaiagents`.

https://claude.ai/code/session_01MZtrpciafckLxpmrn1z6P2
2026-03-27 10:24:18 +00:00
github-actions[bot] ae26edd3f8 docs: auto-update api.md [skip ci] 2026-03-26 18:19:52 +00:00
MervinPraison 6349c88ad9 feat(agent): add budget management features to agents
- Introduced `BudgetExceededError` for handling budget overages.
- Implemented budget tracking in the `Agent` class.
- Added properties for total cost and cost summary.
- Updated various agents to support budget constraints.
- Enhanced lazy loading to include new budget-related components.
- Extensive test coverage added for the new budget features.
2026-03-26 18:19:35 +00:00
Mervin Praison 1d4721d039 feat(ag2): add AG2 framework backend integration (#1156)
feat(ag2): add AG2 framework backend integration
2026-03-26 17:56:52 +00:00
MervinPraisonandFaridun Mirzoev 90915fe790 feat(ag2): add AG2 framework backend integration
Port of PR #1143 (by @faridun-ag2) to main branch.

AG2 (community fork of AutoGen, PyPI: ag2) added as a new framework
option alongside praisonai, crewai, and autogen.

Changes:
- agents_generator.py: AG2 detection + _run_ag2() with LLMConfig,
  GroupChat orchestration, Bedrock support, TERMINATE cleanup
- auto.py: AG2 lazy availability check + validation
- pyproject.toml: [ag2] optional dependency extra
- examples/ag2/: basic, multi-agent, and Bedrock YAML examples
- tests: 16 unit tests + 9 integration tests

Co-authored-by: Faridun Mirzoev <[email protected]>
2026-03-26 17:56:15 +00:00
MervinPraison 29facc8f89 Release v4.5.85 2026-03-26 03:13:24 +00:00
MervinPraison 401521848f feat(version): bump version to 1.5.85
Updated the version number in pyproject.toml and uv.lock to reflect the new release.
2026-03-26 03:13:24 +00:00
Mervin Praison ab97eae954 fix: harden env key validation — malformed keys, root/deployment type guards
fix: harden env key validation — malformed keys, root/deployment type guards
2026-03-26 02:30:10 +00:00
Mervin Praison 5dda0643ca security: cap litellm<=1.82.6 (supply chain incident)
security: cap litellm<=1.82.6 (supply chain incident)
2026-03-26 02:29:20 +00:00
MervinPraison 1b8d7a02d9 security: cap litellm<=1.82.6 (supply chain incident CVE)
LiteLLM v1.82.7 and v1.82.8 contained a credential stealer from a
supply chain attack (compromised PyPI maintainer via Trivy CI/CD).
Both versions removed from PyPI.

PraisonAI was NOT affected (installed 1.81.1), but as a defensive
measure, cap the upper bound at 1.82.6 (latest safe) until LiteLLM
completes their supply-chain review and resumes releases.

Ref: https://docs.litellm.ai/blog/security-update-march-2026
2026-03-26 02:28:46 +00:00
Mervin Praison dbcd36edce fix: Tier 1 security & concurrency fixes (#1134, #1129, #1137)
fix: Tier 1 security & concurrency fixes (#1134, #1129, #1137)
2026-03-26 02:12:56 +00:00
MervinPraison 9ef5391c83 fix: Tier 1 security & concurrency fixes (#1134, #1129, #1137)
#1134: Decouple execute_code from optional deps (black/pylint/autopep8)
- execute_code is now a standalone module-level function
- PythonTools class retained for analyze/format/lint (lazy-init)
- Agents can now use execute_code without heavy optional deps

#1129: Thread-safety for remaining global mutable state
- Add _lazy_import_lock with double-checked locking for all 6 lazy-import
  caches (_rich_console, _rich_live, _llm_module, _main_module, etc.)
- Add _env_cache_lock for class-level env var caches
  (_env_output_mode, _default_model)

#1137: Clarify misleading TODO comment
- Updated TODO in llm.py to clarify this is code-level DRY, not
  duplicate API calls per request
2026-03-26 02:09:40 +00:00
Sebastion 7385adbd60 fix: address second round of review feedback — malformed key validation, root/deployment type guards
- Reject empty strings, keys containing "=" or NUL in _validate_env_key()
  (CodeRabbit: malformed keys should fail during validation, not at os.environ)
- Validate root config is a dict after yaml.safe_load() (handles list/scalar/None)
- Validate deployment section is a dict before .get() calls
- Both raise ValueError caught by the existing handler → clean CLI error
- Add test_malformed_env_keys_rejected, test_non_dict_root_config_rejected,
  test_non_dict_deployment_rejected (7 tests total, all passing)
2026-03-26 00:05:11 +00:00
Mervin Praison 78d7883fb7 Merge pull request #1148 from MervinPraison/fix/1145-1147-approval-dry-thread-safety
fix: deduplicate approval logic and add thread-safety (#1147, #1145)
2026-03-25 17:21:44 +00:00
Mervin Praison 6122531a6a Merge pull request #1144 from sebastiondev/security/fix-cwe78-env-injection
Merging — verified locally, both vulnerable occurrences are patched, all tests pass. Great contribution! 🎉
2026-03-25 10:53:28 +00:00
Claude d704cf1349 fix: deduplicate approval logic and add thread-safety to global state (#1147, #1145)
Extract ~120 lines of duplicated approval code from _execute_tool_impl()
and execute_tool_async() into shared helpers _check_tool_approval_sync()
and _check_tool_approval_async(). Add threading.Lock to protect
_agent_counter, _server_started, _registered_agents, and _shared_apps
against concurrent access in multi-agent scenarios.

https://claude.ai/code/session_01WiGKiwTBj7xo5YoF3PYjWF
2026-03-25 10:10:41 +00:00
Sebastion 8523ef076e fix: address review feedback — fail-closed validation, restore env fixture, non-string key guard, perf, coverage 2026-03-25 03:46:33 +00:00
Mervin Praison 72889d2f73 Merge pull request #1119 from eltociear/patch-1
docs: update README.md
2026-03-24 20:42:24 +00:00
Sebastion f17bd7369c fix: reject dangerous environment variable keys from YAML schedule config (CWE-78)
The --schedule-config YAML file allows setting arbitrary environment
variables via the "environment" section. An attacker who can supply or
modify this file could set keys like LD_PRELOAD, PATH, PYTHONPATH, etc.
to achieve arbitrary code execution.

Add _validate_env_key() with a blocklist of security-sensitive env var
names (dynamic linker, search paths, proxies) and call it before each
os.environ assignment in the schedule-config loading path.

Also add unit tests for the validation function.
2026-03-24 20:41:10 +00:00
MervinPraison ed31e9b280 Release v4.5.83 2026-03-24 04:48:37 +00:00
MervinPraison 69e9e712c7 fix(security): remove hardcoded CHAINLIT_AUTH_SECRET and fix critical issues from #1122/#1125
- Replace hardcoded CHAINLIT_AUTH_SECRET with secrets.token_hex(32) fallback in all 5 UI files
  (agents.py, bot.py, chat.py, code.py, realtime.py) — secret was public in git history
- Use hmac.compare_digest() for constant-time API key comparison in recipe/serve.py
- Disable debug=True by default in deploy.py generated api.py; use DEBUG env var
- Restrict exec() __builtins__ to safe subset in job_workflow.py inline Python execution;
  prevents __import__('os').system() and similar arbitrary code execution patterns

Skipped shell=True changes (would break YAML run: steps with pipes/env vars) and
CORS wildcard changes (would break browser extension without env var migration path).
These require separate architectural discussion.
2026-03-24 04:33:35 +00:00
MervinPraison 7617b10c70 Release v4.5.82 2026-03-21 17:51:25 +00:00
MervinPraison 7939a4f12e fix(security): complete remaining eval/exec/XSS fixes from issue #1120
- Replace eval(e) in unified_server.py with AST-based safe math evaluator
- Replace exec(script_content) in run_with_langtrace.py with subprocess.run()
- Add nh3.clean() output sanitization in api.py to prevent XSS from markdown links
- Replace eval() in basic-tools.ts and custom-tool.ts with regex allowlist + Function constructor
- Replace eval() in simplified-api-test.ts and comprehensive-live.test.ts with safe evaluator

All changes are in example files and dev helper scripts only.
No SDK core code modified. Unit tests: 4341 passed, 0 new failures.
2026-03-21 17:49:40 +00:00
MervinPraison 8cbad0c997 Release v4.5.81 2026-03-21 16:51:27 +00:00
Ikko Eltociear Ashimine 48ed131577 docs: update README.md
minor fix
2026-03-20 13:17:09 +09:00
Mervin Praison f8c6f32d00 Release v4.5.80 2026-03-18 21:09:57 +00:00
Mervin Praison 69ff8e7f03 Release v4.5.79 2026-03-18 15:57:13 +00:00
Mervin Praison feaa81b68f Release v4.5.78 2026-03-18 15:28:36 +00:00
Mervin Praison 39406e904f feat(crawl4ai_tools): enhance availability checks for Crawl4AI and Playwright
Updated the availability check for Crawl4AI to include Playwright browser installation advice. Improved error handling when starting the web crawler, ensuring users are informed if necessary binaries are missing. Incremented version in pyproject.toml to 1.5.78 and added new dependencies for crawling functionality.
2026-03-18 15:26:43 +00:00
Mervin Praison 2bb37b1076 Release v4.5.77 2026-03-18 14:46:46 +00:00
Mervin Praison cc36ac1cf8 feat(scheduler): add execution history logging for scheduled jobs
Implemented logging for job execution history via a new RunRecord class.
This includes methods for logging job runs, retrieving execution history,
and persisting the history to a file. Enhancements to the config store
allow for configurable maximum history depth, improving traceability
of job executions.
2026-03-18 14:39:41 +00:00
Mervin Praison a041a76f0d Release v4.5.76 2026-03-18 11:08:18 +00:00
Mervin Praison f77cca9931 feat(scheduler): add ConfigYamlScheduleStore for schedule management
Introduced a new schedule store utilizing YAML files for
persistence. This allows scheduled jobs to be stored under
the "schedules" key in the existing config.yaml, enhancing
thread safety and maintaining existing config information.
Updated related modules to integrate the new store.
2026-03-18 11:07:33 +00:00
Mervin Praison ef176298b9 feat(scheduler): implement ScheduleStoreProtocol for better schedule management
Added a new ScheduleStoreProtocol to standardize schedule persistence backends.
Updated ScheduleLoop and ScheduleRunner to use the protocol, allowing various
implementations for schedule storage. Introduced _run_schedule_loop in BotOS
to ensure scheduled jobs execute alongside bot tasks. Updated project version to
1.5.75 for this update.
2026-03-18 10:52:42 +00:00
Mervin Praison 31f5cbb1cf Release v4.5.74 2026-03-17 14:57:13 +00:00
Mervin Praison 85ec6cba90 feat(web_crawl_tools): enhance URL handling in web_crawl function
Improved the URL normalization logic to handle comma-separated URLs
passed as a single string. Updated version numbers in pyproject.toml
and uv.lock files to reflect the latest changes.
2026-03-17 14:54:27 +00:00
Mervin Praison c77259eda7 Release v4.5.73 2026-03-17 12:11:44 +00:00
Mervin Praison e69e726ca6 refactor(tools): rename web_crawl.py to web_crawl_tools.py
Updated the import paths in TOOL_MAPPINGS to reflect the new file name.
Bumped version to 1.5.73 in pyproject.toml and uv.lock for the change.
2026-03-17 12:08:30 +00:00
Mervin Praison 3130ae4e19 Release v4.5.72 2026-03-17 11:17:13 +00:00
Mervin Praison c872be20ed feat(email_tools): enhance backend detection and streamline usage
Updated email_tools to auto-detect available email backends (AgentMail or SMTP) based on environment variables. This refactor simplifies usage by providing generic tools while maintaining backend-specific functionalities. Enhanced documentation to reflect these changes and improve usability.
2026-03-17 11:14:39 +00:00
Mervin Praison 4548783a40 Release v4.5.71 2026-03-17 10:41:33 +00:00
Mervin Praison 5b1c39d496 feat(bots): add mode and webhook port configurability
Enhanced BotConfig and related classes to support multiple operation modes (poll, ws, webhook, hybrid) and configurable webhook port. Updated documentation and validation for new parameters in the AgentMailBot for improved flexibility in message handling. Bumped version to 1.5.71.
2026-03-17 10:38:13 +00:00
Mervin Praison 1e3933f5f3 Release v4.5.70 2026-03-17 08:14:59 +00:00
Mervin Praison 203ebe4960 ```
feat(email_tools): add reply_email and create_inbox functions

Implemented reply_email for responding to emails and create_inbox for
creating new email inboxes in the AgentMail system. Updated integration
in the tool mappings and profiles to include new functionalities.
Enhanced tests to cover the new features.
```
2026-03-17 08:12:11 +00:00
Mervin Praison e144d840bb Release v4.5.69 2026-03-16 21:43:41 +00:00
Mervin Praison 47bff65413 feat(mcp): enhance command validation in MCPHandler
Added an allowlist for MCP server commands to improve security by
rejecting disallowed and dangerous commands in the MCPHandler class.
Updated tests to verify the new validation logic and ensure expected
executables are accepted. Bumped version to 1.5.69.
2026-03-16 21:39:47 +00:00
Mervin Praison 66bd9ee2a9 Release v4.5.68 2026-03-16 17:48:36 +00:00
github-actions[bot] 1dd5d7e706 docs: auto-update api.md [skip ci] 2026-03-16 17:45:42 +00:00
Mervin Praison fee74fab53 ```
feat(email): add AgentMail tools for sending and managing emails

- Introduced send_email, list_emails, and read_email functions.
- Added email_tools module for agent interaction.
- Updated tool mappings to include email functionalities.
- Included example YAML configuration for an AgentMail bot.
- Enhanced tests for email tools and added new test cases.
```
2026-03-16 17:45:17 +00:00
Mervin Praison 003f9371a1 Release v4.5.67 2026-03-16 16:31:48 +00:00
Mervin Praison 7234cd5682 ```
feat(bots): add email bot examples and functionalities

This commit introduces a new email bot feature, enabling agents to
interact through email via IMAP/SMTP. It includes example usage,
custom configurations, and a multi-platform approach, enhancing the
bot's usability in diverse environments.
```
2026-03-16 16:31:23 +00:00
Mervin Praison 4373fb3adb docs(README): update performance metrics and clarify agent orchestration
Refined performance section to emphasize speed and reduced overhead.
Updated terminology for clarity in the external agents description.
2026-03-16 12:11:56 +00:00
Mervin Praison e87dd8e37a ```
docs(readme): refine performance section and enhance descriptions

Updated the performance metrics and descriptions to clarify advantages of PraisonAI. Improved consistency in terminology for better readability.
```
2026-03-16 12:08:35 +00:00
Mervin Praison 872cd87638 Release v4.5.65 2026-03-16 12:00:17 +00:00
Mervin Praison 8ea0065f83 ```
feat(bots): add EmailBot for IMAP polling and SMTP sending

This commit introduces the EmailBot, enabling integration with email
services using IMAP for receiving messages and SMTP for sending.
Updates include new environment variables for configuration,
enhancements to the bot registry, and modifications to schema and
initialization logic for seamless integration.
```
2026-03-16 11:58:55 +00:00
Mervin Praison e922eb1e64 Release v4.5.64 2026-03-16 10:02:40 +00:00
Mervin Praison e364aa7519 Release v4.5.63 2026-03-15 15:36:23 +00:00
Mervin Praison e67a11db1f ```
fix(agent): handle potential None value for agent name

Updated agent hash generation to default to "agent" when name is None.
Bumped version to 1.5.63 to reflect this change.
```
2026-03-15 15:31:02 +00:00
Mervin Praison 9071af74ea Merge pull request #1114 from jnMetaCode/fix-agent-name-none-crash
fix: handle None agent name when generating history session ID
2026-03-15 07:12:47 +00:00
Mervin Praison 7821916c56 Merge pull request #1115 from jnMetaCode/fix-model-windows-duplicate-key
fix: correct duplicate gpt-4o-mini key in MODEL_WINDOWS to gpt-4o
2026-03-15 07:12:16 +00:00
Mervin Praison dd1b9a7f1f Release v4.5.62 2026-03-15 05:30:13 +00:00
Mervin Praison 3b006be069 ```
feat(tests): add deep performance profiling integration test

Introduced a new integration test to run cProfile-wrapped Agent prompts.
This test stores detailed function-level breakdown as JSONL for regression
tracking and comparison of performance over time. Updated version number
and modified documentation accordingly.
```
2026-03-15 05:29:29 +00:00
Mervin Praison 4d80c60ecb ```
feat(llm): add support for OpenAI Responses API

- Implemented methods to check model compatibility with the Responses API.
- Added functionality to transform Chat Completions messages to Responses API parameters.
- Updated agent and budgeter context for new model types and limits.
- Cleaned up test cases for database integration to remove unnecessary parameters.
```
2026-03-14 11:53:47 +00:00
Mervin Praison 0cdfde2c04 Release v4.5.60 2026-03-13 16:57:44 +00:00
Mervin Praison 98e2fcccf6 Release v4.5.59 2026-03-13 16:50:14 +00:00
Mervin Praison 1a42ca92d3 Release v4.5.58 2026-03-13 16:30:18 +00:00
Mervin Praison c75d087591 feat(agent): add transparency for tool usage in system prompts
Added a guideline in the system prompt to explain actions before tool calls, enhancing clarity and transparency in interactions. Updated version to 1.5.58 to reflect changes.
2026-03-13 16:27:52 +00:00
Mervin Praison 2f6f496e5d Release v4.5.57 2026-03-13 15:34:30 +00:00
Mervin Praison 42ab69c142 Release v4.5.56 2026-03-13 14:41:14 +00:00
Mervin Praison db62f4368c feat(agent): implement doom recovery mechanism for autonomy
Added a `_doom_recovery_active` flag to manage agent behavior during
doom loops. This prevents normal termination signals when a doom loop
is detected, allowing the system to handle recovery more effectively.
Updated relevant sections in `agent.py` and adjusted unit tests to
reflect changes. Bumped version to 1.5.56.
2026-03-13 14:38:43 +00:00
Mervin Praison fd3daa27fa Release v4.5.55 2026-03-13 14:17:35 +00:00
Mervin Praison 875da3dfe0 feat(agent, llm): enhance agent response handling and tool call logic
Improved handling of agent's chat responses, including better
doom loop detection. Added intermediate narrative emission for LLM
tool calls, enhancing transparency in execution logic.
Updated tests to cover new functionality.
2026-03-13 14:14:00 +00:00
Mervin Praison 620e21b599 test(integration): add 10-step task comparison integration test
This test evaluates the functionality of the tool_completion fix
with a detailed multi-tool prompt. It includes tasks such as
web searching, file creation, execution, and reporting.
2026-03-13 09:18:54 +00:00
Mervin Praison 79ad6b621f Release v4.5.54 2026-03-13 08:35:47 +00:00
github-actions[bot] cbe57c9d52 docs: auto-update api.md [skip ci] 2026-03-13 08:33:42 +00:00
Mervin Praison fa264e1314 feat(agent): enhance tool call completion handling and tracking
- Added completion callback for tool-call completion if the model used tools and produced a substantive response.
- Updated `execute_tool` and `_execute_tool_with_context` methods to include `tool_call_id` for better event correlation.
- Improved logging for tool execution within the agent's framework.
2026-03-13 08:33:24 +00:00
Mervin Praison e71e73cca4 Release v4.5.52 2026-03-13 07:36:38 +00:00
Mervin Praison 9861520be6 Release v4.5.51 2026-03-13 00:26:30 +00:00
Mervin Praison 710624bd69 Release v4.5.49 2026-03-13 00:23:37 +00:00
Mervin Praison a35a6690c4 Release v4.5.48 2026-03-12 16:38:48 +00:00
Mervin Praison 1b9d569de8 Release v4.5.46 2026-03-12 13:59:37 +00:00
Mervin Praison 956e4eeb91 feat(tools): add learning tools for improved user preference management
Introduced `store_learning` and `search_learning` functions to
facilitate categorized storage and retrieval of user insights and
preferences. Enhanced agent interaction by allowing agents to
remember and recollect user behaviors across sessions. Updated
agent state to include a learn manager for integration with the
learning system.
2026-03-12 13:52:36 +00:00
Mervin Praison 654e1e2aec Release v4.5.45 2026-03-12 12:26:17 +00:00
Mervin Praison 2870aa9861 feat(a2a): add authentication support and standalone server feature
- Introduced optional bearer token for POST /a2a requests
- Added serve method for standalone A2A server using FastAPI
- Updated router to enforce authentication when token is set
- Enhanced message parsing to support additional part types
2026-03-12 12:23:31 +00:00
Mervin Praison 7bec22ce77 Release v4.5.44 2026-03-12 12:04:19 +00:00
Mervin Praison 520a46019a feat(docs): enhance user guide with Python examples and CLI reference
Added detailed usage examples for single, multi agents, MCP, custom tools, and persistence. Included a quick reference for CLI commands to improve user onboarding. Removed outdated sections for clarity.
2026-03-12 12:00:48 +00:00
Mervin Praison 7ee1c642d4 docs(README): remove outdated features from the description
Updated the README to eliminate references to specific deployment and connection features that are no longer relevant.
2026-03-12 11:01:41 +00:00
Mervin Praison 397f499dda docs(README): restructure and simplify features section
Updated the "Why PraisonAI?" section for clarity and conciseness,
retaining key features while enabling easier navigation with an
expandable details section to view all features.
2026-03-12 10:56:37 +00:00
Mervin Praison d4dac28434 docs(README): update structure and content for clarity and branding
Modified logo dimensions and enhanced the documentation layout by
refactoring sections related to quick start instructions and added
details on supported LLM providers. Overall goal was to improve
readability and user experience.
2026-03-12 10:53:29 +00:00
Mervin Praison dbca6e0047 feat(docs): update README and add 24 new provider icons
Updated README.md to reflect changes in logo paths, added new features,
and improved contribution guidelines. Added 24 new provider icons to
public directory for better integration visibility.
2026-03-12 10:47:24 +00:00
Mervin Praison ed3da59b2c docs(README): update use cases section and clarify performance benchmarks
Added a new "Use Cases" section outlining practical applications of AI agents
and streamlined the language in the performance benchmarks for clarity.
2026-03-12 10:37:20 +00:00
Mervin Praison 55ab4acf8d docs(README): update feature names and add use cases section
Revised feature names for clarity and consistency, changing "Dashboard UI" to "AgentClaw." Added a new section outlining various use cases for AI agents across multiple industries. Updated links and formatting for better readability.
2026-03-12 10:31:06 +00:00
Mervin Praison e0c1301965 docs(README.md): add example command for using praisonai claw feature
Added a line to the README.md to provide an example command for
installing the claw feature of the praisonai package.
2026-03-12 08:11:14 +00:00
Mervin Praison 79c4002e3c docs(README): clarify section title for PraisonAI Claw
Updated the section title to include "Dashboard UI" for better clarity.
2026-03-12 08:05:33 +00:00
Mervin Praison 2e5a8c5fe5 docs(README): Add section for PraisonAI Claw integration
Included installation instructions and dashboard overview for connecting AI agents to messaging platforms like Telegram, Discord, and WhatsApp.
2026-03-12 08:04:44 +00:00
Mervin Praison 99c811da21 feat(docs): add dashboard image to README
Added a central image of the PraisonAI dashboard to enhance visual appeal
and provide users with a clear reference to the interface.
2026-03-12 01:34:56 +00:00
Mervin Praison 0fb773adcd Release v4.5.43 2026-03-12 00:27:47 +00:00
Mervin Praison d95d3854db feat(dependencies): update praisonai and praisonaiagents to v4.5.43
Updated Dockerfiles and project files to use praisonai version 4.5.43
and praisonaiagents version 1.5.43 for improved functionality and
features. Added new 'claw' command in the CLI for enhanced user
experience.
2026-03-12 00:27:47 +00:00
github-actions[bot] 1383db2fba docs: auto-update api.md [skip ci] 2026-03-12 00:18:34 +00:00
Mervin Praison 75adcfed68 Release v4.5.42 2026-03-12 00:18:16 +00:00
Mervin Praison 3793ef0387 docs(README): update project description for clarity and impact
Updated the project description to highlight key features and capabilities, including automation, supported platforms, and other enhancements for better understanding.
2026-03-11 23:06:42 +00:00
Mervin Praison bf3b7f8e9d docs(README): update feature list with consistent layout and missing details
Adjusted the feature table in README.md for improved readability.
Added missing features and ensured consistent formatting throughout the list.
2026-03-11 22:55:11 +00:00
Mervin Praison a4da8c6ab6 feat(docs): update documentation links and improve clarity
Revised multiple links in the README to point to the correct documentation
pages, specifically changing references from features to concepts to enhance
user navigation and comprehension.
2026-03-11 22:47:54 +00:00
Mervin Praison f59ba1b83d Release v4.5.41 2026-03-11 22:32:27 +00:00
Mervin Praison 8c88766900 feat(docs): update README and versioning for Dashboard UI feature
Added a new entry for the Dashboard UI in the README and updated
the version of `praisonaiagents` to 1.5.41 in the relevant files.
This reflects recent changes and improvements in UI capabilities.
2026-03-11 22:31:09 +00:00
Mervin Praison a6eaa854fe Release v4.5.40 2026-03-11 22:21:06 +00:00
Mervin Praison e36911e712 feat(README, pyproject): update bot features and version number
Updated README.md to highlight new bot functionalities including Telegram,
Discord, Slack, and WhatsApp integration. Increased version to 1.5.40
in `pyproject.toml` for `praisonaiagents`. Expanded dependencies for
claw in the main `pyproject.toml`. Enhanced .gitignore to include
build files.
2026-03-11 21:22:25 +00:00
Mervin Praison c3a6a39ee1 docs(readme): update links to feature documentation in README.md
Replaced outdated links in the README file with new feature documentation
links to ensure users have access to the latest information about the
project's capabilities.
2026-03-11 20:58:02 +00:00
Mervin Praison 15146ff6a7 docs(README): add new features to the documentation table
Added several new features to the README.md including Sessions + Auto-Save, Telemetry, Policy Engine, Thinking Budgets, Background Tasks, Doom Loop Detection, Graph Memory, RAG + Quality-Based RAG, and Sandbox Execution.
2026-03-11 20:55:38 +00:00
Mervin Praison 0d0c2fa762 docs(README): update features section with detailed explanations
Enhanced the features section with specific feature descriptions and
usage examples to provide clearer insights into the framework's
capabilities and benefits for new users.
2026-03-11 20:53:30 +00:00
Mervin Praison c9010fca9f docs(readme): major rewrite of README for clarity and content updates
The README.md has been significantly updated with a focus on clear
guidance and reduced verbosity. Many sections were condensed or
removed, resulting in a net decrease in lines while enhancing
navigation and support links for users.
2026-03-11 20:45:19 +00:00
Mervin Praison 7a36352196 Release v4.5.39 2026-03-11 00:23:03 +00:00
Mervin Praison 6ae4ebedcb Merge branch 'main' of https://github.com/MervinPraison/PraisonAI 2026-03-11 00:20:17 +00:00
Mervin Praison 3377347087 feat(scheduler): add session ID to DeliveryTarget and scheduling commands
Added an optional session_id parameter to DeliveryTarget and scheduling
functions to preserve conversation context during job execution. This
enhancement allows agents to access prior chat history and improves
overall task management flexibility. Updated relevant documentation and
CLI commands accordingly.
2026-03-11 00:19:32 +00:00
github-actions[bot] f47aa248ea docs: auto-update api.md [skip ci] 2026-03-10 18:00:30 +00:00
Mervin Praison 0379e9f196 Release v4.5.38 2026-03-10 18:00:13 +00:00
Mervin Praison c841d46ae9 Release v4.5.37 2026-03-10 15:27:06 +00:00
Mervin Praison cd6fdd0107 feat(memory): implement MongoDB storage backend and memory protocol aliases
Added a MongoDB backend for storage, which allows document-oriented
data storage. Implemented protocol-compliant aliases for memory operations
to improve consistency. Updated related memory management logic to support
new functionality, enhancing overall performance and user experience.
2026-03-10 15:23:43 +00:00
Mervin Praison d840407b12 Release v4.5.36 2026-03-10 14:05:29 +00:00
Mervin Praison b99ca3f0ae Release v4.5.35 2026-03-10 13:09:43 +00:00
Mervin Praison 23c48f6cd5 Release v4.5.34 2026-03-10 11:16:45 +00:00
Mervin Praison d0fa48b110 Release v4.5.33 2026-03-10 06:07:07 +00:00
Mervin Praison 0d2c1bd961 Release v4.5.32 2026-03-10 05:42:59 +00:00
Mervin Praison ef6b1a8913 Release v4.5.31 2026-03-09 14:43:23 +00:00
github-actions[bot] dc9c250adb docs: auto-update api.md [skip ci] 2026-03-09 14:06:13 +00:00
Mervin Praison d1fb090a07 Release v4.5.30 2026-03-09 14:05:55 +00:00
Mervin Praison 8479fffa8c feat(paths): refactor directory management for agents and config loading
Updated various parts of the codebase to centralize directory paths
using the new `paths.py` utility. This improves maintainability and
consistency across the project when accessing directory paths for
plugins, snapshots, memory, and configuration.

Added integration and unit tests to ensure correct behavior of path
configurations.
2026-03-09 13:57:35 +00:00
Mervin Praison 8555b5ad69 Release v4.5.29 2026-03-09 06:18:18 +00:00
JiangNan ad8ee02b5a fix: correct duplicate gpt-4o-mini key in MODEL_WINDOWS to gpt-4o
MODEL_WINDOWS had two entries for gpt-4o-mini and none for gpt-4o.
The first entry was clearly intended to be gpt-4o based on context.
This meant gpt-4o users got no context window match, potentially
causing suboptimal chunking or fallback behavior.

Signed-off-by: JiangNan <[email protected]>
2026-03-08 22:32:24 +08:00
JiangNan 02b9c64c0a fix: handle None agent name when generating history session ID
When an agent is created with instructions but no explicit name, and
memory history is enabled, name can be None. Calling .encode() on None
raises AttributeError. Fall back to "agent" when name is None.

Signed-off-by: JiangNan <[email protected]>
2026-03-08 22:32:21 +08:00
Mervin Praison 16f9325176 Release v4.5.28 2026-03-08 07:03:40 +00:00
Mervin Praison fc9f8c5e1d Release v4.5.27 2026-03-08 00:06:51 +00:00
Mervin Praison f5ee866e6c feat(gateway, session): add gateway integration and lazy loading
- Implement lazy loading for WebSocketGateway and GatewaySession in the
  gateway module, allowing easier access without needing wrapper imports.
- Enhance SessionData to include gateway session ID and agent ID.
- Introduce methods in DefaultSessionStore for querying sessions by agent
  and linking sessions to gateway sessions.
2026-03-08 00:02:39 +00:00
549 changed files with 53099 additions and 16125 deletions

No files matched your search

@@ -5,6 +5,9 @@ inputs:
anthropic_api_key:
description: "Anthropic API key"
required: false
claude_code_oauth_token:
description: "Claude Code OAuth Token for Claude Pro"
required: false
github_token:
description: "GitHub token for Claude to operate with"
required: false
@@ -212,6 +215,13 @@ runs:
- Documentation updates
- Testing and deployment
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.
Respond naturally and helpfully to the user's request using the available tools.
EOF
@@ -253,4 +263,5 @@ runs:
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 }}
@@ -6,8 +6,8 @@ inputs:
description: "Timeout in minutes for execution"
required: false
default: "5"
anthropic_api_key:
description: "Anthropic API key"
claude_code_oauth_token:
description: "Claude Code OAuth token for Pro subscription access"
required: true
github_token:
description: "GitHub token with repo and issues permissions"
@@ -21,7 +21,8 @@ runs:
with:
fetch-depth: 0
- name: Create prompt file
- name: Create prompt
id: generate_prompt
shell: bash
run: |
mkdir -p /tmp/claude-prompts
@@ -75,13 +76,16 @@ runs:
- Your ONLY action should be to apply labels using mcp__github__update_issue
- It's okay to not add any labels if none are clearly applicable
EOF
echo "PROMPT<<DELIMITER" >> $GITHUB_OUTPUT
cat /tmp/claude-prompts/claude-issue-triage-prompt.txt >> $GITHUB_OUTPUT
echo "DELIMITER" >> $GITHUB_OUTPUT
- name: Run Claude Code
uses: ./.github/actions/claude-code-action
with:
prompt_file: /tmp/claude-prompts/claude-issue-triage-prompt.txt
direct_prompt: ${{ steps.generate_prompt.outputs.PROMPT }}
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
install_github_mcp: "true"
timeout_minutes: ${{ inputs.timeout_minutes }}
anthropic_api_key: ${{ inputs.anthropic_api_key }}
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
github_token: ${{ inputs.github_token }}
+8 -1
View File
@@ -22,6 +22,12 @@ inputs:
To enable observability with Google Cloud, include:
{"telemetry": {"enabled": true, "target": "gcp"}, "sandbox": false}
required: false
model:
description: |
The Gemini model to use. Options: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite.
Use flash-lite for highest free-tier quota (1,000 RPD).
required: false
default: 'gemini-3.1-flash-lite-preview'
version:
description: |
The version of @google/gemini-cli to execute.
@@ -70,10 +76,11 @@ runs:
- name: Run Gemini CLI
run: |
gemini --yolo --prompt "$PROMPT"
gemini --yolo --model "$GEMINI_MODEL" --prompt "$PROMPT"
shell: bash
env:
GEMINI_API_KEY: ${{ inputs.GEMINI_API_KEY }}
GEMINI_MODEL: ${{ inputs.model }}
PROMPT: ${{ inputs.prompt }}
branding:
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 429 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 872 B

After

Width:  |  Height:  |  Size: 872 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

+31 -11
View File
@@ -1,26 +1,46 @@
name: Auto Issue Comment with PR Creation
name: Auto Issue Comment
on:
issues:
types: [opened]
workflow_dispatch:
# issues:
# types: [opened]
jobs:
add-comment:
# Trigger Claude by adding the 'claude' label (only if not already labeled)
# Note: @claude in comments only works from human users (not bots)
# The claude.yml workflow listens for issues:labeled event with 'claude' label
trigger-claude:
# Skip if issue was already created with 'claude' label to avoid double-trigger
if: |
!contains(toJSON(github.event.issue.labels), 'claude')
runs-on: ubuntu-latest
permissions:
issues: write
contents: write
pull-requests: write
actions: read
steps:
- name: Add automated comment
- name: Add claude label to trigger analysis
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
github.rest.issues.createComment({
// Check if 'claude' label already exists on this issue
const labels = context.payload.issue.labels.map(l => l.name);
if (labels.includes('claude')) {
console.log('Issue already has claude label, skipping');
return;
}
// Add label to trigger claude.yml via issues:labeled event
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude review this issue and do a detailed analysis and fix this if the existing code doesn\'t have the solution implemented. Making sure it has backward compatibility, no existing features removed. After making those changes, again review the applied changes. Use @web to search if you dont know any information or to find the latest documentation or to find the latest version. Run the code if you think you need to run it to test it. Minimal code change to start with if required any changes. MAINLY IT SHOULD NOT IMPACT ON THE CURRENT SPEED OF EXECUTION of existing features, not increasing the overhead. **Please create a PR using gh tool with your changes**.'
})
labels: ['claude']
});
// Also post instructions as a comment for context
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🤖 Auto-triaging with Claude. Analyzing issue and implementing a fix...\n\nClaude will:\n1. Classify this issue\n2. Search codebase for root cause\n3. Implement a minimal fix following AGENTS.md\n4. Create a PR with the changes'
});
+123 -4
View File
@@ -1,23 +1,142 @@
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
# 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).
on:
issue_comment:
types: [created]
pull_request_review:
types: [submitted]
pull_request:
types: [opened]
jobs:
add-comment:
# ================================================================
# HUMAN PRs: Trigger @copilot AFTER CodeRabbit posts its summary
# ================================================================
copilot-after-coderabbit:
if: |
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')
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Add automated comment
- name: Post Copilot review request (as user via PAT)
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
});
// Check for duplicates — posted by MervinPraison (PAT) or github-actions[bot] (legacy)
const alreadyPosted = comments.data.some(c =>
c.body.includes('@copilot') &&
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
);
if (alreadyPosted) {
console.log('Copilot already triggered, skipping');
return;
}
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude review this pull request and do a detailed analysis and fix this if the existing code doesn\'t have the solution implemented. Making sure it has backward compatibility, no existing features removed. After making those changes, again review the applied changes. Use @web to search if you dont know any information or to find the latest documentation or to find the latest version. Run the code if you think you need to run it to test it. Minimal code change to start with if required any changes. MAINLY IT SHOULD NOT IMPACT ON THE CURRENT SPEED OF EXECUTION of existing features, not increasing the overhead.'
})
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'
})
# Fallback: Trigger @copilot after Qodo review (if Coderabbit doesn't fire)
copilot-after-qodo:
if: |
github.event_name == 'pull_request_review' &&
github.event.review.user.login == 'qodo-code-review[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Post Copilot review request (as user via PAT)
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
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@copilot') &&
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
);
if (alreadyPosted) {
console.log('Copilot already triggered, skipping');
return;
}
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'
})
# ================================================================
# BOT PRs: CodeRabbit/Qodo skip bot-authored PRs by default.
# Trigger them explicitly. Do NOT trigger Copilot here — it will
# be triggered by copilot-after-coderabbit/qodo when they finish.
# ================================================================
bot-pr-trigger-reviews:
if: |
github.event_name == 'pull_request' &&
github.event.action == 'opened' &&
(
github.event.pull_request.user.login == 'github-actions[bot]' ||
github.event.pull_request.user.login == 'praisonai-triage-agent[bot]' ||
github.event.pull_request.user.type == 'Bot'
)
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Trigger CodeRabbit, Qodo, and Gemini reviews
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
// 1. Trigger CodeRabbit review
await github.rest.issues.createComment({
issue_number: pr, owner, repo,
body: '@coderabbitai review'
});
console.log(`Triggered CodeRabbit for bot PR #${pr}`);
// 2. Trigger Qodo review
await github.rest.issues.createComment({
issue_number: pr, owner, repo,
body: '/review'
});
console.log(`Triggered Qodo for bot PR #${pr}`);
// 3. Trigger Gemini review
await github.rest.issues.createComment({
issue_number: pr, owner, repo,
body: '@gemini review this PR'
});
console.log(`Triggered Gemini for bot PR #${pr}`);
// NOTE: @copilot is NOT triggered here.
// It will be triggered by copilot-after-coderabbit or copilot-after-qodo
// once those bots finish, ensuring Copilot always reviews LAST.
console.log('Copilot will be triggered after CodeRabbit/Qodo complete.');
@@ -0,0 +1,58 @@
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'
})
+13 -4
View File
@@ -2,8 +2,9 @@ name: Claude Issue Triage
description: "Automatically triage GitHub issues using Claude Code"
on:
issues:
types: [opened]
workflow_dispatch:
# issues:
# types: [opened]
jobs:
triage-issue:
@@ -12,12 +13,20 @@ jobs:
permissions:
contents: read
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 }}
- name: Run Claude Issue Triage
uses: ./.github/actions/claude-issue-triage-action
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.app-token.outputs.token }}
+56 -109
View File
@@ -6,35 +6,85 @@ on:
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned, labeled]
types: [assigned, labeled, opened]
pull_request_review:
types: [submitted]
jobs:
claude-response:
# Prevent certain bots from triggering this action (but allow github-actions[bot] for auto-comments)
# Allow: human users, github-actions[bot] (auto-comments/labels)
# Block: dependabot, cursor, renovate, other bots
if: |
(github.event.action != 'labeled' || github.event.label.name == 'claude') &&
(github.event_name != 'issue_comment' || contains(github.event.comment.body, '@claude')) &&
(
!contains(github.actor, '[bot]') ||
github.actor == 'github-actions[bot]' ||
github.actor == 'praisonai-triage-agent[bot]'
) &&
github.actor != 'dependabot[bot]' &&
github.actor != 'cursor[bot]' &&
github.actor != 'renovate[bot]' &&
!(contains(github.actor, '[bot]') && github.actor != 'github-actions[bot]')
github.actor != 'renovate[bot]'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
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 }}
- uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_ACCESS_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
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: |
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 1 — READ GUIDELINES:
Read AGENTS.md to understand the architecture rules.
STEP 2 — ARCHITECTURE VALIDATION (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.
- 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.
STEP 3 — IMPLEMENT:
- 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 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, and create PR using `gh pr create`
CRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link.
allowed_tools: |
Bash(git:*)
Bash(python:*)
@@ -55,106 +105,3 @@ jobs:
mcp__github__get_issue_comments
mcp__github__update_issue
timeout_minutes: 30
post-review-comment:
needs: claude-response
if: success() && needs.claude-response.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Check for existing review comment
id: check-comment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const reviewText = '@claude review if the changes made in the above branch are as per the requirements and make changes to the above branch if issues still exist.';
let comments = [];
let issueNumber;
try {
// Determine issue/PR number based on event type
if (context.eventName === 'pull_request_review_comment' || context.eventName === 'pull_request_review') {
issueNumber = context.payload.pull_request?.number || context.issue.number;
} else if (context.eventName === 'issue_comment' && context.payload.issue.pull_request) {
// This is a comment on a PR (not an issue)
issueNumber = context.payload.issue.number;
} else {
// This is a regular issue
issueNumber = context.issue.number;
}
comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber
});
// Look for Claude's completed review comments
const claudeReviewComments = comments.data.filter(comment =>
(comment.user.login === 'claude[bot]' || comment.body.includes('✅') || comment.body.includes('## Summary')) &&
!comment.body.includes('⏳') &&
!comment.body.includes('🔄') &&
!comment.body.includes('Loading...')
);
// Check if our review request comment already exists
const existingReviewRequest = comments.data.find(comment =>
comment.body.includes(reviewText)
);
// Post if Claude has completed a review AND we haven't posted the review request yet
const shouldPost = claudeReviewComments.length > 0 && !existingReviewRequest;
core.setOutput('should-post', shouldPost);
console.log(`Claude review completed: ${claudeReviewComments.length > 0}`);
console.log(`Review request exists: ${!!existingReviewRequest}`);
console.log(`Should post comment: ${shouldPost}`);
} catch (error) {
console.log('Error checking comments:', error);
core.setOutput('should-post', false); // If we can't check, don't post
}
- name: Wait 30 seconds before posting comment
if: steps.check-comment.outputs.should-post == 'true'
run: sleep 30
- name: Post review comment
if: steps.check-comment.outputs.should-post == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
let issueNumber;
let isPR = false;
// Determine issue/PR number and type based on event type
if (context.eventName === 'pull_request_review_comment' || context.eventName === 'pull_request_review') {
issueNumber = context.payload.pull_request?.number || context.issue.number;
isPR = true;
} else if (context.eventName === 'issue_comment' && context.payload.issue.pull_request) {
// This is a comment on a PR (not an issue)
issueNumber = context.payload.issue.number;
isPR = true;
} else {
// This is a regular issue - don't post review comment
issueNumber = context.issue.number;
isPR = false;
}
// Only post review comment on PRs, not regular issues
if (isPR) {
console.log(`Posting review comment on PR #${issueNumber}`);
await github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude review if the changes made in the above branch are as per the requirements and make changes to the above branch if issues still exist. MAINLY IT SHOULD NOT IMPACT ON THE CURRENT SPEED OF EXECUTION of existing features, not increasing the overhead.'
});
} else {
console.log(`Skipping review comment for regular issue #${issueNumber}`);
}
-62
View File
@@ -1,62 +0,0 @@
name: Run tests and upload coverage
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
name: Run tests and collect coverage
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-cov pytest-asyncio
# Install knowledge dependencies from praisonai-agents
uv pip install --system "praisonaiagents[knowledge]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Run tests
run: |
cd src/praisonai
# Run tests - allow some failures for now while we fix remaining issues
pytest --cov=praisonai --cov-branch --cov-report=xml tests/unit/ --tb=no -q || echo "Some tests failed, but coverage was collected"
- name: Upload results to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: src/praisonai/coverage.xml
flags: unit-tests
name: unit-tests-coverage
fail_ci_if_error: false
verbose: true
@@ -1,8 +1,10 @@
name: Gemini Automated Issue Triage
# on:
# issue_comment:
# types: [created]
on:
issue_comment:
types: [created]
workflow_dispatch:
jobs:
triage-issue:
+14 -16
View File
@@ -2,8 +2,8 @@ name: Gemini Issue Review
on:
workflow_dispatch:
issue_comment:
types: [created]
# issue_comment:
# types: [created]
permissions:
contents: write
@@ -20,7 +20,8 @@ jobs:
!contains(github.event.comment.body, '@gemini triage') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
github.event.comment.author_association == 'COLLABORATOR' ||
github.event.comment.author_association == 'CONTRIBUTOR'))
runs-on: ubuntu-latest
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
@@ -39,8 +40,8 @@ jobs:
- name: Set up Git
run: |
git config --local user.email "gemini-bot@github.com"
git config --local user.name "Gemini Bot"
git config --local user.email "[email protected].github.com"
git config --local user.name "MervinPraison"
- name: Get issue data
id: issue_data
@@ -75,16 +76,16 @@ jobs:
echo "ADDITIONAL_INSTRUCTIONS=$ADDITIONAL_INSTRUCTIONS" >> $GITHUB_OUTPUT
- name: Run Gemini CLI
uses: ./.github/actions/gemini
uses: google-github-actions/run-gemini-cli@v0
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
ISSUE_NUMBER: ${{ steps.issue_data.outputs.ISSUE_NUMBER }}
ADDITIONAL_INSTRUCTIONS: ${{ steps.issue_data.outputs.ADDITIONAL_INSTRUCTIONS }}
REPOSITORY: ${{ github.repository }}
with:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OTLP_GCP_WIF_PROVIDER: ${{ secrets.OTLP_GCP_WIF_PROVIDER }}
OTLP_GOOGLE_CLOUD_PROJECT: ${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}
gemini_api_key: ${{ secrets.GEMINI_API_KEY }}
gemini_model: 'gemini-2.5-flash'
prompt: |
You are a helpful GitHub issue assistant. Your task is to analyze and help with GitHub issues.
@@ -103,6 +104,8 @@ jobs:
- Search codebase for related files using `glob`, `search_file_content`, `list_directory`
**STEP 2: If Code Changes Are Needed**
MANDATORY FIRST STEP: Before writing any code, you MUST use `read_file` to carefully read "src/praisonai-agents/AGENTS.md". You must honor this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy!
Create a new branch with 'gemini/' prefix:
- Use: `git checkout -b gemini/issue-${ISSUE_NUMBER}-description`
- Example: `git checkout -b gemini/issue-123-fix-sequential-tools`
@@ -161,7 +164,7 @@ jobs:
Please proceed with helping resolve this issue following this exact workflow.
settings_json: |
settings: |
{
"coreTools": [
"run_shell_command(echo)",
@@ -193,10 +196,5 @@ jobs:
"web_fetch",
"google_web_search",
"save_memory"
],
"telemetry": {
"enabled": false,
"otlpGcpWifProvider": "${{ secrets.OTLP_GCP_WIF_PROVIDER }}",
"googleCloudProject": "${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}"
}
]
}
+4 -2
View File
@@ -1,8 +1,8 @@
name: Gemini PR Review
on:
issue_comment:
types: [created]
# issue_comment:
# types: [created]
workflow_dispatch:
inputs:
pr_number:
@@ -159,6 +159,8 @@ jobs:
- Analyze the code changes thoroughly
**STEP 2: If Code Improvements Are Needed**
MANDATORY FIRST STEP: Before writing any code, you MUST use `read_file` to carefully read "src/praisonai-agents/AGENTS.md". You must honor this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy!
**IMPORTANT:** DO NOT create a new branch! Work on the EXISTING PR branch.
- Use: `git fetch origin` to get latest changes
- Get PR branch name: `gh pr view ${PR_NUMBER} --json headRefName`
+11 -38
View File
@@ -1,25 +1,21 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
name: Python Package (Multi-Version)
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
schedule:
- cron: '0 4 * * 1' # Weekly Monday 4am UTC
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -30,39 +26,16 @@ jobs:
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Install dependencies
run: |
cd src/praisonai
python -m pip install --upgrade pip
python -m pip install flake8 pytest
python -m pip install ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
python -m pip install duckduckgo_search
python -m pip install pytest-asyncio pytest-cov
# Install knowledge dependencies from praisonai-agents
python -m pip install duckduckgo_search pytest pytest-asyncio pytest-cov
python -m pip install "praisonaiagents[knowledge]"
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Debug API Key Status
- name: Run tests
run: |
echo "🔍 Checking API key availability..."
if [ -n "${{ secrets.OPENAI_API_KEY }}" ]; then
echo "✅ GitHub secret OPENAI_API_KEY is available"
echo "🔑 API key starts with: $(echo "$OPENAI_API_KEY" | cut -c1-7)..."
else
echo "⚠️ GitHub secret OPENAI_API_KEY is NOT set - using fallback"
echo "🔑 Using fallback key: sk-test-key..."
fi
echo "🌐 API Base: $OPENAI_API_BASE"
echo "🤖 Model: $OPENAI_MODEL_NAME"
# - name: Lint with flake8
# run: |
# # stop the build if there are Python syntax errors or undefined names
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
cd src/praisonai && python tests/test_runner.py --pattern fast
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --timeout=60 || echo "Some tests failed"
+4 -4
View File
@@ -51,8 +51,8 @@ jobs:
# Sync with upstream commit
- name: Sync with upstream
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
git config --global user.name "MervinPraison"
git config --global user.email "[email protected].github.com"
# Add upstream remote
git remote add upstream https://github.com/MervinPraison/PraisonAI.git
@@ -473,8 +473,8 @@ jobs:
# Create tag at the same commit
- name: Create tag at commit
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
git config --global user.name "MervinPraison"
git config --global user.email "[email protected].github.com"
# Remove tag if it exists locally
git tag -d ${{ github.event.inputs.tag }} || true
+30 -245
View File
@@ -1,41 +1,44 @@
name: Core Tests
on:
on:
push:
branches: [ main, develop ]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-core.yml'
pull_request:
branches: [ main, develop ]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-core.yml'
workflow_dispatch:
jobs:
test-core:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
python-version: [3.11]
steps:
- name: Checkout code
uses: actions/checkout@v4
timeout-minutes: 20
- name: Set up Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
python-version: '3.11'
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
# Install knowledge dependencies from praisonai-agents
uv pip install --system "praisonaiagents[knowledge]"
- name: Set environment variables
@@ -43,246 +46,28 @@ jobs:
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
# Also export to current shell session for immediate availability
export OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}"
export OPENAI_API_BASE="${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}"
export OPENAI_MODEL_NAME="${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}"
export LOGLEVEL=DEBUG
# Verify immediate availability
echo "🔧 Immediate verification in same step:"
echo " OPENAI_API_KEY length in current session: ${#OPENAI_API_KEY}"
echo " OPENAI_API_KEY starts with sk-: $(echo "$OPENAI_API_KEY" | grep -q '^sk-' && echo 'YES' || echo 'NO')"
- name: Debug API Key Status
run: |
echo "🔍 Checking API key availability..."
if [ -n "${{ secrets.OPENAI_API_KEY }}" ]; then
echo "✅ GitHub secret OPENAI_API_KEY is available"
echo "🔑 API key starts with: $(echo "$OPENAI_API_KEY" | cut -c1-7)..."
else
echo "⚠️ GitHub secret OPENAI_API_KEY is NOT set - using fallback"
echo "🔑 Using fallback key: sk-test-key..."
fi
echo "🌐 API Base: $OPENAI_API_BASE"
echo "🤖 Model: $OPENAI_MODEL_NAME"
echo "🐛 Log Level: $LOGLEVEL"
echo "📊 Environment Check:"
echo " - OPENAI_API_KEY length: ${#OPENAI_API_KEY}"
echo " - OPENAI_API_BASE: $OPENAI_API_BASE"
echo " - OPENAI_MODEL_NAME: $OPENAI_MODEL_NAME"
echo " - LOGLEVEL: $LOGLEVEL"
echo "🔍 Is API key actually set?"
echo " - API key starts with sk-: $(echo "$OPENAI_API_KEY" | grep -q '^sk-' && echo 'YES' || echo 'NO')"
echo " - API key is not test key: $([ "$OPENAI_API_KEY" != 'sk-test-key-for-github-actions-testing-only-not-real' ] && echo 'YES' || echo 'NO')"
- name: Debug Environment Variables Raw
run: |
echo "🔧 Raw environment variable check:"
echo "OPENAI_API_KEY set: $(if [ -n "$OPENAI_API_KEY" ]; then echo 'YES'; else echo 'NO'; fi)"
echo "OPENAI_API_KEY length: ${#OPENAI_API_KEY}"
echo "OPENAI_API_KEY first 10 chars: ${OPENAI_API_KEY:0:10}"
echo "OPENAI_API_KEY last 5 chars: ${OPENAI_API_KEY: -5}"
printenv | grep OPENAI || echo "No OPENAI env vars found"
- name: Debug Python Environment Variables
run: |
python -c "
import os
print('🐍 Python Environment Variable Check:')
api_key = os.environ.get('OPENAI_API_KEY', 'NOT_SET')
if api_key != 'NOT_SET':
print(f' ✅ OPENAI_API_KEY: {api_key[:7]}... (length: {len(api_key)})')
else:
print(' ❌ OPENAI_API_KEY: NOT_SET')
print(f' 🌐 OPENAI_API_BASE: {os.environ.get(\"OPENAI_API_BASE\", \"NOT_SET\")}')
print(f' 🤖 OPENAI_MODEL_NAME: {os.environ.get(\"OPENAI_MODEL_NAME\", \"NOT_SET\")}')
print(f' 📋 All OPENAI env vars:')
for key, value in os.environ.items():
if key.startswith('OPENAI'):
print(f' {key}: {value[:10] if len(value) > 10 else value}...')
"
- name: Validate API Key
run: |
echo "🔑 Testing API key validity with minimal OpenAI call..."
python -c "
import os
try:
from openai import OpenAI
client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))
response = client.models.list()
print('✅ API Key is VALID - OpenAI responded successfully')
print(f'📊 Available models: {len(list(response.data))} models found')
except Exception as e:
print(f'❌ API Key is INVALID - Error: {e}')
print('🔍 This explains why all API-dependent tests are failing')
print('💡 The GitHub secret OPENAI_API_KEY needs to be updated with a valid key')
"
continue-on-error: true
- name: Debug PraisonAI API Key Usage
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}
OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}
OPENAI_MODEL_NAME: ${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}
LOGLEVEL: DEBUG
run: |
echo "🔍 Testing PraisonAI API key usage directly..."
cd src/praisonai
python -c "
import os
import sys
sys.path.insert(0, '.')
# Attempt to import SecretStr, otherwise use a dummy class
try:
from pydantic.types import SecretStr
except ImportError:
class SecretStr: # Dummy class if pydantic is not available in this minimal context
def __init__(self, value): self._value = value
def get_secret_value(self): return self._value
def get_key_display_value(key_value):
if isinstance(key_value, SecretStr):
return key_value.get_secret_value()[:10] if key_value.get_secret_value() else 'EMPTY_SECRET'
elif isinstance(key_value, str):
return key_value[:10] if key_value != 'nokey' and key_value != 'NOT_SET' else key_value
return 'INVALID_TYPE'
print('🔧 Direct PraisonAI API Key Check:')
env_api_key = os.environ.get(\"OPENAI_API_KEY\", \"NOT_SET\")
print(f'Environment OPENAI_API_KEY: {get_key_display_value(env_api_key)}...')
from praisonai import PraisonAI
praisonai = PraisonAI()
print(f'PraisonAI config_list: {praisonai.config_list}')
api_key_from_config = praisonai.config_list[0].get('api_key', 'NOT_SET')
print(f'API key from PraisonAI config: {get_key_display_value(api_key_from_config)}...')
from praisonai.inc.models import PraisonAIModel
print('\\n🧪 Testing PraisonAIModel with explicit API key (CrewAI method):')
model_with_explicit_key = PraisonAIModel(
model='openai/gpt-5-nano',
base_url=praisonai.config_list[0].get('base_url'),
api_key=api_key_from_config # This will be a string from praisonai.config_list
)
print(f' Model: {model_with_explicit_key.model}')
print(f' Model name: {model_with_explicit_key.model_name}')
print(f' API key var: {model_with_explicit_key.api_key_var}')
# model_with_explicit_key.api_key is now a string, or 'nokey'
print(f' API key (explicitly passed to PraisonAIModel): {get_key_display_value(model_with_explicit_key.api_key)}...')
print(f' Base URL: {model_with_explicit_key.base_url}')
try:
llm_instance = model_with_explicit_key.get_model()
print(f' ✅ LLM instance created successfully: {type(llm_instance).__name__}')
# langchain_openai.ChatOpenAI stores the key in openai_api_key as SecretStr
llm_api_key_attr = getattr(llm_instance, 'openai_api_key', 'NOT_FOUND')
if llm_api_key_attr != 'NOT_FOUND':
print(f' LLM instance API key: {get_key_display_value(llm_api_key_attr)}...')
else:
print(f' LLM instance API key attribute not found.')
except Exception as e:
print(f' ❌ Failed to create LLM instance: {e}')
import traceback
traceback.print_exc()
"
continue-on-error: true
- name: Run Unit Tests
run: |
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --cov=praisonai --cov-report=term-missing --cov-report=xml --cov-branch || echo "Some tests failed - see output above"
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings \
--cov=praisonai --cov-report=xml --cov-branch \
--timeout=60 || echo "Some unit tests failed"
continue-on-error: true
- name: Run Integration Tests
- name: Run Integration Tests
run: |
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings \
--timeout=60 \
--ignore=tests/integration/test_tracker_complex_tasks.py \
--ignore=tests/integration/test_serve_integration.py \
|| echo "Some integration tests failed"
- name: Debug Directory Structure
run: |
echo "🔍 Debugging directory structure for CrewAI tests..."
cd src/praisonai
echo "Current working directory: $(pwd)"
echo "📁 Contents of current directory:"
ls -la
echo ""
echo "📁 Contents of tests directory:"
ls -la tests/ || echo "❌ tests/ directory not found"
echo ""
echo "📁 Contents of tests/integration:"
ls -la tests/integration/ || echo "❌ tests/integration/ directory not found"
echo ""
echo "📁 Looking for crewai directory:"
find . -name "crewai" -type d 2>/dev/null || echo "❌ No crewai directories found"
echo ""
echo "📁 Full directory tree of tests:"
tree tests/ || find tests/ -type d 2>/dev/null || echo "❌ Cannot explore tests directory"
- name: Test AutoGen Framework
run: |
echo "🤖 Testing AutoGen Framework Integration..."
cd src/praisonai && python tests/test_runner.py --pattern autogen --verbose
- name: Test CrewAI Framework
run: |
echo "⛵ Testing CrewAI Framework Integration..."
cd src/praisonai
echo "🔍 Trying test runner first..."
python tests/test_runner.py --pattern crewai --verbose || {
echo "❌ Test runner failed, trying direct pytest..."
echo "📁 Current directory: $(pwd)"
echo "📁 Looking for CrewAI tests..."
find . -name "*crewai*" -type f 2>/dev/null
echo "🧪 Trying direct pytest on integration/crewai..."
python -m pytest tests/integration/crewai/ -v --tb=short --disable-warnings || {
echo "❌ Direct path failed, trying relative path..."
python -m pytest integration/crewai/ -v --tb=short --disable-warnings || {
echo "❌ All CrewAI test attempts failed"
exit 1
}
}
}
- name: Run Legacy Tests with API Key
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}
OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}
OPENAI_MODEL_NAME: ${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}
LOGLEVEL: DEBUG
run: |
echo "🧪 Running legacy tests with real API key..."
echo "🔧 Final environment check before pytest:"
echo " OPENAI_API_KEY set: $([ -n "$OPENAI_API_KEY" ] && echo 'YES' || echo 'NO')"
echo " OPENAI_API_KEY length: ${#OPENAI_API_KEY}"
echo " OPENAI_API_KEY starts with sk-: $(echo "$OPENAI_API_KEY" | grep -q '^sk-' && echo 'YES' || echo 'NO')"
export OPENAI_API_KEY="$OPENAI_API_KEY"
export OPENAI_API_BASE="$OPENAI_API_BASE"
export OPENAI_MODEL_NAME="$OPENAI_MODEL_NAME"
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings -m "not real" --timeout=60
- name: Upload coverage reports to Codecov
if: matrix.python-version == '3.11'
- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: src/praisonai/coverage.xml
token: ${{ secrets.CODECOV_TOKEN || '' }}
files: src/praisonai/coverage.xml
flags: core-tests
name: core-tests-coverage
fail_ci_if_error: false
verbose: true
- name: Upload Coverage Reports (Artifacts)
if: matrix.python-version == '3.11'
uses: actions/upload-artifact@v4
with:
name: coverage-reports
path: |
src/praisonai/.coverage
src/praisonai/htmlcov/
src/praisonai/coverage.xml
retention-days: 7
fail_ci_if_error: false
+1 -6
View File
@@ -1,14 +1,9 @@
name: Extended Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
# Run nightly at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch: # Allow manual triggering
workflow_dispatch:
jobs:
test-examples:
+22 -6
View File
@@ -3,8 +3,16 @@ name: Optimized Test Suite
on:
push:
branches: [ main, develop ]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-optimized.yml'
pull_request:
branches: [ main, develop ]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-optimized.yml'
schedule:
# Nightly extended tests at 3 AM UTC
- cron: '0 3 * * *'
@@ -149,8 +157,8 @@ jobs:
if: matrix.python-version == '3.11'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: src/praisonai/coverage.xml
token: ${{ secrets.CODECOV_TOKEN || '' }}
files: src/praisonai/coverage.xml
flags: main-tests
fail_ci_if_error: false
@@ -293,8 +301,10 @@ jobs:
steps:
- name: Check if Google key is available
id: check-key
env:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
run: |
if [ -n "${{ secrets.GOOGLE_API_KEY }}" ]; then
if [ -n "$GOOGLE_API_KEY" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
@@ -348,8 +358,10 @@ jobs:
steps:
- name: Check if Groq key is available
id: check-key
env:
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
run: |
if [ -n "${{ secrets.GROQ_API_KEY }}" ]; then
if [ -n "$GROQ_API_KEY" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
@@ -403,8 +415,10 @@ jobs:
steps:
- name: Check if xAI key is available
id: check-key
env:
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
run: |
if [ -n "${{ secrets.XAI_API_KEY }}" ]; then
if [ -n "$XAI_API_KEY" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
@@ -458,8 +472,10 @@ jobs:
steps:
- name: Check if Cohere key is available
id: check-key
env:
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
run: |
if [ -n "${{ secrets.COHERE_API_KEY }}" ]; then
if [ -n "$COHERE_API_KEY" ]; then
echo "has_key=true" >> $GITHUB_OUTPUT
else
echo "has_key=false" >> $GITHUB_OUTPUT
+14 -13
View File
@@ -3,8 +3,16 @@ name: Windows Tests
on:
push:
branches: [main, develop]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-windows.yml'
pull_request:
branches: [main, develop]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-windows.yml'
workflow_dispatch:
jobs:
@@ -14,6 +22,7 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL_NAME: gpt-4o-mini
PYTHONUTF8: 1
steps:
- uses: actions/checkout@v4
@@ -27,13 +36,13 @@ jobs:
run: |
python -m pip install --upgrade pip
cd src/praisonai-agents
pip install .
pip install -e .
shell: bash
- name: Install praisonai wrapper
run: |
cd src/praisonai
pip install .[dev]
pip install -e .
shell: bash
# ==================== CORE IMPORTS ====================
@@ -41,7 +50,7 @@ jobs:
run: |
python -c "from praisonaiagents import Agent, Agents, Workflow; print('Core imports OK')"
python -c "from praisonaiagents import tool, Tools; print('Tool imports OK')"
python -c "from praisonaiagents import Task, WorkflowStep; print('Task/Step imports OK')"
python -c "from praisonaiagents import Task; print('Task imports OK')"
# ==================== AGENT FEATURES ====================
- name: Test Agent instantiation
@@ -66,21 +75,13 @@ jobs:
"
# ==================== WORKFLOW FEATURES ====================
- name: Test Workflow and WorkflowStep
- name: Test Workflow
run: |
python -c "
from praisonaiagents import Workflow, WorkflowStep
from praisonaiagents.workflows import WorkflowOutputConfig
from praisonaiagents import Workflow
w = Workflow(name='test')
s1 = WorkflowStep(name='step1')
s2 = WorkflowStep(name='step2', context=['step1'])
print('Workflow:', w.name)
print('Steps:', s1.name, s2.name)
cfg = WorkflowOutputConfig(verbose=True, stream=False)
w2 = Workflow(name='configured', output=cfg)
print('Configured workflow verbose:', w2.verbose)
"
# ==================== MEMORY FEATURES ====================
-516
View File
@@ -1,516 +0,0 @@
name: Quick Validation Tests
on: [push, pull_request]
jobs:
quick-test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
# Install knowledge dependencies from praisonai-agents
uv pip install --system "praisonaiagents[knowledge]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Backup Root Config Files
run: |
echo "🔄 Backing up root configuration files to prevent default file resolution interference..."
echo "ℹ️ PraisonAI automatically uses 'agents.yaml' in working directory when no file specified"
echo "ℹ️ This can interfere with test files that specify their own YAML files"
if [ -f "agents.yaml" ]; then
mv agents.yaml agents.yaml.backup
echo "✅ Moved root agents.yaml to agents.yaml.backup"
echo " - This prevents default file fallback during tests"
fi
if [ -f "tools.py" ]; then
mv tools.py tools.py.backup
echo "✅ Moved tools.py to tools.py.backup"
fi
- name: Debug API Key Status
run: |
echo "🔍 Checking API key availability..."
if [ -n "${{ secrets.OPENAI_API_KEY }}" ]; then
echo "✅ GitHub secret OPENAI_API_KEY is available"
echo "🔑 API key starts with: $(echo "$OPENAI_API_KEY" | cut -c1-7)..."
else
echo "⚠️ GitHub secret OPENAI_API_KEY is NOT set - using fallback"
echo "🔑 Using fallback key: sk-test-key..."
fi
echo "🌐 API Base: $OPENAI_API_BASE"
echo "🤖 Model: $OPENAI_MODEL_NAME"
echo "🐛 Log Level: $LOGLEVEL"
echo "📊 Environment Check:"
echo " - OPENAI_API_KEY length: ${#OPENAI_API_KEY}"
echo " - OPENAI_API_BASE: $OPENAI_API_BASE"
echo " - OPENAI_MODEL_NAME: $OPENAI_MODEL_NAME"
echo " - LOGLEVEL: $LOGLEVEL"
- name: Debug Python Environment Variables
run: |
python -c "
import os
print('🐍 Python Environment Variable Check:')
api_key = os.environ.get('OPENAI_API_KEY', 'NOT_SET')
if api_key != 'NOT_SET':
print(f' ✅ OPENAI_API_KEY: {api_key[:7]}... (length: {len(api_key)})')
else:
print(' ❌ OPENAI_API_KEY: NOT_SET')
print(f' 🌐 OPENAI_API_BASE: {os.environ.get(\"OPENAI_API_BASE\", \"NOT_SET\")}')
print(f' 🤖 OPENAI_MODEL_NAME: {os.environ.get(\"OPENAI_MODEL_NAME\", \"NOT_SET\")}')
print(f' 📋 All OPENAI env vars:')
for key, value in os.environ.items():
if key.startswith('OPENAI'):
print(f' {key}: {value[:10] if len(value) > 10 else value}...')
"
- name: Find Researcher Role Source
run: |
echo "🔍 Hunting for the mysterious 'Researcher' role..."
cd src/praisonai
python -c "
import os
import yaml
import glob
print('📋 Searching for Researcher role in all YAML files:')
yaml_files = glob.glob('src/praisonai/tests/*.yaml')
for yaml_file in yaml_files:
try:
with open(yaml_file, 'r') as f:
config = yaml.safe_load(f)
# Check if any role contains 'researcher'
roles = config.get('roles', {})
for role_key, role_data in roles.items():
role_name = role_data.get('role', '')
if 'researcher' in role_key.lower() or 'researcher' in role_name.lower():
print(f' 🎯 FOUND in {yaml_file}:')
print(f' Framework: {config.get(\"framework\", \"NOT_SET\")}')
print(f' Role key: {role_key}')
print(f' Role name: {role_name}')
print(f' All roles: {list(roles.keys())}')
print()
except Exception as e:
print(f' ❌ Error reading {yaml_file}: {e}')
print('🔍 Checking for default configurations...')
# Check if there are any default configs or hardcoded roles
try:
import praisonai
print(f' PraisonAI package location: {praisonai.__file__}')
# Check if there are any example YAML files in the package
package_dir = os.path.dirname(praisonai.__file__)
for root, dirs, files in os.walk(package_dir):
for file in files:
if file.endswith(('.yaml', '.yml')):
file_path = os.path.join(root, file)
print(f' 📁 Found YAML in package: {file_path}')
except Exception as e:
print(f' ❌ Error checking package: {e}')
"
continue-on-error: true
- name: Trace AutoGen Execution Path
run: |
echo "🔍 Tracing AutoGen execution to find where it diverges..."
cd src/praisonai
python -c "
import os
import sys
sys.path.insert(0, '.')
try:
from praisonai import PraisonAI
from praisonai.agents_generator import AgentsGenerator
# Test the exact execution path
print('🎯 Testing AutoGen execution path:')
praisonai = PraisonAI(agent_file='tests/autogen-agents.yaml')
print(f' 1. PraisonAI framework: \"{praisonai.framework}\"')
agents_gen = AgentsGenerator(
agent_file='tests/autogen-agents.yaml',
framework=praisonai.framework,
config_list=praisonai.config_list
)
print(f' 2. AgentsGenerator framework: \"{agents_gen.framework}\"')
# Load the YAML to check what it contains
import yaml
with open('src/praisonai/tests/autogen-agents.yaml', 'r') as f:
config = yaml.safe_load(f)
framework = agents_gen.framework or config.get('framework')
print(f' 3. Final framework decision: \"{framework}\"')
print(f' 4. Available frameworks:')
# Check framework availability
try:
import autogen
print(f' ✅ AutoGen available')
except ImportError:
print(f' ❌ AutoGen NOT available')
try:
from praisonaiagents import Agent
print(f' ✅ PraisonAI agents available')
except ImportError:
print(f' ❌ PraisonAI agents NOT available')
try:
from crewai import Agent
print(f' ✅ CrewAI available')
except ImportError:
print(f' ❌ CrewAI NOT available')
print(f' 5. Roles in YAML: {list(config.get(\"roles\", {}).keys())}')
# Now test the actual framework execution
if framework == 'autogen':
print(f' 6. ✅ Should execute _run_autogen')
elif framework == 'praisonai':
print(f' 6. ❌ Would execute _run_praisonai (WRONG!)')
else:
print(f' 6. ❌ Would execute _run_crewai (DEFAULT FALLBACK)')
except Exception as e:
print(f'❌ Error tracing execution: {e}')
import traceback
traceback.print_exc()
"
continue-on-error: true
- name: Debug YAML Loading and Roles
run: |
echo "🔍 Tracing YAML file loading and role creation..."
cd src/praisonai
python -c "
import os
import sys
import yaml
sys.path.insert(0, '.')
print('📁 Available YAML files in src/praisonai/tests/:')
import glob
yaml_files = glob.glob('tests/*.yaml')
for f in yaml_files:
print(f' {f}')
print()
print('📋 Content of autogen-agents.yaml:')
with open('tests/autogen-agents.yaml', 'r') as f:
config = yaml.safe_load(f)
print(f' Framework: {config.get(\"framework\")}')
print(f' Topic: {config.get(\"topic\")}')
print(f' Roles: {list(config.get(\"roles\", {}).keys())}')
for role_key, role_data in config.get('roles', {}).items():
print(f' {role_key} -> {role_data.get(\"role\", \"NO_ROLE\")}')
print()
print('🔍 Checking if execution uses a different YAML:')
# Check other YAML files for 'Researcher' role
for yaml_file in yaml_files:
try:
with open(yaml_file, 'r') as f:
test_config = yaml.safe_load(f)
roles = test_config.get('roles', {})
for role_key, role_data in roles.items():
if 'researcher' in role_key.lower() or 'researcher' in role_data.get('role', '').lower():
print(f' 🎯 FOUND Researcher in {yaml_file}!')
print(f' Framework: {test_config.get(\"framework\")}')
print(f' Role key: {role_key} -> {role_data.get(\"role\")}')
except:
pass
"
continue-on-error: true
- name: Debug Framework Detection
run: |
echo "🔍 Testing framework detection and config flow..."
python -c "
import os
import sys
import yaml
sys.path.insert(0, '.')
print('🔧 Testing framework detection:')
# Load the YAML file
with open('tests/autogen-agents.yaml', 'r') as f:
config = yaml.safe_load(f)
print(f' 📋 YAML framework: {config.get(\"framework\", \"NOT_SET\")}')
print(f' 📋 YAML topic: {config.get(\"topic\", \"NOT_SET\")}')
try:
from praisonai import PraisonAI
from praisonai.agents_generator import AgentsGenerator
# Test PraisonAI initialization
praisonai = PraisonAI(agent_file='tests/autogen-agents.yaml')
print(f' 🎯 PraisonAI framework: {praisonai.framework}')
# Test AgentsGenerator initialization
agents_gen = AgentsGenerator(
agent_file='tests/autogen-agents.yaml',
framework=praisonai.framework,
config_list=praisonai.config_list
)
print(f' ⚙️ AgentsGenerator framework: {agents_gen.framework}')
print(f' ⚙️ Final framework decision: {agents_gen.framework or config.get(\"framework\")}')
# Check config_list
print(f' 🔑 Config list model: {praisonai.config_list[0].get(\"model\")}')
print(f' 🔑 Config list API key: {praisonai.config_list[0].get(\"api_key\", \"NOT_SET\")[:10]}...')
except Exception as e:
print(f'❌ Error in framework detection: {e}')
"
continue-on-error: true
- name: Debug PraisonAIModel API Key Flow
run: |
echo "🔍 Testing PraisonAIModel API key handling..."
cd src/praisonai
python -c "
import os
import sys
sys.path.insert(0, '.')
print('🔑 Environment API Key Check:')
env_key = os.environ.get('OPENAI_API_KEY', 'NOT_FOUND')
print(f' OPENAI_API_KEY: {env_key[:10] if env_key != \"NOT_FOUND\" else \"NOT_FOUND\"}...')
try:
from praisonai.inc.models import PraisonAIModel
# Test PraisonAIModel with openai/gpt-5-nano (from YAML)
model = PraisonAIModel(model='openai/gpt-5-nano')
print('🤖 PraisonAIModel Configuration:')
print(f' model: {model.model}')
print(f' model_name: {model.model_name}')
print(f' api_key_var: {model.api_key_var}')
print(f' api_key: {model.api_key[:10] if model.api_key != \"nokey\" else \"DEFAULT_NOKEY\"}...')
print(f' base_url: {model.base_url}')
if model.api_key == 'nokey':
print('❌ FOUND THE ISSUE: PraisonAIModel is using default \"nokey\" instead of environment variable!')
else:
print('✅ PraisonAIModel has valid API key from environment')
except Exception as e:
print(f'❌ Error testing PraisonAIModel: {e}')
"
continue-on-error: true
- name: Validate API Key
run: |
echo "🔑 Testing API key validity with minimal OpenAI call..."
python -c "
import os
try:
from openai import OpenAI
client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))
# Make a minimal API call to test key validity
response = client.models.list()
print('✅ API Key is VALID - OpenAI responded successfully')
print(f'📊 Available models: {len(list(response.data))} models found')
except Exception as e:
print(f'❌ API Key is INVALID - Error: {e}')
print('🔍 This explains why all API-dependent tests are failing')
print('💡 The GitHub secret OPENAI_API_KEY needs to be updated with a valid key')
"
continue-on-error: true
- name: Test Direct PraisonAI Execution
run: |
echo "🧪 Testing direct PraisonAI execution (what works locally)..."
python -m praisonai src/praisonai/tests/autogen-agents.yaml
continue-on-error: true
- name: Comprehensive Execution Debug
run: |
echo "🔍 Comprehensive debugging of PraisonAI execution path..."
cd src/praisonai
python -c "
import os
import sys
import yaml
sys.path.insert(0, '.')
print('=' * 60)
print('🔍 COMPREHENSIVE EXECUTION DEBUG')
print('=' * 60)
# Check current working directory
print(f'📁 Current working directory: {os.getcwd()}')
# List files in current directory
print('📋 Files in current directory:')
for f in os.listdir('.'):
print(f' {f}')
print()
print('📋 Files in src/praisonai/tests/ directory:')
for f in os.listdir('tests'):
if f.endswith('.yaml'):
print(f' src/praisonai/tests/{f}')
# Check if root agents.yaml exists
print()
if os.path.exists('agents.yaml'):
print('❌ ROOT agents.yaml EXISTS (this is the problem!)')
with open('agents.yaml', 'r') as f:
root_config = yaml.safe_load(f)
print(f' Root framework: {root_config.get(\"framework\")}')
print(f' Root roles: {list(root_config.get(\"roles\", {}).keys())}')
else:
print('✅ ROOT agents.yaml does NOT exist (good!)')
# Test the actual execution path
print()
print('🎯 Testing EXACT execution path:')
try:
from praisonai import PraisonAI
from praisonai.agents_generator import AgentsGenerator
# Test with full path
test_file = 'src/praisonai/tests/autogen-agents.yaml'
print(f' Using test file: {test_file}')
print(f' File exists: {os.path.exists(test_file)}')
# Load the test file directly
with open(test_file, 'r') as f:
test_config = yaml.safe_load(f)
print(f' Test file framework: {test_config.get(\"framework\")}')
print(f' Test file roles: {list(test_config.get(\"roles\", {}).keys())}')
# Create PraisonAI instance
praisonai = PraisonAI(agent_file=test_file)
print(f' PraisonAI.agent_file: {praisonai.agent_file}')
print(f' PraisonAI.framework: {praisonai.framework}')
# Create AgentsGenerator
agents_gen = AgentsGenerator(
agent_file=test_file,
framework=praisonai.framework,
config_list=praisonai.config_list
)
print(f' AgentsGenerator.agent_file: {agents_gen.agent_file}')
print(f' AgentsGenerator.framework: {agents_gen.framework}')
# Test what would happen in generate_crew_and_kickoff
print()
print('🔧 Testing generate_crew_and_kickoff logic:')
# Simulate the loading logic
if agents_gen.agent_yaml:
loaded_config = yaml.safe_load(agents_gen.agent_yaml)
print(' Would load from agent_yaml')
else:
if agents_gen.agent_file == '/app/api:app' or agents_gen.agent_file == 'api:app':
agents_gen.agent_file = 'agents.yaml'
print(f' Would change agent_file to: {agents_gen.agent_file}')
try:
with open(agents_gen.agent_file, 'r') as f:
loaded_config = yaml.safe_load(f)
print(f' Successfully loaded: {agents_gen.agent_file}')
except FileNotFoundError:
print(f' FileNotFoundError: {agents_gen.agent_file}')
loaded_config = None
if loaded_config:
final_framework = agents_gen.framework or loaded_config.get('framework')
print(f' Final framework decision: {final_framework}')
print(f' Loaded roles: {list(loaded_config.get(\"roles\", {}).keys())}')
if 'researcher' in loaded_config.get('roles', {}):
print(' ❌ FOUND Researcher role in loaded config!')
else:
print(' ✅ No Researcher role in loaded config')
except Exception as e:
print(f'❌ Error during execution debug: {e}')
import traceback
traceback.print_exc()
"
continue-on-error: true
- name: Run Fast Tests
run: |
# Run the fastest, most essential tests with coverage
cd src/praisonai && python -m pytest tests/unit/test_core_agents.py -v --tb=short --disable-warnings --cov=praisonai --cov-report=xml --cov-branch
- name: Run Real API Tests
run: |
echo "🔑 Running real API tests with actual OpenAI API key..."
cd src/praisonai && python -m pytest tests/test_agents_playbook.py -v --tb=short --disable-warnings -m real
continue-on-error: true
- name: Run E2E Real Tests
run: |
echo "🧪 Running E2E real tests..."
cd src/praisonai && python -m pytest tests/e2e/ -v --tb=short --disable-warnings -m real
continue-on-error: true
- name: Run Legacy Example Tests
run: |
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings -m "not real" --timeout=60
continue-on-error: true
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: src/praisonai/coverage.xml
flags: quick-validation
name: quick-validation-coverage
fail_ci_if_error: false
verbose: true
- name: Restore Root Config Files
run: |
echo "🔄 Restoring root configuration files..."
if [ -f "agents.yaml.backup" ]; then
mv agents.yaml.backup agents.yaml
echo "✅ Restored agents.yaml"
fi
if [ -f "tools.py.backup" ]; then
mv tools.py.backup tools.py
echo "✅ Restored tools.py"
fi
if: always() # Always run this step, even if previous steps failed
+3 -3
View File
@@ -52,6 +52,6 @@ jobs:
with:
commit_message: "docs: auto-update api.md [skip ci]"
file_pattern: 'api.md'
commit_user_name: "github-actions[bot]"
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
commit_user_name: "MervinPraison"
commit_user_email: "454862+MervinPraison@users.noreply.github.com"
commit_author: "MervinPraison <454862+MervinPraison@users.noreply.github.com>"
+10 -7
View File
@@ -27,11 +27,13 @@ jobs:
fetch-depth: 0
- name: Checkout PraisonAIDocs
id: checkout_docs
continue-on-error: true
uses: actions/checkout@v4
with:
repository: MervinPraison/PraisonAIDocs
path: PraisonAIDocs
token: ${{ secrets.DOCS_PAT }}
token: ${{ secrets.SECRETS_ADMIN_PAT }}
- name: Set up Python
uses: actions/setup-python@v5
@@ -43,6 +45,7 @@ jobs:
pip install pyyaml
- name: Generate docs parity trackers
if: steps.checkout_docs.outcome == 'success'
run: |
cd $GITHUB_WORKSPACE
python3 src/praisonai/scripts/generate_docs_parity.py --docs-root PraisonAIDocs/docs --copy-docs
@@ -65,16 +68,16 @@ jobs:
with:
commit_message: "docs: auto-update documentation parity trackers [skip ci]"
file_pattern: 'src/praisonai-agents/DOCS_PARITY.md src/praisonai-ts/DOCS_PARITY.md src/praisonai-rust/DOCS_PARITY.md'
commit_user_name: "github-actions[bot]"
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
commit_user_name: "MervinPraison"
commit_user_email: "454862+MervinPraison@users.noreply.github.com"
commit_author: "MervinPraison <454862+MervinPraison@users.noreply.github.com>"
- name: Commit and push changes to PraisonAIDocs
if: steps.check_changes.outputs.changes == 'true'
if: steps.check_changes.outputs.changes == 'true' && steps.checkout_docs.outcome == 'success'
run: |
cd PraisonAIDocs
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "MervinPraison"
git config user.email "454862+MervinPraison@users.noreply.github.com"
git add docs/features/DOCS_PARITY.md docs/js/DOCS_PARITY.md docs/rust/DOCS_PARITY.md
git diff --staged --quiet || git commit -m "docs: auto-update documentation parity trackers [skip ci]"
git push
+6 -3
View File
@@ -29,6 +29,9 @@ jobs:
with:
python-version: '3.11'
- name: Install dependencies
run: pip install pyyaml
- name: Generate parity trackers
run: |
cd $GITHUB_WORKSPACE
@@ -52,7 +55,7 @@ jobs:
with:
commit_message: "docs: auto-update feature parity trackers [skip ci]"
file_pattern: 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md'
commit_user_name: "github-actions[bot]"
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
commit_user_name: "MervinPraison"
commit_user_email: "454862+MervinPraison@users.noreply.github.com"
commit_author: "MervinPraison <454862+MervinPraison@users.noreply.github.com>"
+5 -1
View File
@@ -21,6 +21,7 @@ docs.sh
other
output
!src/praisonai/praisonai/cli/output/
!src/praisonai-agents/praisonaiagents/output/
.files
threads.db
@@ -93,4 +94,7 @@ Cargo.lock
**/*.rs.bk
*.pdb
.windsurf
.agent
.agent
src/praisonai/build*.egg-info/
src/praisonai/PraisonAI.egg-info/
+620 -3095
View File
File diff suppressed because it is too large. Load diff
+40 -59
View File
@@ -28,39 +28,23 @@ from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepR
Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">achat</a>(prompt: str, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, task_name = None, task_description = None, task_id = None, attachments = None)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">aexecute</a>(task, context = None)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">agent_id</a>()</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">arun</a>(prompt: str, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">as_tool</a>(description: Optional[str] = None, tool_name: Optional[str] = None) -> 'Handoff'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">astart</a>(prompt: str, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat</a>(prompt, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, stream = None, task_name = None, task_description = None, task_id = None, config = None, force_retrieval = False, skip_retrieval = False, attachments = None, tool_choice = None)</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_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">check_pending_approvals</a>() -> Dict[str, Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clean_json_output</a>(output: str) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clear_history</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>()</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">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>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">delete_history</a>(index: int) -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">delete_history_matching</a>(pattern: str) -> int</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">cost_summary</a>() -> dict</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">diff</a>(from_hash: Optional[str] = None)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">display_name</a>() -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">ephemeral</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute</a>(task, context = None)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute_tool</a>(function_name, arguments)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute_tool_async</a>(function_name: str, arguments: Dict[str, Any]) -> Any</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">from_template</a>(uri: str, config: Optional[Dict[str, Any]] = None, offline: bool = False, **kwargs) -> 'Agent'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">generate_task</a>() -> 'Task'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_available_tools</a>() -> List[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_history_size</a>() -> int</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_learn_context</a>() -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_memory_context</a>(query: Optional[str] = None) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_recommended_stage</a>(prompt: str) -> str</code>
@@ -68,37 +52,29 @@ 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">iter_stream</a>(prompt: str, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">launch</a>(path: str = '/', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_model</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">pending_approval_count</a>() -> int</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">prune_history</a>(keep_last: int = 5) -> int</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>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>(value: Optional[Any]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">query</a>(question: str, **kwargs) -> 'RAGResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag_query</a>(question: str, **kwargs) -> 'RAGResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">redo</a>() -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieval_config</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieval_config</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieve</a>(query: str, **kwargs) -> 'ContextPack'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run</a>(prompt: str, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous_async</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until_async</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">session_id</a>() -> Optional[str]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">start</a>(prompt: str = None, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">submit_for_approval</a>(function_name: str, arguments: Dict[str, Any]) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">switch_model</a>(new_model: str) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs: Any) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>(value: Optional[Any]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>() -> Optional[int]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value: Optional[int]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">total_cost</a>() -> float</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">undo</a>() -> bool</code>
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">astart</a>()</code>
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">start</a>()</code>
@@ -250,6 +226,7 @@ Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_context_for_task</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_in_output: Optional[bool] = None) -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">calculate_quality_metrics</a>(output: str, expected_output: str, llm: Optional[str] = None, custom_prompt: Optional[str] = None) -> Dict[str, float]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">close_connections</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">compute_quality_score</a>(completeness: float, relevance: float, clarity: float, accuracy: float, weights: Dict[str, float] = None) -> float</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_long_term</a>(memory_id: str) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memories</a>(memory_ids: List[str]) -> int</code>
@@ -381,7 +358,7 @@ from praisonaiagents import GuardrailAction, GuardrailConfig, LLMGuardrail
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: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]</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>
# Planning
@@ -750,8 +727,6 @@ Methods:
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval_sync</a>(request: ApprovalRequest) -> ApprovalDecision</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">embedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">embeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/dimensions.py">get_dimensions</a>(model_name: str) -> int</code>
# Wrapper (praisonai)
@@ -784,8 +759,10 @@ Methods:
* <code title="cli">praisonai benchmark profile <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark sdk <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark workflow <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai bot agentmail <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot bot-callback <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot discord <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot email <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot slack <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot start <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot telegram <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
@@ -802,6 +779,7 @@ Methods:
* <code title="cli">praisonai call call-main <a href="./src/praisonai/praisonai/cli/commands/call.py">--help</a></code>
* <code title="cli">praisonai chat <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai chat chat-main <a href="./src/praisonai/praisonai/cli/commands/chat.py">--help</a></code>
* <code title="cli">praisonai claw claw <a href="./src/praisonai/praisonai/cli/commands/claw.py">--help</a></code>
* <code title="cli">praisonai code <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai code code-main <a href="./src/praisonai/praisonai/cli/commands/code.py">--help</a></code>
* <code title="cli">praisonai commit commit-main <a href="./src/praisonai/praisonai/cli/commands/commit.py">--help</a></code>
@@ -809,6 +787,8 @@ Methods:
* <code title="cli">praisonai completion completion-callback <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
* <code title="cli">praisonai completion fish <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
* <code title="cli">praisonai completion zsh <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
* <code title="cli">praisonai config doctor <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config env <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config get <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config list <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config path <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
@@ -868,7 +848,11 @@ Methods:
* <code title="cli">praisonai examples run <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
* <code title="cli">praisonai examples run-all <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
* <code title="cli">praisonai examples stats <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
* <code title="cli">praisonai flow flow-start <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
* <code title="cli">praisonai flow version <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
* <code title="cli">praisonai gateway channels <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway gateway-callback <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
@@ -906,6 +890,7 @@ Methods:
* <code title="cli">praisonai package install <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai package list <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai package uninstall <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai paths show <a href="./src/praisonai/praisonai/cli/commands/paths.py">--help</a></code>
* <code title="cli">praisonai plugins create <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins disable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins discover <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
@@ -963,6 +948,7 @@ Methods:
* <code title="cli">praisonai sandbox recreate <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox sandbox-callback <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox status <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai schedule add <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule delete <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
@@ -1035,12 +1021,7 @@ Methods:
* <code title="cli">praisonai train llm <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train show <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train train-callback <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai ui bot <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui chat <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui code <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui gradio <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui realtime <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui ui-main <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui ui <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai version check <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
* <code title="cli">praisonai version show <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
* <code title="cli">praisonai version version-callback <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
@@ -1193,7 +1174,7 @@ export { // Python parity additions
export { // Subagent Tool (agent-as-tool pattern)
SubagentTool, BaseTool, FunctionTool, ToolRegistry, ToolResult, ToolValidationError, createDelegator, createSubagentTool, createSubagentTools, createTool, getRegistry, getTool, registerTool, tool, validateTool } from "./tools";
export { airweaveSearch, bedrockBrowserClick, bedrockBrowserFill, bedrockBrowserNavigate, bedrockCodeInterpreter, codeExecution, codeMode, createCustomTool, exaSearch, firecrawlCrawl, firecrawlScrape, parallelSearch, perplexitySearch, registerCustomTool, registerLocalTool, registerNpmTool, superagentGuard, superagentRedact, superagentVerify, tavilyCrawl, tavilyExtract, tavilySearch, valyuBioSearch, valyuCompanyResearch, valyuEconomicsSearch, valyuFinanceSearch, valyuPaperSearch, valyuPatentSearch, valyuSecSearch, valyuWebSearch } from "./tools/builtins";
export { MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, resetToolsRegistry } from "./tools/registry";
export { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getRegistry, getTool, getToolsRegistry, get_registry, get_tool, registerTool, register_tool, resetToolsRegistry, validateTool, validate_tool } from "./tools/registry";
export type { InstallHints, PraisonTool, RedactionHooks, RegisteredTool, ToolCapabilities, ToolExecutionContext, ToolExecutionResult, ToolFactory, ToolHooks, ToolInstallStatus, ToolLimits, ToolLogger, ToolMetadata, ToolMiddleware, ToolParameterProperty, ToolParameterSchema } from "./tools/registry";
export { registerBuiltinTools, tools } from "./tools/tools";
export { // Classes
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.5.26" \
"praisonai>=4.5.112" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+61
View File
@@ -0,0 +1,61 @@
# PraisonAI 🦞 Claw — All-in-one: dashboard + bots + gateway + agents
#
# Build: docker build -f docker/Dockerfile.claw -t praisonai:claw ..
# Run: docker run -p 8082:8082 -e OPENAI_API_KEY=sk-xxx praisonai:claw
# Health: curl http://localhost:8082/health
#
# Override CMD for other modes:
# Bot: docker run -e ... praisonai:claw praisonai bot telegram
# Gateway: docker run -e ... praisonai:claw praisonai gateway start
# Serve: docker run -e ... praisonai:claw praisonai serve agents.yaml
# Shell: docker run --rm -it praisonai:claw bash
FROM python:3.12-slim AS base
# ── Environment ──────────────────────────────────────────────────────────────
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
AIUI_PORT=8082 \
AIUI_HOST=0.0.0.0 \
AIUI_DATA_DIR=/data
WORKDIR /app
# ── System dependencies ─────────────────────────────────────────────────────
# tini: PID 1 signal handling (graceful shutdown on Fly/Railway/k8s)
# curl: HEALTHCHECK probe
# build-essential: compile native Python wheels (chromadb, etc.)
RUN apt-get update && apt-get install -y --no-install-recommends \
tini \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# ── Install PraisonAI 🦞 ────────────────────────────────────────────────────
# Single command installs: AIUI dashboard, agents SDK (memory/knowledge/llm/mcp),
# bot channels (Telegram/Discord/Slack), gateway, search tools, and more.
RUN pip install --no-cache-dir "praisonai[claw]"
# ── Data volume ──────────────────────────────────────────────────────────────
# Persistence for sessions, config, memory, knowledge stores
RUN mkdir -p /data
VOLUME /data
# ── Entrypoint ───────────────────────────────────────────────────────────────
# Ensures /data exists and forwards all signals via tini
RUN printf '#!/bin/sh\n\
set -e\n\
mkdir -p /data 2>/dev/null || true\n\
exec "$@"\n' > /usr/local/bin/entrypoint.sh \
&& chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 8082
# ── Health check ─────────────────────────────────────────────────────────────
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -f http://localhost:8082/health || exit 1
# ── Run ──────────────────────────────────────────────────────────────────────
# Default: AIUI dashboard via praisonai claw. Override CMD for bot/gateway/serve.
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
CMD ["praisonai", "claw", "--port", "8082"]
+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.26" \
"praisonai>=4.5.112" \
"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.26" \
"praisonai>=4.5.112" \
"praisonai[ui]" \
"praisonai[crewai]"
+25
View File
@@ -1,6 +1,29 @@
version: '3.8'
services:
# PraisonAI 🦞 Claw — Recommended all-in-one service
claw:
build:
context: ..
dockerfile: docker/Dockerfile.claw
volumes:
- claw_data:/data
ports:
- "8082:8082"
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
restart: unless-stopped
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8082/health" ]
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
networks:
- praison_network
# ─── Legacy services (kept for backward compatibility) ─────────────────
# PraisonAI UI Service
ui:
build:
@@ -92,6 +115,8 @@ services:
# For bot services (Slack, Discord, Telegram), see docker/bots/
volumes:
claw_data:
driver: local
praison_data:
driver: local
+30
View File
@@ -0,0 +1,30 @@
framework: ag2
topic: "Research the latest developments in AI agents"
# Install: pip install "praisonai[ag2]"
# Run: praisonai --framework ag2 examples/ag2/ag2_basic.yaml
# or praisonai run examples/ag2/ag2_basic.yaml --framework ag2
roles:
research_agent:
role: "AI Research Specialist"
goal: "Research and summarise the latest developments in AI agent frameworks"
backstory: |
You are an experienced AI researcher with deep knowledge of multi-agent
systems, large language models, and the latest trends in AI tooling.
You excel at synthesising complex technical topics into clear summaries.
tasks:
research_task:
description: |
Research and summarise the latest developments in AI agent frameworks
for the topic: {topic}
Focus on:
1. Key frameworks and their unique capabilities
2. Recent innovations and improvements
3. Community adoption and ecosystem growth
4. Practical use cases and success stories
expected_output: |
A concise research summary covering the key developments,
major frameworks, and practical insights. Include 3-5 bullet
points of the most important findings.
+42
View File
@@ -0,0 +1,42 @@
framework: ag2
topic: "Cloud-native AI deployment strategies on AWS"
# AG2 exclusive feature: native AWS Bedrock support via LLMConfig(api_type="bedrock")
#
# Prerequisites:
# pip install "praisonai[ag2]"
# aws configure (or set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION)
#
# Run:
# praisonai --framework ag2 examples/ag2/ag2_bedrock.yaml
#
# The AG2 adapter detects api_type="bedrock" from the llm config and uses
# LLMConfig(api_type="bedrock", model=...) — no OPENAI_API_KEY required.
# AWS credentials are sourced from boto3 (env vars, ~/.aws/credentials, IAM role).
roles:
cloud_architect:
role: "AWS Cloud Architect"
goal: "Design and explain cloud-native AI deployment strategies on AWS"
backstory: |
You are an AWS Solutions Architect specialising in AI/ML workloads.
You have deep expertise in Amazon Bedrock, SageMaker, ECS, and Lambda,
and you help organisations deploy AI agents at scale securely and cost-effectively.
llm:
model: "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0"
api_type: "bedrock"
aws_region: "us-east-1"
tasks:
architecture_task:
description: |
Design a cloud-native deployment strategy for AI agents on AWS for: {topic}
Cover:
1. Recommended AWS services (Bedrock, ECS, Lambda, etc.)
2. Scalability and cost optimisation patterns
3. Security and compliance considerations
4. A simple reference architecture overview
expected_output: |
A concise architecture guide with service recommendations,
a high-level deployment diagram description, and key
best practices for production AI agent deployments on AWS.
+54
View File
@@ -0,0 +1,54 @@
framework: ag2
topic: "The impact of open-source AI on enterprise software development"
# Install: pip install "praisonai[ag2]"
# Run: praisonai --framework ag2 examples/ag2/ag2_multi_agent.yaml
# or praisonai run examples/ag2/ag2_multi_agent.yaml --framework ag2
#
# This example demonstrates AG2's GroupChat multi-agent coordination.
# Both agents participate in a collaborative conversation managed by
# a GroupChatManager until the task is complete.
roles:
researcher:
role: "Research Specialist"
goal: "Gather and analyse information on the given topic"
backstory: |
You are a meticulous researcher who excels at finding relevant
information, analysing trends, and presenting data-backed insights.
You always cite your reasoning and structure your findings clearly.
tasks:
research_task:
description: |
Research the topic: {topic}
Investigate:
1. Current state and adoption rates
2. Key players and projects driving the trend
3. Technical advantages and challenges
4. Business impact and cost implications
expected_output: |
A structured research briefing with findings on the topic,
including key data points, trends, and technical observations.
writer:
role: "Technical Content Writer"
goal: "Transform research findings into clear, engaging written content"
backstory: |
You are a skilled technical writer who turns complex research into
accessible, well-structured articles. You focus on clarity, logical
flow, and actionable takeaways for a professional audience.
tasks:
writing_task:
description: |
Using the research findings provided by the Research Specialist,
write a concise article on: {topic}
The article should:
1. Open with a compelling hook
2. Present key findings logically
3. Include practical implications for developers
4. Close with a forward-looking conclusion
expected_output: |
A 400-500 word article suitable for a technical blog,
with clear sections, professional tone, and concrete takeaways.
+2 -2
View File
@@ -37,7 +37,7 @@ def example_simple_enable():
instructions="You are a helpful assistant",
memory=True, # Enable memory with defaults
reflection=True, # Enable self-reflection
web=True, # Enable web search + fetch
web=WebConfig(fetch=False), # Enable web search + fetch
)
print(f"Agent created with memory={agent._memory_instance is not None}")
print(f" self_reflect={agent.self_reflect}")
@@ -152,7 +152,7 @@ def example_string_presets():
instructions="You are a verbose assistant",
output="verbose", # String preset for output
execution="fast", # String preset for execution
web="tavily", # String preset for web provider
web=["tavily", {"fetch": False}], # String preset with override for web provider
)
print(f"Output preset 'verbose': verbose={agent.verbose}, metrics={agent.metrics}")
print(f"Execution preset 'fast': max_iter={agent.max_iter}")
+3 -3
View File
@@ -5,14 +5,14 @@ both files to verify everything looks good.
"""
from praisonaiagents import Agent
from praisonaiagents.tools import (
write_file, read_file, execute_command, list_directory
write_file, read_file, execute_command, list_files
)
agent = Agent(
instructions="You are a helpful coding assistant.",
output="editor",
tools=[write_file, read_file, execute_command, list_directory],
approval=True,
tools=[write_file, read_file, execute_command, list_files],
approval=False,
)
agent.start(
"Create a Python file at /tmp/fibonacci.py that generates the first 20 "
+4 -4
View File
@@ -5,22 +5,22 @@ markdown report, reads it back, lists files, and gets system info.
"""
from praisonaiagents import Agent
from praisonaiagents.tools import (
write_file, read_file, execute_command, list_directory, get_system_info
write_file, read_file, execute_command, list_files, get_system_info
)
try:
from praisonaiagents.tools import web_search
tools = [web_search, write_file, read_file, execute_command,
list_directory, get_system_info]
list_files, get_system_info]
except ImportError:
tools = [write_file, read_file, execute_command,
list_directory, get_system_info]
list_files, get_system_info]
agent = Agent(
instructions="You are a helpful research assistant.",
output="editor",
tools=tools,
approval=True,
approval=False,
)
agent.start(
"Research the top 3 Python web frameworks (Django, FastAPI, Flask), then: "
+4 -4
View File
@@ -5,20 +5,20 @@ it, and lists created files.
"""
from praisonaiagents import Agent
from praisonaiagents.tools import (
write_file, execute_command, list_directory
write_file, execute_command, list_files
)
try:
from praisonaiagents.tools import web_search
tools = [web_search, write_file, execute_command, list_directory]
tools = [web_search, write_file, execute_command, list_files]
except ImportError:
tools = [write_file, execute_command, list_directory]
tools = [write_file, execute_command, list_files]
agent = Agent(
instructions="You are a helpful research assistant.",
output="editor",
tools=tools,
approval=True,
approval=False,
)
agent.start(
"Search the web for the current weather in Tokyo, create a JSON file at "
+9 -1
View File
@@ -19,7 +19,15 @@ async function main() {
},
execute: async ({ expression }) => {
try {
const result = eval(expression);
// Only allow safe numeric math characters
if (!/^[0-9+\-*/.() ]+$/.test(expression)) {
return 'Error: Only basic math expressions are allowed';
}
// Use Function constructor scoped to math only, no globals
const result = new Function(`"use strict"; return (${expression})`)();
if (typeof result !== 'number' || !isFinite(result)) {
return 'Error: Invalid result';
}
return `Result: ${result}`;
} catch (e) {
return 'Error: Invalid expression';
+7 -1
View File
@@ -38,7 +38,13 @@ const calculatorTool = createTool({
},
run: (params: { expression: string }) => {
try {
return eval(params.expression);
// Only allow safe numeric math characters
if (!/^[0-9+\-*/.() ]+$/.test(params.expression)) {
return 'Error: Only basic math expressions are allowed';
}
const result = new Function(`"use strict"; return (${params.expression})`)();
if (typeof result !== 'number' || !isFinite(result)) return 'Error: Invalid result';
return result;
} catch {
return 'Error: Invalid expression';
}
+2 -2
View File
@@ -4,8 +4,8 @@ from praisonai.persistence import create_conversation_store
store = create_conversation_store("sqlite", path="/tmp/multi_agent.db")
agent1 = Agent(name="Researcher", llm="gpt-4o-mini", db=store, session_id="shared-session")
agent2 = Agent(name="Writer", llm="gpt-4o-mini", db=store, session_id="shared-session")
agent1 = Agent(name="Researcher", llm="gpt-4o-mini", memory={"backend": "sqlite", "session_id": "shared-session", "db": store})
agent2 = Agent(name="Writer", llm="gpt-4o-mini", memory={"backend": "sqlite", "session_id": "shared-session", "db": store})
r1 = agent1.chat("Research topic: AI in healthcare. Key finding: reduces diagnosis time by 50%")
print(f"Researcher: {r1[:50]}...")
+216
View File
@@ -0,0 +1,216 @@
"""
Email Bot Example — Deploy an AI Agent to Your Mailbox
This example shows how to deploy PraisonAI agents as email bots
using IMAP/SMTP. The bot monitors an inbox, processes incoming emails,
and replies intelligently using an LLM-powered agent.
Supported providers:
- Gmail (with App Password)
- Outlook/Microsoft 365
- Any IMAP/SMTP server
Environment Variables:
export OPENAI_API_KEY=your_key
export [email protected]
export EMAIL_APP_PASSWORD=your_app_password
export EMAIL_IMAP_SERVER=imap.gmail.com # optional, default: imap.gmail.com
export EMAIL_SMTP_SERVER=smtp.gmail.com # optional, default: smtp.gmail.com
Usage:
python bot_email_example.py
"""
import asyncio
import os
from praisonaiagents import Agent
# ── Example 1: Minimal — Bot() wrapper (recommended) ──────────────
def example_minimal():
"""Minimal email bot — 3 lines to start."""
from praisonai.bots import Bot
agent = Agent(
name="email_assistant",
instructions="You are a helpful email assistant. Reply concisely and professionally.",
llm="gpt-4o-mini",
)
# Token and server resolved from env vars automatically:
# EMAIL_APP_PASSWORD, EMAIL_ADDRESS, EMAIL_IMAP_SERVER, EMAIL_SMTP_SERVER
bot = Bot("email", agent=agent)
bot.run() # Blocks until Ctrl+C
# ── Example 2: Custom configuration ───────────────────────────────
def example_with_config():
"""Email bot with explicit configuration."""
from praisonai.bots import Bot
from praisonaiagents import BotConfig
config = BotConfig(
polling_interval=30.0, # Check inbox every 30s
allowed_users=[ # Only respond to these senders
"[email protected]",
"[email protected]",
],
max_message_length=10000, # Allow long email replies
session_ttl=86400, # Clear session after 24h inactivity
metadata={
"imap_folder": "INBOX", # Which folder to monitor
},
)
agent = Agent(
name="support_agent",
instructions="""You are a technical support agent for ExampleCo.
Guidelines:
- Be professional and empathetic
- Provide clear step-by-step solutions
- If you don't know the answer, escalate to [email protected]
- Keep replies under 500 words
""",
llm="gpt-4o-mini",
)
bot = Bot("email", agent=agent, config=config)
bot.run()
# ── Example 3: Direct EmailBot with custom handlers ──────────────
def example_direct():
"""Direct EmailBot usage with custom message handler and commands."""
from praisonai.bots import EmailBot
agent = Agent(
name="smart_inbox",
instructions="You help triage and respond to support emails.",
llm="gpt-4o-mini",
)
bot = EmailBot(
token=os.environ.get("EMAIL_APP_PASSWORD", ""),
agent=agent,
email_address=os.environ.get("EMAIL_ADDRESS", ""),
imap_server=os.environ.get("EMAIL_IMAP_SERVER", "imap.gmail.com"),
smtp_server=os.environ.get("EMAIL_SMTP_SERVER", "smtp.gmail.com"),
)
# Custom message handler — runs on every incoming email
@bot.on_message
async def log_incoming(message):
print(f"📧 New email from: {message.sender.username}")
print(f" Subject: {message.metadata.get('subject', '(no subject)')}")
# Custom command — triggered when subject starts with "/status"
@bot.on_command("status")
async def handle_status(message):
await bot.send_message(
channel_id=message.sender.user_id,
content={
"subject": "Re: Bot Status",
"body": f"🤖 Bot is running!\nProcessed emails: {bot._emails_processed}",
},
reply_to=message.message_id,
)
asyncio.run(bot.start())
# ── Example 4: Multi-platform with BotOS ─────────────────────────
def example_multi_platform():
"""Run email bot alongside Telegram and Discord bots."""
from praisonai.bots import BotOS, Bot
agent = Agent(
name="omni_assistant",
instructions="You are a helpful assistant available on email, Telegram, and Discord.",
llm="gpt-4o-mini",
)
botos = BotOS(agent=agent, platforms=["email", "telegram", "discord"])
botos.run() # All three bots run concurrently
# ── Example 5: Probe connectivity ─────────────────────────────────
async def example_probe():
"""Test email server connectivity without starting the bot."""
from praisonai.bots import Bot
bot = Bot("email")
result = await bot.probe()
print(f"Connection OK: {result.ok}")
print(f"Platform: {result.platform}")
if result.bot_username:
print(f"Email: {result.bot_username}")
if result.error:
print(f"Error: {result.error}")
print(f"Latency: {result.elapsed_ms:.0f}ms")
# ── Example 6: YAML config (for praisonai bot CLI) ───────────────
YAML_EXAMPLE = """
# bot.yaml — Save this file and run: praisonai bot --config bot.yaml
agent:
name: email_support
instructions: >
You are a professional support agent.
Reply concisely and helpfully.
llm: gpt-4o-mini
tools:
- search_web
platforms:
email:
token: ${EMAIL_APP_PASSWORD}
"""
# ── Main ──────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
examples = {
"minimal": ("Minimal email bot", example_minimal),
"config": ("With custom config", example_with_config),
"direct": ("Direct EmailBot with handlers", example_direct),
"multi": ("Multi-platform (email+telegram+discord)", example_multi_platform),
"probe": ("Test connectivity", lambda: asyncio.run(example_probe())),
}
if len(sys.argv) > 1 and sys.argv[1] in examples:
name = sys.argv[1]
desc, fn = examples[name]
print(f"Running: {desc}")
fn()
else:
print("PraisonAI Email Bot Examples")
print("=" * 40)
print()
print("Usage: python bot_email_example.py <example>")
print()
print("Available examples:")
for key, (desc, _) in examples.items():
print(f" {key:10s} {desc}")
print()
print("Required environment variables:")
print(" OPENAI_API_KEY Your OpenAI API key")
print(" EMAIL_ADDRESS Bot's email address")
print(" EMAIL_APP_PASSWORD App password (Gmail: myaccount.google.com/apppasswords)")
print()
print("Optional:")
print(" EMAIL_IMAP_SERVER IMAP server (default: imap.gmail.com)")
print(" EMAIL_SMTP_SERVER SMTP server (default: smtp.gmail.com)")
print()
print("YAML config example:")
print(YAML_EXAMPLE)
+117
View File
@@ -0,0 +1,117 @@
"""
SandlockSandbox Security Demo
Demonstrates how to use SandlockSandbox for kernel-level code isolation.
Requires: pip install 'praisonai[sandbox]' (installs sandlock)
"""
import asyncio
from praisonaiagents.sandbox import ResourceLimits, SecurityPolicy, SandboxConfig
async def demonstrate_sandlock_security():
"""Show SandlockSandbox blocking malicious operations."""
try:
from praisonai.sandbox import SandlockSandbox
except ImportError:
print("❌ SandlockSandbox requires: pip install 'praisonai[sandbox]'")
print(" Falling back to SubprocessSandbox (less secure)")
from praisonai.sandbox import SubprocessSandbox
sandbox = SubprocessSandbox()
else:
# Create sandbox with strict security policy
config = SandboxConfig.native(
writable_paths=["./safe_workspace"], # Only allow writes to this dir
network=False, # Block all network access
)
sandbox = SandlockSandbox(config)
print("✅ Using SandlockSandbox with kernel-level isolation")
await sandbox.start()
# Test 1: Safe code execution
print("\n🟢 Test 1: Safe Python code")
result = await sandbox.execute(
"print('Hello from isolated sandbox!')",
limits=ResourceLimits.minimal()
)
print(f"Status: {result.status}")
print(f"Output: {result.stdout}")
# Test 2: Attempt file system access (should be blocked by Landlock)
print("\n🔒 Test 2: Attempt to read sensitive files")
result = await sandbox.execute("""
try:
with open('/etc/passwd', 'r') as f:
print(f.read()[:100])
except Exception as e:
print(f'Access blocked: {e}')
""")
print(f"Status: {result.status}")
print(f"Output: {result.stdout}")
if result.stderr:
print(f"Stderr: {result.stderr}")
# Test 3: Attempt network access (should be blocked)
print("\n🌐 Test 3: Attempt network access")
result = await sandbox.execute("""
try:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('example.com', 80))
print('Network connection successful')
s.close()
except Exception as e:
print(f'Network blocked: {e}')
""")
print(f"Status: {result.status}")
print(f"Output: {result.stdout}")
# Test 4: Attempt subprocess execution (blocked by seccomp)
print("\n🚫 Test 4: Attempt subprocess execution")
result = await sandbox.execute("""
try:
import subprocess
result = subprocess.run(['ls', '/'], capture_output=True, text=True)
print(f'Command output: {result.stdout}')
except Exception as e:
print(f'Subprocess blocked: {e}')
""")
print(f"Status: {result.status}")
print(f"Output: {result.stdout}")
# Test 5: Resource limits in action
print("\n⏱️ Test 5: Resource limits (timeout)")
result = await sandbox.execute(
"import time; time.sleep(100)", # Will timeout
limits=ResourceLimits(timeout_seconds=2)
)
print(f"Status: {result.status}")
print(f"Error: {result.error}")
await sandbox.stop()
# Show security features
status = sandbox.get_status()
if 'features' in status:
print(f"\n🛡️ Security features enabled:")
for feature, enabled in status['features'].items():
emoji = "✅" if enabled else "❌"
print(f" {emoji} {feature}: {enabled}")
if __name__ == "__main__":
print("🔒 SandlockSandbox Security Demonstration")
print("="*50)
asyncio.run(demonstrate_sandlock_security())
print("\n" + "="*50)
print("🎯 Key Security Benefits:")
print(" • Landlock: Kernel-enforced filesystem allowlisting")
print(" • seccomp-bpf: System call filtering")
print(" • Resource limits: Memory, CPU, process constraints")
print(" • Network isolation: Block unauthorized connections")
print(" • ~5ms overhead: Faster than Docker containers")
print(" • No root required: User-space security")
@@ -0,0 +1,123 @@
"""Social Media Cross-Posting Agent Example.
Demonstrates using PraisonAI Agent with all social media tools
to cross-post content across multiple platforms.
Required environment variables (set only the ones you need):
# X (Twitter)
X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_SECRET
# LinkedIn
LINKEDIN_ACCESS_TOKEN, LINKEDIN_PERSON_URN
# Facebook
FACEBOOK_PAGE_ACCESS_TOKEN, FACEBOOK_PAGE_ID
# Instagram
INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_BUSINESS_ACCOUNT_ID
# TikTok
TIKTOK_ACCESS_TOKEN
# Threads
THREADS_ACCESS_TOKEN, THREADS_USER_ID
# Pinterest
PINTEREST_ACCESS_TOKEN
# Medium
MEDIUM_TOKEN
# WordPress (uses praisonaiwp-cli config — no env vars needed)
# OpenAI (for the agent)
OPENAI_API_KEY
Usage:
python social_media_crosspost.py
"""
from praisonaiagents import Agent
from praisonai_tools import (
# X (Twitter)
post_to_x,
# LinkedIn
linkedin_post_text,
linkedin_post_image,
linkedin_post_article,
# Facebook
facebook_post_text,
facebook_post_image,
facebook_post_link,
# Instagram
instagram_post_image,
instagram_post_reel,
# TikTok
tiktok_post_video,
tiktok_post_photo,
# Threads
threads_post_text,
threads_post_image,
# Pinterest
pinterest_create_pin,
pinterest_list_boards,
# Medium
medium_publish_post,
# WordPress
create_wp_post,
check_wp_duplicate,
)
# Create a social media manager agent
agent = Agent(
name="Social Media Manager",
instructions="""You are a social media manager. You help users cross-post content
across multiple platforms including X (Twitter), LinkedIn, Facebook, Instagram,
TikTok, Threads, Pinterest, Medium, and WordPress.
When asked to cross-post:
1. Adapt the content format for each platform (character limits, hashtags, etc.)
2. Use the appropriate tool for each platform
3. Report which platforms succeeded and which had errors
Platform-specific notes:
- X: 280 char limit
- LinkedIn: 3000 char limit, professional tone
- Facebook: No strict limit, supports links
- Instagram: Needs image URL (public URL), max 2200 chars caption
- TikTok: Video/photo only, 150 char title
- Threads: 500 char limit
- Pinterest: Needs board_id and image URL
- Medium: Full article support (markdown/HTML), default to 'draft' status
- WordPress: Full article support (HTML/Gutenberg blocks), check duplicates first
""",
tools=[
post_to_x,
linkedin_post_text,
linkedin_post_image,
linkedin_post_article,
facebook_post_text,
facebook_post_image,
facebook_post_link,
instagram_post_image,
instagram_post_reel,
tiktok_post_video,
tiktok_post_photo,
threads_post_text,
threads_post_image,
pinterest_create_pin,
pinterest_list_boards,
medium_publish_post,
create_wp_post,
check_wp_duplicate,
],
)
# Example: Post to all text-based platforms
result = agent.start(
"Post this announcement to LinkedIn and Threads: "
"'PraisonAI v2.0 is here! Build AI agents in 3 lines of code. "
"Multi-agent workflows, memory, tools, and more. Try it: pip install praisonaiagents'"
)
print(result)
@@ -0,0 +1,49 @@
"""HeyGen + ElevenLabs Video Generation Example.
Generate an AI avatar video using HeyGen with ElevenLabs voice.
The agent autonomously: generates audio → uploads to HeyGen → creates video → waits for completion.
Required env vars:
HEYGEN_API_KEY: HeyGen API key (https://app.heygen.com/settings/api)
ELEVENLABS_API_KEY: ElevenLabs API key (https://elevenlabs.io)
OPENAI_API_KEY: OpenAI API key (for the agent LLM)
Usage:
export HEYGEN_API_KEY=your_key
export ELEVENLABS_API_KEY=your_key
export OPENAI_API_KEY=your_key
python video_heygen_elevenlabs.py
"""
from praisonaiagents import Agent
from praisonai_tools import (
elevenlabs_speak_to_file,
heygen_upload_asset,
heygen_generate_video_with_audio,
heygen_wait_for_video,
)
agent = Agent(
name="Video Producer",
instructions="""You create AI avatar videos. Follow these steps:
Step 1: Call elevenlabs_speak_to_file with text and output_path="/tmp/speech.mp3"
Step 2: Call heygen_upload_asset with file_path="/tmp/speech.mp3", content_type="audio/mpeg"
Step 3: Call heygen_generate_video_with_audio with the avatar_id and audio_asset_id from Step 2
Step 4: Call heygen_wait_for_video with the video_id from Step 3
Step 5: Report the final video URL
""",
tools=[
elevenlabs_speak_to_file,
heygen_upload_asset,
heygen_generate_video_with_audio,
heygen_wait_for_video,
],
)
result = agent.start(
'Create a video with this script: "Welcome to PraisonAI! '
'Build powerful AI agents in just three lines of code." '
'Use avatar_id: "YOUR_AVATAR_ID"'
)
print(result)
+19 -1
View File
@@ -134,10 +134,28 @@ def main():
except Exception as e:
return {"error": str(e)}
# Safe AST-based math evaluator (no eval on arbitrary expressions)
def _safe_calculate(expression: str) -> str:
import ast
try:
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return "Error: Invalid characters in expression"
tree = ast.parse(expression, mode="eval")
for node in ast.walk(tree):
if not isinstance(node, (ast.Expression, ast.BinOp, ast.UnaryOp,
ast.Constant, ast.Add, ast.Sub, ast.Mult,
ast.Div, ast.USub, ast.UAdd)):
return "Error: Only basic math operations are allowed"
result = eval(compile(tree, "<expr>", "eval"), {"__builtins__": {}})
return f"Result: {result}"
except Exception as e:
return f"Error: {e}"
# MCP endpoints
mcp_tools = {
"search": {"description": "Search the web", "func": lambda q: f"Results for: {q}"},
"calculate": {"description": "Calculate expression", "func": lambda e: str(eval(e)) if e.replace(" ", "").replace("+", "").replace("-", "").replace("*", "").replace("/", "").replace(".", "").isdigit() or True else "Error"},
"calculate": {"description": "Calculate expression", "func": _safe_calculate},
}
@app.get("/mcp/tools")
+1 -5
View File
@@ -5,11 +5,7 @@ from praisonaiagents import Agent
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge={
"vector_store": "chroma",
"path": "/tmp/chroma_demo"
}
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "chroma", "path": "/tmp/chroma_demo"}}
)
# Chat - agent uses knowledge for RAG
+1 -5
View File
@@ -5,11 +5,7 @@ from praisonaiagents import Agent
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge={
"vector_store": "lancedb",
"path": "/tmp/lancedb_test"
}
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "lancedb", "path": "/tmp/lancedb_test"}}
)
# Chat - agent uses knowledge for RAG
+1 -5
View File
@@ -5,11 +5,7 @@ from praisonaiagents import Agent
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge={
"vector_store": "lancedb",
"path": "/tmp/lancedb_demo"
}
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "lancedb", "path": "/tmp/lancedb_demo"}}
)
# Chat - agent uses knowledge for RAG
+1 -5
View File
@@ -11,11 +11,7 @@ url = os.getenv("PGVECTOR_URL", "postgresql://postgres:postgres@localhost:5433/p
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge={
"vector_store": "pgvector",
"url": url
}
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "pgvector", "url": url}}
)
# Chat - agent uses knowledge for RAG
+1 -5
View File
@@ -11,11 +11,7 @@ url = os.getenv("PGVECTOR_URL", "postgresql://postgres:postgres@localhost:5433/p
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge={
"vector_store": "pgvector",
"url": url
}
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "pgvector", "url": url}}
)
# Chat - agent uses knowledge for RAG
+1 -1
View File
@@ -15,8 +15,8 @@ if not api_key:
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge={
"sources": ["./docs/guide.pdf"],
"vector_store": "pinecone",
"api_key": api_key,
"environment": "us-east-1"
+1 -5
View File
@@ -11,11 +11,7 @@ url = os.getenv("QDRANT_URL", "http://localhost:6333")
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge={
"vector_store": "qdrant",
"url": url
}
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "qdrant", "url": url}}
)
# Chat - agent uses knowledge for RAG
+1 -1
View File
@@ -16,8 +16,8 @@ if not weaviate_key:
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge={
"sources": ["./docs/guide.pdf"],
"vector_store": "weaviate",
"url": weaviate_url,
"api_key": weaviate_key
+27
View File
@@ -0,0 +1,27 @@
# PraisonAI AgentMail Bot Configuration (Zero-Code)
#
# AgentMail = API-first email (no IMAP/SMTP). Programmatic inboxes.
#
# Usage:
# praisonai bot start --config agentmail-bot.yaml
#
# Requires:
# pip install praisonai agentmail
# export AGENTMAIL_API_KEY=am_...
#
# Optional env vars:
# AGENTMAIL_INBOX_ID=inbox_... (reuse existing inbox)
# AGENTMAIL_DOMAIN=mycompany.com (custom domain for new inboxes)
# Agent configuration
agent:
name: "Email Assistant"
instructions: "You are a helpful email assistant. Keep replies concise and professional."
model: "gpt-4o-mini"
memory: true
# Channel configuration (multi-channel format)
channels:
agentmail:
token: "${AGENTMAIL_API_KEY}"
# Optional: inbox_id, domain are resolved from env vars automatically
+6 -6
View File
@@ -34,14 +34,14 @@ variables:
agents: # Canonical: use 'agents' instead of 'roles'
classifier:
role: Request Classifier
instructions: # Canonical |
instructions: | # Canonical
You are an expert at classifying requests into categories.
Respond with ONLY one word: 'technical', 'creative', or 'research'.
goal: Classify incoming requests accurately
researcher:
role: Research Analyst
instructions: # Canonical |
instructions: | # Canonical
You are a skilled research analyst with expertise in technology trends.
You provide concise, factual research findings with citations.
goal: Research topics thoroughly and provide insights
@@ -52,28 +52,28 @@ agents: # Canonical: use 'agents' instead of 'roles'
tech_expert:
role: Technical Expert
instructions: # Canonical |
instructions: | # Canonical
You are a technical expert with deep knowledge in AI and ML.
You provide detailed technical explanations.
goal: Handle technical questions with expertise
creative_writer:
role: Creative Writer
instructions: # Canonical |
instructions: | # Canonical
You are a creative writer who can make complex topics engaging.
You write in a clear, accessible style.
goal: Create engaging content from research
market_analyst:
role: Market Analyst
instructions: # Canonical |
instructions: | # Canonical
You analyze market trends and competitive landscapes.
You provide actionable business insights.
goal: Analyze market trends and opportunities
aggregator:
role: Synthesizer
instructions: # Canonical |
instructions: | # Canonical
You combine findings from multiple sources into coherent reports.
You identify patterns and key insights.
goal: Synthesize all findings into comprehensive reports
@@ -0,0 +1,87 @@
# HeyGen + ElevenLabs Video Generation Workflow
#
# Sequential multi-agent workflow:
# 1. Audio Agent → generates speech with ElevenLabs
# 2. Upload Agent → uploads audio to HeyGen
# 3. Video Agent → triggers HeyGen video generation
# 4. Status Agent → polls and waits for video completion
#
# Usage:
# python video_heygen_elevenlabs_workflow.py
#
# Required env vars:
# HEYGEN_API_KEY, ELEVENLABS_API_KEY, OPENAI_API_KEY
framework: praisonai
process: sequential
agents:
audio_generator:
role: Audio Generator
goal: Generate speech audio from text using ElevenLabs
instructions: |
You generate speech audio files using ElevenLabs text-to-speech.
When given text, call elevenlabs_speak_to_file with the text and output_path.
IMPORTANT: Only pass 'text' and 'output_path' parameters. Use all defaults for other params.
Report the output_path and audio_bytes from the result.
tools:
- elevenlabs_speak_to_file
asset_uploader:
role: Asset Uploader
goal: Upload audio files to HeyGen as assets
instructions: |
You upload audio files to HeyGen for video generation.
Call heygen_upload_asset with the file_path and content_type="audio/mpeg".
Report the asset "id" from the result - this is critical for the next step.
tools:
- heygen_upload_asset
video_generator:
role: Video Generator
goal: Generate HeyGen avatar video using uploaded audio
instructions: |
You create HeyGen avatar videos using external audio.
Call heygen_generate_video_with_audio with the avatar_id and audio_asset_id.
The audio_asset_id comes from the previous step's upload result.
Report the video_id from the result data.
tools:
- heygen_generate_video_with_audio
video_monitor:
role: Video Monitor
goal: Wait for video completion and report the final URL
instructions: |
You monitor HeyGen video generation status.
Call heygen_wait_for_video with the video_id from the previous step.
Use poll_interval=10 and max_wait=300.
When complete, report the video_url from the result.
tools:
- heygen_wait_for_video
steps:
- agent: audio_generator
action: |
Generate speech audio for this text: "{{script}}"
Save to output_path: "/tmp/workflow_speech.mp3"
- agent: asset_uploader
action: |
Upload the audio file at "/tmp/workflow_speech.mp3" to HeyGen.
Use content_type "audio/mpeg".
- agent: video_generator
action: |
Generate a HeyGen video using:
- avatar_id: "{{avatar_id}}"
- audio_asset_id: use the asset id from the previous upload step
- agent: video_monitor
action: |
Wait for the video to complete. Use the video_id from the previous step.
Poll every 10 seconds, max wait 300 seconds.
Report the final video URL.
variables:
script: "Welcome to PraisonAI! Build powerful AI agents in just three lines of code. Fast, simple, and production-ready."
avatar_id: "YOUR_AVATAR_ID"
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Run the HeyGen + ElevenLabs YAML workflow.
This loads the YAML workflow and registers the praisonai_tools
functions so the YAML parser can resolve them by name.
Usage:
export HEYGEN_API_KEY=your_key
export ELEVENLABS_API_KEY=your_key
export OPENAI_API_KEY=your_key
python video_heygen_elevenlabs_workflow.py
"""
import os
from pathlib import Path
from praisonai_tools import (
elevenlabs_speak_to_file,
heygen_upload_asset,
heygen_generate_video_with_audio,
heygen_wait_for_video,
)
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
# Register praisonai_tools functions for YAML tool resolution
tool_registry = {
"elevenlabs_speak_to_file": elevenlabs_speak_to_file,
"heygen_upload_asset": heygen_upload_asset,
"heygen_generate_video_with_audio": heygen_generate_video_with_audio,
"heygen_wait_for_video": heygen_wait_for_video,
}
# Parse YAML workflow
yaml_path = Path(__file__).parent / "video_heygen_elevenlabs.yaml"
parser = YAMLWorkflowParser(tool_registry=tool_registry)
workflow = parser.parse_file(yaml_path)
print("=" * 60)
print("HeyGen + ElevenLabs YAML Workflow")
print("=" * 60)
# Run the workflow
result = workflow.start()
print("\n" + "=" * 60)
print("WORKFLOW RESULT:")
print("=" * 60)
print(result)
@@ -0,0 +1,275 @@
---
description: Review, validate, and merge GitHub pull requests or triage issues
---
# PR / Issue Review Workflow
When given a GitHub PR or Issue URL, follow this procedure end-to-end.
---
## Step 0 — Determine Type
Parse the URL to identify whether it is a **Pull Request** or an **Issue**.
```
https://github.com/MervinPraison/PraisonAI/pull/<NUMBER> → PR
https://github.com/MervinPraison/PraisonAI/issues/<NUMBER> → Issue
```
- **PR** → go to Step 1
- **Issue** → go to Step 8
---
## Step 1 — Fetch PR Metadata
// turbo
```bash
cd /Users/praison/praisonai-package && \
gh pr view <NUMBER> --json title,body,author,state,additions,deletions,changedFiles,headRefName,baseRefName,labels
```
Record: title, author, branch, file count, +/- lines.
---
## Step 2 — Read the Full Diff
// turbo
```bash
cd /Users/praison/praisonai-package && gh pr diff <NUMBER>
```
If the diff is very large (>500 lines), pipe through `head -500` first, then read remaining chunks.
---
## Step 3 — Review the Diff
For each changed file, evaluate against **PraisonAI engineering principles** (from AGENTS.md):
| Principle | What to Check |
|-----------|---------------|
| **Protocol-driven core** | No heavy implementations in `praisonaiagents/` (core SDK) |
| **Lazy imports** | Optional deps imported inside functions, not at module level |
| **DRY** | No code duplication; reuse existing abstractions |
| **Naming conventions** | `add_X()`, `get_X()`, `XConfig`, `XProtocol`, `XAdapter` |
| **Async-safe** | No blocking I/O in async context; asyncio primitives used |
| **Multi-agent safe** | No shared mutable global state without locks |
| **Thread safety** | Double-checked locking for lazy caches; `threading.Lock` for mutables |
| **Security** | No hardcoded secrets; `hmac.compare_digest` for comparisons; restricted `exec()` |
| **Backward compat** | Public API changes require deprecation cycle |
| **Test coverage** | Tests included; TDD pattern; both unit + agentic tests |
**Red flags to catch:**
- Module-level imports of optional deps (chromadb, litellm, etc.)
- Hardcoded credentials or `debug=True` in production paths
- Direct string comparison for secrets (timing attacks)
- `exec()`/`eval()` without sandbox
- Shared mutable globals without `threading.Lock`
- Missing error handling or swallowed exceptions
---
## Step 4 — Checkout and Run Tests Locally
// turbo
```bash
cd /Users/praison/praisonai-package && git checkout main && git pull origin main
```
// turbo
```bash
cd /Users/praison/praisonai-package && gh pr checkout <NUMBER>
```
Run targeted tests for changed files first:
```bash
cd /Users/praison/praisonai-package/src/praisonai-agents && \
python -m pytest tests/ -x -q \
--ignore=tests/integration/test_whatsapp_web_real.py \
--ignore=tests/unit/tools/test_profiles.py \
--tb=short 2>&1 | tail -20
```
For PRs touching security-sensitive code, also run:
```bash
# Sandbox tests
python -m pytest tests/unit/tools/test_python_tools_sandbox.py -v
# CWE-78 env injection tests
python -m pytest src/praisonai/tests/unit/test_cwe78_env_injection.py -v
# Thread-safety tests
python -m pytest tests/unit/test_thread_safety.py -v
```
---
## Step 5 — Verify No Regressions
Run the full test suite:
```bash
cd /Users/praison/praisonai-package/src/praisonai-agents && \
python -m pytest tests/ -q \
--ignore=tests/integration/test_whatsapp_web_real.py \
--ignore=tests/unit/tools/test_profiles.py \
--tb=no 2>&1 | tail -5
```
**Acceptable result:** All tests pass, or only pre-existing flaky tests fail (e.g., `test_file_memory.py::test_memory_persistence_across_agent_instances`).
**Unacceptable:** Any NEW failure introduced by the PR.
---
## Step 6 — Approve or Request Changes
**If the PR is good:**
```bash
cd /Users/praison/praisonai-package && \
gh pr review <NUMBER> --approve --body "LGTM — reviewed diff, tests pass, follows PraisonAI engineering principles."
```
**If changes are needed:**
```bash
cd /Users/praison/praisonai-package && \
gh pr review <NUMBER> --request-changes --body "Requesting changes: <specific feedback>"
```
---
## Step 7 — Merge
```bash
cd /Users/praison/praisonai-package && \
gh pr merge <NUMBER> --merge --subject "<PR title>"
```
Verify merge:
// turbo
```bash
cd /Users/praison/praisonai-package && gh pr view <NUMBER> --json state,mergedAt
```
Then return to main:
// turbo
```bash
cd /Users/praison/praisonai-package && git checkout main && git pull origin main
```
---
## Step 8 — Issue Triage (if URL is an Issue)
### 8a. Fetch issue details
// turbo
```bash
cd /Users/praison/praisonai-package && \
gh issue view <NUMBER> --json title,body,author,state,labels,comments
```
### 8b. Classify severity
| Tier | Criteria | Examples |
|------|----------|---------|
| **Tier 1 — Critical** | Security vuln, data loss, crash in hot path | exec() without sandbox, hardcoded secrets, unprotected globals |
| **Tier 2 — High** | Performance regression, broken feature, wrong output | Duplicate API calls, memory leak, silent failure |
| **Tier 3 — Medium** | DX issue, missing validation, doc gap | Bad error messages, missing CLI parity, stale docs |
| **Tier 4 — Low** | Cosmetic, code style, minor refactor | Trailing whitespace, naming inconsistency |
### 8c. Deep analysis (for Tier 1-2)
1. Read ALL relevant source files end-to-end
2. Trace the code path that the issue describes
3. Identify root cause with file paths and line numbers
4. Assess blast radius (what else could be affected)
5. Determine if the issue is **actually what it claims** (re-classify if needed)
### 8d. Delegate Implementation to GitHub Workflow Agents
> **KEY PHILOSOPHY: We act as the Manager, GitHub Actions execute the Workflow.**
**DO NOT implement the fix yourself locally.** PraisonAI uses autonomous GitHub Action workflows (such as `gemini-issue-review.yml` and `claude.yml`) to automatically resolve issues and create PRs. Your job as the local agent is to supervise, analyze gaps, and trigger these cloud agents to do the heavy lifting. If there is a gap (e.g. strict test violations), provide detailed relevant information in the PR/Issue and trigger the agents again.
1. Issue the command to trigger the cloud agent on the issue:
```bash
cd /Users/praison/praisonai-package && \
gh issue comment <NUMBER> --body "@gemini please review and fix this issue"
```
*(Note: You can use `@claude` or `@gemini` depending on preference or active bots).*
2. Check the GitHub Actions logs (`gh run list`) and wait for the cloud agent to open a Pull Request (`gh pr list`).
3. Once the cloud bot opens a Pull Request for the issue, return to **Step 1** to review the PR, test the branch locally, and merge it if it passes the checks.
---
## Step 9 — (Skipped) Local PR Creation
> **Note:** As the local agent, you should delegate code fixes to the cloud agents. Only implement code locally and create a PR if the USER explicitly forces you to bypass the cloud agents. If forced, use standard prefixes: `fix:`, `feat:`, `security:`, `refactor:`, `docs:`, `test:`.
---
## Step 10 — Security-Specific Checks
When reviewing security-related PRs, additionally verify:
### Credentials / Secrets
```bash
# Check for hardcoded secrets
grep -rn "secret\|password\|token\|api_key" --include="*.py" <changed-files> | \
grep -v "test_\|#\|environ\|getenv\|config"
```
### Timing Attacks
```bash
# Ensure hmac.compare_digest for secret comparisons
grep -rn "==.*api_key\|==.*secret\|==.*token" --include="*.py" <changed-files>
```
### Authentication & Workflow Secrets
When reviewing GitHub Actions workflows (especially ones using cloud actions like `claude-code-action`):
- **OIDC Validation**: Any workflow using OIDC to authenticate with a cloud provider (e.g. AWS Bedrock for Claude, GCP for Vertex) **MUST** include `id-token: write` in the job's `permissions`. Without it, you will see `Could not fetch an OIDC token` errors.
- **Valid PAT Usage**: When injecting tokens into workflows via `github_token:`, ensure you use `secrets.GH_TOKEN`. `secrets.PAT_TOKEN` has been known to be invalid or cause `401 Bad credentials` errors when curling GitHub resources. By design, `@copilot` mentions also strictly require `GH_TOKEN`.
### Supply Chain
```bash
# Check dependency versions
pip show <package-name>
# Check for known compromised versions
pip install <package>==999 2>&1 | grep "from versions"
```
### IoC Scanning
```bash
# Check for known malicious files
find $(python -c "import site; print(site.getsitepackages()[0])") -name "<suspicious-file>" 2>/dev/null
```
---
## Quick Reference — Common Commands
| Action | Command |
|--------|---------|
| View PR | `gh pr view <N> --json title,body,state,additions,deletions` |
| View diff | `gh pr diff <N>` |
| Checkout PR | `gh pr checkout <N>` |
| List open PRs | `gh pr list` |
| List open issues | `gh issue list` |
| View issue | `gh issue view <N>` |
| Approve PR | `gh pr review <N> --approve` |
| Request changes | `gh pr review <N> --request-changes --body "..."` |
| Merge PR | `gh pr merge <N> --merge` |
| Create PR | `gh pr create --title "..." --body "..." --head <branch> --base main` |
| Run tests | `cd src/praisonai-agents && python -m pytest tests/ -q --tb=no` |
| Check dep version | `pip show <package>` |
@@ -0,0 +1,44 @@
---
description: PR review chain — how automated reviews flow for every PR
---
## Review Chain (Sequential)
> **KEY PHILOSOPHY: We act as the Manager, GitHub Actions execute the Workflow.**
> As the local agent, our job is to analyze, supervise, and coordinate the cloud bots. If we identify a gap in the review chain or code (e.g., missing tests), provide detailed relevant information and trigger the agents (like `@claude`) again.
```
PR opened
↓
CodeRabbit (@coderabbitai) ─── auto for human PRs, triggered via comment for bot PRs
Qodo (/review) ─── auto for human PRs, triggered via comment for bot PRs
Gemini (@gemini) ─── triggered via comment for bot PRs/Issues
↓ (~3-5 min) ↓ (auto triggers Claude on completion)
Copilot (@copilot) ─── triggered ONLY after CodeRabbit or Qodo post their review
↓
Claude (@claude) ─── triggered ONLY after Copilot OR Gemini finishes (final reviewer)
```
## Workflow Files
| File | Trigger | Does what |
|------|---------|-----------|
| `auto-pr-comment.yml` | `issue_comment`, `pull_request_review`, `pull_request:opened` | Triggers Copilot after CodeRabbit/Qodo finish. For bot PRs: triggers CodeRabbit+Qodo+Gemini first. |
| `chain-claude-after-copilot.yml` | `pull_request_review:submitted`, `issue_comment` | Triggers Claude after Copilot reviews, AND automatically after Gemini Code Assist finishes fixing issues/PRs. |
| `claude.yml` | `issue_comment`, `pull_request_review_comment`, `issues:assigned/labeled` | Claude responds to @claude mentions. |
## Bot PR fix
CodeRabbit and Qodo skip `github-actions[bot]` authored PRs by default. The `bot-pr-trigger-reviews` job in `auto-pr-comment.yml` explicitly triggers them via comments.
## Key rules
- **Copilot ignores bot comments.** All `@copilot` mentions MUST use `GH_TOKEN` (not `GITHUB_TOKEN`) so comments post as `MervinPraison`.
- Copilot NEVER triggers before CodeRabbit/Qodo complete.
- Claude NEVER triggers before Copilot completes.
- Duplicate `@copilot` comments are prevented by checking existing comments.
- PraisonAI PRs: NEVER merge automatically — user merges manually.
## Security & Credentials
- **OIDC Configuration (`id-token: write`)**: Any workflow utilizing the `anthropics/claude-code-action@beta` (or similar actions requiring JWTs for AWS Bedrock/GCP Vertex) MUST define `id-token: write` under the job's `permissions` block. Missing this causes deep, opaque failures like "Could not fetch an OIDC token" paired with `Bad credentials` execution limits.
- **Valid `github_token` Payload**: Always pass `secrets.GH_TOKEN` into `github_token` inputs rather than `secrets.PAT_TOKEN`, as the latter has historically proven invalid and caused silent 401 exceptions on curl requests behind the scenes.
+3 -1
View File
@@ -93,4 +93,6 @@ chroma_db/
.praisonai
.windsurf
.agent
.ruff_cache
*.sfdx
.sfdx/
+75 -24
View File
@@ -21,9 +21,11 @@ Simpler than competitors • More extensible • Faster • Agent-centric
| **Minimal API** | Fewer parameters, sensible defaults, explicit overrides |
| **Performance-First** | Lazy loading, optional dependencies, no hot-path regressions |
| **Production-Ready** | Safe by default, multi-agent safe, async-safe |
Powerful, lightweight, highly reliable and robust
Easy and developer frienldy for non developers
Easy and user friendly
| **Open Core** | Core free and open source; clear paid upgrade path (support/cloud/services) |
Powerful, lightweight, highly reliable and robust.
Easy and developer-friendly for non-developers.
Each feature runs 3 ways: **CLI, YAML, Python**.
---
## 2. Repository Structure
@@ -96,22 +98,33 @@ Easy and user friendly
| Module | Size | Purpose |
|--------|------|---------|
| `agent/` | 1.4M | Agent class, protocols, handoff, autonomy |
| `memory/` | 984K | Memory protocols, adapters, file memory |
| `context/` | 960K | Context management, artifacts, fast context |
| `tools/` | 732K | Tool SDK, decorators, registry, protocols |
| `llm/` | 620K | LLM client, model router |
| `workflows/` | 456K | Workflow engine, patterns (Route, Parallel, Loop) |
| `knowledge/` | 436K | Knowledge/RAG protocols, vector stores |
| `hooks/` | 260K | Hook system, middleware, events |
| `eval/` | 240K | Evaluation framework |
| `agent/` | 1.7M | Agent class, protocols, handoff, autonomy |
| `memory/` | 784K | Memory protocols, adapters, file memory |
| `context/` | 840K | Context management, artifacts, fast context |
| `tools/` | 816K | Tool SDK, decorators, registry, protocols |
| `llm/` | 756K | LLM client, model router |
| `workflows/` | 472K | Workflow engine, patterns (Route, Parallel, Loop) |
| `mcp/` | 404K | Model Context Protocol integration |
| `eval/` | 384K | Evaluation framework |
| `agents/` | 376K | Multi-agent orchestration (AgentTeam, AgentFlow) |
| `knowledge/` | 300K | Knowledge/RAG protocols, vector stores |
| `rag/` | 264K | RAG protocols, retriever, reranker |
| `plugins/` | 56K | Plugin manager, registry |
| `policy/` | 64K | Policy engine |
| `config/` | 252K | Feature configs (ExecutionConfig, OutputConfig, etc.) |
| `session/` | 224K | Session management, persistence |
| `plugins/` | 204K | Plugin manager, registry |
| `hooks/` | 184K | Hook system, middleware, events |
| `storage/` | 136K | Storage protocols, adapters |
| `scheduler/` | 132K | Schedule models, store, parser, runner |
| `trace/` | 116K | Trace protocols, context events |
| `approval/` | 92K | Human-in-the-loop approval protocols |
| `bots/` | 80K | BotOS protocols, config |
| `policy/` | 76K | Policy engine |
| `gateway/` | 60K | Gateway protocols |
| `sandbox/` | 56K | Code execution sandbox |
| `bus/` | 48K | Event bus |
| `streaming/` | 56K | Streaming events, callbacks |
| `trace/` | 152K | Trace protocols, context events |
| `scheduler/` | 48K | Schedule models, store, parser, runner |
| `streaming/` | 44K | Streaming events, callbacks |
| `conditions/` | 40K | Conditional logic protocols |
| `db/` | 32K | Database adapter protocols |
### 3.2 Protocol-First Design
@@ -123,6 +136,8 @@ Pattern: Each major module has a protocols.py file
- Protocols define WHAT (interface contract)
- Adapters implement HOW (concrete implementation)
- Naming: XProtocol for interfaces, XAdapter for implementations
(ALWAYS suffix with "Protocol" — never use bare concept names like TraceSink)
- File placement: All protocols live in protocols.py within their module
- Users can implement any protocol for custom behavior
```
@@ -286,9 +301,19 @@ save() / load() # Disk persistence (NOT store)
# Configuration
class XConfig: # Configuration dataclass (MemoryConfig, HooksConfig)
# Protocols
class XProtocol(Protocol): # Abstract interface
class XAdapter: # Implementation
# Protocols — ALWAYS suffix with "Protocol"
class XProtocol(Protocol): # Abstract interface (TraceSinkProtocol, MemoryProtocol)
class XAdapter: # Concrete implementation (ChromaAdapter, FileAdapter)
# ✅ CORRECT protocol naming:
class MemoryProtocol(Protocol): ... # Clear it's a protocol
class SessionStoreProtocol(Protocol): ... # Descriptive + Protocol suffix
class TraceSinkProtocol(Protocol): ... # In protocols.py + suffixed
# ❌ WRONG protocol naming:
class TraceSink(Protocol): ... # Missing Protocol suffix — ambiguous
class IMemory(Protocol): ... # TypeScript convention, not Python
class MemoryBase(Protocol): ... # "Base" implies ABC, not Protocol
# Execution
run() # Synchronous, blocking
@@ -500,9 +525,9 @@ class MyDbAdapter(DbAdapter):
---
## 7. CLI Representation
## 7. CLI & YAML Representation
Every feature must have CLI parity:
Every feature must run 3 ways: **Python, CLI, and YAML**.
```bash
# Basic agent
@@ -624,6 +649,30 @@ pytest tests/unit/
pytest --cov=praisonaiagents tests/
```
### 9.4 Real Agentic Test (MANDATORY)
Every feature MUST include a real agentic test — not just smoke tests.
A **real agentic test** means the agent actually runs and calls the LLM:
```python
# ✅ REAL agentic test — agent runs end-to-end
from praisonaiagents import Agent
agent = Agent(name="test", instructions="You are a helpful assistant")
result = agent.start("Say hello in one sentence")
print(result) # Must produce actual LLM output
# ❌ SMOKE test only — NOT sufficient alone
agent = Agent(name="test") # Just object construction
assert agent.name == "test" # No LLM call
```
**Rules:**
- Both smoke AND real agentic tests are required
- Agent MUST call `agent.start()` with a real prompt
- Agent MUST call the LLM and produce a text response
- Print full output so developers can verify end-to-end behavior
---
## 10. Implementation Checklist
@@ -634,8 +683,10 @@ For every feature/change:
- [ ] **No new deps**: Use optional dependencies only
- [ ] **Lazy imports**: Heavy deps imported inside functions
- [ ] **Naming**: Follow conventions (add_*, get_*, XConfig)
- [ ] **Tests**: TDD - write failing tests first
- [ ] **Tests**: TDD — write failing tests first
- [ ] **Real agentic test**: Agent must call LLM end-to-end (§9.4)
- [ ] **CLI**: Add corresponding CLI command/option
- [ ] **YAML**: Ensure YAML config parity with Python API
- [ ] **Docs**: Update Mintlify documentation
- [ ] **Examples**: Add to examples/ directory
- [ ] **Multi-agent safe**: No shared mutable state
@@ -841,7 +892,7 @@ platforms:
---
## 13. Paid Value Path
## 14. Paid Value Path
The core remains **free and open source**. Clear upgrade paths to paid value:
+192
View File
@@ -0,0 +1,192 @@
# Centralized Logging in PraisonAI Agents
This document explains the enhanced centralized logging system implemented to address inconsistent logging patterns across the codebase.
## Problem Addressed
- **53+ scattered logger instances** with no unified configuration
- **No standard log format** across modules
- **Mixed logging approaches** (logger.debug(), print(), console.print())
- **No structured logging option** for production deployments
- **Difficult filtering** by subsystem in multi-agent runs
## Solution
Enhanced the existing `praisonaiagents/_logging.py` with:
1. **Consistent naming convention**: All loggers follow `praisonaiagents.<module>` pattern
2. **Structured logging support**: JSON output for production environments
3. **Backward compatibility**: Existing code continues to work
4. **Centralized configuration**: Single place to configure all logging
5. **Optional extra data**: Structured data for better observability
## Quick Start
### Basic Usage
```python
# New recommended approach
from praisonaiagents import get_logger
# Automatic module detection
logger = get_logger()
logger.info("This is a log message")
# Explicit module name
logger = get_logger(__name__)
logger.info("Explicit module logging")
```
### Structured Logging
```python
import os
from praisonaiagents import get_logger, configure_structured_logging
# Enable structured JSON logging
os.environ['PRAISONAI_STRUCTURED_LOGS'] = 'true'
configure_structured_logging()
# Logger with extra structured data
logger = get_logger(extra_data={"agent_id": "assistant", "session": "123"})
logger.info("Task completed", extra={"task_id": "task_001", "duration": 1.5})
```
Output:
```json
{
"timestamp": "2026-03-30T21:35:00Z",
"level": "INFO",
"logger": "praisonaiagents.agent.agent",
"message": "Task completed",
"module": "agent",
"function": "start",
"line": 42,
"agent_id": "assistant",
"session": "123",
"task_id": "task_001",
"duration": 1.5
}
```
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `LOGLEVEL` | `WARNING` | Global log level (DEBUG, INFO, WARNING, ERROR) |
| `PRAISONAI_STRUCTURED_LOGS` | `false` | Enable JSON structured logging |
### Per-Module Configuration
```bash
# Set specific module to DEBUG level
export LOGLEVEL=INFO
# Configure in Python:
logging.getLogger('praisonaiagents.memory').setLevel(logging.DEBUG)
```
## Migration Guide
### For New Code
Use the new logging utilities:
```python
# ✅ Recommended - new code
from praisonaiagents import get_logger
logger = get_logger()
```
### For Existing Code
**No changes required** - existing code continues to work:
```python
# ✅ Still works - existing code
import logging
logger = logging.getLogger(__name__)
```
**Optional enhancement** for better observability:
```python
# ✅ Enhanced - gradually migrate
from praisonaiagents._logging import get_logger
logger = get_logger(__name__, extra_data={"subsystem": "agent"})
```
## Naming Convention
The `get_logger()` function automatically ensures consistent naming:
| Input | Output Logger Name |
|-------|--------------------|
| `"my_module"` | `"praisonaiagents.my_module"` |
| `"praisonaiagents.existing"` | `"praisonaiagents.existing"` (unchanged) |
| `"praisonai.other"` | `"praisonaiagents.other"` |
| `"__main__"` | `"praisonaiagents.main"` |
| `None` (auto-detect) | `"praisonaiagents.<current_module>"` |
## Production Deployment
### ELK Stack / Elasticsearch
```python
import os
from praisonaiagents import configure_structured_logging
# Enable JSON logging for log shipping
os.environ['PRAISONAI_STRUCTURED_LOGS'] = 'true'
configure_structured_logging()
```
### CloudWatch / Splunk
```python
import logging
from praisonaiagents import get_logger, StructuredFormatter
# Custom handler with structured format
handler = logging.StreamHandler()
handler.setFormatter(StructuredFormatter())
logger = get_logger()
logger.addHandler(handler)
```
### Filtering by Subsystem
With structured logging enabled, you can easily filter logs:
```bash
# Filter by agent subsystem
jq 'select(.logger | startswith("praisonaiagents.agent"))' logs.json
# Filter by specific agent
jq 'select(.agent_id == "assistant")' logs.json
# Filter by session
jq 'select(.session == "session_123")' logs.json
```
## Backward Compatibility
- ✅ All existing `logging.getLogger(__name__)` calls continue to work
- ✅ No performance impact on existing code
- ✅ No breaking changes to public APIs
- ✅ Gradual migration path available
## Examples
See the updated modules for examples:
- `praisonaiagents/plugins/builtin/logging_plugin.py` - Logging plugin with extra data
- `praisonaiagents/memory/memory.py` - Memory module with subsystem tagging
## Benefits
1. **Unified format**: All PraisonAI logs follow the same format
2. **Better observability**: Structured data for monitoring and debugging
3. **Easy filtering**: Filter by subsystem, agent, or session
4. **Production ready**: JSON output for log aggregation systems
5. **Zero breaking changes**: Existing code continues to work unchanged
6. **Performance conscious**: No import-time overhead for normal usage
+20
View File
@@ -0,0 +1,20 @@
import cProfile
import pstats
import io
from praisonaiagents import Agent
# Warm up
for _ in range(100):
_ = Agent(name="Test", llm="gpt-4o-mini", output="silent")
pr = cProfile.Profile()
pr.enable()
for _ in range(5000):
_ = Agent(name="Test", llm="gpt-4o-mini", output="silent")
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumtime')
ps.print_stats(30)
print(s.getvalue())
@@ -0,0 +1,31 @@
import cProfile
import pstats
import io
import time
from praisonaiagents import Agent
def func():
return Agent(name='Test', llm='gpt-4o-mini', output="silent")
# Warm up to fill caches
for _ in range(100):
func()
start_time = time.perf_counter()
for _ in range(1000):
func()
end_time = time.perf_counter()
avg_time = (end_time - start_time) / 1000 * 1_000_000
print(f"Time without profile: {avg_time:.2f} μs")
pr = cProfile.Profile()
pr.enable()
for _ in range(1000):
func()
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumtime')
ps.print_stats(30)
print(s.getvalue())
+103 -39
View File
@@ -59,18 +59,7 @@ _logging.configure_root_logger()
# Import configuration (lightweight, no heavy deps)
from . import _config
# Lightweight imports that don't trigger heavy dependency chains
from .tools.tools import Tools
from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
from .tools.decorator import tool, FunctionTool
from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
# db and obs are lazy-loaded via __getattr__ for performance
# Sub-packages for organized imports (pa.config, pa.tools, etc.)
# These enable: import praisonaiagents as pa; pa.config.MemoryConfig
from . import config
from . import tools
# Note: db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
# Note: tools, config, memory, workflows, db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
# Embedding API - LAZY LOADED via __getattr__ for performance
# Supports: embedding, embeddings, aembedding, aembeddings, EmbeddingResult, get_dimensions
@@ -125,6 +114,19 @@ def _get_lazy_cache():
# ============================================================================
_LAZY_IMPORTS = {
# Tools (moved from eager imports for lazy loading)
'Tools': ('praisonaiagents.tools.tools', 'Tools'),
'BaseTool': ('praisonaiagents.tools.base', 'BaseTool'),
'ToolResult': ('praisonaiagents.tools.base', 'ToolResult'),
'ToolValidationError': ('praisonaiagents.tools.base', 'ToolValidationError'),
'validate_tool': ('praisonaiagents.tools.base', 'validate_tool'),
'tool': ('praisonaiagents.tools.decorator', 'tool'),
'FunctionTool': ('praisonaiagents.tools.decorator', 'FunctionTool'),
'get_registry': ('praisonaiagents.tools.registry', 'get_registry'),
'register_tool': ('praisonaiagents.tools.registry', 'register_tool'),
'get_tool': ('praisonaiagents.tools.registry', 'get_tool'),
'ToolRegistry': ('praisonaiagents.tools.registry', 'ToolRegistry'),
# Main display utilities (imports rich)
'TaskOutput': ('praisonaiagents.main', 'TaskOutput'),
'ReflectionOutput': ('praisonaiagents.main', 'ReflectionOutput'),
@@ -179,9 +181,7 @@ _LAZY_IMPORTS = {
'HandoffDepthError': ('praisonaiagents.agent.handoff', 'HandoffDepthError'),
'HandoffTimeoutError': ('praisonaiagents.agent.handoff', 'HandoffTimeoutError'),
# Embedding API
'embedding': ('praisonaiagents.embedding.embed', 'embedding'),
'embeddings': ('praisonaiagents.embedding.embed', 'embedding'),
# Embedding API (Note: embedding/embeddings handled in custom_handler to override subpackage)
'aembedding': ('praisonaiagents.embedding.embed', 'aembedding'),
'aembeddings': ('praisonaiagents.embedding.embed', 'aembedding'),
'embed': ('praisonaiagents.embedding.embed', 'embed'),
@@ -203,6 +203,9 @@ _LAZY_IMPORTS = {
# Agent classes
'Agent': ('praisonaiagents.agent.agent', 'Agent'),
'BudgetExceededError': ('praisonaiagents.agent.agent', 'BudgetExceededError'),
'Heartbeat': ('praisonaiagents.agent.heartbeat', 'Heartbeat'),
'HeartbeatConfig': ('praisonaiagents.agent.heartbeat', 'HeartbeatConfig'),
'ImageAgent': ('praisonaiagents.agent.image_agent', 'ImageAgent'),
'VideoAgent': ('praisonaiagents.agent.video_agent', 'VideoAgent'),
'VideoConfig': ('praisonaiagents.agent.video_agent', 'VideoConfig'),
@@ -344,6 +347,14 @@ _LAZY_IMPORTS = {
'ObservabilityEventType': ('praisonaiagents.escalation.observability', 'EventType'),
'DoomLoopDetector': ('praisonaiagents.escalation.doom_loop', 'DoomLoopDetector'),
'MemoryBackend': ('praisonaiagents.config.feature_configs', 'MemoryBackend'),
'LearnConfig': ('praisonaiagents.config.feature_configs', 'LearnConfig'),
'LearnScope': ('praisonaiagents.config.feature_configs', 'LearnScope'),
'LearnMode': ('praisonaiagents.config.feature_configs', 'LearnMode'),
'LearnBackend': ('praisonaiagents.config.feature_configs', 'LearnBackend'),
'LearnProtocol': ('praisonaiagents.memory.learn.protocols', 'LearnProtocol'),
'AsyncLearnProtocol': ('praisonaiagents.memory.learn.protocols', 'AsyncLearnProtocol'),
'LearnManagerProtocol': ('praisonaiagents.memory.learn.protocols', 'LearnManagerProtocol'),
'LearnManager': ('praisonaiagents.memory.learn', 'LearnManager'),
'ChunkingStrategy': ('praisonaiagents.config.feature_configs', 'ChunkingStrategy'),
'GuardrailAction': ('praisonaiagents.config.feature_configs', 'GuardrailAction'),
'WebSearchProvider': ('praisonaiagents.config.feature_configs', 'WebSearchProvider'),
@@ -481,8 +492,61 @@ _LAZY_IMPORTS = {
'PraisonConfig': ('praisonaiagents.config.loader', 'PraisonConfig'),
'PluginsConfig': ('praisonaiagents.config.loader', 'PluginsConfig'),
'DefaultsConfig': ('praisonaiagents.config.loader', 'DefaultsConfig'),
# Centralized Logging Utilities
'get_logger': ('praisonaiagents._logging', 'get_logger'),
'configure_structured_logging': ('praisonaiagents._logging', 'configure_structured_logging'),
'StructuredFormatter': ('praisonaiagents._logging', 'StructuredFormatter'),
}
# ============================================================================
# SUBPACKAGE FUNCTION OVERRIDES
# ============================================================================
# Some subpackage names conflict with function names we want to export.
# Override them here to return the function instead of the module.
# ============================================================================
# Override 'embedding' and 'embeddings' at module level to prevent subpackage import
# These need to be set after _LAZY_IMPORTS is defined but before __getattr__ is created
def _get_embedding_func():
"""Lazy getter for embedding function."""
# Import with alias to avoid overwriting the module proxy
from .embedding.embed import embedding as _embedding_func
return _embedding_func
# Create lazy properties that override the submodule
class _EmbeddingProxy:
"""Proxy object that loads embedding function on first access."""
def __init__(self):
self._func = None
def _load(self):
"""Load the actual embedding function if not already loaded."""
if self._func is None:
self._func = _get_embedding_func()
return self._func
def __call__(self, *args, **kwargs):
return self._load()(*args, **kwargs)
def __getattr__(self, name):
return getattr(self._load(), name)
@property
def __wrapped__(self):
"""Support for inspect.signature() and functools.wraps."""
return self._load()
@property
def __signature__(self):
"""Support for inspect.signature()."""
import inspect
return inspect.signature(self._load())
def __repr__(self):
return f"<lazy proxy for {self._load()!r}>"
_global_embedding_proxy = _EmbeddingProxy()
def _custom_handler(name, cache):
"""Handle special cases that need custom logic."""
@@ -494,39 +558,39 @@ def _custom_handler(name, cache):
cache['AgentManager'] = value
cache['Agents'] = value
return value
# Task removed in v4.0.0 - use Task instead
if name == "Task":
raise ImportError(
"Task has been removed in v4.0.0. Use Task instead.\n"
"Migration: Replace 'from praisonaiagents import Task' with 'from praisonaiagents import Task'\n"
"Task supports all Task features including action, handler, loop_over, etc."
)
# Handle embedding specifically if missing from module dict due to submodule reload
if name in ("embedding", "embeddings"):
return _global_embedding_proxy
# Module imports (return the module itself)
if name == 'tools':
import importlib
mod = importlib.import_module('.tools', 'praisonaiagents')
cache['tools'] = mod
return mod
if name == 'config':
import importlib
mod = importlib.import_module('.config', 'praisonaiagents')
cache['config'] = mod
return mod
if name == 'memory':
import importlib
return importlib.import_module('.memory', 'praisonaiagents')
mod = importlib.import_module('.memory', 'praisonaiagents')
cache['memory'] = mod
return mod
if name == 'workflows':
import importlib
return importlib.import_module('.workflows', 'praisonaiagents')
mod = importlib.import_module('.workflows', 'praisonaiagents')
cache['workflows'] = mod
return mod
raise AttributeError(f"Not handled by custom_handler: {name}")
# ============================================================================
# SUBPACKAGE FUNCTION OVERRIDES
# ============================================================================
# Some subpackage names conflict with function names we want to export.
# Override them here to return the function instead of the module.
# ============================================================================
# Override 'embedding' to return the function, not the subpackage
from .embedding.embed import embedding as _embedding_func
embedding = _embedding_func
# Also provide embeddings alias
embeddings = _embedding_func
# Override the submodule with our function proxy
embedding = _global_embedding_proxy
embeddings = embedding # embeddings is an alias
# Create the __getattr__ function using centralized utility
@@ -534,7 +598,7 @@ __getattr__ = create_lazy_getattr_with_fallback(
mapping=_LAZY_IMPORTS,
module_name=__name__,
cache=_lazy_cache,
fallback_modules=['tools', 'memory', 'config', 'workflows'],
fallback_modules=[], # Note: 'embedding' excluded to avoid conflict with embedding() function
custom_handler=_custom_handler
)
@@ -4,8 +4,9 @@ This module consolidates all logging configuration in one place to avoid duplica
"""
import os
import json
import logging
from typing import List
from typing import Any, Dict, List, Optional, Union
# ========================================================================
# ENVIRONMENT CONFIGURATION
@@ -161,5 +162,150 @@ def initialize_logging():
# It will be called lazily when LLM class is instantiated
# ========================================================================
# STRUCTURED LOGGING SUPPORT
# ========================================================================
class StructuredFormatter(logging.Formatter):
"""JSON formatter for structured logging in production environments."""
_STANDARD_FIELDS: frozenset = frozenset({
"timestamp", "level", "logger", "message",
"module", "function", "line", "exc_info", "stack_info",
})
def format(self, record: logging.LogRecord) -> str:
"""Format log record as structured JSON."""
log_data: Dict[str, Any] = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno,
}
# Include exception info if present
if record.exc_info:
log_data["exc_info"] = self.formatException(record.exc_info)
if record.stack_info:
log_data["stack_info"] = self.formatStack(record.stack_info)
# Merge extra fields without overwriting standard log fields
if hasattr(record, 'extra_data'):
extra = {k: v for k, v in record.extra_data.items() if k not in self._STANDARD_FIELDS}
log_data.update(extra)
return json.dumps(log_data)
def configure_structured_logging():
"""Configure structured JSON logging for production environments.
Call this function to enable structured logging across all PraisonAI modules.
Useful for log aggregation systems like ELK, Splunk, or CloudWatch.
Example:
import os
from praisonaiagents._logging import configure_structured_logging
# Enable structured logging
os.environ['PRAISONAI_STRUCTURED_LOGS'] = 'true'
configure_structured_logging()
"""
if os.environ.get('PRAISONAI_STRUCTURED_LOGS', '').lower() == 'true':
structured_formatter = StructuredFormatter()
# Reconfigure the root logger handler so propagating loggers get JSON output
for handler in logging.getLogger().handlers:
handler.setFormatter(structured_formatter)
# Also reconfigure any praisonaiagents loggers that have their own handlers
for logger_name in logging.Logger.manager.loggerDict:
if logger_name.startswith('praisonaiagents.'):
child_logger = logging.getLogger(logger_name)
for handler in child_logger.handlers:
handler.setFormatter(structured_formatter)
# ========================================================================
# CONSISTENT LOGGER NAMING
# ========================================================================
class _ExtraDataAdapter(logging.LoggerAdapter):
"""LoggerAdapter that injects extra_data into every log record."""
def process(self, msg: str, kwargs: Dict[str, Any]) -> tuple:
kwargs.setdefault('extra', {})
kwargs['extra']['extra_data'] = self.extra
return msg, kwargs
def get_logger(
name: Optional[str] = None,
*,
extra_data: Optional[Dict[str, Any]] = None,
) -> Union[logging.Logger, logging.LoggerAdapter]:
"""Get a logger with consistent naming convention for PraisonAI modules.
This function ensures all loggers follow the 'praisonaiagents.<module>' pattern
and provides optional structured logging support.
Args:
name: Logger name. If None, uses the calling module's __name__.
If it doesn't start with 'praisonaiagents.', the prefix is added.
extra_data: Optional dict of extra data to include in all log records.
Returns:
Logger instance with consistent naming and optional structured data.
Example:
# In any module file:
from praisonaiagents._logging import get_logger
logger = get_logger(__name__)
# Or with automatic detection:
logger = get_logger()
# With extra structured data:
logger = get_logger(extra_data={"agent_id": "assistant", "session": "123"})
"""
import inspect
# Auto-detect module name if not provided
if name is None:
frame = inspect.currentframe()
try:
caller_frame = frame.f_back
name = caller_frame.f_globals.get('__name__', 'unknown')
finally:
del caller_frame # avoid reference cycle / frame leak
del frame
# Ensure consistent naming convention
if not name.startswith('praisonaiagents.'):
if name == '__main__':
name = 'praisonaiagents.main'
elif name == 'praisonai' or name.startswith('praisonai.'):
# Handle cases like 'praisonai.something' -> 'praisonaiagents.something'
name = 'praisonaiagents' + name[len('praisonai'):]
else:
# Add prefix for non-praisonai modules
name = f'praisonaiagents.{name}'
base_logger = logging.getLogger(name)
# Wrap with adapter when extra structured data is requested
if extra_data:
return _ExtraDataAdapter(base_logger, extra_data)
return base_logger
# ========================================================================
# BACKWARD COMPATIBILITY ALIASES
# ========================================================================
# These maintain compatibility with existing code
getLogger = get_logger # Snake case alias
get_praisonai_logger = get_logger # Descriptive alias
# Auto-initialize on import (but NOT litellm)
initialize_logging()
@@ -1,4 +1,12 @@
"""Agent module for AI agents - uses lazy loading for performance"""
"""Agent module for AI agents - uses lazy loading for performance
The Agent class has been decomposed into focused modules for better maintainability:
- tool_execution: Tool calling and execution functionality
- chat_handler: Chat and conversation management
- session_manager: Session persistence and state management
Backward compatibility is maintained via mixins in the main Agent class.
"""
# Lazy loading cache
_lazy_cache = {}
@@ -13,6 +21,18 @@ def __getattr__(name):
from .agent import Agent
_lazy_cache[name] = Agent
return Agent
if name == 'BudgetExceededError':
from .agent import BudgetExceededError
_lazy_cache[name] = BudgetExceededError
return BudgetExceededError
if name == 'Heartbeat':
from .heartbeat import Heartbeat
_lazy_cache[name] = Heartbeat
return Heartbeat
if name == 'HeartbeatConfig':
from .heartbeat import HeartbeatConfig
_lazy_cache[name] = HeartbeatConfig
return HeartbeatConfig
# Specialized agents - lazy loaded (import rich)
if name == 'ImageAgent':
@@ -141,10 +161,39 @@ def __getattr__(name):
_lazy_cache[name] = value
return value
# Decomposed mixins - for advanced use cases
if name == 'ToolExecutionMixin':
from .tool_execution import ToolExecutionMixin
_lazy_cache[name] = ToolExecutionMixin
return ToolExecutionMixin
elif name == 'ChatHandlerMixin':
from .chat_handler import ChatHandlerMixin
_lazy_cache[name] = ChatHandlerMixin
return ChatHandlerMixin
elif name == 'SessionManagerMixin':
from .session_manager import SessionManagerMixin
_lazy_cache[name] = SessionManagerMixin
return SessionManagerMixin
elif name == 'ChatMixin':
from .chat_mixin import ChatMixin
_lazy_cache[name] = ChatMixin
return ChatMixin
elif name == 'ExecutionMixin':
from .execution_mixin import ExecutionMixin
_lazy_cache[name] = ExecutionMixin
return ExecutionMixin
elif name == 'MemoryMixin':
from .memory_mixin import MemoryMixin
_lazy_cache[name] = MemoryMixin
return MemoryMixin
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
'Agent',
'BudgetExceededError',
'Heartbeat',
'HeartbeatConfig',
'ImageAgent',
'VideoAgent',
'VideoConfig',
@@ -198,4 +247,11 @@ __all__ = [
'MemoryAwareAgentProtocol',
'FullAgentProtocol',
'ContextEngineerProtocol',
# Decomposed mixins (for advanced use cases)
'ToolExecutionMixin',
'ChatHandlerMixin',
'SessionManagerMixin',
'ChatMixin',
'ExecutionMixin',
'MemoryMixin',
]
File diff suppressed because it is too large. Load diff
@@ -9,6 +9,7 @@ Follows the Agent() class patterns:
"""
import os
import logging
from praisonaiagents._logging import get_logger
import warnings
from dataclasses import dataclass, field
from typing import Optional, Any, Dict, Union, BinaryIO
@@ -17,7 +18,6 @@ from pathlib import Path
# Filter out Pydantic warning about fields
warnings.filterwarnings("ignore", "Valid config keys have changed in V2", UserWarning)
# ─────────────────────────────────────────────────────────────────────────────
# AudioConfig - Configuration dataclass following feature_configs.py patterns
# ─────────────────────────────────────────────────────────────────────────────
@@ -59,7 +59,6 @@ class AudioConfig:
"api_key": self.api_key,
}
# ─────────────────────────────────────────────────────────────────────────────
# AudioAgent Class - Agent-centric audio processing
# ─────────────────────────────────────────────────────────────────────────────
@@ -192,10 +191,10 @@ class AudioAgent:
def _configure_logging(self, verbose: Union[bool, int]) -> None:
"""Configure logging levels."""
if isinstance(verbose, int) and verbose >= 10:
logging.getLogger("litellm").setLevel(logging.DEBUG)
get_logger("litellm").setLevel(logging.DEBUG)
else:
logging.getLogger("litellm").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
get_logger("litellm").setLevel(logging.WARNING)
get_logger("httpx").setLevel(logging.WARNING)
def _get_model_params(self, model: Optional[str] = None) -> Dict[str, Any]:
"""Build parameters for LiteLLM calls."""
@@ -35,8 +35,9 @@ from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List, Set
from enum import Enum
import logging
from praisonaiagents._logging import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
# ============================================================================
# Unified Stage Enum (G-DUP-1 fix: single source of truth)
@@ -72,6 +73,15 @@ _ESCALATION_TO_AUTONOMY_SIGNAL = {
# Reverse map for converting autonomy signal strings back to EscalationSignal values
_AUTONOMY_TO_ESCALATION_SIGNAL = {v: k for k, v in _ESCALATION_TO_AUTONOMY_SIGNAL.items()}
# Valid autonomy modes
VALID_AUTONOMY_MODES = {"caller", "iterative"}
# Smart default mode based on level
_LEVEL_TO_DEFAULT_MODE = {
"suggest": "caller",
"auto_edit": "caller",
"full_auto": "iterative",
}
@dataclass
class AutonomyConfig:
@@ -83,6 +93,10 @@ class AutonomyConfig:
Attributes:
enabled: Whether autonomy is enabled
level: Autonomy level (suggest, auto_edit, full_auto)
mode: Execution mode — "caller" (single chat call, wrapper-equivalent)
or "iterative" (internal loop with completion detection).
Defaults based on level: suggest/auto_edit → "caller",
full_auto → "iterative".
max_iterations: Maximum iterations before stopping
doom_loop_threshold: Number of repeated actions to trigger doom loop
auto_escalate: Whether to automatically escalate complexity
@@ -99,18 +113,21 @@ class AutonomyConfig:
Usage::
# Simple — full_auto auto-enables tracking + sandbox
# Simple — default caller mode (wrapper-equivalent, no wasteful loop)
Agent(autonomy=True) # level=suggest, mode=caller
# Full auto — iterative loop with completion detection
Agent(autonomy="full_auto")
# Advanced — explicit config
# Ralph Loop — explicit iterative mode with completion promise
Agent(autonomy=AutonomyConfig(
level="full_auto",
track_changes=True,
sandbox=SandboxConfig.native(writable_paths=["./src"]),
mode="iterative",
completion_promise="COMPLETE",
))
"""
enabled: bool = True
level: str = "suggest"
mode: Optional[str] = None # None = smart default based on level
max_iterations: int = 20
doom_loop_threshold: int = 3
auto_escalate: bool = True
@@ -131,15 +148,21 @@ class AutonomyConfig:
f"Invalid autonomy level: {self.level!r}. "
f"Must be one of {sorted(VALID_AUTONOMY_LEVELS)}"
)
# Smart default mode based on level
if self.mode is None:
self.mode = _LEVEL_TO_DEFAULT_MODE.get(self.level, "caller")
elif self.mode not in VALID_AUTONOMY_MODES:
raise ValueError(
f"Invalid autonomy mode: {self.mode!r}. "
f"Must be one of {sorted(VALID_AUTONOMY_MODES)}"
)
# Auto-enable track_changes for full_auto if not explicitly set
if self.track_changes is None:
self.track_changes = (self.level == "full_auto")
# Default snapshot directory
if self.snapshot_dir is None:
import os
self.snapshot_dir = os.path.join(
os.path.expanduser("~"), ".praisonai", "snapshots"
)
from ..paths import get_snapshots_dir
self.snapshot_dir = str(get_snapshots_dir())
@property
def effective_track_changes(self) -> bool:
@@ -158,6 +181,7 @@ class AutonomyConfig:
return cls(
enabled=data.get("enabled", True),
level=level,
mode=data.get("mode"), # None = smart default in __post_init__
max_iterations=data.get("max_iterations", 20),
doom_loop_threshold=data.get("doom_loop_threshold", 3),
auto_escalate=data.get("auto_escalate", True),
@@ -171,7 +195,6 @@ class AutonomyConfig:
default_tools=data.get("default_tools"),
)
class AutonomySignal(str, Enum):
"""Signals detected from prompts for autonomy decisions.
@@ -190,26 +213,27 @@ class AutonomySignal(str, Enum):
COMPLEX_KEYWORDS = "complex_keywords"
def __init_subclass__(cls, **kwargs):
import warnings
warnings.warn(
"AutonomySignal is deprecated. Use EscalationSignal instead.",
DeprecationWarning,
stacklevel=2,
from ..utils.deprecation import warn_deprecated_param
warn_deprecated_param(
"AutonomySignal class",
since="1.0.0",
removal="2.0.0",
alternative="use EscalationSignal from praisonaiagents.escalation.types instead",
stacklevel=3
)
super().__init_subclass__(**kwargs)
def _warn_autonomy_signal():
"""Emit deprecation warning when AutonomySignal is accessed."""
import warnings
warnings.warn(
"AutonomySignal is deprecated. Use EscalationSignal from "
"praisonaiagents.escalation.types instead.",
DeprecationWarning,
stacklevel=3,
from ..utils.deprecation import warn_deprecated_param
warn_deprecated_param(
"AutonomySignal",
since="1.0.0",
removal="2.0.0",
alternative="use EscalationSignal from praisonaiagents.escalation.types instead",
stacklevel=4
)
class AutonomyTrigger:
"""Detects signals from prompts for autonomy decisions.
@@ -255,7 +279,6 @@ class AutonomyTrigger:
pass
return self._delegate.recommend_stage(esc_signals)
@dataclass
class AutonomyResult:
"""Result of an autonomous execution.
@@ -290,7 +313,6 @@ class AutonomyResult:
"""
return self.output or ""
class DoomLoopTracker:
"""Tracks actions to detect doom loops with graduated recovery.
@@ -380,12 +402,19 @@ class DoomLoopTracker:
"""
self._delegate.mark_progress(marker)
def record_response(self, text: str) -> None:
"""Record model response text for content streaming loop detection.
Args:
text: The model's response text
"""
self._delegate.record_response(text)
def reset(self) -> None:
"""Reset the tracker completely."""
self._delegate.start_session()
self._recovery_attempts = 0
class AutonomyMixin:
"""Helper-only trait for autonomy utilities.
@@ -0,0 +1,118 @@
"""
Chat handler mixin for the Agent class.
Contains chat management, response processing, and message formatting.
Extracted from agent.py for better modularity and maintainability.
"""
import logging
import time
from typing import Optional, Any, Dict, List, TYPE_CHECKING
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
class ChatHandlerMixin:
"""Mixin providing chat handling methods for the Agent class.
This mixin focuses on high-level chat coordination and response processing,
while the actual LLM interaction is handled by ChatMixin.
"""
def _format_chat_response(self, response: str, metadata: Optional[Dict[str, Any]] = None) -> str:
"""Format chat response with any necessary post-processing.
Args:
response: Raw response from the LLM
metadata: Optional metadata about the response
Returns:
Formatted response string
"""
if not response:
return ""
# Basic response formatting - can be extended with additional logic
formatted = response.strip()
# Add any metadata-based formatting if needed
if metadata and metadata.get('add_timestamp'):
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
formatted = f"[{timestamp}] {formatted}"
return formatted
def _validate_chat_input(self, prompt: str) -> bool:
"""Validate chat input before processing.
Args:
prompt: Input prompt to validate
Returns:
True if valid, False otherwise
"""
if not prompt or not isinstance(prompt, str):
logger.warning("Invalid chat input: prompt must be a non-empty string")
return False
if len(prompt.strip()) == 0:
logger.warning("Invalid chat input: prompt cannot be empty or whitespace only")
return False
return True
def _prepare_chat_context(self, prompt: str, **kwargs) -> Dict[str, Any]:
"""Prepare context for chat processing.
Args:
prompt: Input prompt
**kwargs: Additional context parameters
Returns:
Context dictionary for chat processing
"""
context = {
'prompt': prompt,
'timestamp': time.time(),
'agent_name': getattr(self, 'name', None) or 'Agent',
}
# Add any additional context from kwargs
context.update(kwargs)
return context
def _handle_chat_error(self, error: Exception, prompt: str) -> str:
"""Handle chat processing errors gracefully.
Args:
error: The exception that occurred
prompt: The original prompt that caused the error
Returns:
Error response message
"""
logger.error(f"Chat error occurred: {error}", exc_info=True)
# Return a user-friendly error message without exposing internal details.
# Include a timestamp reference so users can correlate with server logs.
error_ref = time.strftime('%Y%m%d%H%M%S')
return f"I encountered an error while processing your request (ref: {error_ref}). Please try again."
def _log_chat_interaction(self, prompt: str, response: str, duration: Optional[float] = None):
"""Log chat interaction for debugging and analytics.
Args:
prompt: Input prompt
response: Generated response
duration: Processing duration in seconds
"""
agent_name = getattr(self, 'name', None) or 'Agent'
duration_str = f" (took {duration:.2f}s)" if duration else ""
logger.debug(f"[{agent_name}] Chat interaction{duration_str}")
logger.debug(f"[{agent_name}] Prompt: {prompt[:100]}{'...' if len(prompt) > 100 else ''}")
logger.debug(f"[{agent_name}] Response: {response[:100]}{'...' if len(response) > 100 else ''}")
File diff suppressed because it is too large. Load diff
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field, asdict
from typing import Any, Dict, List, Optional, Union
import asyncio
import logging
from praisonaiagents._logging import get_logger
@dataclass
class CodeConfig:
@@ -29,7 +29,6 @@ class CodeConfig:
"""Convert config to dictionary."""
return {k: v for k, v in asdict(self).items() if v is not None}
class CodeAgent:
"""Agent for code generation, execution, review, and refactoring.
@@ -96,7 +95,7 @@ class CodeAgent:
self._console = None
# Configure logging
self._logger = logging.getLogger(f"CodeAgent.{name}")
self._logger = get_logger(f"CodeAgent.{name}")
if not verbose:
self._logger.setLevel(logging.WARNING)
@@ -309,8 +308,9 @@ Follow best practices and coding standards."""
# Clean up temp file
try:
os.unlink(temp_file)
except:
pass
except Exception as e:
# Temp file cleanup failed - not critical
get_logger(__name__).debug(f"Failed to clean up temp file {temp_file}: {e}")
async def aexecute(self, code: str, language: str = "python", **kwargs) -> Dict[str, Any]:
"""Execute code asynchronously.
@@ -14,6 +14,7 @@ This class implements proper Context Engineering principles following the PRD te
import os
import json
import logging
from praisonaiagents._logging import get_logger
from datetime import datetime
from pathlib import Path
from typing import Optional, Any, Dict, Union, List, TYPE_CHECKING
@@ -94,7 +95,6 @@ async def _async_subprocess_run(cmd: list, timeout: int = 60) -> tuple:
from ..agent.agent import Agent
from ..task import Task
class ContextAgent(Agent):
"""
Advanced Context Engineering Agent - Comprehensive context generation for AI coding assistants.
@@ -162,7 +162,8 @@ class ContextAgent(Agent):
# Enhanced logging and output management
self.debug_mode = os.getenv('LOGLEVEL', '').lower() == 'debug'
self.output_dir = Path(".praisonai/prp") # Save in .praisonai/prp folder
from ..paths import get_project_prp_dir
self.output_dir = get_project_prp_dir() # Save in .praisonai/prp folder
self.setup_output_directories() # Create directories first
self.setup_logging() # Then setup logging
@@ -206,7 +207,7 @@ class ContextAgent(Agent):
"""Setup comprehensive logging based on debug mode."""
try:
# Create logger
self.logger = logging.getLogger(f"ContextAgent_{id(self)}")
self.logger = get_logger(f"ContextAgent_{id(self)}")
self.logger.setLevel(logging.DEBUG if self.debug_mode else logging.INFO)
# Clear existing handlers
@@ -246,7 +247,7 @@ class ContextAgent(Agent):
except Exception as e:
print(f"⚠️ Warning: Could not setup logging: {e}")
# Create a minimal logger as fallback
self.logger = logging.getLogger(f"ContextAgent_{id(self)}")
self.logger = get_logger(f"ContextAgent_{id(self)}")
self.logger.setLevel(logging.INFO)
def setup_output_directories(self):
@@ -2444,7 +2445,7 @@ This PRP provides:
"""
asyncio = _get_asyncio()
# Run sync method in executor for now (subprocess calls are blocking)
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None,
self.analyze_codebase_with_gitingest,
@@ -2460,7 +2461,7 @@ This PRP provides:
Async version of generate_prp.
"""
asyncio = _get_asyncio()
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None,
lambda: self.generate_comprehensive_prp(feature_request, context_analysis)
@@ -2475,13 +2476,12 @@ This PRP provides:
Async version of create_implementation_blueprint.
"""
asyncio = _get_asyncio()
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None,
lambda: self.build_implementation_blueprint(feature_request, context_analysis)
)
def create_context_agent(llm: Optional[Union[str, Any]] = None, **kwargs) -> ContextAgent:
"""
Factory function to create a ContextAgent following Context Engineering and PRD methodology.
@@ -0,0 +1,135 @@
"""Cost persistence — saves agent cost_summary to disk after runs.
Writes JSON files to ``~/.praisonai/costs/`` for CLI dashboard consumption.
Zero overhead when not used (this module is never imported unless explicitly called).
Usage::
from praisonaiagents.agent.cost_persistence import save_cost_report, load_cost_report
save_cost_report(agent, session_name="my_run")
report = load_cost_report("my_run")
"""
import json
import logging
from praisonaiagents._logging import get_logger
import os
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
logger = get_logger(__name__)
COST_DIR = Path.home() / ".praisonai" / "costs"
def save_cost_report(
agent,
session_name: Optional[str] = None,
) -> str:
"""Persist an agent's cost_summary to disk.
Args:
agent: Agent instance with cost_summary property.
session_name: Optional session identifier. Auto-generated if None.
Returns:
Path to the saved JSON file.
"""
COST_DIR.mkdir(parents=True, exist_ok=True)
name = session_name or f"{agent.name}_{int(time.time())}"
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name)
summary = getattr(agent, "cost_summary", None)
if summary is None:
summary = {
"tokens_in": 0,
"tokens_out": 0,
"cost": 0.0,
"llm_calls": 0,
}
report = {
"session": name,
"agent_name": getattr(agent, "name", "unknown"),
"timestamp": time.time(),
"iso_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
**summary,
}
filepath = COST_DIR / f"{safe_name}.json"
with open(filepath, "w") as f:
json.dump(report, f, indent=2)
logger.info(f"[costs] Saved cost report to {filepath}")
return str(filepath)
def load_cost_report(session_name: str) -> Optional[Dict[str, Any]]:
"""Load a cost report by session name.
Args:
session_name: Session identifier.
Returns:
Parsed JSON dict, or None if not found.
"""
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in session_name)
filepath = COST_DIR / f"{safe_name}.json"
if filepath.exists():
with open(filepath) as f:
return json.load(f)
return None
def list_cost_reports(limit: int = 20) -> List[Dict[str, Any]]:
"""List recent cost reports, sorted by timestamp (newest first).
Args:
limit: Max number of reports to return.
Returns:
List of parsed JSON report dicts.
"""
if not COST_DIR.exists():
return []
reports = []
for fpath in sorted(COST_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
try:
with open(fpath) as f:
reports.append(json.load(f))
except (json.JSONDecodeError, OSError):
continue
if len(reports) >= limit:
break
return reports
def format_cost_table(reports: List[Dict[str, Any]], as_json: bool = False) -> str:
"""Format cost reports for terminal display.
Args:
reports: List of report dicts from list_cost_reports().
as_json: If True, returns JSON string instead of table.
Returns:
Formatted string for terminal output.
"""
if as_json:
return json.dumps(reports, indent=2)
if not reports:
return "No cost reports found."
lines = []
lines.append(f"{'Session':<30} {'Agent':<20} {'Cost ($)':<12} {'Tokens In':<12} {'Tokens Out':<12} {'LLM Calls':<10}")
lines.append("-" * 96)
for r in reports:
lines.append(
f"{r.get('session', '?'):<30} "
f"{r.get('agent_name', '?'):<20} "
f"${r.get('cost', 0):<11.4f} "
f"{r.get('tokens_in', 0):<12} "
f"{r.get('tokens_out', 0):<12} "
f"{r.get('llm_calls', 0):<10}"
)
return "\n".join(lines)
@@ -35,13 +35,13 @@ Example:
import os
import logging
from praisonaiagents._logging import get_logger
import time
import asyncio
from typing import List, Optional, Any, Dict, Literal
from dataclasses import dataclass, field
from enum import Enum
# Data classes for Deep Research response types
@dataclass
class Citation:
@@ -54,7 +54,6 @@ class Citation:
def __repr__(self):
return f"Citation(title='{self.title}', url='{self.url}')"
@dataclass
class ReasoningStep:
"""Represents a reasoning step in the research process."""
@@ -64,7 +63,6 @@ class ReasoningStep:
def __repr__(self):
return f"ReasoningStep(text='{self.text[:50]}...')" if len(self.text) > 50 else f"ReasoningStep(text='{self.text}')"
@dataclass
class WebSearchCall:
"""Represents a web search call made during research."""
@@ -74,7 +72,6 @@ class WebSearchCall:
def __repr__(self):
return f"WebSearchCall(query='{self.query}', status='{self.status}')"
@dataclass
class CodeExecutionStep:
"""Represents a code execution step during research."""
@@ -84,7 +81,6 @@ class CodeExecutionStep:
def __repr__(self):
return f"CodeExecutionStep(input='{self.input_code[:50]}...')" if len(self.input_code) > 50 else f"CodeExecutionStep(input='{self.input_code}')"
@dataclass
class MCPCall:
"""Represents an MCP tool call during research."""
@@ -95,7 +91,6 @@ class MCPCall:
def __repr__(self):
return f"MCPCall(name='{self.name}', server='{self.server_label}')"
@dataclass
class FileSearchCall:
"""Represents a file search call (Gemini-specific)."""
@@ -104,7 +99,6 @@ class FileSearchCall:
def __repr__(self):
return f"FileSearchCall(stores={self.store_names})"
@dataclass
class DeepResearchResponse:
"""
@@ -149,14 +143,12 @@ class DeepResearchResponse:
sources.append({"title": c.title, "url": c.url})
return sources
class Provider(Enum):
"""Supported Deep Research providers."""
OPENAI = "openai"
GEMINI = "gemini"
LITELLM = "litellm"
class DeepResearchAgent:
"""
Agent for performing deep research using multiple provider APIs.
@@ -295,7 +287,7 @@ You are a professional research analyst. When conducting research:
self._gemini_client = None
# Configure logging
self.logger = logging.getLogger(__name__)
self.logger = get_logger(__name__)
if self.verbose:
self.logger.debug(f"DeepResearchAgent initialized: provider={self.provider.value}, model={self.model}")
@@ -868,6 +860,78 @@ You are a professional research analyst. When conducting research:
return self._parse_gemini_response(interaction)
async def _research_gemini_async(
self,
query: str,
instructions: Optional[str] = None,
model: Optional[str] = None,
file_search: bool = False,
file_search_stores: Optional[List[Dict[str, Any]]] = None,
grounding_file_prompt: Optional[str] = None,
) -> DeepResearchResponse:
"""
Async version of _research_gemini with non-blocking polling.
Uses asyncio.sleep() to prevent blocking the event loop.
"""
import asyncio
import time
if self.provider != Provider.GEMINI:
raise ValueError("This method requires GEMINI provider")
if not self.gemini_client:
raise RuntimeError("Gemini client is required for Deep Research")
create_params = {
"query": query,
"file_search": file_search,
}
if instructions:
create_params["instructions"] = instructions
if model:
create_params["model"] = model
if file_search_stores:
create_params["file_search_stores"] = file_search_stores
if grounding_file_prompt:
create_params["grounding_file_prompt"] = grounding_file_prompt
interaction = self.gemini_client.interactions.create(**create_params)
if self.verbose:
self.logger.debug(f"Gemini research started (async): {interaction.id}")
start_time = time.time()
while True:
interaction = self.gemini_client.interactions.get(interaction.id)
if interaction.status == "completed":
if self.verbose:
self.logger.debug("Gemini research completed (async)")
break
elif interaction.status == "failed":
error = getattr(interaction, 'error', 'Unknown error')
raise RuntimeError(f"Gemini Deep Research failed: {error}")
elif interaction.status == "cancelled":
raise RuntimeError("Gemini Deep Research was cancelled")
elapsed = time.time() - start_time
if elapsed > self.max_wait_time:
raise TimeoutError(
f"Gemini Deep Research timed out after {self.max_wait_time}s"
)
if self.verbose:
self.logger.debug(f"Research in progress (async)... ({elapsed:.0f}s)")
# Use async sleep to avoid blocking event loop
await asyncio.sleep(self.poll_interval)
return self._parse_gemini_response(interaction)
def _research_gemini_streaming(
self,
create_params: Dict[str, Any],
@@ -1183,17 +1247,13 @@ You are a professional research analyst. When conducting research:
try:
if self.provider == Provider.GEMINI:
# Gemini async - run sync in executor
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None,
lambda: self._research_gemini(
query=query,
instructions=instructions,
model=model,
file_search=file_search if file_search is not None else self.enable_file_search,
file_search_stores=file_search_stores or self.file_search_stores,
)
# Use async Gemini method for non-blocking polling
result = await self._research_gemini_async(
query=query,
instructions=instructions,
model=model,
file_search=file_search if file_search is not None else self.enable_file_search,
file_search_stores=file_search_stores or self.file_search_stores,
)
elif self.provider == Provider.LITELLM:
# LiteLLM async
@@ -12,12 +12,12 @@ Follows the Agent() class patterns with:
import os
import logging
from praisonaiagents._logging import get_logger
import math
from dataclasses import dataclass
from typing import Optional, Dict, Any, Union, List
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
@dataclass
class EmbeddingConfig:
@@ -44,7 +44,6 @@ class EmbeddingConfig:
"api_key": self.api_key,
}
class EmbeddingAgent:
"""
A specialized agent for generating text embeddings.
@@ -164,10 +163,10 @@ class EmbeddingAgent:
def _configure_logging(self, verbose: Union[bool, int]) -> None:
"""Configure logging levels."""
if isinstance(verbose, int) and verbose >= 10:
logging.getLogger("litellm").setLevel(logging.DEBUG)
get_logger("litellm").setLevel(logging.DEBUG)
else:
logging.getLogger("litellm").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
get_logger("litellm").setLevel(logging.WARNING)
get_logger("httpx").setLevel(logging.WARNING)
def _get_model_params(self, model: Optional[str] = None) -> Dict[str, Any]:
"""Build parameters for LiteLLM calls."""
File diff suppressed because it is too large. Load diff
@@ -15,6 +15,7 @@ from dataclasses import dataclass, field
from enum import Enum
import inspect
import logging
from praisonaiagents._logging import get_logger
import asyncio
import threading
import time
@@ -22,8 +23,7 @@ import time
if TYPE_CHECKING:
from .agent import Agent
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
class ContextPolicy(Enum):
"""Policy for context sharing during handoff."""
@@ -32,7 +32,6 @@ class ContextPolicy(Enum):
NONE = "none" # No context sharing
LAST_N = "last_n" # Share last N messages
@dataclass
class HandoffConfig:
"""
@@ -98,19 +97,16 @@ class HandoffConfig:
data["context_policy"] = ContextPolicy(data["context_policy"])
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
class HandoffError(Exception):
"""Base exception for handoff errors."""
pass
class HandoffCycleError(HandoffError):
"""Raised when a cycle is detected in handoff chain."""
def __init__(self, chain: List[str]):
self.chain = chain
super().__init__(f"Handoff cycle detected: {' -> '.join(chain)}")
class HandoffDepthError(HandoffError):
"""Raised when max handoff depth is exceeded."""
def __init__(self, depth: int, max_depth: int):
@@ -118,7 +114,6 @@ class HandoffDepthError(HandoffError):
self.max_depth = max_depth
super().__init__(f"Max handoff depth exceeded: {depth} > {max_depth}")
class HandoffTimeoutError(HandoffError):
"""Raised when handoff times out."""
def __init__(self, timeout: float, agent_name: str):
@@ -126,41 +121,34 @@ class HandoffTimeoutError(HandoffError):
self.agent_name = agent_name
super().__init__(f"Handoff to {agent_name} timed out after {timeout}s")
# Thread-local storage for tracking handoff chains
_handoff_context = threading.local()
def _get_handoff_chain() -> List[str]:
"""Get current handoff chain from thread-local storage."""
if not hasattr(_handoff_context, 'chain'):
_handoff_context.chain = []
return _handoff_context.chain
def _get_handoff_depth() -> int:
"""Get current handoff depth."""
return len(_get_handoff_chain())
def _push_handoff(agent_name: str) -> None:
"""Push agent to handoff chain."""
chain = _get_handoff_chain()
chain.append(agent_name)
def _pop_handoff() -> Optional[str]:
"""Pop agent from handoff chain."""
chain = _get_handoff_chain()
return chain.pop() if chain else None
def _clear_handoff_chain() -> None:
"""Clear the handoff chain."""
if hasattr(_handoff_context, 'chain'):
_handoff_context.chain = []
@dataclass
class HandoffInputData:
"""Data passed to a handoff target agent."""
@@ -170,7 +158,6 @@ class HandoffInputData:
handoff_depth: int = 0
handoff_chain: List[str] = field(default_factory=list)
@dataclass
class HandoffResult:
"""Result of a handoff operation."""
@@ -671,7 +658,6 @@ class Handoff:
return handoff_tool
def handoff(
agent: 'Agent',
tool_name_override: Optional[str] = None,
@@ -760,7 +746,6 @@ def handoff(
config=config,
)
# Handoff filters - common patterns for filtering handoff data
class handoff_filters:
"""Common handoff input filters."""
@@ -827,14 +812,12 @@ class handoff_filters:
return data
# Recommended prompt prefix for agents that use handoffs
RECOMMENDED_PROMPT_PREFIX = """You have the ability to transfer tasks to specialized agents when appropriate.
When you determine that a task would be better handled by another agent with specific expertise,
use the transfer tool to hand off the task. The receiving agent will have the full context of
the conversation and will continue helping the user."""
def prompt_with_handoff_instructions(base_prompt: str, agent: 'Agent') -> str:
"""
Add handoff instructions to an agent's prompt.
@@ -0,0 +1,176 @@
"""Heartbeat — runs an agent on a schedule, delivers results via callback.
Standalone wrapper class. Does NOT modify the Agent class.
Usage::
from praisonaiagents import Agent, Heartbeat
agent = Agent(instructions="Check server status")
hb = Heartbeat(agent, schedule="every 30m", prompt="Report status")
hb.start() # blocking loop
# hb.start(blocking=False) # background thread
"""
import asyncio
import logging
from praisonaiagents._logging import get_logger
import threading
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Optional, Union
logger = get_logger(__name__)
@dataclass
class HeartbeatConfig:
"""Configuration for Heartbeat scheduler.
Attributes:
schedule: Human-friendly schedule expression. Supports:
- Keywords: "hourly", "daily", "weekly"
- Intervals: "every 30m", "every 6h", "*/30m", "*/10s"
- Cron: "cron:0 7 * * *"
- One-shot: "at:2026-03-01T09:00:00"
prompt: Override prompt sent to agent each tick. None = use agent's default.
on_result: Callback receiving (result_text: str). Default: log to stdout.
on_error: "retry" | "skip" | callable(error). Default: "retry".
max_retries: Max consecutive retries before skipping. Default: 3.
"""
schedule: str = "hourly"
prompt: Optional[str] = None
on_result: Optional[Callable] = None
on_error: Union[str, Callable] = "retry"
max_retries: int = 3
class Heartbeat:
"""Standalone heartbeat coordinator.
Runs an Agent on a schedule and delivers results via callback.
Does NOT add any params to the Agent class.
Usage::
from praisonaiagents import Agent, Heartbeat
agent = Agent(instructions="Monitor server health")
hb = Heartbeat(agent, schedule="hourly")
hb.start()
"""
def __init__(
self,
agent,
schedule: str = "hourly",
prompt: Optional[str] = None,
on_result: Optional[Callable] = None,
on_error: Union[str, Callable] = "retry",
max_retries: int = 3,
):
self.agent = agent
self.config = HeartbeatConfig(
schedule=schedule,
prompt=prompt,
on_result=on_result,
on_error=on_error,
max_retries=max_retries,
)
self._running = False
self._thread: Optional[threading.Thread] = None
self._interval_seconds = self._resolve_interval(schedule)
def start(self, blocking: bool = True) -> None:
"""Start the heartbeat loop.
Args:
blocking: If True, blocks the calling thread. If False,
runs in a background daemon thread.
"""
self._running = True
if blocking:
self._loop()
else:
self._thread = threading.Thread(target=self._loop, daemon=True)
self._thread.start()
def stop(self) -> None:
"""Stop the heartbeat loop."""
self._running = False
@property
def is_running(self) -> bool:
return self._running
# ── internals ────────────────────────────────────────────────────
def _loop(self) -> None:
"""Main polling loop."""
retries = 0
while self._running:
try:
result = self._tick()
retries = 0 # Reset on success
self._deliver(result)
except Exception as e:
retries += 1
logger.error(f"[heartbeat] {self.agent.name}: error on tick #{retries}: {e}")
if callable(self.config.on_error):
self.config.on_error(e)
elif self.config.on_error == "skip":
logger.warning(f"[heartbeat] Skipping error: {e}")
else: # "retry"
if retries >= self.config.max_retries:
logger.error(
f"[heartbeat] {self.agent.name}: max retries "
f"({self.config.max_retries}) reached, skipping."
)
retries = 0
time.sleep(self._interval_seconds)
def _tick(self) -> str:
"""Execute one heartbeat tick — run the agent and return result."""
prompt = self.config.prompt or "Run your scheduled check."
result = self.agent.start(prompt)
return str(result) if result else ""
def _deliver(self, result: str) -> None:
"""Deliver result via callback or log."""
if self.config.on_result:
self.config.on_result(result)
else:
logger.info(f"[heartbeat] {self.agent.name}: {result[:200]}")
@staticmethod
def _resolve_interval(schedule: str) -> float:
"""Convert schedule string to interval in seconds.
Uses the scheduler parser if available, otherwise falls back to
simple keyword parsing.
"""
try:
from ..scheduler.parser import parse_schedule
sched = parse_schedule(schedule)
if sched.every_seconds:
return float(sched.every_seconds)
except (ImportError, Exception):
pass
# Fallback keyword resolution
keywords = {
"hourly": 3600,
"daily": 86400,
"weekly": 604800,
}
lower = schedule.lower().strip()
if lower in keywords:
return float(keywords[lower])
# Try parsing "every Xm/h/s" patterns
import re
match = re.match(r"(?:every\s+)?(\d+)\s*(s|m|h)", lower)
if match:
val, unit = int(match.group(1)), match.group(2)
multipliers = {"s": 1, "m": 60, "h": 3600}
return float(val * multipliers.get(unit, 60))
return 3600.0 # Default to hourly
@@ -7,12 +7,10 @@ from typing import Optional, Any, Dict, Union, List
from ..agent.agent import Agent
from pydantic import BaseModel, Field
import logging
from praisonaiagents._logging import get_logger
import warnings
# Filter out Pydantic warning about fields
warnings.filterwarnings("ignore", "Valid config keys have changed in V2", UserWarning)
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
class ImageGenerationConfig(BaseModel):
"""Configuration for image generation settings."""
style: str = Field(default="natural", description="Style of the generated image")
@@ -80,10 +78,10 @@ class ImageAgent(Agent):
# Only suppress logs if not in debug mode
if not isinstance(verbose, bool) and verbose >= 10:
# Enable detailed debug logging
logging.getLogger("asyncio").setLevel(logging.DEBUG)
logging.getLogger("selector_events").setLevel(logging.DEBUG)
logging.getLogger("litellm.utils").setLevel(logging.DEBUG)
logging.getLogger("litellm.main").setLevel(logging.DEBUG)
get_logger("asyncio").setLevel(logging.DEBUG)
get_logger("selector_events").setLevel(logging.DEBUG)
get_logger("litellm.utils").setLevel(logging.DEBUG)
get_logger("litellm.main").setLevel(logging.DEBUG)
if hasattr(self, 'litellm'):
self.litellm.suppress_debug_messages = False
self.litellm.set_verbose = True
@@ -91,12 +89,12 @@ class ImageAgent(Agent):
warnings.resetwarnings()
else:
# Suppress debug logging for normal operation
logging.getLogger("asyncio").setLevel(logging.WARNING)
logging.getLogger("selector_events").setLevel(logging.WARNING)
logging.getLogger("litellm.utils").setLevel(logging.WARNING)
logging.getLogger("litellm.main").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
get_logger("asyncio").setLevel(logging.WARNING)
get_logger("selector_events").setLevel(logging.WARNING)
get_logger("litellm.utils").setLevel(logging.WARNING)
get_logger("litellm.main").setLevel(logging.WARNING)
get_logger("httpx").setLevel(logging.WARNING)
get_logger("httpcore").setLevel(logging.WARNING)
if hasattr(self, 'litellm'):
self.litellm.suppress_debug_messages = True
self.litellm._logging._disable_debugging()
@@ -155,6 +153,7 @@ class ImageAgent(Agent):
# Gemini provider doesn't support response_format parameter
# Apply this filter if provider is explicitly 'gemini' or as fallback for gemini models
config.pop('response_format', None)
from rich.progress import Progress, SpinnerColumn, TextColumn
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
@@ -0,0 +1,385 @@
"""
Tool Loop Detection for PraisonAI Agents.
Detects when an agent is stuck calling the same tool repeatedly with
identical arguments and no progress.
Inspired by OpenClaw's tool-loop-detection.ts (624 lines, 4 detectors).
Adapted to Python with PraisonAI's protocol-driven, zero-dep philosophy.
Zero Performance Impact:
- Disabled by default (enabled=False)
- Uses stdlib only: hashlib, json
- History is per-agent-instance (no global mutable state)
Usage:
from praisonaiagents.agent.loop_detection import (
LoopDetectionConfig, detect_tool_loop, record_tool_call, record_tool_outcome
)
"""
from __future__ import annotations
import hashlib
import json
import logging
from praisonaiagents._logging import get_logger
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Literal, Optional, TypedDict
logger = get_logger(__name__)
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
@dataclass
class LoopDetectionConfig:
"""
Configuration for tool loop detection.
Attributes:
enabled: Whether loop detection is active. Default False (opt-in).
history_size: Sliding window size for tool call history.
warn_threshold: Number of identical (tool, args) calls before warning.
critical_threshold: Number before critical/circuit-break. Must > warn.
detectors: Which detectors are active.
"""
enabled: bool = False
history_size: int = 30
warn_threshold: int = 10
critical_threshold: int = 20
detectors: Dict[str, bool] = field(default_factory=lambda: {
"generic_repeat": True,
"poll_no_progress": True,
"ping_pong": True,
})
def __post_init__(self) -> None:
# Auto-correct: critical must always be > warn
if self.critical_threshold <= self.warn_threshold:
self.critical_threshold = self.warn_threshold + 1
# ---------------------------------------------------------------------------
# Result type
# ---------------------------------------------------------------------------
class LoopDetectionResult(TypedDict, total=False):
stuck: bool
level: Optional[Literal["warning", "critical"]]
detector: Optional[str]
message: Optional[str]
count: int
_NOT_STUCK: LoopDetectionResult = {"stuck": False}
# ---------------------------------------------------------------------------
# History record
# ---------------------------------------------------------------------------
class ToolCallRecord(TypedDict, total=False):
tool_name: str
args_hash: str
result_hash: Optional[str]
timestamp: float
# ---------------------------------------------------------------------------
# Hashing helpers
# ---------------------------------------------------------------------------
def _stable_json(value: Any) -> str:
"""Deterministic JSON serialization (sorted keys, str fallback)."""
if value is None or isinstance(value, (bool, int, float, str)):
return json.dumps(value)
if isinstance(value, (list, tuple)):
return "[" + ",".join(_stable_json(v) for v in value) + "]"
if isinstance(value, dict):
keys = sorted(value.keys())
parts = [json.dumps(k) + ":" + _stable_json(value[k]) for k in keys]
return "{" + ",".join(parts) + "}"
# Fallback for non-serializable objects
try:
return json.dumps(str(value))
except Exception:
return '"<unserializable>"'
def _sha256_hex(text: str, prefix_len: int = 16) -> str:
return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()[:prefix_len]
def hash_tool_call(tool_name: str, args: Any) -> str:
"""
Compute a stable hash for a (tool_name, args) pair.
Args:
tool_name: Name of the tool being called
args: Arguments dict or any serializable value
Returns:
16-character hex string
"""
stable = _stable_json({"t": tool_name, "a": args})
return _sha256_hex(stable)
def _hash_result(result: Any) -> Optional[str]:
"""Hash a tool result for no-progress detection."""
if result is None:
return None
try:
stable = _stable_json(result)
return _sha256_hex(stable)
except Exception:
return None
# ---------------------------------------------------------------------------
# Record tool call / outcome
# ---------------------------------------------------------------------------
def record_tool_call(
history: List[ToolCallRecord],
tool_name: str,
args: Any,
config: Optional[LoopDetectionConfig] = None,
) -> None:
"""
Append a tool call to the history (before execution).
Args:
history: Mutable list of ToolCallRecord
tool_name: Tool being called
args: Tool arguments
config: Loop detection config (for history_size)
"""
max_size = config.history_size if config else 30
history.append(ToolCallRecord(
tool_name=tool_name,
args_hash=hash_tool_call(tool_name, args),
result_hash=None,
timestamp=time.time(),
))
# Trim to sliding window
if len(history) > max_size:
del history[: len(history) - max_size]
def record_tool_outcome(
history: List[ToolCallRecord],
tool_name: str,
args: Any,
result: Any,
config: Optional[LoopDetectionConfig] = None,
) -> None:
"""
Update the most recent matching record with result hash (after execution).
Args:
history: Mutable list of ToolCallRecord
tool_name: Tool that was called
args: Arguments that were passed
result: The return value of the tool
config: Loop detection config
"""
args_hash = hash_tool_call(tool_name, args)
result_hash = _hash_result(result)
if result_hash is None:
return
# Find the latest matching record without a result_hash
for i in range(len(history) - 1, -1, -1):
rec = history[i]
if rec["tool_name"] == tool_name and rec["args_hash"] == args_hash and rec.get("result_hash") is None:
history[i] = ToolCallRecord(
tool_name=rec["tool_name"],
args_hash=rec["args_hash"],
result_hash=result_hash,
timestamp=rec["timestamp"],
)
return
# ---------------------------------------------------------------------------
# Detectors
# ---------------------------------------------------------------------------
def _known_poll_tool(tool_name: str) -> bool:
"""Heuristic: tools whose names suggest polling/waiting."""
_poll_keywords = ("status", "poll", "check", "wait", "ping", "health", "command_status")
name_lower = tool_name.lower()
return any(kw in name_lower for kw in _poll_keywords)
def _count_generic_repeat(
history: List[ToolCallRecord],
args_hash: str,
tool_name: str,
) -> int:
"""Count how many times this exact (tool, args) appears in history."""
return sum(
1 for rec in history
if rec["tool_name"] == tool_name and rec["args_hash"] == args_hash
)
def _no_progress_streak(
history: List[ToolCallRecord],
args_hash: str,
tool_name: str,
) -> int:
"""
Count consecutive tail records with identical (args_hash, result_hash).
Returns 0 if any record has no result_hash or a different result_hash.
"""
streak = 0
first_result_hash: Optional[str] = None
for rec in reversed(history):
if rec["tool_name"] != tool_name or rec["args_hash"] != args_hash:
continue
rh = rec.get("result_hash")
if rh is None:
continue
if first_result_hash is None:
first_result_hash = rh
streak = 1
elif rh == first_result_hash:
streak += 1
else:
break # result changed — no longer a streak
return streak
def _ping_pong_streak(
history: List[ToolCallRecord],
current_hash: str,
) -> int:
"""
Detect alternating pattern: A, B, A, B, ...
Returns count of consecutive alternating tail entries, 0 if not ping-pong.
"""
if len(history) < 2:
return 0
last = history[-1]
# Find the "other" hash in history (most recent != last)
other_hash: Optional[str] = None
for rec in reversed(history[:-1]):
if rec["args_hash"] != last["args_hash"]:
other_hash = rec["args_hash"]
break
if other_hash is None:
return 0
# Count tail alternation
count = 0
for i, rec in enumerate(reversed(history)):
expected = last["args_hash"] if i % 2 == 0 else other_hash
if rec["args_hash"] != expected:
break
count += 1
# Current call must continue the ping-pong pattern
if current_hash != (other_hash if count % 2 == 1 else last["args_hash"]):
return 0
return count + 1 # include current call
# ---------------------------------------------------------------------------
# Main detection function
# ---------------------------------------------------------------------------
def detect_tool_loop(
history: List[ToolCallRecord],
tool_name: str,
args: Any,
config: Optional[LoopDetectionConfig] = None,
) -> LoopDetectionResult:
"""
Detect if agent is stuck in a tool call loop.
Args:
history: Mutable sliding window of ToolCallRecord
tool_name: Tool being called now
args: Arguments for this call
config: Loop detection configuration
Returns:
LoopDetectionResult with stuck=False (safe) or stuck=True (warning/critical)
"""
if config is None or not config.enabled:
return _NOT_STUCK
args_hash = hash_tool_call(tool_name, args)
detectors = config.detectors or {}
warn = config.warn_threshold
critical = config.critical_threshold
# --- poll_no_progress detector ---
if detectors.get("poll_no_progress", True) and _known_poll_tool(tool_name):
streak = _no_progress_streak(history, args_hash, tool_name)
if streak >= critical:
msg = (
f"CRITICAL: '{tool_name}' called {streak} times with identical args "
f"and identical result. Detected stuck polling loop. Stop and report failure."
)
logger.error(f"[loop_detection] {msg}")
return LoopDetectionResult(
stuck=True, level="critical",
detector="poll_no_progress", message=msg, count=streak,
)
if streak >= warn:
msg = (
f"WARNING: '{tool_name}' has returned identical results {streak} times. "
f"This looks like a stuck poll. Increase wait time or report failure."
)
logger.warning(f"[loop_detection] {msg}")
return LoopDetectionResult(
stuck=True, level="warning",
detector="poll_no_progress", message=msg, count=streak,
)
# --- ping_pong detector ---
if detectors.get("ping_pong", True):
pp_count = _ping_pong_streak(history, args_hash)
if pp_count >= critical:
msg = (
f"CRITICAL: Ping-pong loop detected ({pp_count} alternating calls). "
f"Agent is oscillating between two tool states. Stop and report failure."
)
logger.error(f"[loop_detection] {msg}")
return LoopDetectionResult(
stuck=True, level="critical",
detector="ping_pong", message=msg, count=pp_count,
)
if pp_count >= warn:
msg = (
f"WARNING: Alternating tool pattern detected ({pp_count} calls). "
f"Stop ping-pong and try a different approach."
)
logger.warning(f"[loop_detection] {msg}")
return LoopDetectionResult(
stuck=True, level="warning",
detector="ping_pong", message=msg, count=pp_count,
)
# --- generic_repeat detector ---
if detectors.get("generic_repeat", True) and not _known_poll_tool(tool_name):
count = _count_generic_repeat(history, args_hash, tool_name)
if count >= critical:
msg = (
f"CRITICAL: '{tool_name}' called {count} times with identical args. "
f"Agent is stuck. Execution blocked."
)
logger.error(f"[loop_detection] {msg}")
return LoopDetectionResult(
stuck=True, level="critical",
detector="generic_repeat", message=msg, count=count,
)
if count >= warn:
msg = (
f"WARNING: '{tool_name}' called {count} times with identical args. "
f"If not making progress, stop and report failure."
)
logger.warning(f"[loop_detection] {msg}")
return LoopDetectionResult(
stuck=True, level="warning",
detector="generic_repeat", message=msg, count=count,
)
return _NOT_STUCK
@@ -0,0 +1,600 @@
"""
Memory, caching, and session persistence mixin for the Agent class.
Contains all methods for chat history management, caching, session
persistence, auto-memory, and auto-learning. Extracted from agent.py
for maintainability.
"""
import os
import logging
from praisonaiagents._logging import get_logger
# Fallback helpers to avoid circular imports
def _get_console():
from rich.console import Console
return Console
def _get_live():
from rich.live import Live
return Live
def _get_display_functions():
from ..main import (
display_error, display_instruction, display_interaction,
display_generating, display_self_reflection, ReflectionOutput,
adisplay_instruction, execute_sync_callback
)
return {
'display_error': display_error,
'display_instruction': display_instruction,
'display_interaction': display_interaction,
'display_generating': display_generating,
'display_self_reflection': display_self_reflection,
'ReflectionOutput': ReflectionOutput,
'adisplay_instruction': adisplay_instruction,
'execute_sync_callback': execute_sync_callback,
}
logger = logging.getLogger(__name__)
import contextlib
from typing import List, Optional, Any, Dict, Generator, TYPE_CHECKING
from collections import OrderedDict
if TYPE_CHECKING:
pass
class MemoryMixin:
"""Mixin providing memory methods for the Agent class."""
def _cache_put(self, cache_dict, key, value):
"""Thread-safe LRU cache put operation.
Args:
cache_dict: The cache dictionary (OrderedDict)
key: Cache key
value: Value to cache
"""
with self._cache_lock:
# Move to end if already exists (LRU update)
if key in cache_dict:
del cache_dict[key]
# Add new entry
cache_dict[key] = value
# Evict oldest if over limit
while len(cache_dict) > self._max_cache_size:
cache_dict.popitem(last=False) # Remove oldest (FIFO)
def _add_to_chat_history(self, role, content):
"""Thread-safe method to add messages to chat history.
Args:
role: Message role ("user", "assistant", "system")
content: Message content
"""
with self._history_lock:
self.chat_history.append({"role": role, "content": content})
def _add_to_chat_history_if_not_duplicate(self, role, content):
"""Thread-safe method to add messages to chat history only if not duplicate.
Atomically checks for duplicate and adds message under the same lock to prevent TOCTOU races.
Args:
role: Message role ("user", "assistant", "system")
content: Message content
Returns:
bool: True if message was added, False if duplicate was detected
"""
with self._history_lock:
# Check for duplicate within the same critical section
if (self.chat_history and
self.chat_history[-1].get("role") == role and
self.chat_history[-1].get("content") == content):
return False
# Not a duplicate, add the message
self.chat_history.append({"role": role, "content": content})
return True
def _get_chat_history_length(self):
"""Thread-safe method to get chat history length."""
with self._history_lock:
return len(self.chat_history)
def _truncate_chat_history(self, length):
"""Thread-safe method to truncate chat history to specified length.
Args:
length: Target length for chat history
"""
with self._history_lock:
self.chat_history = self.chat_history[:length]
def _cache_get(self, cache_dict, key):
"""Thread-safe LRU cache get operation.
Args:
cache_dict: The cache dictionary (OrderedDict)
key: Cache key
Returns:
Value if found, None otherwise
"""
with self._cache_lock:
if key not in cache_dict:
return None
# Move to end (mark as recently used)
value = cache_dict[key]
del cache_dict[key]
cache_dict[key] = value
return value
def clear_history(self):
"""Clear all chat history.
Also resets _auto_save_last_index to prevent silent message loss
when auto_save is enabled.
"""
self.chat_history = []
# Reset auto-save index to prevent stale index causing message loss
self._auto_save_last_index = 0
def prune_history(self, keep_last: int = 5) -> int:
"""
Prune chat history to keep only the last N messages.
Useful for cleaning up large history after image analysis sessions
to prevent context window saturation.
Args:
keep_last: Number of recent messages to keep
Returns:
Number of messages deleted
"""
with self._history_lock:
if len(self.chat_history) <= keep_last:
return 0
deleted_count = len(self.chat_history) - keep_last
self.chat_history = self.chat_history[-keep_last:]
# Reset auto-save index to match new history length
self._auto_save_last_index = len(self.chat_history)
return deleted_count
def delete_history(self, index: int) -> bool:
"""
Delete a specific message from chat history by index.
Supports negative indexing (-1 for last message, etc.).
Args:
index: Message index (0-based, supports negative indexing)
Returns:
True if deleted, False if index out of range
"""
with self._history_lock:
try:
del self.chat_history[index]
# Adjust auto-save index if deletion affects saved range
if hasattr(self, '_auto_save_last_index'):
self._auto_save_last_index = min(
self._auto_save_last_index,
len(self.chat_history)
)
return True
except IndexError:
return False
def delete_history_matching(self, pattern: str) -> int:
"""
Delete all messages matching a pattern.
Useful for removing all image-related messages after processing.
Args:
pattern: Substring to match in message content
Returns:
Number of messages deleted
"""
with self._history_lock:
original_len = len(self.chat_history)
self.chat_history = [
msg for msg in self.chat_history
if pattern.lower() not in msg.get("content", "").lower()
]
deleted_count = original_len - len(self.chat_history)
# Adjust auto-save index if deletion affects saved range
if deleted_count > 0 and hasattr(self, '_auto_save_last_index'):
self._auto_save_last_index = min(
self._auto_save_last_index,
len(self.chat_history)
)
return deleted_count
def get_history_size(self) -> int:
"""Get the current number of messages in chat history."""
return len(self.chat_history)
@contextlib.contextmanager
def ephemeral(self) -> Generator[None, None, None]:
"""
Context manager for ephemeral conversations.
Messages within this block are NOT permanently stored in chat_history.
History is restored to pre-block state after exiting.
Example:
with agent.ephemeral():
response = agent.chat("[IMAGE] Analyze this")
# After block, history is restored - image NOT persisted
"""
# Save current history state
with self._history_lock:
saved_history = self.chat_history.copy()
try:
yield
finally:
# Restore history to pre-block state
with self._history_lock:
self.chat_history = saved_history
def _init_db_session(self):
"""Initialize DB session if db adapter is provided (lazy, first chat only)."""
if self._db is None or self._db_initialized:
return
# Generate session_id if not provided: default to per-hour ID (YYYYMMDDHH-agentname)
if self._session_id is None:
import hashlib
from datetime import datetime, timezone
# Per-hour session ID: YYYYMMDDHH (UTC) + agent name hash for uniqueness
hour_str = datetime.now(timezone.utc).strftime("%Y%m%d%H")
agent_hash = hashlib.md5((self.name or "agent").encode()).hexdigest()[:6]
self._session_id = f"{hour_str}-{agent_hash}"
# Call db adapter's on_agent_start to get previous messages
try:
history = self._db.on_agent_start(
agent_name=self.name,
session_id=self._session_id,
user_id=self.user_id,
metadata={"role": self.role, "goal": self.goal}
)
# Restore chat history from previous session
if history:
for msg in history:
self.chat_history.append({
"role": msg.role,
"content": msg.content
})
logging.info(f"Resumed session {self._session_id} with {len(history)} messages")
except Exception as e:
logging.warning(f"Failed to initialize DB session: {e}")
self._db_initialized = True
self._current_run_id = None # Track current run
def _init_session_store(self):
"""
Initialize session store for JSON-based persistence (lazy, first chat only).
This is used when session_id is provided but no DB adapter.
Enables automatic session persistence with zero configuration.
"""
if self._session_store_initialized:
return
# Only initialize if session_id is provided and no DB adapter
if self._session_id is None or self._db is not None:
self._session_store_initialized = True
return
try:
from ..session import get_default_session_store
self._session_store = get_default_session_store()
# Restore chat history from previous session
history = self._session_store.get_chat_history(self._session_id)
if history:
# Only restore if chat_history is empty (avoid duplicates)
if not self.chat_history:
for msg in history:
self.chat_history.append({
"role": msg["role"],
"content": msg["content"]
})
logging.info(f"Restored session {self._session_id} with {len(history)} messages from JSON store")
# Set agent info
self._session_store.set_agent_info(
self._session_id,
agent_name=self.name,
user_id=self.user_id,
)
except Exception as e:
logging.warning(f"Failed to initialize session store: {e}")
self._session_store = None
self._session_store_initialized = True
def _start_run(self, input_content: str):
"""Start a new run (turn) for persistence tracking."""
if self._db is None:
return
import uuid
self._current_run_id = f"run-{uuid.uuid4().hex[:12]}"
try:
if hasattr(self._db, 'on_run_start'):
self._db.on_run_start(
session_id=self._session_id,
run_id=self._current_run_id,
input_content=input_content,
metadata={"agent_name": self.name}
)
except Exception as e:
logging.warning(f"Failed to start run: {e}")
def _end_run(self, output_content: str, status: str = "completed", metrics: dict = None):
"""End the current run (turn)."""
if self._db is None or self._current_run_id is None:
return
try:
if hasattr(self._db, 'on_run_end'):
self._db.on_run_end(
session_id=self._session_id,
run_id=self._current_run_id,
output_content=output_content,
status=status,
metrics=metrics or {},
metadata={"agent_name": self.name}
)
except Exception as e:
logging.warning(f"Failed to end run: {e}")
self._current_run_id = None
def _persist_message(self, role: str, content: str):
"""Persist a message to the DB or session store."""
# Try DB adapter first
if self._db is not None:
try:
if role == "user":
self._db.on_user_message(self._session_id, content)
elif role == "assistant":
self._db.on_agent_message(self._session_id, content)
except Exception as e:
logging.warning(f"Failed to persist message to DB: {e}")
return
# Fall back to session store (JSON-based)
if self._session_store is not None and self._session_id is not None:
try:
if role == "user":
self._session_store.add_user_message(self._session_id, content)
elif role == "assistant":
self._session_store.add_assistant_message(self._session_id, content)
except Exception as e:
logging.warning(f"Failed to persist message to session store: {e}")
def session_id(self) -> Optional[str]:
"""Get the current session ID."""
return self._session_id
def _load_history_context(self):
"""Load history from past sessions into context.
Note: This functionality is now handled via context= param with ManagerConfig.
This method is kept for backward compatibility but is a no-op.
Use context=ManagerConfig(history_sessions=N) to load past sessions.
"""
pass
def _process_auto_memory(self, user_message: str, assistant_response: str):
"""Process auto-memory extraction after agent response.
Called after each agent response when auto_memory=True in MemoryConfig.
Uses AutoMemory to extract and store memorable content (names, preferences,
facts) from the conversation. No-op when auto_memory is disabled.
Args:
user_message: The user's input message
assistant_response: The agent's response
"""
if not self._auto_memory or not self._memory_instance:
return
try:
from ..memory.auto_memory import AutoMemory
# Lazy-create AutoMemory wrapper on first use
if not hasattr(self, '_auto_memory_instance') or self._auto_memory_instance is None:
self._auto_memory_instance = AutoMemory(
self._memory_instance,
enabled=True,
verbose=1 if getattr(self, 'verbose', False) else 0
)
self._auto_memory_instance.process_interaction(
user_message=str(user_message),
assistant_response=str(assistant_response),
)
except Exception as e:
logging.debug(f"Auto-memory extraction failed: {e}")
def _process_auto_learning(self):
"""Process auto-learning extraction after agent response.
Called after each agent response when LearnMode.AGENTIC is set.
Uses LearnManager.process_conversation() to extract and store learnings
(persona, insights, patterns) from the conversation. No-op when learning
is disabled or mode is not AGENTIC.
"""
# Quick exit if no learn config or mode is not AGENTIC
if not self._learn_config:
return
# Check if mode is AGENTIC (auto-extract learnings)
from ..memory.learn.protocols import LearnMode
mode = getattr(self._learn_config, 'mode', None)
# Handle PROPOSE mode — extract but store as pending for user approval
if (isinstance(mode, LearnMode) and mode == LearnMode.PROPOSE) or \
(isinstance(mode, str) and mode == 'propose'):
learn_manager = None
if self._memory_instance and hasattr(self._memory_instance, 'learn'):
learn_manager = self._memory_instance.learn
if learn_manager:
try:
recent_messages = self.chat_history[-2:] if len(self.chat_history) >= 2 else self.chat_history
if recent_messages:
# Use same extraction as AGENTIC, but don't auto-store
result = learn_manager.process_conversation(
messages=recent_messages,
llm=getattr(self._learn_config, 'llm', None) or self.llm,
extract_only=True,
)
# Move extracted items to pending queue
if result:
for p in result.get("persona", []):
if p:
learn_manager.add_pending(p, category="persona")
for i in result.get("insights", []):
if i:
learn_manager.add_pending(i, category="insights")
for pt in result.get("patterns", []):
if pt:
learn_manager.add_pending(pt, category="patterns")
except Exception as e:
logging.debug(f"PROPOSE mode learning extraction failed: {e}")
return
if mode is None or (isinstance(mode, LearnMode) and mode != LearnMode.AGENTIC):
return
if isinstance(mode, str) and mode != 'agentic':
return
# Get LearnManager from memory instance
learn_manager = None
if self._memory_instance and hasattr(self._memory_instance, 'learn'):
learn_manager = self._memory_instance.learn
if not learn_manager:
return
try:
# Process recent conversation (last 2 messages: user + assistant)
recent_messages = self.chat_history[-2:] if len(self.chat_history) >= 2 else self.chat_history
if recent_messages:
learn_manager.process_conversation(
messages=recent_messages,
llm=getattr(self._learn_config, 'llm', None) or self.llm,
)
except Exception as e:
logging.debug(f"Auto-learning extraction failed: {e}")
def _auto_save_session(self):
"""Auto-save session if auto_save is enabled.
G-1 FIX: Routes to SessionStore instead of Memory.save_session() to
maintain clean separation between conversation history (SessionStore)
and semantic memory (Memory).
Issue 1 FIX: Track last saved index to avoid duplicate insertion when
called multiple times in the same session.
"""
if not self.auto_save:
return
try:
# Filter out history markers before saving
clean_history = [
{k: v for k, v in msg.items() if k != "_from_history"}
for msg in self.chat_history
]
# Issue 1 FIX: Only save NEW messages since last save
# Track last saved index to avoid duplicates
last_saved = getattr(self, '_auto_save_last_index', 0)
new_messages = clean_history[last_saved:]
if not new_messages:
return # Nothing new to save
# G-1 FIX: Use SessionStore for conversation history persistence
# This maintains clean separation: Memory = facts, SessionStore = turns
if self._session_store is not None:
# Persist only NEW messages to SessionStore
for msg in new_messages:
self._session_store.add_message(
self.auto_save, # Use auto_save name as session_id
role=msg.get("role", "user"),
content=msg.get("content", ""),
)
# Update last saved index
self._auto_save_last_index = len(clean_history)
logging.debug(f"Auto-saved {len(new_messages)} new messages to SessionStore: {self.auto_save}")
elif self._memory_instance and hasattr(self._memory_instance, 'save_session'):
# Fallback to Memory.save_session() for backward compatibility
# Memory.save_session() replaces entire history, so no duplicate issue
self._memory_instance.save_session(
name=self.auto_save,
conversation_history=clean_history,
metadata={"agent_name": self.name, "user_id": self.user_id}
)
logging.debug(f"Auto-saved session to Memory: {self.auto_save}")
except Exception as e:
logging.debug(f"Error auto-saving session: {e}")
def _save_output_to_file(self, content: str) -> bool:
"""Save agent output to file if output_file is configured.
Args:
content: The response content to save
Returns:
True if file was saved, False otherwise
"""
if not self._output_file:
return False
try:
import os
# Expand user home directory and resolve path
file_path = os.path.expanduser(self._output_file)
file_path = os.path.abspath(file_path)
# Create parent directories if they don't exist
parent_dir = os.path.dirname(file_path)
if parent_dir and not os.path.exists(parent_dir):
os.makedirs(parent_dir, exist_ok=True)
# Write content to file
with open(file_path, 'w', encoding='utf-8') as f:
f.write(str(content))
# Print success message to terminal
print(f"✅ Output saved to {file_path}")
logging.debug(f"Output saved to file: {file_path}")
return True
except Exception as e:
logging.warning(f"Failed to save output to file '{self._output_file}': {e}")
print(f"⚠️ Failed to save output to {self._output_file}: {e}")
return False
@@ -9,12 +9,12 @@ import threading
import heapq
import time
import logging
from praisonaiagents._logging import get_logger
from typing import Optional, Any, List, Tuple
from dataclasses import dataclass, field
from enum import Enum
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
class MessagePriority(Enum):
"""Message priority levels."""
@@ -23,7 +23,6 @@ class MessagePriority(Enum):
HIGH = 10
URGENT = 20
@dataclass(order=True)
class QueuedMessage:
"""A message in the queue with priority and timestamp."""
@@ -36,7 +35,6 @@ class QueuedMessage:
# Negate priority for max-heap behavior (higher priority = lower number in heap)
self.priority = -self.priority
class AgentMessageQueue:
"""
Thread-safe message queue for agent prompts.
@@ -158,7 +156,6 @@ class AgentMessageQueue:
"""Total number of messages ever enqueued."""
return self._message_count
class AsyncAgentMessageQueue:
"""
Async-compatible message queue for agent prompts.
@@ -9,6 +9,7 @@ Follows the Agent() class patterns:
"""
import os
import logging
from praisonaiagents._logging import get_logger
import warnings
from dataclasses import dataclass
from typing import Optional, Any, Dict, Union, List
@@ -16,7 +17,6 @@ from pathlib import Path
warnings.filterwarnings("ignore", "Valid config keys have changed in V2", UserWarning)
@dataclass
class OCRConfig:
"""Configuration for OCR settings."""
@@ -38,7 +38,6 @@ class OCRConfig:
"api_key": self.api_key,
}
class OCRAgent:
"""
A specialized agent for OCR (Optical Character Recognition).
@@ -143,10 +142,10 @@ class OCRAgent:
def _configure_logging(self, verbose: Union[bool, int]) -> None:
"""Configure logging levels."""
if isinstance(verbose, int) and verbose >= 10:
logging.getLogger("litellm").setLevel(logging.DEBUG)
get_logger("litellm").setLevel(logging.DEBUG)
else:
logging.getLogger("litellm").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
get_logger("litellm").setLevel(logging.WARNING)
get_logger("httpx").setLevel(logging.WARNING)
def _get_model_params(self, model: Optional[str] = None) -> Dict[str, Any]:
"""Build parameters for LiteLLM calls."""
@@ -34,6 +34,7 @@ Example:
"""
import logging
from praisonaiagents._logging import get_logger
from typing import List, Optional, Any, Dict
from dataclasses import dataclass, field
from enum import Enum
@@ -47,7 +48,6 @@ class ExpandStrategy(Enum):
CREATIVE = "creative"
AUTO = "auto"
@dataclass
class ExpandResult:
"""Result of a prompt expansion operation."""
@@ -59,7 +59,6 @@ class ExpandResult:
def __repr__(self):
return f"ExpandResult(strategy={self.strategy_used.value}, original_len={len(self.original_prompt)}, expanded_len={len(self.expanded_prompt)})"
class PromptExpanderAgent:
"""
Agent for expanding short prompts into detailed, comprehensive prompts.
@@ -213,7 +212,7 @@ Expanded prompt:"""
self._agent = None # Lazy initialized
# Setup logging
self.logger = logging.getLogger(__name__)
self.logger = get_logger(__name__)
if self.verbose:
self.logger.setLevel(logging.DEBUG)
Loaded 100 of 549 files, more files were not shown because too many files have changed in this diff. Show more