Compare commits

..
Author SHA1 Message Date
DhivyaandDhivya-Bharathy 8536325cc1 fix(examples): phase-3 API alignment for consolidated params on v4.6.52 (#1864)
Co-authored-by: Dhivya-Bharathy <[email protected]>
2026-06-08 07:47:16 +01:00
praisonai-triage-agent[bot]andMervin Praison 8b4db44159 fix: resolve Gemini reviewer issues - remove redundant arguments and fix temp file pollution
- Remove redundant argument from workflow.run() in basic_workflow.py since variables are already set in constructor
- Fix temp file pollution by using system temp directory instead of repository directory in auto_rag_agent_basic.py and knowledge-agents-task.py

Co-authored-by: Mervin Praison <[email protected]>
2026-06-05 10:18:45 +00:00
Dhivya-Bharathy 3d3fdd045e fix(examples): align AgentTeam memory and knowledge configs with v4.6.52 API 2026-06-05 15:37:37 +05:30
praisonai-triage-agent[bot]andMervin Praison bfa9d0ca48 fix: correct TypeError in 3 example files by using proper parameter names
- Fix MemoryConfig(memory=...) → user_id=... in agent_centric_api.py
- Fix Session(memory=...) → user_id=... in session-example.py
- Fix build_context_for_task(memory=...) → user_id=... in memory-quality-example.py

Co-authored-by: Mervin Praison <[email protected]>
2026-06-05 11:26:13 +05:30
Dhivya-Bharathy 3f14324036 fix(examples): resolve syntax errors and align with consolidated-params API 2026-06-04 16:30:02 +05:30
a9f4bd593e fix: wrapper layer gaps - InteractiveRuntime lifecycle, tool resolution, cli_backend support (#1797)
* fix: wrapper layer gaps - InteractiveRuntime lifecycle, tool resolution, cli_backend support

Fixes #1646

- Issue #1: Fixed InteractiveRuntime lifecycle bug where runtime was stopped before agents.start()
  * Removed premature stop() in inner finally block at lines 1234-1240
  * Added proper loop closure check in cleanup block
  * Runtime now stays alive until after agent execution completes

- Issue #2: Consolidated tool resolution implementations
  * Added instantiate=True parameter to ToolResolver.resolve() for automatic class instantiation
  * Removed hardcoded standard_tools whitelist in agents_generator.py
  * Replaced custom tool resolution in bots_cli.py and job_workflow.py with unified ToolResolver
  * All entry points now use the same 4-step fallback chain consistently

- Issue #3: Fixed cli_backend support across frameworks
  * Added resolve_cli_backend_config() in cli_backends module for unified YAML/Python handling
  * Updated Agent wrapper class to accept both string and dict cli_backend configurations
  * Added framework compatibility validation with clear error messages
  * YAML and Python entry points now handle cli_backend identically

- Added comprehensive regression tests to prevent future regressions

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

* fix: address reviewer feedback - test fixes, validation improvements, error handling

- Fix broken test imports and patch paths in test_wrapper_layer_regression.py
- Replace hasattr(__call__) with callable() per Ruff B004
- Fix CLI backend validation to handle both 'roles' and 'agents' keys
- Add input validation for empty strings and malformed dicts in resolve_cli_backend_config
- Include class tools in local tools.py resolution
- Add per-tool error handling to prevent single tool failures from breaking all tools
- Remove unused logger parameter from resolve_cli_backend_config
- Fix exception handling in _resolve_yaml_cli_backend to not mask errors

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-06-04 09:44:31 +01:00
998dd9477f fix: wrapper layer key gaps - native async, template substitution, per-agent LLM (fixes #1758) (#1794)
* fix: wrapper layer key gaps - native async, template substitution, per-agent LLM (fixes #1758)

- Add native async execution path with arun() protocol and implementations
- Fix silent template substitution failure using brace-safe regex
- Add per-agent LLM support in praisonai adapter
- Remove orphaned safe_format() function to eliminate DRY violation

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

* fix: critical async path bugs and feature parity gaps

- Fixed missing _extract_topic causing AttributeError in all arun() calls
- Restored complete feature parity between sync/async execution paths:
  - Canonical agents→roles format conversion
  - Full tool loading (YAML tools, tools.py, tools/ directory)
  - AutoGen version detection and adapter selection
  - AgentOps initialization
  - Proper CLI config merging via _merge_cli_config()
- Fixed broken unit tests by restoring safe_format function
- Improved _arun_yaml_workflow with proper YAMLWorkflowParser integration
- Ensured async path mirrors sync path logic exactly

Resolves all critical issues identified by automated reviewers:
Greptile, CodeRabbit, Gemini, and Copilot

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
2026-06-04 09:42:24 +01:00
0aa0d5b598 fix: make CLI session store cross-platform compatible (fixes #1836) (#1837)
* fix: make CLI session store cross-platform compatible (fixes #1836)

- Replace hard fcntl import with optional import pattern
- Add Windows msvcrt locking support in UnifiedSessionStore
- Implement graceful degradation when no locking available
- Fix Path handling in session store constructor
- Follows same pattern as praisonaiagents/session/store.py fix from #1792

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

* fix: resolve critical Windows locking bugs in CLI session store

- Fix msvcrt.locking position consistency by seeking to 0 before lock/unlock
- Use blocking locks (LK_LOCK/LK_RLCK) instead of non-blocking (LK_NBLCK)
- Prevent truncation race by using r+b mode and locking before truncate
- Add flush()+fsync() before unlock to ensure data persistence
- Maintain backward compatibility on Unix systems

Fixes critical OSError on every Windows save/load operation.
Addresses issues identified by Greptile and CodeRabbit reviewers.

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-03 11:29:59 +01:00
MervinPraison 676f2d6588 docs: auto-update feature parity trackers [skip ci] 2026-06-03 09:49:42 +00:00
cursor[bot] 90aacf064b fix: Telegram command security bypass and MCP result queue races (#1835)
Fixes security follow-up from #1791. Includes regression tests for Telegram allowlist and MCP queue concurrency.
2026-06-03 10:49:06 +01:00
MervinPraison 0a8ce9eb7d Release v4.6.52 2026-06-03 10:07:09 +01:00
MervinPraison 9fcac3a951 Remove deprecated test script for tool search implementation, consolidating testing functionality into existing modules. 2026-06-02 20:17:33 +01:00
MervinPraison 6878780000 Release v4.6.51 2026-06-02 20:07:49 +01:00
e55789e8cf fix: HierarchicalSessionStore stale extended cache after cross-instance writes (#1785)
* fix: reload HierarchicalSessionStore extended cache on reads

PR #1759 fixed stale reads for DefaultSessionStore but left
_extended_cache out of sync. get_extended_session could return
truncated message lists after cross-process or cross-instance writes.

Align extended cache with _read_session_fresh and clear it on
invalidate_cache. Regression test added.

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

* fix: atomic cache invalidation and stale cache writes in HierarchicalSessionStore

- Fix race condition in invalidate_cache() by clearing both caches under single lock
- Fix stale cache overwrites in write methods (set_title, share_session, unshare_session, revert_to_*, create_session parent update, auto_title)
- Replace load-modify-save pattern with _modify_session_locked for atomic updates
- Add regression test for concurrent write preservation

Addresses critical issues identified by code reviewers (Qodo/CodeRabbit)

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

---------

Co-authored-by: Cursor Agent <[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-06-02 20:06:34 +01:00
Maulanaandulsreall e0fb8e7dd1 fix: harden JWT secret handling and replace eval() in examples (#1793)
Addresses remaining attack surface from security advisories:

GHSA-3qg8-5g3r-79v5 (JWT signing key - CRITICAL):
  auth_service.py: PLATFORM_ENV no longer defaults to 'dev' when unset.
  When PLATFORM_JWT_SECRET is not set and PLATFORM_ENV is not explicitly
  'dev', an ephemeral random secret is auto-generated with a warning.
  This prevents production deployments from silently using the well-known
  'dev-secret-change-me' key.

GHSA-vg22-4gmj-prxw (unauthenticated eval - CRITICAL):
  Replaced all eval() calls in official examples and benchmarks with an
  AST-based safe math evaluator that only allows numeric literals and
  arithmetic operators. Files fixed:
  - examples/python/a2a/a2a-server.py (the primary advisory target)
  - examples/mcp/multi_server.py
  - examples/python/mcp/mcp-tools-server.py
  - examples/python/mcp/mcp-tools-server-sse.py
  - examples/mcp_server/custom_tools_server.py
  - examples/python/mcp_server_example.py
  - examples/python/custom_tools/tools_with_latency.py
  - examples/eval/reliability_example.py
  - src/praisonai-agents/benchmarks/real_benchmark.py

The character-filter approach (allowlist '0123456789+-*/.()') was
insufficient because () enables attribute access chains. AST-based
evaluation only permits numeric literals and binary/unary arithmetic.

GHSA-hvhp-v2gc-268q and GHSA-9cr9-25q5-8prj (file read/write):
  Already fixed in prior commits via _resolve_rule_path() and
  _resolve_cwd_yaml_path() containment helpers.

Co-authored-by: ulsreall <[email protected]>
2026-06-02 20:01:09 +01:00
bc73dd1db9 Issue #1540: Changes from Claude (#1792)
* fix: make session store fcntl import cross-platform compatible (fixes #1540)

- Add conditional fcntl import like file_memory.py already has
- Use _HAS_FCNTL flag to check fcntl availability before usage
- Windows uses msvcrt for locking, Unix uses fcntl when available
- Fixes Windows test collection failure due to missing fcntl module

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

* fix: add one-time warning for degraded file locking

Addresses CodeRabbit feedback about silent no-op locking when fcntl
is unavailable on non-Windows platforms. Adds module-level sentinel
to emit warning only once, following AGENTS.md error handling principles.

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

* fix: improve fcntl import robustness and fix unreachable code

- Replace platform-based fcntl import with try/except for better cross-platform compatibility
- Fix unreachable warning code branch that could never execute
- Remove misleading msvcrt reference in Unix-only warning message
- Now gracefully handles environments where fcntl is unavailable (GAE, Pyodide, etc.)
- All reviewer feedback from Qodo, CodeRabbit, Gemini, Copilot, and Greptile addressed

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

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

* fix: harden optional fcntl import and add regression test

* test: cover missing fcntl import path for session store

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-06-02 19:59:48 +01:00
5216274924 fix: Gateway Telegram polling path bypasses security controls (fixes #1747) (#1791)
* fix: Gateway Telegram polling path bypasses security controls (fixes #1747)

- Extract shared process_inbound_telegram_message() security pipeline
- Enforce channel allowlist, user allowlist, pairing, and group policies
- Used by both standalone bot and gateway polling paths
- Add comprehensive tests for security enforcement
- Prevents unauthorized access in gateway mode

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

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

* fix: resolve security pipeline bugs from code review

- Fix case sensitivity bug in bot mention check (lowercase bot_username)
- Fix user allowlist logic to match BotConfig.is_user_allowed semantics
- Fix test failures by using private chat type for allowlist tests
- Clean up unused imports and trailing whitespace

Addresses critical bugs identified by CodeRabbit, Greptile, and Copilot reviewers.

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

* fix: align Telegram security pipeline with BotConfig semantics

* fix: tighten Telegram security checks and test coverage

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-06-02 19:58:07 +01:00
2e5eca1db6 fix: disable streaming by default in sync chat methods (fixes #1733) (#1734)
* fix: disable streaming by default in sync chat methods (fixes #1733)

Fixes the 'Streaming is not supported in sync OpenAIAdapter' error when using
multi-agent functionality with Deepseek provider.

Root cause:
- _chat_completion() and _execute_unified_chat_completion() methods in
  chat_mixin.py defaulted to stream=True
- This caused sync OpenAIAdapter to reject requests with streaming enabled
- Multi-agent workflows use sync execution path, triggering the error

Changes:
- Changed stream=True to stream=False as default in both sync methods
- Sync adapters now receive non-streaming requests by default
- Async methods can still use streaming when explicitly requested
- Maintains backward compatibility as streaming was already broken in sync context

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

* test: replace ad-hoc script with unit test for sync stream defaults

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/c4a541cc-a3dc-4637-85d9-53f7090dcb35

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

* test: clarify helper naming in sync stream default tests

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/c4a541cc-a3dc-4637-85d9-53f7090dcb35

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

* test: harden sync stream default AST checks

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/c4a541cc-a3dc-4637-85d9-53f7090dcb35

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

* test: align stream default test names with target methods

Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/c4a541cc-a3dc-4637-85d9-53f7090dcb35

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

* docs: clarify async/sync stream default asymmetry

Addresses reviewer feedback about intentional difference between:
- sync methods: stream=False (sync adapters don't support streaming)
- async methods: stream=True (async adapters do support streaming)

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

* feat: implement smart streaming fallback for better UX

- Change sync method defaults: stream=False → stream=None
- Add intelligent fallback: try streaming first, gracefully fall back to non-streaming if unsupported
- Maintains UX benefits of streaming while ensuring compatibility with all adapters
- Benefits: users see real-time progress when possible, silent fallback when needed
- Updated tests to validate new None defaults
- Async methods unchanged (still default stream=True)

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

* cleanup: remove test file

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-02 19:56:27 +01:00
f51fe294e7 fix: HierarchicalSessionStore stale cache could wipe session messages (#1781)
* fix: HierarchicalSessionStore stale extended cache could wipe messages

After DefaultSessionStore began reloading on reads (#1759),
HierarchicalSessionStore still served writes from a separate
_extended_cache that could lag disk. set_title, share_session,
and revert paths then saved truncated message lists.

Reload via _read_session_fresh for all extended reads and sync
both caches. Add regression test for set_title after cross-store writes.

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

* perf: optimize HierarchicalSessionStore with smart cache invalidation

- Add file modification time-based cache validation
- Restore performance while maintaining data integrity
- Preserve force_reload API contract
- Add comprehensive regression tests

Fixes performance regression from stale cache fix while
preventing message loss from concurrent writes.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Mervin Praison <[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-06-02 19:55:02 +01:00
a1b7ed5c91 fix: wrapper layer architectural gaps (#1763)
* fix: wrapper layer architectural gaps - async-sync bridge, unified registry, framework logic separation (fixes #1762)

- Gap 1: Create separate sync/async SQLite stores, remove problematic sync wrappers
  - Add SyncSQLiteConversationStore with thread-safe operations
  - Remove sync wrappers from AsyncSQLiteConversationStore
  - Update factory to support mode parameter (sync/async/auto)

- Gap 2: Replace manual __getattr__ ladder with unified registry
  - Extend existing PluginRegistry with lazy __getattr__ support
  - Create IntegrationRegistry replacing 70-line manual dispatch
  - Consolidate four parallel registries into data-driven approach

- Gap 3: Move framework logic from orchestrator to adapters
  - Add resolve() and setup() methods to FrameworkAdapter protocol
  - Extract AutoGen version logic to adapter.resolve()
  - Centralize AgentOps init in observability/hooks module
  - Remove framework-specific code from agents_generator.py

Restores protocol-driven core principle and eliminates event loop issues.

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

* fix: restore lazy integration exports and parameterize sync sqlite paging

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-06-02 19:48:58 +01:00
aaed51eee8 fix: replace undefined AgentFlow with Workflow in workflow_conditional.py (closes #1750) (#1765)
* fix: replace undefined AgentFlow with Workflow in workflow_conditional.py (closes #1750)

* Update workflow_conditional.py

---------

Co-authored-by: botbikamordehai2-sketch <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-02 14:46:31 +01:00
48407be580 fix: architectural gaps in wrapper package - thread safety, async safety, feature parity (#1771)
* fix: architectural gaps in wrapper package - thread safety, async safety, feature parity

- Fix TOCTOU race in jobs server singleton initialization with thread-safe locking
- Replace deprecated asyncio.get_event_loop() calls with asyncio.run() and asyncio.to_thread()
- Restore AsyncAgentScheduler feature parity: add missing timeout and max_cost parameters
- Ensure async/sync scheduler classes have identical safety features

Fixes #1770

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

* fix: address critical architectural issues in PR #1771

- Fix event loop splitting in CLI standardise.py by using single asyncio.run() lifecycle
- Fix budget scheduler stopping issue by setting stop event when budget exceeded
- Complete asyncio migration in e2b.py by replacing remaining get_event_loop() calls
- Update factory function to expose timeout and max_cost parameters

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-02 14:45:31 +01:00
0ad42ff2c5 fix: MCP client lifecycle - thin core wiring, OAuth honesty, remote support (#1777)
* fix: MCP client lifecycle - thin core wiring, OAuth honesty, remote support

Implements comprehensive fixes for MCP client integration addressing
broken imports, misleading OAuth documentation, and missing remote
server support. Adds thin wiring between config and agents following
protocol-driven architecture.

Tier A - Critical Fixes:
- Fix MCPConfig.to_mcp_instance() import from praisonaiagents.mcp
- Mark OAuth CLI commands as experimental until real implementation
- Update doctor checks to use canonical ~/.praisonai/mcp/ paths

Tier B - Core Agent Value:
- Add MCPClientProtocol interface for extensibility
- Create load_mcp_tools() helper for agent-centric tool loading
- Wire tool filtering with allowed_tools/disabled_tools parameters
- Add safe environment policy (baseline + explicit config only)
- Prepare tool name collision handling framework

Tier C - Wrapper Improvements:
- Add remote server support to mcp test/sync CLI commands
- Create YAML MCP configuration example

Tests: All smoke tests pass, no performance regressions (13ms import)

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

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

* fix: Critical MCP client lifecycle issues - syntax, env, filters, async

Fixes identified by code reviewers (CodeRabbit, Greptile, Copilot):

1. Fixed syntax error in CLI - orphaned except blocks (CRITICAL)
2. Fixed environment inheritance regression in mcp test command
3. Fixed filter precedence - allowed_tools now properly wins over disabled_tools
4. Added missing SYSTEMROOT/COMSPEC for Windows stdio MCP servers
5. Added async protocol methods for non-blocking MCP operations
6. Fixed typer.Exit propagation in remote server tests
7. Updated YAML example to remove unsupported description fields
8. Fixed canonical path discovery in doctor checks fallback
9. Updated loader docs to clarify prefix_tools not yet implemented

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-02 14:45:09 +01:00
2943c3b77e fix: HierarchicalSessionStore stale writes lose concurrent chat messages (#1790)
* fix: prevent HierarchicalSessionStore stale writes on fork and metadata updates

PR #1745 fixed add_message races but fork_session, create_session parent
linking, snapshots, and revert still saved stale in-memory sessions and could
overwrite concurrent add_message writes.

Route parent/field updates through _modify_session_locked and add a regression
test for fork_session concurrent with add_message.

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

* fix: address Qodo reviewer bugs in HierarchicalSessionStore

1. Fix revert_to_message() - validate index before writing to prevent
   unnecessary session writes on invalid indices

2. Fix revert_to_snapshot() - use read-only lookup to avoid double writes
   and unnecessary writes when snapshot not found

3. Fix test_fork_session_preserves_concurrent_messages() - use proper
   concurrent testing with two store instances and threading to simulate
   real race conditions

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

---------

Co-authored-by: Cursor Agent <[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-06-02 14:43:32 +01:00
84627f593c fix: reload session file on read to prevent stale chat history (#1764)
_load_session returned an in-memory cache without checking disk when a
session JSON file already existed. A second DefaultSessionStore instance
(or direct file edits) could add messages that get_chat_history never saw
until invalidate_cache was called manually.

Always reload from disk under FileLock when the session file exists.
Adds regression test for cross-instance reads.

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-06-02 14:01:40 +01:00
927020cea8 fix: session message loss in managed stream and bot history saves (#1768)
* fix: session message loss in managed stream and bot history saves

- Persist user/assistant messages and state after LocalManagedAgent.stream()
- Add DefaultSessionStore.set_chat_history for atomic locked replace
- BotSessionManager uses set_chat_history instead of clear + re-add loop
- DbSessionAdapter.update_session_metadata accepts **fields like file store
- Add regression tests for stream persistence and metadata API

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

* fix: preserve timestamps and metadata in set_chat_history

- Fix SessionMessage construction in set_chat_history to preserve
  original timestamp when provided in input messages
- Also preserve metadata field for completeness
- Prevents loss of temporal ordering information during history replacement

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

---------

Co-authored-by: Cursor Agent <[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>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-06-02 14:00:16 +01:00
MervinPraison 23ca2a80d6 docs: auto-update feature parity trackers [skip ci] 2026-06-02 12:56:41 +00:00
f931173e60 feat: implement Tool Search progressive MCP/plugin tool disclosure (#1778)
* feat: implement Tool Search progressive MCP/plugin tool disclosure

Implements Hermes-parity progressive disclosure layer for MCP and non-core plugin tools:

Phase P0 Complete:
- Added ToolSearchConfig with auto/on/off modes and threshold controls
- Implemented BM25 search with substring fallback (no external dependencies)
- Created bridge tools (tool_search, tool_describe, tool_call) with lazy loading
- Integrated into Agent class with tool_search parameter
- Updated chat_mixin for tool assembly and bridge mode detection
- Added tool_execution unwrapping with security validation
- Marked all MCP tools as deferrable (__praisonai_deferrable__ = True)
- Comprehensive unit tests covering all core functionality

Design Invariants (from Hermes):
1. Core tools never defer (PRAISONAI_CORE_TOOLS)
2. Unknown tools stay visible (never silently dropped)
3. Stateless catalog (rebuilt every assembly)
4. Unwrap before trace/stream (hooks see real tool names)
5. Session scoping (agent's own tool list only)
6. Auto threshold (10% context window default)
7. Zero overhead when disabled (opt-in, lazy imports)

Default: tool_search=False (opt-in). Zero overhead when disabled.
Auto mode activates based on token threshold vs context window.
Core SDK tools (file/shell/web/schedule/memory/clarify) never defer.

Fixes #1775

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

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

* fix: address critical Tool Search issues

- Fix unconditional bridge tool hijacking that broke user tools when disabled
- Resolve duplicate ToolSearchConfig classes by consolidating to tools module
- Add missing resolve_tool_search resolver function
- Fix hot path performance issues in BM25 scoring
- Add input validation for tool_call bridge args
- Fix cache metadata mismatches in chat_mixin
- Remove unnecessary f-string prefixes
- Strip __praisonai_deferrable__ from provider payloads
- Preserve tool_search config in clone_for_channel
- Improve type validation in agent constructor
- Fix overly broad MCP name heuristic

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-02 13:56:03 +01:00
ba12269d5d fix: wrapper layer gaps - async support, lazy imports, event loop fixes (#1789)
* fix: wrapper layer gaps - add async support, fix heavy imports, stream relay event loop (fixes #1788)

- Add async arun() methods to FrameworkAdapter protocol and BaseFrameworkAdapter
- Override arun() in AutoGenV4Adapter for async-native execution without run_sync wrapper
- Fix AutoGenAdapter.is_available() to use centralized lazy probe instead of full import autogen
- Fix Gateway._make_stream_relay to capture event loop in async context, not threaded callback
- Add agenerate_crew_and_kickoff() async method for user-facing async API
- Promote stream relay errors from debug to warning for better visibility

All fixes address the wrapper's stated principles: async-safe by default, lazy imports, no hot-path performance impact.

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

* fix: align async framework path with sync behavior and add regression tests

* fix: align async framework path with sync behavior and add regression tests

- Fix async _arun_framework() to honor config.get('autogen_version') like sync path
- Stop silently swallowing RuntimeError from run_sync() - re-raise event loop errors
- Remove duplicate import os in autogen_adapter.py
- Enhance stream relay error logging with full traceback info

All critical reviewer bugs addressed. Architecture compliant with AGENTS.md.

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-06-02 13:52:34 +01:00
MervinPraison 7839196391 Release v4.6.50 2026-06-02 07:19:27 +01:00
praisonai-triage-agent[bot]andMervin Praison e958968307 fix: wrapper layer gaps - case normalization, lazy loading, async persistence (#1767)
* fix: wrapper layer gaps - case normalization, lazy loading, async persistence (fixes #1766)

- Gap 1: Fix PluginRegistry case normalization inconsistency by normalizing names during storage and using lazy loaders
- Gap 2: Implement lazy loading in PluginRegistry by storing loaders instead of immediately executing them
- Gap 3: Add async persistence support with aon_* methods and achat wrapping in agent hooks

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

* fix: critical bugs in wrapper layer - registry consistency and async event loop blocking

- Fix registry inconsistency: register() now writes to both _loaders and _items
- Fix event loop blocking: PersistentAgent.achat uses asyncio.to_thread for sync fallback
- Fix threading deadlock: use RLock and release lock before calling loader()
- Add async session end methods (aend_session) for proper async lifecycle

Addresses critical P1/P2 issues identified by code reviewers.

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

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
2026-06-02 07:16:38 +01:00
MervinPraison 1c26a5bf14 docs: auto-update api.md [skip ci] 2026-06-02 06:15:46 +00:00
ba8cd6b3c1 feat: implement Hermes-style persistent kanban system (#1769)
* feat: implement Hermes-style persistent kanban system

Implements comprehensive kanban task management system for PraisonAI wrapper:

Phase 1 - Persistent Store & Host Integration:
- SQLite kanban store with WAL, CAS, and dependency DAG support
- Task models with status enum and dataclasses
- Multi-board support via environment variables
- Host bridge injection for PraisonAIUI integration
- Updated claw navigation with kanban & jobs-board pages

Phase 2 - Agent Tools & Dispatcher:
- Complete agent tool suite (create, list, show, complete, block, etc.)
- Gateway dispatcher for task claiming and worker spawning
- Hook event integration (graceful degradation if unavailable)
- Lazy tool loading in tools module

Phase 3 - CLI & Human Operations:
- Full CLI command suite (praisonai kanban list, create, show, etc.)
- Multi-board management and task lifecycle operations
- Manual dispatcher triggering and task recovery commands
- Integrated with main CLI app registration

Features:
- Compatible with PraisonAIUI kanban API endpoints
- Parent/child task dependencies with automatic promotion
- Atomic operations with optimistic concurrency control
- Comprehensive logging and event trails
- Environment variable configuration support

Testing:
- Unit tests for SQLite store CRUD operations
- Integration tests for host injection and bridge setup
- Smoke tests for basic functionality validation

Fixes #1720

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

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

* fix: address 6 critical issues in kanban system implementation

- Fix subprocess deadlock by redirecting output to temp files instead of PIPE
- Enable foreign key constraints on all SQLite connections for proper cascades
- Replace immediate dispatcher startup with proper event handler registration
- Add persistent CLI board switching via ~/.praisonai/kanban_config.json
- Prevent database locking with connection-reusing internal methods
- Replace deprecated datetime.utcnow() with datetime.now(timezone.utc)
- Fix optimistic locking by incrementing version in claim/release operations

Fixes all issues identified in code review by Gemini.

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-02 07:15:18 +01:00
8327f8e8b8 fix: accept multi-channel bot.yaml keys with platform field (#1772)
* fix: accept multi-channel bot.yaml keys with platform field (#1757 regression)

Onboard generates channels like telegram_cfo with platform: telegram.
BotYamlSchema rejected any channel key not exactly matching a platform name,
so praisonai doctor reported invalid bot.yaml after multi-bot onboard.

Validate using channel.platform when present; extend ChannelConfigSchema for
gateway-style fields used in generated configs.

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

* fix: use monkeypatch for env isolation in multi-channel test

- Replace direct os.environ manipulation with pytest monkeypatch
- Ensures test environment isolation and automatic cleanup
- Addresses CodeRabbit review feedback

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

---------

Co-authored-by: Cursor Agent <[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-06-02 07:13:57 +01:00
praisonai-triage-agent[bot]andMervin Praison f78ba6348c fix: eliminate wrapper layer architecture issues (#1780)
* fix: eliminate wrapper layer architecture issues (fixes #1779)

- Remove redundant framework availability wrappers in auto.py that only delegated to is_available()
- Make agents_generator module-level constants lazy via __getattr__() to avoid import-time probes
- Fix async bridge singleton creation (now lazy) and encapsulation breaks in run_sync()
- Deprecate AutoGen-specific methods in ToolRegistry to guide toward proper separation

Addresses three key architectural violations:
1. DRY violation: 8 wrapper functions that added no value over is_available()
2. Import-time work: 6 package probes ran on every agents_generator import
3. Encapsulation break: run_sync() accessed private _BG._lock directly

Performance impact: Eliminates import-time package probing and eager singleton creation.

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

* fix: address reviewer feedback for architecture issues

1. Fix shutdown() lazy-init defeat - read _BG directly instead of calling _get_bg()
2. Fix tool registry race condition - move _autogen_adapters init inside lock
3. Add __all__/__dir__ for legacy flag discoverability in agents_generator
4. Suppress internal deprecation warnings for backward compatibility paths

Addresses feedback from Greptile and CodeRabbit reviewers.

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

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
2026-06-02 07:11:40 +01:00
Putra Jadi Muktianddymux 4f3daa8628 docs: fix broken Full Flow docs link in README (#1782)
The link pointed to /docs/concepts/flow which returns 404. The actual
page is published at /docs/concepts/agentflow.

Co-authored-by: dymux <[email protected]>
2026-06-02 07:10:32 +01:00
d7f68a0870 fix: complete AsyncAgentScheduler parity, fail-loud pip installs, remove broken resolver (fixes #1783) (#1784)
* fix: complete AsyncAgentScheduler parity, fail-loud pip installs, remove broken resolver

Fixes #1783

Three critical gap fixes in the PraisonAI wrapper layer:

1. **AsyncAgentScheduler Feature Parity**
   - Add missing timeout and max_cost parameters to match sync version
   - Port budget tracking logic with cost estimation
   - Port timeout support using asyncio.wait_for()
   - Add from_yaml() and from_recipe() class methods for YAML/recipe support
   - Update stats methods to include cost tracking
   - Remove 16 TODO markers now that features are ported

2. **Fail-Loud Pip Install Failures**
   - Convert silent pip install failures to raised RuntimeError exceptions
   - Fix managed_local.py _install_packages() to fail early on pip errors
   - Fix _install_packages_in_compute() to raise on compute pip failures
   - Eliminates confusing ImportError deep in tool execution

3. **Remove Broken reset_default_resolver Definition**
   - Remove second definition that shadows working first definition
   - Eliminates NameError on function call due to undefined globals
   - Preserves working ContextVar-based implementation at line 545

All changes maintain backward compatibility while fixing production-blocking issues
in async scheduling, managed agent bootstrap, and tool resolution.

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

* fix: restore async stats API and safe budget-stop behavior

* fix: tighten async budget checks and stats parity

* fix: remove redundant asyncio import in from_recipe method

Removes the unnecessary nested import asyncio statement inside the
astart method, since asyncio is already imported at module level.

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

---------

Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-02 07:09:42 +01:00
Anna Tao 1bf62b0416 feat: add CLIENT SETNAME for connection identification (#1787)
Adds client_name="praisonai_valkey_client" to the shared Valkey client
factory so all PraisonAI connections are identifiable via CLIENT LIST.

Signed-off-by: Anna Tao <[email protected]>
2026-06-02 07:08:46 +01:00
187 changed files with 9220 additions and 1259 deletions

No files matched your search

+1 -1
View File
@@ -382,7 +382,7 @@ praisonai flow
Open **http://localhost:7861** — use the **Agent** and **Agent Team** components to create sequential or parallel workflows. Connect Chat Input → Agent Team → Chat Output for instant multi-agent pipelines.
> 📖 [Full Flow docs](https://docs.praison.ai/docs/concepts/flow) — visual agent building, component reference, and deployment
> 📖 [Full Flow docs](https://docs.praison.ai/docs/concepts/agentflow) — visual agent building, component reference, and deployment
### 8. PraisonAI UI 🤖 (Clean Chat)
+12
View File
@@ -911,6 +911,18 @@ Methods:
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai hooks list <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai hooks remove <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai kanban block <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban boards <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban comment <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban complete <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban create <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban dispatch <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban link <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban list <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban move <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban reclaim <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban show <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban unblock <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai knowledge add <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
* <code title="cli">praisonai knowledge index <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
* <code title="cli">praisonai knowledge list <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.48" \
"praisonai>=4.6.52" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.48" \
"praisonai>=4.6.52" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.48" \
"praisonai>=4.6.52" \
"praisonai[ui]" \
"praisonai[crewai]"
+3 -3
View File
@@ -35,7 +35,7 @@ def run_quick_comparison():
prompt="What is 2+2?",
iterations=2,
paths=["openai_sdk", "praisonai_agent"],
output="verbose"
verbose=True
)
# Print comparison table
@@ -56,7 +56,7 @@ def run_full_benchmark():
report = handler.run_full_benchmark(
prompt="Hi",
iterations=3,
output="verbose"
verbose=True
)
# Print full report
@@ -78,7 +78,7 @@ def run_agent_benchmark():
prompt="Explain Python in one sentence",
iterations=3,
paths=["openai_sdk", "praisonai_agent"],
output="verbose"
verbose=True
)
# Show timeline diagrams
+1 -1
View File
@@ -22,7 +22,7 @@ custom_modes = {
}
# Create handler with verbose output
handler = CompareHandler(output="verbose")
handler = CompareHandler(verbose=True)
# Example: Manually create comparison results for analysis
# (useful for testing without API calls)
+1 -1
View File
@@ -22,7 +22,7 @@ modes = parse_modes("basic,tools,planning")
print(f"\nParsed modes: {modes}")
# Create compare handler
handler = CompareHandler(output="verbose")
handler = CompareHandler(verbose=True)
# Compare modes (this will run actual agent calls)
# Uncomment to run:
@@ -15,7 +15,7 @@ agent = Agent(
# With array [preset, overrides]
agent_override = Agent(
instructions="You are a helpful assistant.",
memory=["redis", {"user_id": "custom_user"}],
memory=MemoryConfig(backend="redis", user_id="custom_user"),
)
if __name__ == "__main__":
@@ -2,7 +2,7 @@
from praisonaiagents import Agent
from praisonaiagents import AgentFlow, Task
from praisonaiagents.workflows.workflow_configs import (
WorkflowOutputConfig, WorkflowPlanningConfig, WorkflowMemoryConfig,
WorkflowPlanningConfig, WorkflowMemoryConfig,
)
# Full-featured workflow with all consolidated params
@@ -32,7 +32,7 @@ workflow = AgentFlow(
),
],
# Workflow-level consolidated params
output=WorkflowOutputConfig(output="verbose", stream=True),
output="verbose",
planning=WorkflowPlanningConfig(enabled=True, reasoning=True),
memory=WorkflowMemoryConfig(backend="file"),
# Agent-like params
+13 -1
View File
@@ -15,8 +15,20 @@ workflow = AgentFlow(
],
output="verbose",
planning=True,
variables={"topic": "AI agents"},
)
if __name__ == "__main__":
result = workflow.run(variables={"topic": "AI agents"})
import os
import sys
if not os.environ.get("OPENAI_API_KEY"):
print("Set OPENAI_API_KEY to run this example.")
sys.exit(0)
if sys.platform == "win32":
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
result = workflow.run()
print(result)
+2 -2
View File
@@ -24,7 +24,7 @@ if has_api_key:
input_text="What is 2 + 2?",
expected_output="4",
num_iterations=3, # Run 3 times for statistical significance
output="verbose"
verbose=True
)
# Run evaluation
@@ -42,7 +42,7 @@ else:
input_text="What is 2 + 2?",
expected_output="4",
num_iterations=1,
output="verbose"
verbose=True
)
# Test with mock output
+2 -2
View File
@@ -27,7 +27,7 @@ if has_api_key:
scoring_type="numeric", # Score 1-10
threshold=7.0, # Pass if score >= 7
num_iterations=2,
output="verbose"
verbose=True
)
# Run evaluation
@@ -45,7 +45,7 @@ if has_api_key:
agent=agent,
input_text="Tell me a joke",
scoring_type="binary", # Pass or Fail
output="verbose"
verbose=True
)
binary_result = binary_evaluator.run(print_summary=True)
+1 -1
View File
@@ -26,7 +26,7 @@ if has_api_key:
num_iterations=10, # Run 10 benchmark iterations
warmup_runs=2, # 2 warmup runs before measurement
track_memory=True, # Track memory usage
output="verbose"
verbose=True
)
# Run evaluation
+15 -4
View File
@@ -16,8 +16,19 @@ def search_web(query: str) -> str:
def calculate(expression: str) -> str:
"""Calculate a math expression."""
return str(eval(expression))
"""Calculate a math expression safely."""
import ast, operator
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.Pow: operator.pow,
ast.USub: operator.neg, ast.UAdd: operator.pos}
def _ev(n):
if isinstance(n, ast.Expression): return _ev(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return n.value
if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.left), _ev(n.right))
raise ValueError(f"Unsupported: {ast.dump(n)}")
return str(_ev(ast.parse(expression, mode="eval")))
# Check if we have an API key
@@ -37,7 +48,7 @@ if has_api_key:
input_text="Search for the weather in Paris and calculate 25 * 4",
expected_tools=["search_web", "calculate"], # Tools that should be called
forbidden_tools=["delete_file"], # Tools that should NOT be called
output="verbose"
verbose=True
)
# Run evaluation
@@ -59,7 +70,7 @@ evaluator = ReliabilityEvaluator(
agent=mock_agent,
expected_tools=["search_web", "calculate"], # Tools that should be called
forbidden_tools=["delete_file"], # Tools that should NOT be called
output="verbose"
verbose=True
)
result2 = evaluator.evaluate_tool_calls(
+1 -1
View File
@@ -184,7 +184,7 @@ Answer questions based ONLY on the provided knowledge context.
When asked about codes, provide the EXACT code from the documents.
Be concise but accurate.""",
knowledge=[temp_dir],
user_id="compression_demo_user",
memory={"user_id": "compression_demo_user"},
output="verbose",
)
@@ -164,7 +164,7 @@ Answer questions based ONLY on the provided knowledge context.
When asked about department codes, provide the EXACT code from the documents.
Be specific about which department you're referring to.""",
knowledge=[temp_dir],
user_id="hierarchy_demo_user",
memory={"user_id": "hierarchy_demo_user"},
output="verbose",
)
@@ -124,7 +124,7 @@ def main():
# Show strategy selection for different corpus sizes
print(f"\nStrategy selection by corpus size:")
for size in [100, 1000, 10000, 50000, 100000]:
strategy = select_strategy(corpus_tokens=size)
strategy = select_strategy(CorpusStats(file_count=max(1, size // 1000), total_tokens=size))
print(f" {size:>6} tokens -> {strategy.value}")
except ImportError as e:
@@ -142,7 +142,7 @@ Answer questions based ONLY on the provided knowledge context.
When asked about verification codes, provide the EXACT code from the documents.
If information is not in the context, say 'Information not found in documents.'""",
knowledge=[temp_dir],
user_id="strategy_demo_user",
memory={"user_id": "strategy_demo_user"},
output="verbose", # Use new consolidated param
)
+1 -1
View File
@@ -65,7 +65,7 @@ def main():
name="PolicyExpert",
instructions="You are a company policy expert. Answer questions based ONLY on the provided knowledge. Quote exact values when asked about codes or numbers.",
knowledge=[temp_dir], # Add directory with documents
user_id="example_user", # Required for mem0 backend
memory={"user_id": "example_user"},
)
print("\n1. Testing Agent.chat() with context-required question:")
@@ -50,7 +50,7 @@ def main():
name="HR_Assistant_User1",
instructions="You are an HR assistant.",
knowledge=[temp_dir],
user_id="user_alice", # Knowledge scoped to Alice
memory={"user_id": "user_alice"},
)
response = user1_agent.chat("What is the policy on remote work?")
@@ -80,7 +80,7 @@ def main():
name="Personal_HR_Bot",
instructions="You are a personal HR assistant.",
knowledge=[temp_dir],
user_id="user_bob",
memory={"user_id": "user_bob"},
agent_id="hr_bot_v2",
)
@@ -49,7 +49,7 @@ Only authorized personnel with code WRENCH-992 may access this project.
name="AgentA_Ingester",
instructions="You ingest and store knowledge.",
knowledge=[temp_dir],
user_id="user_alice", # Scoped to Alice
memory={"user_id": "user_alice"},
output="silent",
)
@@ -103,7 +103,7 @@ Only authorized personnel with code WRENCH-992 may access this project.
name="AgentB_Querier",
instructions="Answer questions based on the provided knowledge. If you don't have the information, say so.",
knowledge=[temp_dir],
user_id="user_alice", # Same scope as Agent A
memory={"user_id": "user_alice"},
output="silent",
)
@@ -17,8 +17,10 @@ from praisonaiagents import Agent
agent = Agent(
name="CockroachDB Agent",
instructions="You are a helpful assistant.",
db={"database_url": os.getenv("COCKROACHDB_URL")},
session_id="crdb-demo-session",
memory={
"session_id": "crdb-demo-session",
"db": {"database_url": os.getenv("COCKROACHDB_URL")},
},
)
result = agent.start("What is distributed SQL? Explain in 2 sentences.")
@@ -18,8 +18,10 @@ from praisonaiagents import Agent
agent = Agent(
name="Neon Agent",
instructions="You are a helpful assistant.",
db={"database_url": os.getenv("NEON_DATABASE_URL")},
session_id="neon-demo-session",
memory={
"session_id": "neon-demo-session",
"db": {"database_url": os.getenv("NEON_DATABASE_URL")},
},
)
# Option 2: Direct URL
@@ -20,8 +20,10 @@ from praisonaiagents import Agent
agent = Agent(
name="Supabase Agent",
instructions="You are a helpful assistant.",
db={"database_url": os.getenv("SUPABASE_DATABASE_URL")},
session_id="supabase-demo-session",
memory={
"session_id": "supabase-demo-session",
"db": {"database_url": os.getenv("SUPABASE_DATABASE_URL")},
},
)
result = agent.start("What is Supabase? Explain in 2 sentences.")
@@ -18,11 +18,13 @@ from praisonaiagents import Agent
agent = Agent(
name="Turso Agent",
instructions="You are a helpful assistant.",
db={
"database_url": os.getenv("TURSO_DATABASE_URL"),
"auth_token": os.getenv("TURSO_AUTH_TOKEN"),
memory={
"session_id": "turso-demo-session",
"db": {
"database_url": os.getenv("TURSO_DATABASE_URL"),
"auth_token": os.getenv("TURSO_AUTH_TOKEN"),
},
},
session_id="turso-demo-session",
)
result = agent.start("What is edge computing? Explain in 2 sentences.")
+31 -8
View File
@@ -19,20 +19,43 @@ import os
def calculate(expression: str) -> str:
"""
A simple calculator tool that evaluates mathematical expressions.
A simple calculator tool that evaluates mathematical expressions safely.
Args:
expression: A mathematical expression to evaluate (e.g., "2 + 2")
Returns:
The result of the calculation
"""
import ast
import operator
_OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
def _safe_eval(node):
if isinstance(node, ast.Expression):
return _safe_eval(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.UnaryOp) and type(node.op) in _OPS:
return _OPS[type(node.op)](_safe_eval(node.operand))
if isinstance(node, ast.BinOp) and type(node.op) in _OPS:
return _OPS[type(node.op)](_safe_eval(node.left), _safe_eval(node.right))
raise ValueError(f"Unsupported expression: {ast.dump(node)}")
try:
# Safe evaluation of mathematical expressions
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return "Error: Invalid characters in expression"
result = eval(expression)
tree = ast.parse(expression, mode="eval")
result = _safe_eval(tree)
return f"Result: {result}"
except Exception as e:
return f"Error: {str(e)}"
+12 -6
View File
@@ -24,12 +24,18 @@ def main():
def calculate(expression: str) -> str:
"""Safely evaluate a mathematical expression."""
try:
# Only allow safe math operations
allowed = set("0123456789+-*/.(). ")
if not all(c in allowed for c in expression):
return "Error: Invalid characters in expression"
result = eval(expression) # Safe due to character filtering
return f"Result: {result}"
import ast, operator
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.Pow: operator.pow,
ast.USub: operator.neg, ast.UAdd: operator.pos}
def _ev(n):
if isinstance(n, ast.Expression): return _ev(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return n.value
if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.left), _ev(n.right))
raise ValueError(f"Unsupported: {ast.dump(n)}")
return f"Result: {_ev(ast.parse(expression, mode='eval'))}"
except Exception as e:
return f"Error: {e}"
+1 -2
View File
@@ -10,8 +10,7 @@ from praisonaiagents import Agent
agent = Agent(
name="FlexBot",
instructions="You are a helpful assistant.",
llm="gpt-4o-mini",
llm_config={"configurable": True}
llm={"model": "gpt-4o-mini", "configurable": True},
)
if __name__ == "__main__":
+1 -1
View File
@@ -21,7 +21,7 @@ agent = Agent(
name="ContextBot",
instructions="You help show context information.",
tools=[show_context],
session_id="my-session-123"
memory={"session_id": "my-session-123"},
)
if __name__ == "__main__":
+5 -3
View File
@@ -50,10 +50,12 @@ print("=== Qdrant Knowledge Store Demo (Agent-First) ===")
agent = Agent(
name="KnowledgeAssistant",
instructions="You are a helpful assistant with access to technical documentation.",
knowledge=[doc_path],
knowledge={
"vector_store": "qdrant",
"url": "http://localhost:6333"
"sources": [doc_path],
"vector_store": {
"provider": "qdrant",
"url": "http://localhost:6333",
},
}
)
+1 -1
View File
@@ -57,8 +57,8 @@ def main():
agent = Agent(
name="SecureAgent",
instructions="You are a file management assistant.",
policy=engine
)
agent.policy = engine
print("\n--- Agent with Policy Enforcement Created ---")
print(f"Agent: {agent.name}")
+29 -2
View File
@@ -21,9 +21,36 @@ def search_web(query: str) -> str:
return f"Search results for: {query}"
def calculate(expression: str) -> str:
"""Calculate a mathematical expression."""
"""Calculate a mathematical expression safely."""
import ast
import operator
_OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
def _eval(node):
if isinstance(node, ast.Expression):
return _eval(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.UnaryOp) and type(node.op) in _OPS:
return _OPS[type(node.op)](_eval(node.operand))
if isinstance(node, ast.BinOp) and type(node.op) in _OPS:
return _OPS[type(node.op)](_eval(node.left), _eval(node.right))
raise ValueError(f"Unsupported expression: {ast.dump(node)}")
try:
return f"Result: {eval(expression)}"
tree = ast.parse(expression, mode="eval")
return f"Result: {_eval(tree)}"
except Exception:
return "Invalid expression"
+1 -1
View File
@@ -75,7 +75,7 @@ class ContinuousCameraMonitor:
agents=[self.vision_agent],
tasks=[task],
process="sequential",
verbose=0 # Reduced verbosity for continuous monitoring
output="silent", # Reduced verbosity for continuous monitoring
)
return agents.start()
@@ -8,7 +8,7 @@ Docs: https://docs.praison.ai/cli/git-integration
from praisonai.cli.features import GitIntegrationHandler
# Initialize
handler = GitIntegrationHandler(output="verbose")
handler = GitIntegrationHandler(verbose=True)
git = handler.initialize(repo_path=".")
# Check if it's a git repo
+1 -1
View File
@@ -8,7 +8,7 @@ Docs: https://docs.praison.ai/cli/repo-map
from praisonai.cli.features import RepoMapHandler
# Initialize with current directory
handler = RepoMapHandler(output="verbose")
handler = RepoMapHandler(verbose=True)
repo_map = handler.initialize(root=".")
# Get the repository map
@@ -9,7 +9,7 @@ from praisonai.cli.features import SandboxExecutorHandler
from praisonai.cli.features.sandbox_executor import SandboxMode
# Initialize with basic sandbox
handler = SandboxExecutorHandler(output="verbose")
handler = SandboxExecutorHandler(verbose=True)
sandbox = handler.initialize(mode="basic")
print(f"Sandbox mode: {handler.get_mode()}")
+1 -2
View File
@@ -25,8 +25,7 @@ pdf_agent = Agent(
goal="Help users understand and extract information from PDF documents",
backstory="You are an expert at reading, analyzing, and answering questions about PDF documents. You provide accurate, detailed answers based on the document content.",
instructions="Read the provided PDF document and answer questions about its content. Be specific and cite relevant sections when possible.",
knowledge=["document.pdf"], # Replace with your PDF file path
knowledge=config,
knowledge={**config, "sources": ["document.pdf"]}, # Replace with your PDF path
reflection=True,
@@ -1,6 +1,8 @@
from praisonaiagents import Agent, Task, AgentTeam
import logging
import tempfile
import os
import sys
import logging
from praisonaiagents import Agent, Task, AgentTeam
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -17,14 +19,20 @@ config = {
}
}
# Create sample file in system temp directory
_sample_path = os.path.join(tempfile.gettempdir(), "sample_knowledge.txt")
if not os.path.exists(_sample_path):
with open(_sample_path, "w", encoding="utf-8") as f:
f.write("Mervin Praison is the creator of PraisonAI.\n")
# Create an agent with knowledge capabilities
knowledge_agent = Agent(
name="KnowledgeAgent",
role="Information Specialist",
goal="Store and retrieve knowledge efficiently",
backstory="Expert in managing and utilizing stored knowledge",
knowledge=["sample.pdf"],
knowledge=config)
knowledge={**config, "sources": [_sample_path]},
)
# Define a task for the agent
knowledge_task = Task(
@@ -39,8 +47,16 @@ agents = AgentTeam(
agents=[knowledge_agent],
tasks=[knowledge_task],
process="sequential",
user_id="user1"
)
# Start execution
result = agents.start()
if __name__ == "__main__":
if not os.environ.get("OPENAI_API_KEY"):
print("Set OPENAI_API_KEY to run this example.")
sys.exit(0)
if sys.platform == "win32":
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
result = agents.start()
print(result)
@@ -138,8 +138,7 @@ def main():
agent = Agent(
name="AI Research Assistant",
instructions="You are an AI research assistant. Use your knowledge base to answer questions about artificial intelligence topics.",
knowledge=sample_documents, # Will use default knowledge config
knowledge=rerank_config, # Use reranking config
knowledge={**rerank_config, "sources": sample_documents},
llm="gpt-4o-mini"
)
+7 -3
View File
@@ -32,8 +32,12 @@ rag_task = Task(
agents = AgentTeam(
agents=[rag_agent],
tasks=[rag_task],
user_id="user1"
)
# Start Agents
agents.start()
if __name__ == "__main__":
import os
import sys
if not os.environ.get("OPENAI_API_KEY"):
print("Set OPENAI_API_KEY to run this example.")
sys.exit(0)
agents.start()
@@ -16,7 +16,7 @@ agents = AgentTeam(
agents=[agent],
tasks=[task],
process="workflow",
max_iter=30
execution={"max_iter": 30},
)
agents.start()
@@ -61,7 +61,6 @@ def example_with_wrappers():
role="Data Analyst",
goal="Analyze data and provide insights",
llm="gpt-4o-mini",
request_id="request_2" # Unique request ID
)
# Operations are automatically tracked
@@ -49,7 +49,18 @@ def calculate_with_tracking(expression: str) -> str:
time.sleep(0.05)
try:
result = eval(expression)
import ast, operator
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.Pow: operator.pow,
ast.USub: operator.neg, ast.UAdd: operator.pos}
def _ev(n):
if isinstance(n, ast.Expression): return _ev(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return n.value
if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.left), _ev(n.right))
raise ValueError(f"Unsupported: {ast.dump(n)}")
result = _ev(ast.parse(expression, mode="eval"))
return f"Result: {result}"
except Exception as e:
return f"Error: {str(e)}"
+2 -1
View File
@@ -132,7 +132,8 @@ agents = AgentTeam(
agents=[qa_generator, total_questions_evaluator, cot_generator, upload_to_huggingface],
tasks=[generate_task, evaluate_total_questions_task, generate_cot_task, upload_to_huggingface_task],
process="workflow",
max_iter=30, output="minimal"
execution={"max_iter": 30},
output="minimal",
)
agents.start()
+3 -3
View File
@@ -18,7 +18,7 @@ openai_profile = AuthProfile(
provider="openai",
api_key=os.environ.get("OPENAI_API_KEY", "sk-..."),
priority=1, # Highest priority
rate_limit=100,
rate_limit_rpm=100,
)
anthropic_profile = AuthProfile(
@@ -26,7 +26,7 @@ anthropic_profile = AuthProfile(
provider="anthropic",
api_key=os.environ.get("ANTHROPIC_API_KEY", "sk-ant-..."),
priority=2, # Second priority
rate_limit=50,
rate_limit_rpm=50,
)
groq_profile = AuthProfile(
@@ -34,7 +34,7 @@ groq_profile = AuthProfile(
provider="groq",
api_key=os.environ.get("GROQ_API_KEY", "gsk-..."),
priority=3, # Third priority (fast but limited)
rate_limit=30,
rate_limit_rpm=30,
)
# Configure failover behavior
+3 -3
View File
@@ -5,7 +5,7 @@ import os
def main():
# Initialize memory config
memory_config = {
"provider": "rag",
"backend": "sqlite",
"use_embedding": True,
"storage": {
"type": "sqlite",
@@ -105,13 +105,13 @@ def main():
agents=[researcher, retriever],
tasks=[store_task, verify_task, query_task, query_both_task], # Use same verbose level as memory
memory={
"provider": "rag",
"embedder": {
"provider": "openai",
"config": {
"model": "text-embedding-3-small"
}
}
},
"config": {"provider": "sqlite"},
}
)
@@ -6,6 +6,7 @@ built-in validation and safety mechanisms for secure agent operations.
"""
from praisonaiagents import Agent, Task, AgentTeam
from praisonaiagents.config.feature_configs import GuardrailConfig
from praisonaiagents.tools import internet_search
print("=== Production Guardrails Patterns Example ===\n")
@@ -38,8 +39,8 @@ production_agent = Agent(
goal="Generate safe, compliant content for production use",
backstory="Production-ready agent with built-in safety and compliance checks",
tools=[internet_search],
guardrails=content_guardrail,
max_retries=3)
guardrails=GuardrailConfig(validator=content_guardrail, max_retries=3),
)
# Create secure task with validation
secure_task = Task(
+1 -1
View File
@@ -40,7 +40,7 @@ agent = Agent(
"execute_command"
],
memory=True,
web_search=True,
web=True,
auto_approve_tools=True,
)
+12 -4
View File
@@ -36,10 +36,18 @@ def calculate(expression: str) -> dict:
expression: Math expression (e.g., "2 + 2 * 3")
"""
try:
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return {"error": "Invalid characters"}
return {"expression": expression, "result": eval(expression)}
import ast, operator
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.Pow: operator.pow,
ast.USub: operator.neg, ast.UAdd: operator.pos}
def _ev(n):
if isinstance(n, ast.Expression): return _ev(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return n.value
if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.left), _ev(n.right))
raise ValueError(f"Unsupported: {ast.dump(n)}")
return {"expression": expression, "result": _ev(ast.parse(expression, mode="eval"))}
except Exception as e:
return {"error": str(e)}
+12 -6
View File
@@ -47,12 +47,18 @@ def calculate(expression: str) -> Dict[str, Any]:
The result of the calculation
"""
try:
# Safe evaluation of mathematical expressions
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return {"error": "Invalid characters in expression"}
result = eval(expression) # Use safer alternative in production
import ast, operator
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.Pow: operator.pow,
ast.USub: operator.neg, ast.UAdd: operator.pos}
def _ev(n):
if isinstance(n, ast.Expression): return _ev(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return n.value
if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.left), _ev(n.right))
raise ValueError(f"Unsupported: {ast.dump(n)}")
result = _ev(ast.parse(expression, mode="eval"))
return {"expression": expression, "result": result}
except Exception as e:
return {"error": str(e)}
+12 -7
View File
@@ -49,13 +49,18 @@ def calculate(expression: str) -> Dict[str, Any]:
The result of the calculation
"""
try:
# Safe evaluation of mathematical expressions
# In production, use a proper math parser
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return {"error": "Invalid characters in expression"}
result = eval(expression) # Note: Use safer alternative in production
import ast, operator
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.Pow: operator.pow,
ast.USub: operator.neg, ast.UAdd: operator.pos}
def _ev(n):
if isinstance(n, ast.Expression): return _ev(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return n.value
if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.left), _ev(n.right))
raise ValueError(f"Unsupported: {ast.dump(n)}")
result = _ev(ast.parse(expression, mode="eval"))
return {"expression": expression, "result": result}
except Exception as e:
return {"error": str(e)}
@@ -12,8 +12,8 @@ print("=== Advanced Graph Memory Integration Example ===\n")
# Configure simple graph memory
memory_config = {
"provider": "mem0",
"config": {
"provider": "mem0",
"vector_store": {
"provider": "chroma",
"config": {
@@ -60,8 +60,8 @@ query_task = Task(
agents_system = AgentTeam(
agents=[knowledge_agent],
tasks=[build_task, query_task],
memory=True,
memory=memory_config, output="verbose"
memory=memory_config,
output="verbose",
)
print("Starting graph memory demonstration...")
@@ -12,8 +12,8 @@ print("=== Advanced Memory Search and Optimization Example ===\n")
# Configure optimized memory settings
memory_config = {
"provider": "mem0",
"config": {
"provider": "mem0",
"vector_store": {
"provider": "chroma",
"config": {
@@ -61,8 +61,8 @@ search_task = Task(
agents_system = AgentTeam(
agents=[research_agent],
tasks=[build_memory_task, search_task],
memory=True,
memory=memory_config, output="verbose"
memory=memory_config,
output="verbose",
)
print("Starting memory search optimization demonstration...")
@@ -86,13 +86,13 @@ def main():
agents=[researcher, retriever],
tasks=[store_task, retrieve_task],
memory={
"provider": "rag",
"embedder": {
"provider": "gemini",
"config": {
"model": "text-embedding-004",
}
}
},
"config": {"provider": "sqlite"},
}
)
+1 -2
View File
@@ -152,8 +152,7 @@ def main():
agents_system = AgentTeam(
agents=[researcher],
tasks=[build_knowledge_task, query_knowledge_task, expand_knowledge_task],
memory=True,
memory=memory_config
memory=memory_config,
)
print("\n🚀 Starting graph memory demonstration...")
@@ -30,7 +30,7 @@ def main():
try:
# Create memory instance
memory = Memory(config={
"provider": "rag",
"backend": "sqlite",
"use_embedding": False,
"agent_name": "demo_agent", # Used in trace events
})
@@ -32,9 +32,8 @@ def init_agent():
return Agent(
name="Knowledge Agent",
instructions="You answer questions based on the provided knowledge.",
knowledge=["kag-research-paper.pdf"],
knowledge=config,
user_id="user1",
knowledge={**config, "sources": ["kag-research-paper.pdf"]},
memory={"user_id": "user1"},
llm="deepseek-r1"
)
@@ -30,9 +30,12 @@ config = {
agent = Agent(
name="Knowledge Agent",
instructions="You answer questions based on the provided knowledge.",
knowledge=["kag-research-paper.pdf"], # Indexing
knowledge=config,
user_id="user1",
knowledge={
"sources": ["kag-research-paper.pdf"],
"vector_store": config["vector_store"],
"embedder": config["embedder"],
},
memory={"user_id": "user1"},
llm="deepseek-r1"
)
@@ -31,10 +31,9 @@ llm_config = {
agent = Agent(
instructions="You are a helpful Assistant specialized in scientific explanations. "
"Provide clear, accurate, and engaging responses.",
llm=llm_config, # Pass the detailed configuration
# Enable detailed output
markdown=True, # Format responses in markdown
reflection=True # Enable self-reflection
llm=llm_config,
output={"markdown": True},
reflection=True,
)
# Test the agent
@@ -189,8 +189,8 @@ def main():
business_system = AgentTeam(
agents=[data_manager, knowledge_curator, business_analyst, customer_service],
tasks=business_tasks,
memory=True,
memory=mongodb_memory_config, output="verbose"
memory=mongodb_memory_config,
output="verbose",
)
# Execute the comprehensive business pipeline
@@ -55,8 +55,10 @@ def main():
retrieve information from a comprehensive MongoDB knowledge base. You excel
at finding relevant information, synthesizing knowledge from multiple sources,
and providing accurate, context-aware responses.""",
knowledge=mongodb_knowledge_config,
knowledge=[os.path.join(os.path.dirname(__file__), "llms.md")],
knowledge={
**mongodb_knowledge_config,
"sources": [os.path.join(os.path.dirname(__file__), "llms.md")],
},
memory=True,
llm="gpt-4o-mini"
)
@@ -28,8 +28,8 @@ if not os.environ.get("OPENAI_API_KEY"):
def main():
# MongoDB memory configuration
mongodb_memory_config = {
"provider": "mongodb",
"config": {
"provider": "mongodb",
"connection_string": "mongodb://localhost:27017/", # Replace with your MongoDB connection string
"database": "praisonai_memory",
"use_vector_search": True, # Enable Atlas Vector Search (requires MongoDB Atlas)
@@ -93,8 +93,8 @@ def main():
research_system = AgentTeam(
agents=[research_agent],
tasks=research_tasks,
memory=True,
memory=mongodb_memory_config, output="verbose"
memory=mongodb_memory_config,
output="verbose",
)
# Execute the research pipeline
+1 -1
View File
@@ -50,7 +50,7 @@ agent = Agent(
name="TechSupport",
instructions="You are a technical support assistant.",
knowledge=[sample_doc],
user_id="comparison_user",
memory={"user_id": "comparison_user"},
llm="openai/gpt-4o-mini",
output="minimal",
)
+4 -3
View File
@@ -12,6 +12,7 @@ Environment:
"""
import os
import tempfile
from praisonaiagents import Agent, AutoRagAgent
# Ensure API key is set
@@ -20,8 +21,8 @@ if not os.environ.get("OPENAI_API_KEY"):
exit(1)
# Create a sample document for testing
sample_doc = "/tmp/sample_doc.txt"
with open(sample_doc, "w") as f:
sample_doc = os.path.join(tempfile.gettempdir(), "sample_doc.txt")
with open(sample_doc, "w", encoding="utf-8") as f:
f.write("""
PraisonAI Framework Documentation
@@ -44,7 +45,7 @@ agent = Agent(
name="DocBot",
instructions="You are a helpful documentation assistant. Answer questions based on the provided context.",
knowledge=[sample_doc],
user_id="example_user", # Required for RAG retrieval
memory={"user_id": "example_user"},
llm="openai/gpt-4o-mini",
output="minimal",
)
+1 -1
View File
@@ -38,7 +38,7 @@ agent = Agent(
name="PolicyBot",
instructions="You are a customer service assistant.",
knowledge=[sample_doc],
user_id="policy_user",
memory={"user_id": "policy_user"},
llm="openai/gpt-4o-mini",
output="minimal",
)
+1 -1
View File
@@ -44,7 +44,7 @@ agent = Agent(
name="MLExpert",
instructions="You are a machine learning expert. Answer questions based on the provided context.",
knowledge=[sample_doc],
user_id="rag_user",
memory={"user_id": "rag_user"},
llm="openai/gpt-4o-mini",
output="minimal",
)
+1 -1
View File
@@ -59,7 +59,7 @@ agent = Agent(
name="APIDocBot",
instructions="You are an API documentation assistant.",
knowledge=[sample_doc],
user_id="hybrid_user",
memory={"user_id": "hybrid_user"},
llm="openai/gpt-4o-mini",
output="minimal",
)
+1 -1
View File
@@ -44,7 +44,7 @@ agent = Agent(
name="ResearchBot",
instructions="You are a research assistant. Always cite your sources.",
knowledge=[doc1, doc2],
user_id="citation_user",
memory={"user_id": "citation_user"},
llm="openai/gpt-4o-mini",
output="minimal",
)
@@ -28,7 +28,7 @@ def example_basic_persistence():
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant. Be very brief.",
session_id=session_id,
memory={"session_id": session_id},
output="minimal",
)
@@ -59,7 +59,7 @@ def example_session_restoration():
agent1 = Agent(
name="MemoryBot",
instructions="You are a helpful assistant. Be very brief.",
session_id=session_id,
memory={"session_id": session_id},
output="minimal",
)
@@ -72,7 +72,7 @@ def example_session_restoration():
agent2 = Agent(
name="MemoryBot",
instructions="You are a helpful assistant. Be very brief.",
session_id=session_id,
memory={"session_id": session_id},
output="minimal",
)
@@ -172,7 +172,7 @@ print("="*70)
# Create sessions for different users working on the same project
reviewer_session = Session(
session_id="peer_review_001",
user_id="user_reviewer_01", # Different user
user_id="user_reviewer_01",
storage_path=session_dir
)
@@ -14,13 +14,12 @@ def main():
# Initialize memory with quality-based configuration
memory_config = {
"provider": "rag",
"use_embedding": True,
"rag_db_path": "quality_memory_db"
"backend": "sqlite",
"user_id": "quality_demo_user",
}
memory = Memory(config=memory_config, verbose=5)
print(f"📦 Initialized memory system with provider: {memory_config['provider']}")
print(f"📦 Initialized memory system with backend: {memory_config['backend']}")
# Create agent with memory
agent = Agent(
@@ -28,7 +27,6 @@ def main():
role="Information Quality Specialist",
instructions="Store and retrieve information with quality assessment",
memory=memory,
user_id="quality_demo_user"
)
print(f"🤖 Created agent: {agent.name}")
+1 -1
View File
@@ -15,7 +15,7 @@ def main():
# Create a session with persistent storage
session = Session(
session_id="demo_chat_001",
user_id="demo_user"
user_id="demo_user",
)
print(f"📝 Created session: {session}")
@@ -88,9 +88,8 @@ def main():
workflow = AgentTeam(
agents=[researcher, analyzer, writer],
tasks=[research_task, analysis_task, report_task],
memory=True,
memory={"user_id": "research_project_001"},
process="workflow",
user_id="research_project_001",
)
print(f"🤖 Created workflow with {len(workflow.agents)} agents and {len(workflow.tasks)} tasks")
@@ -14,7 +14,7 @@ def main():
goal="Find information about a topic",
backstory="You are an expert researcher who finds comprehensive information.",
llm="gpt-4o-mini", # Use a cost-effective model for examples
metrics=True # 🎯 SIMPLIFIED: Just add this parameter!
output={"metrics": True} # Token metrics via consolidated output config
)
# Create a simple task
@@ -14,7 +14,7 @@ def main():
goal="Analyze data and provide insights",
backstory="You are an experienced data analyst with expertise in market research.",
llm="gpt-4o-mini",
metrics=True # 🎯 Enable metrics tracking
output={"metrics": True}
)
writer_agent = Agent(
@@ -23,7 +23,7 @@ def main():
goal="Create well-structured content",
backstory="You are a skilled technical writer who creates clear, engaging content.",
llm="gpt-4o-mini",
metrics=True # 🎯 Enable metrics tracking
output={"metrics": True}
)
# Create multiple tasks
@@ -14,7 +14,7 @@ def main():
goal="Conduct comprehensive market research",
backstory="You are a seasoned market researcher with 10+ years of experience.",
llm="gpt-4o-mini",
metrics=True # 🎯 Enable automatic metrics tracking
output={"metrics": True}
)
strategy_agent = Agent(
@@ -23,7 +23,7 @@ def main():
goal="Develop actionable business strategies",
backstory="You are a strategic consultant who translates research into business plans.",
llm="gpt-4o-mini",
metrics=True # 🎯 Enable automatic metrics tracking
output={"metrics": True}
)
# Create tasks that will generate substantial token usage
@@ -22,7 +22,8 @@ rag_agent = Agent(
You can use the internet_search function to search the web when needed.""",
llm="gpt-4o",
markdown=True)
output={"markdown": True},
)
if __name__ == "__main__":
print("🤖 Thai Recipe RAG Agent is ready!")
@@ -32,9 +32,8 @@ def init_agent():
return Agent(
name="Knowledge Agent",
instructions="You answer questions based on the provided knowledge.",
knowledge=["kag-research-paper.pdf"],
knowledge=config,
user_id="user1",
knowledge={**config, "sources": ["kag-research-paper.pdf"]},
memory={"user_id": "user1"},
llm="deepseek-r1"
)
@@ -264,13 +264,12 @@ agents = AgentTeam(
tasks=[tool_query_task, pentester_analysis_task, ciso_review_task],
process="hierarchical",
manager_llm="gpt-4o-mini",
memory=True, # Enable shared memory for domain context
memory={
"provider": "rag",
"backend": "sqlite",
"config": {
"collection_name": f"{domain.replace('.', '_')}_security_analysis"
}
}
},
},
)
# Main Execution Workflow
@@ -5,7 +5,7 @@ Demonstrates using should_run to conditionally execute agent steps
based on input or previous results.
"""
from praisonaiagents import Agent, Workflow, Task
from praisonaiagents import Agent, AgentFlow, Task
# Condition functions
def is_technical(ctx) -> bool:
@@ -53,7 +53,7 @@ workflow = AgentFlow(
# Step 4: Function - formats output
format_output
],
default_llm="gpt-4o-mini" # Used for action-based steps
llm="gpt-4o-mini", # Used for action-based steps
)
if __name__ == "__main__":
+1 -1
View File
@@ -207,7 +207,7 @@ def api_augmented_rag():
instructions="""You are a financial assistant with access to market APIs.
Provide accurate financial information and context.
Always note that prices are subject to change.""",
user_id="api_demo"
memory={"user_id": "api_demo"},
)
queries = [
+9 -7
View File
@@ -51,13 +51,15 @@ print("=" * 60)
agent = Agent(
name="Knowledge Agent",
instructions="You are a helpful assistant that answers questions based on the provided knowledge. Be concise.",
knowledge=[".praison/test_docs/overview.txt"],
retrieval_config={
"policy": "always", # Always retrieve for this demo
"top_k": 3, # Get top 3 chunks
"citations": True, # Include citations
"max_context_tokens": 2000,
}
knowledge={
"sources": [".praison/test_docs/overview.txt"],
"config": {
"policy": "always",
"top_k": 3,
"citations": True,
"max_context_tokens": 2000,
},
},
)
# Test 1: Basic chat with retrieval
+1 -1
View File
@@ -60,7 +60,7 @@ print("=" * 50)
agent_with_discovery = Agent(
name="Multi-Skill Agent",
instructions="You are a versatile assistant with multiple skills.",
skills_dirs=["./"], # Scan current directory for skill subdirectories
skills=["./"], # Scan current directory for skill subdirectories
)
if agent_with_discovery.skill_manager:
+40
View File
@@ -0,0 +1,40 @@
# Example: Agent with MCP Tool Integration
#
# This example shows how to configure an agent with MCP (Model Context Protocol)
# servers for additional tool capabilities.
#
# Usage:
# praisonai workflow run --file examples/yaml/agent-with-mcp.yaml
agent:
name: mcp_assistant
instructions: |
You are a helpful assistant with access to filesystem and time tools.
Use the MCP tools when needed to help users with file operations or time queries.
llm: gpt-4o-mini
# MCP server configuration
mcp:
servers:
filesystem:
# Filesystem access for the agent
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-filesystem"
- "."
env: {}
enabled: true
time:
# Time and date utilities
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-time"
env: {}
enabled: true
# NOTE: Per-server tool filtering (tools.include/exclude) is not yet supported
# by the MCP server config schema. Tool filtering should be applied after
# calling mcp.get_tools() in your application code.
+1 -1
View File
@@ -93,7 +93,7 @@ def example_3_execute_yaml_directly():
result = manager.execute_yaml(
yaml_file,
input_data="The future of AI",
output="verbose"
verbose=True,
)
print(f"Status: {result['status']}")
+2 -1
View File
@@ -95,4 +95,5 @@ chroma_db/
.windsurf
.ruff_cache
*.sfdx
.sfdx/
.sfdx/
.cursor/
@@ -79,10 +79,20 @@ def get_time(timezone: Literal["est", "pst"]) -> str:
def calculate(expression: str) -> str:
"""Calculate a math expression."""
"""Calculate a math expression safely."""
try:
result = eval(expression)
return f"Result: {result}"
import ast, operator
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.Pow: operator.pow,
ast.USub: operator.neg, ast.UAdd: operator.pos}
def _ev(n):
if isinstance(n, ast.Expression): return _ev(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return n.value
if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in _OPS: return _OPS[type(n.op)](_ev(n.left), _ev(n.right))
raise ValueError(f"Unsupported: {ast.dump(n)}")
return f"Result: {_ev(ast.parse(expression, mode='eval'))}"
except Exception:
return "Error: Invalid expression"
@@ -384,6 +384,7 @@ _LAZY_IMPORTS = {
'MultiAgentExecutionConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentExecutionConfig'),
'MultiAgentPlanningConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentPlanningConfig'),
'MultiAgentMemoryConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentMemoryConfig'),
'ToolSearchConfig': ('praisonaiagents.config.feature_configs', 'ToolSearchConfig'),
# Parameter resolver
'resolve': ('praisonaiagents.config.param_resolver', 'resolve'),
@@ -556,6 +556,7 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
backend: Optional[Any] = None, # External managed agent backend (e.g., ManagedAgentIntegration)
cli_backend: Optional[Union[str, Any]] = None, # CLI backend for delegating turns (e.g., "claude-code")
interrupt_controller: Optional['InterruptController'] = None, # G2: Cooperative cancellation
tool_search: Optional[Union[bool, str, Dict[str, Any], 'ToolSearchConfig']] = False, # Progressive tool disclosure
sandbox: Optional[Union[bool, 'SandboxConfig']] = None, # Sandbox for safe code execution
):
"""Initialize an Agent instance.
@@ -660,6 +661,14 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
When provided, agent delegates entire conversation turns to the CLI tool
instead of using the built-in LLM. Enables session continuity and
tool integration through external AI coding assistants.
tool_search: Progressive tool disclosure configuration. Accepts:
- bool: False=disabled (default), True=auto mode
- str: Mode ("auto", "on", "off")
- Dict[str, Any]: Config overrides (e.g. {"threshold_pct": 15})
- ToolSearchConfig: Custom configuration
When enabled, replaces large tool schemas with bridge tools (tool_search,
tool_describe, tool_call) to save context. Core SDK tools never defer.
Auto mode activates based on token threshold. Opt-in feature.
Raises:
ValueError: If all of name, role, goal, backstory, and instructions are None.
@@ -1430,6 +1439,34 @@ class Agent(SandboxMixin, UnifiedExecutionMixin, ToolExecutionMixin, ChatHandler
web_search = False
web_fetch = False
# ─────────────────────────────────────────────────────────────────────
# Resolve TOOL_SEARCH param
# ─────────────────────────────────────────────────────────────────────
# Fast path: None/False -> disabled (zero overhead)
if tool_search is None or tool_search is False:
self._tool_search_config = None
elif tool_search is True:
# True -> auto mode with defaults
from ..tools.tool_search import ToolSearchConfig as _ToolSearchConfig
self._tool_search_config = _ToolSearchConfig(enabled="auto")
elif isinstance(tool_search, str):
# String mode ("auto", "on", "off")
from ..tools.tool_search import ToolSearchConfig as _ToolSearchConfig
self._tool_search_config = _ToolSearchConfig.from_raw(tool_search)
elif isinstance(tool_search, dict):
# Dict -> config overrides
from ..tools.tool_search import ToolSearchConfig as _ToolSearchConfig
self._tool_search_config = _ToolSearchConfig(**tool_search)
else:
from ..tools.tool_search import ToolSearchConfig as _ToolSearchConfig
if isinstance(tool_search, _ToolSearchConfig):
self._tool_search_config = tool_search
else:
raise TypeError(
"tool_search must be False/None, True, a mode string, "
"a dict of ToolSearchConfig fields, or ToolSearchConfig"
)
# ============================================================
# END CONSOLIDATED PARAMS EXTRACTION
# ============================================================
@@ -2026,6 +2063,7 @@ Your Goal: {self.goal}
'skills': getattr(self, '_skills_config', None),
'approval': getattr(self, '_approval_config', None),
'learn': getattr(self, '_learn_config', None),
'tool_search': getattr(self, '_tool_search_config', None),
# Tool configuration
'tool_timeout': getattr(self, '_tool_timeout', None),
@@ -331,6 +331,7 @@ Your Goal: {self.goal}"""
- Callable functions
- String function names
- Objects with to_openai_tool() method
- Tool Search progressive disclosure (if enabled)
Args:
tools: List of tools in various formats or None to use self.tools
@@ -344,10 +345,14 @@ Your Goal: {self.goal}"""
if not tools:
return []
# Check cache first
# Check cache first - include tool_search config in cache key for safety
tools_key = self._get_tools_cache_key(tools)
cached_tools = self._cache_get(self._formatted_tools_cache, tools_key)
if cached_tools is not None:
tool_search_enabled = getattr(self, '_tool_search_config', None) is not None
cache_key = f"{tools_key}:tool_search={tool_search_enabled}"
cached_entry = self._cache_get(self._formatted_tools_cache, cache_key)
if cached_entry is not None:
cached_tools, cached_metadata = cached_entry
self._tool_search_metadata = cached_metadata
return cached_tools
formatted_tools = []
@@ -399,9 +404,46 @@ Your Goal: {self.goal}"""
logging.error(f"Tools are not JSON serializable: {e}")
return []
# Cache the formatted tools with LRU eviction
self._cache_put(self._formatted_tools_cache, tools_key, formatted_tools)
return formatted_tools
# Apply tool search assembly if enabled (after formatting, before caching)
if hasattr(self, '_tool_search_config') and self._tool_search_config is not None:
try:
from ..tools.tool_search import assemble_tool_defs
# Get context length from LLM config if available
context_length = getattr(self, '_context_window_size', None)
# Assemble tools with bridge mode check
assembled_tools, metadata = assemble_tool_defs(
tool_defs=formatted_tools,
config=self._tool_search_config,
context_length=context_length
)
# Store metadata for bridge tool dispatch
self._tool_search_metadata = metadata
formatted_tools = assembled_tools
except ImportError:
# Tool search module not available, continue with original tools
logging.warning("Tool search requested but tool_search module not available")
# Strip __praisonai_deferrable__ from provider-facing tool payloads
# Keep the marker only for internal tool classification
cleaned_tools = []
for tool in formatted_tools:
if isinstance(tool, dict) and "__praisonai_deferrable__" in tool:
tool_copy = tool.copy()
tool_copy.pop("__praisonai_deferrable__", None)
cleaned_tools.append(tool_copy)
else:
cleaned_tools.append(tool)
# Cache the formatted tools with LRU eviction, including tool search metadata
self._cache_put(
self._formatted_tools_cache,
cache_key,
(cleaned_tools, getattr(self, "_tool_search_metadata", None)),
)
return cleaned_tools
def _build_multimodal_prompt(
self,
@@ -507,7 +549,7 @@ Your Goal: {self.goal}"""
emit_events=True
)
def _chat_completion(self, messages, temperature=1.0, tools=None, stream=True, reasoning_steps=False, task_name=None, task_description=None, task_id=None, response_format=None, _retry_depth=0):
def _chat_completion(self, messages, temperature=1.0, tools=None, stream=None, reasoning_steps=False, task_name=None, task_description=None, task_id=None, response_format=None, _retry_depth=0):
start_time = time.time()
# --- Context compaction (opt-in via ExecutionConfig.context_compaction) ---
@@ -577,6 +619,39 @@ Your Goal: {self.goal}"""
# Use the new _format_tools_for_completion helper method
formatted_tools = self._format_tools_for_completion(tools)
# Smart fallback for streaming: try streaming first, fall back to non-streaming if unsupported
if stream is None:
# Auto-detect: prefer streaming for better UX, fallback if adapter doesn't support it
try:
# First attempt: try with streaming enabled for better user experience
stream_callback = self.stream_emitter.emit if hasattr(self, 'stream_emitter') else None
final_response = self._execute_unified_chat_completion(
messages=messages,
temperature=temperature,
tools=formatted_tools,
stream=True, # Try streaming first
reasoning_steps=reasoning_steps,
task_name=task_name,
task_description=task_description,
task_id=task_id,
response_format=response_format,
stream_callback=stream_callback,
emit_events=True
)
return final_response
except ValueError as e:
if "Streaming is not supported" in str(e):
# Fallback: retry with non-streaming for sync adapters
logging.debug(f"{self.name}: Streaming not supported by adapter, falling back to non-streaming")
stream = False # Set for the main execution below
else:
raise # Re-raise if it's a different ValueError
except Exception:
# For any other exception, fall back to non-streaming
logging.debug(f"{self.name}: Streaming attempt failed, falling back to non-streaming")
stream = False # Set for the main execution below
# If stream was explicitly set or fallback occurred, use the specified/fallback value
try:
# NEW: Unified protocol dispatch path (Issue #1304, #1362)
# UNIFIED: Single protocol-driven dispatch path (fixes DRY violation)
@@ -774,7 +849,7 @@ Your Goal: {self.goal}"""
messages,
temperature=1.0,
tools=None,
stream=True,
stream=None,
reasoning_steps=False,
task_name=None,
task_description=None,
@@ -806,6 +881,47 @@ Your Goal: {self.goal}"""
# Cache the dispatcher
self._unified_dispatcher = dispatcher
# Smart fallback for streaming: try streaming first, fall back to non-streaming if unsupported
if stream is None:
# Auto-detect: prefer streaming for better UX, fallback if adapter doesn't support it
try:
# First attempt: try with streaming enabled for better user experience
if stream_callback is None and hasattr(self, 'stream_emitter'):
stream_callback = getattr(self.stream_emitter, 'emit', None)
final_response = self._unified_dispatcher.chat_completion(
messages=messages,
tools=tools,
tool_choice=getattr(self, 'tool_choice', None),
temperature=temperature,
max_tokens=getattr(self, 'max_tokens', None),
stream=True, # Try streaming first
response_format=response_format,
execute_tool_fn=getattr(self, 'execute_tool', None),
console=self.console if (self.verbose or True) else None, # Enable console for streaming
display_fn=self._display_generating if self.verbose else None,
stream_callback=stream_callback,
emit_events=emit_events,
verbose=self.verbose,
max_iterations=10,
reasoning_steps=reasoning_steps,
task_name=task_name,
task_description=task_description,
task_id=task_id,
agent_name=getattr(self, 'name', 'assistant')
)
return final_response
except ValueError as e:
if "Streaming is not supported" in str(e):
# Fallback: retry with non-streaming for sync adapters
logging.debug(f"Agent: Streaming not supported by adapter, falling back to non-streaming")
stream = False # Set for the main execution below
else:
raise # Re-raise if it's a different ValueError
except Exception:
# For any other exception, fall back to non-streaming
logging.debug(f"Agent: Streaming attempt failed, falling back to non-streaming")
stream = False # Set for the main execution below
# Execute unified dispatch with all necessary parameters
# Includes all parameters from both legacy paths to ensure full compatibility
try:
@@ -857,7 +973,7 @@ Your Goal: {self.goal}"""
messages,
temperature=1.0,
tools=None,
stream=True,
stream=True, # Async methods keep stream=True default (async adapters support streaming vs sync smart fallback)
reasoning_steps=False,
task_name=None,
task_description=None,
@@ -135,6 +135,12 @@ class ToolExecutionMixin:
"""
logging.debug(f"{self.name} executing tool {function_name} with arguments: {arguments}")
# Handle bridge tool unwrapping BEFORE trace/stream/hooks (design invariant #6)
# Only intercept when tool_search is active; otherwise fall through to real tool execution
if (getattr(self, '_tool_search_config', None) is not None and
function_name in ("tool_search", "tool_describe", "tool_call")):
return self._handle_bridge_tool_call(function_name, arguments, tool_call_id)
# NOTE: tool_call callback is triggered by display_tool_call in openai_client.py
# Do NOT call it here to avoid duplicate output
@@ -954,3 +960,97 @@ class ToolExecutionMixin:
def pending_approval_count(self) -> int:
"""Number of approval requests still waiting."""
return len(self._pending_approvals)
def _handle_bridge_tool_call(self, function_name: str, arguments: Dict[str, Any], tool_call_id: Optional[str] = None) -> Any:
"""
Handle bridge tool calls (tool_search, tool_describe, tool_call).
This implements the tool search unwrapping logic before trace/stream/hooks
as required by design invariant #6.
Args:
function_name: Bridge tool name
arguments: Arguments passed to bridge tool
tool_call_id: Optional tool call ID
Returns:
Result of bridge tool execution or unwrapped real tool call
"""
# Ensure tool search metadata is available
if not hasattr(self, '_tool_search_metadata') or self._tool_search_metadata is None:
return "Tool search not available or not in bridge mode"
metadata = self._tool_search_metadata
# Check if we're in bridge mode
if not metadata.get("bridge_mode", False):
return "Tool search not in bridge mode"
# Get deferrable tools from metadata
deferrable_tools = metadata.get("deferrable_tools", [])
if function_name == "tool_search":
# Handle tool_search bridge call
try:
from ..tools.tool_search import dispatch_tool_search
query = arguments.get("query", "")
limit = arguments.get("limit", None)
result = dispatch_tool_search(
query=query,
limit=limit,
deferrable_tools=deferrable_tools,
config=self._tool_search_config
)
return json.dumps(result, indent=2)
except ImportError:
return "Tool search module not available"
except Exception as e:
logging.error(f"Error in tool_search: {e}")
return f"Error searching tools: {e}"
elif function_name == "tool_describe":
# Handle tool_describe bridge call
try:
from ..tools.tool_search import dispatch_tool_describe
tool_name = arguments.get("tool_name", "")
result = dispatch_tool_describe(
tool_name=tool_name,
deferrable_tools=deferrable_tools
)
return json.dumps(result, indent=2)
except ImportError:
return "Tool search module not available"
except Exception as e:
logging.error(f"Error in tool_describe: {e}")
return f"Error describing tool: {e}"
elif function_name == "tool_call":
# Handle tool_call bridge - unwrap and recurse with real tool
try:
from ..tools.tool_search import resolve_underlying_call
# Unwrap the real tool call
real_function_name, real_arguments = resolve_underlying_call(function_name, arguments)
# Validate that the real tool is in our deferrable set (security check)
deferrable_names = {
tool_def.get("function", {}).get("name", "")
for tool_def in deferrable_tools
}
if real_function_name not in deferrable_names:
return f"Tool '{real_function_name}' is not available for execution"
# Recursively execute the real tool (this will go through normal execution path)
return self.execute_tool(real_function_name, real_arguments, tool_call_id)
except ImportError:
return "Tool search module not available"
except Exception as e:
logging.error(f"Error in tool_call unwrap: {e}")
return f"Error executing tool: {e}"
else:
return f"Unknown bridge tool: {function_name}"
@@ -39,6 +39,7 @@ __all__ = [
"CachingConfig",
"HooksConfig",
"SkillsConfig",
"ToolSearchConfig",
# Type aliases
"MemoryParam",
"KnowledgeParam",
@@ -122,6 +123,7 @@ _MODULE_MAP = {
"CachingConfig": "feature_configs",
"HooksConfig": "feature_configs",
"SkillsConfig": "feature_configs",
"ToolSearchConfig": "feature_configs",
"MemoryParam": "feature_configs",
"KnowledgeParam": "feature_configs",
"PlanningParam": "feature_configs",
@@ -8,6 +8,7 @@ Provides dataclasses for consolidated feature configuration:
- ReflectionConfig: Self-reflection settings
- GuardrailConfig: Safety and validation
- WebConfig: Web search and fetch
- ToolSearchConfig: Progressive tool disclosure
All configs follow the agent-centric pattern:
- False: Feature disabled (zero overhead)
@@ -16,21 +17,22 @@ All configs follow the agent-centric pattern:
- Instance: Pre-configured manager/engine
Usage:
from praisonaiagents import Agent, MemoryConfig, KnowledgeConfig
from praisonaiagents import Agent, MemoryConfig, KnowledgeConfig, ToolSearchConfig
# Simple enable
agent = Agent(instructions="...", memory=True)
agent = Agent(instructions="...", memory=True, tool_search=True)
# With config
agent = Agent(
instructions="...",
memory=MemoryConfig(backend="redis", user_id="user123"),
knowledge=KnowledgeConfig(sources=["docs/"], rerank=True),
tool_search=ToolSearchConfig(enabled="auto", threshold_pct=15),
)
"""
from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional, Callable, Tuple, Union
from typing import Dict, List, Any, Optional, Callable, Tuple, Union, FrozenSet
from enum import Enum
# Import AutonomyConfig from canonical location (no circular dep)
@@ -1072,6 +1074,25 @@ class MultiAgentMemoryConfig:
}
# Import ToolSearchConfig from tools module to avoid duplication
def __get_tool_search_config():
try:
from ..tools.tool_search import ToolSearchConfig as _ToolSearchConfig
return _ToolSearchConfig
except ImportError:
# Fallback minimal config if tools module not available
@dataclass
class FallbackToolSearchConfig:
enabled: Union[bool, str] = "auto"
threshold_pct: float = 10.0
search_default_limit: int = 5
max_search_limit: int = 20
core_tools: Optional[FrozenSet[str]] = None
return FallbackToolSearchConfig
ToolSearchConfig = __get_tool_search_config()
class AutonomyLevel(str, Enum):
"""Autonomy levels for agent behavior."""
SUGGEST = "suggest"
@@ -1092,6 +1113,7 @@ CachingParam = Union[bool, CachingConfig]
HooksParam = Union[List[Any], HooksConfig]
SkillsParam = Union[List[str], SkillsConfig]
AutonomyParam = Union[bool, Dict[str, Any], "AutonomyConfig"]
ToolSearchParam = Union[bool, str, Dict[str, Any], ToolSearchConfig]
# =============================================================================
@@ -1343,6 +1365,21 @@ def resolve_autonomy(value: AutonomyParam) -> Optional[AutonomyConfig]:
return value
def resolve_tool_search(value: ToolSearchParam) -> Optional[ToolSearchConfig]:
"""Resolve tool_search= parameter following precedence ladder."""
if value is None or value is False:
return None
if value is True:
return ToolSearchConfig()
if isinstance(value, str):
return ToolSearchConfig(enabled=value)
if isinstance(value, dict):
return ToolSearchConfig(**value)
if isinstance(value, ToolSearchConfig):
return value
return value
__all__ = [
# Enums
"MemoryBackend",
@@ -1368,6 +1405,7 @@ __all__ = [
"HooksConfig",
"SkillsConfig",
"AutonomyConfig",
"ToolSearchConfig",
# Config classes (Multi-Agent)
"MultiAgentHooksConfig",
"MultiAgentOutputConfig",
@@ -1388,6 +1426,7 @@ __all__ = [
"HooksParam",
"SkillsParam",
"AutonomyParam",
"ToolSearchParam",
# Precedence ladder resolvers
"resolve_memory",
"resolve_knowledge",
@@ -1399,4 +1438,5 @@ __all__ = [
"resolve_execution",
"resolve_caching",
"resolve_autonomy",
"resolve_tool_search",
]
@@ -83,11 +83,23 @@ def __getattr__(name):
elif name == "OAUTH_CALLBACK_PATH":
from .mcp_oauth_callback import OAUTH_CALLBACK_PATH
return OAUTH_CALLBACK_PATH
# Protocols (lazy loaded)
elif name == "MCPClientProtocol":
from .protocols import MCPClientProtocol
return MCPClientProtocol
# Loader (lazy loaded)
elif name == "load_mcp_tools":
from .loader import load_mcp_tools
return load_mcp_tools
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
# Client
"MCP",
# Protocols (lazy loaded)
"MCPClientProtocol",
# Loader (lazy loaded)
"load_mcp_tools",
# Server (lazy loaded)
"ToolsMCPServer",
"launch_tools_mcp_server",
@@ -0,0 +1,92 @@
"""
MCP Tools Loader for PraisonAI Agents.
Provides a thin helper function to load MCP tools for agents
following the agent-centric design principles in AGENTS.md.
"""
from typing import List, Optional
from ..memory.mcp_config import MCPConfig, MCPConfigManager
from .mcp import MCP
def load_mcp_tools(
names: Optional[List[str]] = None,
*,
configs: Optional[List[MCPConfig]] = None,
prefix_tools: bool = True,
) -> List[MCP]:
"""
Load MCP tools for use with agents.
This is a thin helper that bridges the gap between configuration
and agent tool setup, following the "few lines to run an agent"
principle from AGENTS.md.
Args:
names: Specific config names to load (None = all enabled)
configs: Optional injected configs from wrapper TOML loader
prefix_tools: Whether to prefix tool names when multiple servers
are loaded to avoid collisions (default: True).
NOTE: Currently not implemented - collisions possible with multiple servers.
Returns:
List of MCP client instances ready for agent use
Examples:
# Load all enabled MCP servers from JSON config
>>> from praisonaiagents import Agent
>>> from praisonaiagents.mcp import load_mcp_tools
>>> agent = Agent(name="assistant", tools=load_mcp_tools())
# Load specific servers only
>>> tools = load_mcp_tools(["filesystem", "github"])
>>> agent = Agent(name="coder", tools=tools)
# Inject configs from wrapper TOML
>>> from praisonai.cli.configuration import get_config_loader
>>> loader = get_config_loader()
>>> config = loader.load()
>>> toml_configs = [MCPConfig(...) for server in config.mcp.servers]
>>> tools = load_mcp_tools(configs=toml_configs)
"""
mcp_clients: List[MCP] = []
if configs is not None:
# Use injected configs from wrapper (TOML loading)
target_configs = configs
if names:
# Filter by names if specified
target_configs = [c for c in configs if c.name in names]
else:
# Load from MCPConfigManager (JSON files)
manager = MCPConfigManager()
if names:
target_configs = [manager.get_config(name) for name in names]
target_configs = [c for c in target_configs if c and c.enabled]
else:
target_configs = manager.get_enabled_configs()
for config in target_configs:
if not config.enabled:
continue
# Convert config to MCP instance
mcp = config.to_mcp_instance()
if mcp:
# Apply tool filtering if configured (B3)
# TODO: Wire include/exclude filters when B3 is implemented
# Apply tool prefix if multiple servers (B4)
if prefix_tools and len(target_configs) > 1:
# Sanitize server name for use as prefix
server_name = config.name.replace('-', '_').replace(' ', '_')
# TODO: Tool name prefixing not yet implemented
# Need to modify tool names in MCP instance to avoid collisions
# e.g., "read_file" -> "filesystem_read_file"
pass
mcp_clients.append(mcp)
return mcp_clients
+133 -30
View File
@@ -29,8 +29,8 @@ class MCPToolRunner(threading.Thread):
super().__init__(daemon=True)
self.server_params = server_params
self.queue = queue.Queue()
self.result_queue = queue.Queue()
self.initialized = threading.Event()
self._init_error = None
self.tools = []
self.timeout = timeout
self._tool_timings = {}
@@ -66,12 +66,12 @@ class MCPToolRunner(threading.Thread):
if item is None: # Shutdown signal
break
tool_name, arguments = item
response_queue, tool_name, arguments = item
try:
result = await session.call_tool(tool_name, arguments)
self.result_queue.put((True, result))
response_queue.put((True, result))
except Exception as e:
self.result_queue.put((False, str(e)))
response_queue.put((False, str(e)))
except queue.Empty:
pass
@@ -80,8 +80,8 @@ class MCPToolRunner(threading.Thread):
except asyncio.CancelledError:
break
except Exception as e:
self._init_error = f"MCP initialization error: {str(e)}"
self.initialized.set() # Ensure we don't hang
self.result_queue.put((False, f"MCP initialization error: {str(e)}"))
def call_tool(self, tool_name, arguments):
"""Call an MCP tool and wait for the result."""
@@ -100,16 +100,25 @@ class MCPToolRunner(threading.Thread):
if telemetry:
telemetry.track_tool_usage(tool_name, success=False, execution_time=0)
return f"Error: MCP initialization timed out after {self.timeout} seconds"
if self._init_error:
if telemetry:
telemetry.track_tool_usage(tool_name, success=False, execution_time=0)
return f"Error: {self._init_error}"
# Start timing after initialization check
start_time = time.time()
is_success = False
response_queue = queue.Queue(maxsize=1)
try:
# Put request in queue
self.queue.put((tool_name, arguments))
# Put request in queue with caller-specific response channel
self.queue.put((response_queue, tool_name, arguments))
# Wait for result
success, result = self.result_queue.get()
# Wait for result with timeout
try:
success, result = response_queue.get(timeout=self.timeout)
except queue.Empty:
return f"Error: MCP tool call timed out after {self.timeout} seconds"
if not success:
return f"Error: {result}"
@@ -187,7 +196,8 @@ class MCP:
```
"""
def __init__(self, command_or_string=None, args=None, *, command=None, timeout=60, debug=False, **kwargs):
def __init__(self, command_or_string=None, args=None, *, command=None, timeout=60, debug=False,
allowed_tools: Optional[List[str]] = None, disabled_tools: Optional[List[str]] = None, **kwargs):
"""
Initialize the MCP connection and get tools.
@@ -201,7 +211,13 @@ class MCP:
command: Alternative parameter name for backward compatibility
timeout: Timeout in seconds for MCP server initialization and tool calls (default: 60)
debug: Enable debug logging for MCP operations (default: False)
allowed_tools: Include whitelist - only these tools will be available (default: None = all tools)
disabled_tools: Exclude blacklist - these tools will be filtered out (default: None = no exclusions)
**kwargs: Additional parameters for StdioServerParameters
Note:
If both allowed_tools and disabled_tools are specified, allowed_tools takes precedence
(include wins over exclude).
"""
# Check if MCP is available
if not MCP_AVAILABLE:
@@ -240,6 +256,8 @@ class MCP:
# Store additional parameters
self.timeout = timeout
self.debug = debug
self.allowed_tools = allowed_tools
self.disabled_tools = disabled_tools
# Check if this is a WebSocket URL (ws:// or wss://)
if isinstance(command_or_string, str) and re.match(r'^wss?://', command_or_string):
@@ -254,7 +272,7 @@ class MCP:
auth_token=auth_token,
options=kwargs
)
self._tools = list(self.websocket_client.tools)
self._tools = self._apply_tool_filters(list(self.websocket_client.tools))
self.is_sse = False
self.is_http_stream = False
self.is_websocket = True
@@ -268,7 +286,7 @@ class MCP:
# Legacy SSE URL - use SSE transport for backward compatibility
from .mcp_sse import SSEMCPClient
self.sse_client = SSEMCPClient(command_or_string, debug=debug, timeout=timeout)
self._tools = list(self.sse_client.tools)
self._tools = self._apply_tool_filters(list(self.sse_client.tools))
self.is_sse = True
self.is_http_stream = False
self.is_npx = False
@@ -295,7 +313,7 @@ class MCP:
timeout=timeout,
options=transport_options
)
self._tools = list(self.http_stream_client.tools)
self._tools = self._apply_tool_filters(list(self.http_stream_client.tools))
self.is_sse = False
self.is_http_stream = True
self.is_npx = False
@@ -325,20 +343,10 @@ class MCP:
self.is_sse = False
self.is_http_stream = False
# Ensure UTF-8 encoding in environment for Docker compatibility
env = kwargs.get('env', {})
if not env:
env = os.environ.copy()
# Always set Python encoding
env['PYTHONIOENCODING'] = 'utf-8'
# Only set locale variables on Unix systems
if platform.system() != 'Windows':
env.update({
'LC_ALL': 'C.UTF-8',
'LANG': 'C.UTF-8'
})
# Build safe environment for stdio MCP servers
# Use safe baseline + explicit env from config (B5 security policy)
custom_env = kwargs.get('env', {})
env = self._build_safe_env(custom_env)
kwargs['env'] = env
@@ -369,7 +377,7 @@ class MCP:
self._initialize_npx_mcp_tools(cmd, arguments)
else:
# Generate tool functions immediately and store them
self._tools = self._generate_tool_functions()
self._tools = self._apply_tool_filters(self._generate_tool_functions())
def _generate_tool_functions(self) -> List[Callable]:
"""
@@ -489,7 +497,7 @@ class MCP:
logging.debug(f"Initializing NPX MCP tools with command: {cmd} {' '.join(arguments)}")
# Generate tool functions using the regular MCP approach
self._tools = self._generate_tool_functions()
self._tools = self._apply_tool_filters(self._generate_tool_functions())
if self.debug:
logging.debug(f"Generated {len(self._tools)} NPX MCP tools")
@@ -499,6 +507,100 @@ class MCP:
logging.error(f"Failed to initialize NPX MCP tools: {e}")
raise RuntimeError(f"Failed to initialize NPX MCP tools: {e}")
def _apply_tool_filters(self, raw_tools: List[Any]) -> List[Any]:
"""
Apply tool filtering based on allowed_tools and disabled_tools.
Args:
raw_tools: Unfiltered tool list
Returns:
Filtered tool list
"""
if not self.allowed_tools and not self.disabled_tools:
return raw_tools
# Import filter functions
from .mcp_utils import filter_tools_by_allowlist, filter_disabled_tools
# Convert tools to the format expected by filter functions (list of dicts with 'name')
# Raw tools might be function objects, so we need to extract names
tool_defs = []
for tool in raw_tools:
if hasattr(tool, '__name__'):
tool_defs.append({"name": tool.__name__, "_tool_obj": tool})
elif isinstance(tool, dict) and 'name' in tool:
tool_defs.append(tool)
else:
# Skip tools that don't have a clear name
continue
# Include wins over exclude - apply allowlist exclusively if provided
if self.allowed_tools:
tool_defs = filter_tools_by_allowlist(tool_defs, self.allowed_tools)
elif self.disabled_tools:
tool_defs = filter_disabled_tools(tool_defs, self.disabled_tools)
# Convert back to original format
filtered_tools = []
for tool_def in tool_defs:
if "_tool_obj" in tool_def:
filtered_tools.append(tool_def["_tool_obj"])
else:
filtered_tools.append(tool_def)
return filtered_tools
def _build_safe_env(self, custom_env: Optional[dict] = None) -> dict:
"""
Build a safe environment for stdio MCP servers.
Merges only safe baseline environment variables with explicit custom ones,
following the security policy outlined in AGENTS.md.
Args:
custom_env: Optional custom environment variables from config
Returns:
Safe environment dictionary
"""
# Safe baseline environment variables
safe_baseline = {
'PATH': os.environ.get('PATH', ''),
'HOME': os.environ.get('HOME', os.path.expanduser('~')),
'USER': os.environ.get('USER', ''),
'LANG': os.environ.get('LANG', 'C.UTF-8'),
'LC_ALL': os.environ.get('LC_ALL', 'C.UTF-8'),
'PYTHONIOENCODING': 'utf-8',
}
# Platform-specific safe variables
if platform.system() == 'Windows':
safe_baseline.update({
'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''),
'COMSPEC': os.environ.get('COMSPEC', ''),
'USERNAME': os.environ.get('USERNAME', ''),
'USERPROFILE': os.environ.get('USERPROFILE', ''),
'APPDATA': os.environ.get('APPDATA', ''),
'LOCALAPPDATA': os.environ.get('LOCALAPPDATA', ''),
'TEMP': os.environ.get('TEMP', ''),
'TMP': os.environ.get('TMP', ''),
})
else:
safe_baseline.update({
'TMPDIR': os.environ.get('TMPDIR', '/tmp'),
'SHELL': os.environ.get('SHELL', '/bin/sh'),
})
# Start with safe baseline
env = {k: v for k, v in safe_baseline.items() if v} # Only include non-empty values
# Add explicit custom environment variables from config
if custom_env:
env.update(custom_env)
return env
def __iter__(self) -> Iterable[Callable]:
"""
Allow the MCP instance to be used directly as an iterable of tools.
@@ -609,7 +711,8 @@ class MCP:
"name": tool.name,
"description": tool.description if hasattr(tool, 'description') else f"Call the {tool.name} tool",
"parameters": parameters
}
},
"__praisonai_deferrable__": True # Mark MCP tools as deferrable for tool search
})
return openai_tools
@@ -156,7 +156,8 @@ class HTTPStreamMCPTool:
"name": self.name,
"description": self.description,
"parameters": fixed_schema
}
},
"__praisonai_deferrable__": True # Mark MCP tools as deferrable for tool search
}
class HTTPStreamTransport:
@@ -121,7 +121,8 @@ class SSEMCPTool:
"name": self.name,
"description": self.description,
"parameters": fixed_schema
}
},
"__praisonai_deferrable__": True # Mark MCP tools as deferrable for tool search
}
class SSEMCPClient:
@@ -363,7 +363,8 @@ class WebSocketMCPTool:
"name": self.name,
"description": self.description,
"parameters": fixed_schema
}
},
"__praisonai_deferrable__": True # Mark MCP tools as deferrable for tool search
}
class WebSocketMCPClient:
Loaded 100 of 187 files, more files were not shown because too many files have changed in this diff. Show more