Compare commits

...
71 Commits
Author SHA1 Message Date
Mervin Praison a14ca673da Release v3.12.2 2026-01-30 10:24:14 +00:00
github-actions[bot] 02d9d1e04f docs: auto-update api.md [skip ci] 2026-01-30 08:31:24 +00:00
Mervin Praison d3ee549ccc Release v3.12.1 2026-01-30 08:31:09 +00:00
github-actions[bot] 63fbd04af3 docs: auto-update api.md [skip ci] 2026-01-30 08:00:39 +00:00
Mervin Praison 51f95ad904 Release v3.12.0 2026-01-30 08:00:22 +00:00
Mervin Praison 44b800df0e Release v3.11.14 2026-01-30 05:27:32 +00:00
Mervin Praison 6e3a99b8b6 Release v3.11.13 2026-01-29 20:52:07 +00:00
Mervin Praison b7d53b8f11 Release v3.11.12 2026-01-29 18:23:32 +00:00
Mervin Praison 643fac5762 Release v3.11.11 2026-01-29 18:07:10 +00:00
Mervin Praison 92b71cbc8f Release v3.11.10 2026-01-29 17:59:29 +00:00
Mervin Praison d7555ba1b2 Release v3.11.9 2026-01-29 17:53:32 +00:00
github-actions[bot] 22dd13ec66 docs: auto-update api.md [skip ci] 2026-01-29 17:40:47 +00:00
Mervin Praison ba40e818e4 Release v3.11.8 2026-01-29 17:40:31 +00:00
Mervin Praison 12f829321c update 2026-01-29 16:39:37 +00:00
Mervin Praison 149ed5bd94 refactor(docker): move bot services to dedicated docker/bots/ folder
- Create docker/bots/ with own Dockerfile, docker-compose.yml, README
- Remove bot services from main docker-compose.yml
- Add .env.template with bot tokens
- Add README with setup instructions for Slack/Discord/Telegram
2026-01-29 16:16:31 +00:00
Mervin Praison 15f2937dd5 feat(docker): add bot Dockerfile and services
- Add Dockerfile.bot for Slack/Discord/Telegram bots
- Add slack-bot, discord-bot, telegram-bot services to docker-compose
- Add bot tokens to .env.template
- Secrets passed at runtime via environment variables
2026-01-29 16:15:23 +00:00
Mervin Praison d28199a18d feat(bots): fix asyncio error and add thread configuration
- Fix asyncio.run() error by using run_in_executor() for sync agent.chat()
- Add reply_in_thread config option (default: False for inline replies)
- Add thread_threshold for auto-threading long responses (default: 500 chars)
- Add INFO logging for message received/sent flow
2026-01-29 16:11:21 +00:00
Mervin Praison 0d3d6bb852 Release v3.11.4 2026-01-29 15:18:30 +00:00
Mervin Praison 5f93f83c9f fix: register bot/gateway/sandbox commands in CLI
- Add 'bot', 'gateway', 'sandbox', 'wizard', 'migrate', 'security', 'persistence' to LEGACY_COMMANDS in legacy.py
- Add same commands to special_commands list in main.py:797 (argparse fix)
- Fix handle_bot_command to parse list args from main.py
- 'praisonai bot slack --token $TOKEN' now works correctly
2026-01-29 14:23:40 +00:00
Mervin Praison c9786fbd1e Release v3.11.3 2026-01-29 14:17:18 +00:00
github-actions[bot] 95e8e38998 docs: auto-update api.md [skip ci] 2026-01-29 13:45:28 +00:00
Mervin Praison 73f8961e61 Release v3.11.2 2026-01-29 13:45:08 +00:00
github-actions[bot] 4caf10a982 docs: auto-update api.md [skip ci] 2026-01-29 12:31:08 +00:00
Mervin Praison fb42a1010a Release v3.11.1 2026-01-29 12:30:51 +00:00
github-actions[bot] fe60dd40af docs: auto-update api.md [skip ci] 2026-01-29 10:45:19 +00:00
Mervin Praison 7f37d754a7 Release v3.11.0 2026-01-29 10:45:00 +00:00
github-actions[bot] 26f60fb056 docs: auto-update api.md [skip ci] 2026-01-28 13:20:05 +00:00
Mervin Praison 0f0b640184 Release v3.10.27 2026-01-28 13:19:48 +00:00
Mervin Praison d857dfc6b7 Release v3.10.26 2026-01-27 15:07:21 +00:00
github-actions[bot] 0135a9e09f docs: auto-update api.md [skip ci] 2026-01-27 05:52:59 +00:00
Mervin Praison 847b5e6372 Release v3.10.25 2026-01-27 05:52:43 +00:00
github-actions[bot] 05a7265c4e docs: auto-update api.md [skip ci] 2026-01-26 18:39:42 +00:00
Mervin Praison de1734c29a Release v3.10.24 2026-01-26 18:39:28 +00:00
github-actions[bot] f0e91b11ae docs: auto-update api.md [skip ci] 2026-01-26 17:47:05 +00:00
Mervin Praison 6e1a9143e2 Release v3.10.23 2026-01-26 17:46:49 +00:00
github-actions[bot] 10d76d3a85 docs: auto-update api.md [skip ci] 2026-01-26 17:06:34 +00:00
Mervin Praison a38a9c06c6 Release v3.10.22 2026-01-26 17:06:19 +00:00
github-actions[bot] 9c90416ab5 docs: auto-update api.md [skip ci] 2026-01-26 08:08:44 +00:00
Mervin Praison efa5247344 Release v3.10.21 2026-01-26 08:08:18 +00:00
Mervin Praison 720213d82d Merge pull request #1100 from MervinPraison/pr-1099-fix
Pr 1099 fix
2026-01-25 00:47:27 +00:00
Loganaden Velvindron f6af16ef5e Fix range check 2026-01-25 00:46:37 +00:00
Loganaden Velvindron 8439b98ec2 Fix CVE-2026-22218 2026-01-25 00:45:51 +00:00
Loganaden Velvindron 92271fc118 Fix CVE-2026-22218
Allow building with chainlit 2.9.4 to fix CVE-2026-22218
2026-01-25 00:45:32 +00:00
Mervin Praison 1b43dc6a4b Update naming convention guide with list_X pattern and clarify decorator usage 2026-01-25 00:43:02 +00:00
Mervin Praison 929cefa55e Release v3.10.20 2026-01-25 00:36:03 +00:00
Mervin Praison 225843e39b Release v3.10.19 2026-01-25 00:25:39 +00:00
Mervin Praison 77913183a9 Release v3.10.18 2026-01-24 14:33:15 +00:00
Mervin Praison e5b5d747f4 Release v3.10.17 2026-01-24 09:26:02 +00:00
Mervin Praison 8cb779c51c Release v3.10.16 2026-01-24 07:32:55 +00:00
github-actions[bot] 020ca46c90 docs: auto-update api.md [skip ci] 2026-01-24 07:01:25 +00:00
Mervin Praison 557e243eed Release v3.10.15 2026-01-24 07:01:13 +00:00
Mervin Praison 0628204fce Update README.md 2026-01-23 14:46:55 +00:00
Mervin Praison 2a11bf5221 Release v3.10.14 2026-01-23 12:58:40 +00:00
github-actions[bot] ecc2182d24 docs: auto-update api.md [skip ci] 2026-01-23 12:16:33 +00:00
Mervin Praison c6b91b2d57 Release v3.10.13 2026-01-23 12:16:16 +00:00
github-actions[bot] a5ed17945f docs: auto-update api.md [skip ci] 2026-01-23 10:33:02 +00:00
Mervin Praison ff3ca3052d Release v3.10.12 2026-01-23 10:32:48 +00:00
github-actions[bot] 6f1f4a5363 docs: auto-update api.md [skip ci] 2026-01-23 07:32:54 +00:00
Mervin Praison dd9dfa5970 Release v3.10.11 2026-01-23 07:32:37 +00:00
Mervin Praison 5f4fc8fa2f Release v3.10.10 2026-01-23 05:18:03 +00:00
github-actions[bot] dd9dd5d5d9 docs: auto-update api.md [skip ci] 2026-01-23 03:47:00 +00:00
Mervin Praison d2c629b82c Release v3.10.9 2026-01-23 03:46:46 +00:00
Mervin Praison 609ac5f7a6 Release v3.10.8 2026-01-22 14:13:38 +00:00
Mervin Praison 75c72e909e Release v3.10.7 2026-01-22 13:47:01 +00:00
Mervin Praison 9492ff8de1 Release v3.10.6 2026-01-22 11:24:49 +00:00
Mervin Praison a0a3aa1a53 Release v3.10.5 2026-01-22 08:30:15 +00:00
Mervin Praison 6a20001551 Refactor port availability test to use socket.socket and relax import and profiling time assertions. 2026-01-22 07:49:51 +00:00
Mervin Praison 577295c147 Release v3.10.4 2026-01-22 07:06:28 +00:00
github-actions[bot] 06f64a551c docs: auto-update api.md [skip ci] 2026-01-22 05:16:01 +00:00
Mervin Praison 928b824180 Release v3.10.3 2026-01-22 05:15:48 +00:00
github-actions[bot] 1631973256 docs: auto-update api.md [skip ci] 2026-01-21 17:30:54 +00:00
381 changed files with 64574 additions and 3454 deletions

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
name: Performance Benchmarks
on:
workflow_dispatch:
inputs:
iterations:
description: 'Number of benchmark iterations'
required: false
default: '100'
fail_on_regression:
description: 'Fail if performance regresses'
required: false
default: 'true'
jobs:
benchmark:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e src/praisonai-agents[all]
pip install agno crewai langgraph
- name: Run Import Time Benchmark
id: import_time
run: |
cd src/praisonai-agents
python benchmarks/import_time.py 2>&1 | tee import_time_output.txt
# Extract import time
IMPORT_TIME=$(grep -oP 'Median:\s+\K[\d.]+' import_time_output.txt || echo "0")
echo "import_time=$IMPORT_TIME" >> $GITHUB_OUTPUT
# Check threshold (200ms)
if (( $(echo "$IMPORT_TIME > 200" | bc -l) )); then
echo "::warning::Import time ${IMPORT_TIME}ms exceeds 200ms threshold"
if [ "${{ inputs.fail_on_regression }}" == "true" ]; then
echo "import_passed=false" >> $GITHUB_OUTPUT
fi
else
echo "import_passed=true" >> $GITHUB_OUTPUT
fi
- name: Run Memory Usage Benchmark
id: memory
run: |
cd src/praisonai-agents
python benchmarks/memory_usage.py 2>&1 | tee memory_output.txt
# Extract peak memory
PEAK_MEMORY=$(grep -oP 'Peak:\s+\K[\d.]+' memory_output.txt || echo "0")
echo "peak_memory=$PEAK_MEMORY" >> $GITHUB_OUTPUT
# Check threshold (30MB)
if (( $(echo "$PEAK_MEMORY > 30" | bc -l) )); then
echo "::warning::Memory usage ${PEAK_MEMORY}MB exceeds 30MB threshold"
if [ "${{ inputs.fail_on_regression }}" == "true" ]; then
echo "memory_passed=false" >> $GITHUB_OUTPUT
fi
else
echo "memory_passed=true" >> $GITHUB_OUTPUT
fi
- name: Run Simple Benchmark
id: simple
run: |
cd src/praisonai-agents
python benchmarks/simple_benchmark.py 2>&1 | tee simple_output.txt
# Extract PraisonAI time
PRAISON_TIME=$(grep -oP 'PraisonAI\s+\K[\d.]+' simple_output.txt | head -1 || echo "0")
echo "praison_time=$PRAISON_TIME" >> $GITHUB_OUTPUT
- name: Run Tools Benchmark
id: tools
run: |
cd src/praisonai-agents
python benchmarks/tools_benchmark.py 2>&1 | tee tools_output.txt
# Extract PraisonAI time with tools
PRAISON_TOOLS_TIME=$(grep -oP 'PraisonAI\s+\K[\d.]+' tools_output.txt | head -1 || echo "0")
echo "praison_tools_time=$PRAISON_TOOLS_TIME" >> $GITHUB_OUTPUT
- name: Run Parameter Impact Benchmark
id: param_impact
run: |
cd src/praisonai-agents
python benchmarks/param_impact_benchmark.py --no-save --iterations ${{ inputs.iterations }} 2>&1 | tee param_impact_output.txt
- name: Check Lazy Imports
id: lazy_imports
run: |
cd src/praisonai-agents
python -c "
import sys
from praisonaiagents import Agent
agent = Agent(name='Test', output='silent')
# Check litellm is NOT loaded
if 'litellm' in sys.modules:
print('FAIL: litellm loaded during silent agent creation')
exit(1)
else:
print('PASS: litellm not loaded (lazy import working)')
"
- name: Generate Benchmark Summary
if: always()
run: |
echo "## Performance Benchmark Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Value | Threshold | Status |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|-----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Import Time | ${{ steps.import_time.outputs.import_time }}ms | <200ms | ${{ steps.import_time.outputs.import_passed == 'true' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Peak Memory | ${{ steps.memory.outputs.peak_memory }}MB | <30MB | ${{ steps.memory.outputs.memory_passed == 'true' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Agent Instantiation | ${{ steps.simple.outputs.praison_time }}μs | - | ℹ️ |" >> $GITHUB_STEP_SUMMARY
echo "| Agent with Tools | ${{ steps.tools.outputs.praison_tools_time }}μs | - | ℹ️ |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Lazy Import Check" >> $GITHUB_STEP_SUMMARY
echo "✅ litellm not loaded during silent agent creation" >> $GITHUB_STEP_SUMMARY
- name: Fail on Regression
if: inputs.fail_on_regression == 'true'
run: |
if [ "${{ steps.import_time.outputs.import_passed }}" == "false" ]; then
echo "::error::Import time regression detected"
exit 1
fi
if [ "${{ steps.memory.outputs.memory_passed }}" == "false" ]; then
echo "::error::Memory usage regression detected"
exit 1
fi
echo "All performance checks passed!"
-1
View File
@@ -137,7 +137,6 @@ PraisonAI Agents is the **fastest AI agent framework** for agent instantiation.
| PraisonAI (LiteLLM) | 7.56 | 2.00x |
| PydanticAI | 226.94 | 60.16x |
| LangGraph | 4,558.71 | 1,209x |
| CrewAI | 15,607.92 | 4,138x |
<details>
<summary>Run benchmarks yourself</summary>
+706 -48
View File
@@ -3,11 +3,27 @@
This file is auto-generated. Do not edit manually.
Regenerate with: `praisonai docs api-md --write`
# Shared Types
Types:
```python
from praisonaiagents import ContextPolicy, GuardrailResult, HandoffConfig, HandoffCycleError, HandoffDepthError, HandoffError, HandoffInputData, HandoffResult, HandoffTimeoutError, ReflectionOutput, StepResult, TaskOutput, ToolResult, ToolValidationError, WorkflowContext
```
Methods:
* <code title="class GuardrailResult">GuardrailResult.<a href="./src/praisonai-agents/praisonaiagents/guardrails/guardrail_result.py">from_tuple</a>(result: Tuple[bool, Any]) -> 'GuardrailResult'</code>
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_dict</a>(data: Dict[str, Any]) -> 'HandoffConfig'</code>
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">json</a>() -> Optional[str]</code>
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">to_dict</a>() -> dict</code>
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_dict</a>() -> Dict[str, Any]</code>
# Agents
Types:
```python
from praisonaiagents import Agent, Agents
from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
```
Methods:
@@ -22,7 +38,7 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat</a>(prompt, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, stream = None, task_name = None, task_description = None, task_id = None, config = None, force_retrieval = False, skip_retrieval = False, attachments = None)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat</a>(prompt, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, stream = None, task_name = None, task_description = None, task_id = None, config = None, force_retrieval = False, skip_retrieval = False, attachments = None, tool_choice = None)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value)</code>
@@ -33,7 +49,6 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">delete_history</a>(index: int) -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">delete_history_matching</a>(pattern: str) -> int</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">display_generating</a>(content: str, start_time: float)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">display_name</a>() -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">ephemeral</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute</a>(task, context = None)</code>
@@ -64,87 +79,666 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieve</a>(query: str, **kwargs) -> 'ContextPack'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run</a>(prompt: str, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous_async</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">session_id</a>() -> Optional[str]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">start</a>(prompt: str = None, **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">switch_model</a>(new_model: str) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">context_manager</a>()</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">current_plan</a>()</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">default_completion_checker</a>(task, agent_output)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">delete_state</a>(key: str) -> bool</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">display_token_usage</a>()</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">execute_task</a>(task_id)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_agent_details</a>(agent_name)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_state</a>() -> Dict[str, Any]</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">astart</a>()</code>
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">start</a>()</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">achat</a>(message: str, **kwargs) -> str</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">chat</a>(message: str, **kwargs) -> str</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">name</a>() -> str</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">rag</a>() -> Optional['RAG']</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">aanalyze_codebase</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">acreate_implementation_blueprint</a>(feature_request: str, context_analysis: Optional[Dict[str, Any]] = None) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">agenerate_prp</a>(feature_request: str, context_analysis: Optional[Dict[str, Any]] = None) -> str</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_codebase</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_codebase_with_gitingest</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_integration_points</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_test_patterns</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">build_implementation_blueprint</a>(feature_request: str, context_analysis: Dict[str, Any] = None) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">compile_context_documentation</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_implementation_blueprint</a>(feature_request: str, context_analysis: Optional[Dict[str, Any]] = None) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_quality_gates</a>(requirements: List[str]) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_validation_framework</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">execute_prp</a>(prp_file_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">extract_implementation_patterns</a>(project_path: str, ast_analysis: Dict[str, Any] = None) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">generate_comprehensive_prp</a>(feature_request: str, context_analysis: Dict[str, Any] = None) -> str</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">generate_feature_prp</a>(feature_request: str) -> str</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">generate_prp</a>(feature_request: str, context_analysis: Optional[Dict[str, Any]] = None) -> str</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">get_agent_interaction_summary</a>() -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">log_debug</a>(message: str, **kwargs)</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">perform_ast_analysis</a>(project_path: str) -> Dict[str, Any]</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">save_comprehensive_session_report</a>()</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">save_markdown_output</a>(content: str, filename: str, section_title: str = 'Output')</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">setup_logging</a>()</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">setup_output_directories</a>()</code>
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">start</a>(input_text: str) -> str</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">aresearch</a>(query: str, instructions: Optional[str] = None, model: Optional[str] = None, summary_mode: Optional[Literal['auto', 'detailed', 'concise']] = None, web_search: Optional[bool] = None, code_interpreter: Optional[bool] = None, mcp_servers: Optional[List[Dict[str, Any]]] = None, file_ids: Optional[List[str]] = None, file_search: Optional[bool] = None, file_search_stores: Optional[List[str]] = None) -> DeepResearchResponse</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">async_openai_client</a>()</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">clarify</a>(query: str, model: Optional[str] = None) -> str</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">follow_up</a>(query: str, previous_interaction_id: str, model: Optional[str] = None) -> DeepResearchResponse</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">gemini_client</a>()</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">openai_client</a>()</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">research</a>(query: str, instructions: Optional[str] = None, model: Optional[str] = None, summary_mode: Optional[Literal['auto', 'detailed', 'concise']] = None, web_search: Optional[bool] = None, code_interpreter: Optional[bool] = None, mcp_servers: Optional[List[Dict[str, Any]]] = None, file_ids: Optional[List[str]] = None, file_search: Optional[bool] = None, file_search_stores: Optional[List[str]] = None, stream: bool = True) -> DeepResearchResponse</code>
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">rewrite_query</a>(query: str, model: Optional[str] = None) -> str</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">achat</a>(prompt: str, temperature: float = 0.2, tools: Optional[List[Any]] = None, output_json: Optional[str] = None, output_pydantic: Optional[Any] = None, reasoning_steps: bool = False, **kwargs) -> Union[str, Dict[str, Any]]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">aedit</a>(image: str, prompt: str, mask: Optional[str] = None, n: int = 1, size: Optional[str] = None, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">agenerate</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">agenerate_image</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">avariation</a>(image: str, n: int = 1, size: Optional[str] = None, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">chat</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">edit</a>(image: str, prompt: str, mask: Optional[str] = None, n: int = 1, size: Optional[str] = None, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">generate</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">generate_image</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">litellm</a>()</code>
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">variation</a>(image: str, n: int = 1, size: Optional[str] = None, **kwargs) -> Dict[str, Any]</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">analyze_context</a>(context: str) -> str</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">analyze_context_sync</a>(context: str) -> str</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">create_plan</a>(request: str, agents: List['Agent'], tasks: Optional[List['Task']] = None, context: Optional[str] = None) -> Plan</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">create_plan_sync</a>(request: str, agents: List['Agent'], tasks: Optional[List['Task']] = None, context: Optional[str] = None) -> Plan</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">is_tool_allowed</a>(tool_name: str) -> bool</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">refine_plan</a>(plan: Plan, feedback: str) -> Plan</code>
* <code title="class PlanningAgent">PlanningAgent.<a href="./src/praisonai-agents/praisonaiagents/planning/planner.py">refine_plan_sync</a>(plan: Plan, feedback: str) -> Plan</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">agent</a>()</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand</a>(prompt: str, strategy: ExpandStrategy = ..., context: Optional[str] = None) -> ExpandResult</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_basic</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_creative</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_detailed</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_structured</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">add_abbreviation</a>(abbrev: str, expansion: str) -> None</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">add_abbreviations</a>(abbreviations: Dict[str, str]) -> None</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">agent</a>()</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite</a>(query: str, strategy: RewriteStrategy = ..., chat_history: Optional[List[Dict[str, str]]] = None, context: Optional[str] = None, num_queries: int = None) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_basic</a>(query: str) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_contextual</a>(query: str, chat_history: List[Dict[str, str]]) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_hyde</a>(query: str) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_multi_query</a>(query: str, num_queries: int = None) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_step_back</a>(query: str) -> RewriteResult</code>
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_sub_queries</a>(query: str) -> RewriteResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_context_agent</a>(llm: Optional[Union[str, Any]] = None, **kwargs) -> ContextAgent</code>
# Tools
Types:
```python
from praisonaiagents import Tools, tool
from praisonaiagents import BaseTool, FunctionTool, ToolRegistry, Tools, get_registry, get_tool, register_tool, tool, validate_tool
```
Methods:
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">__call__</a>(**kwargs) -> Any</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">get_schema</a>() -> Dict[str, Any]</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">run</a>(**kwargs) -> Any</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">safe_run</a>(**kwargs) -> ToolResult</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate</a>() -> bool</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_class</a>() -> bool</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">__call__</a>(*args, **kwargs) -> Any</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">injected_params</a>() -> Dict[str, Any]</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">run</a>(**kwargs) -> Any</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">clear</a>() -> None</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_plugins</a>() -> int</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_all</a>() -> Dict[str, Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_base_tools</a>() -> List[BaseTool]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools</a>() -> List[str]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False) -> None</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">unregister</a>(name: str) -> bool</code>
* <code title="class Tools">Tools.<a href="./src/praisonai-agents/praisonaiagents/tools/tools.py">internet_search</a>(*args, **kwargs)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_registry</a>() -> ToolRegistry</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None) -> None</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">tool</a>(func: Optional[Callable] = None) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_tool</a>(tool: Any) -> bool</code>
# Workflows
Types:
```python
from praisonaiagents import Loop, Parallel, Pipeline, Repeat, Route, Workflow, loop, parallel, repeat, route
```
Methods:
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">arun</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False) -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">astart</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False) -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">from_template</a>(uri: str, config: Optional[Dict[str, Any]] = None, offline: bool = False, **kwargs) -> 'Workflow'</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">memory_config</a>() -> Optional[Dict[str, Any]]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_complete</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_error</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_start</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_workflow_complete</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_workflow_start</a>() -> Optional[Callable]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">planning_llm</a>() -> Optional[str]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">reasoning</a>() -> bool</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">run</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False, stream: bool = None) -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">start</a>(input: str = '', **kwargs) -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">stream</a>() -> bool</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>() -> bool</code>
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>(value: bool)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">loop</a>(step: Any = None, steps: Optional[List[Any]] = None, over: Optional[str] = None, from_csv: Optional[str] = None, from_file: Optional[str] = None, var_name: str = 'item', parallel: bool = False, max_workers: Optional[int] = None, output_variable: Optional[str] = None) -> Loop</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">parallel</a>(steps: List) -> Parallel</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">repeat</a>(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None, max_iterations: int = 10) -> Repeat</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">route</a>(routes: Dict[str, List], default: Optional[List] = None) -> Route</code>
# DB
Types:
```python
from praisonaiagents import db
```
# Memory
Types:
```python
from praisonaiagents import Memory, MemoryBackend, MemoryConfig
```
Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_context_for_task</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_in_output: Optional[bool] = None) -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">calculate_quality_metrics</a>(output: str, expected_output: str, llm: Optional[str] = None, custom_prompt: Optional[str] = None) -> Dict[str, float]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">compute_quality_score</a>(completeness: float, relevance: float, clarity: float, accuracy: float, weights: Dict[str, float] = None) -> float</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_long_term</a>(memory_id: str) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memories</a>(memory_ids: List[str]) -> int</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memories_matching</a>(query: str, memory_type: Optional[str] = None, limit: int = 10) -> int</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memory</a>(memory_id: str, memory_type: Optional[str] = None) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_short_term</a>(memory_id: str) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">finalize_task_output</a>(content: str, agent_name: str, quality_score: float, threshold: float = 0.7, metrics: Dict[str, Any] = None, task_id: str = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_all_memories</a>() -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_learn_context</a>() -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">learn</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_all</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_entity_only</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_long_term</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_short_term</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_user_memory</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search</a>(query: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_entity</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_user_memory</a>(user_id: str, query: str, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_with_quality</a>(query: str, min_quality: float = 0.0, memory_type: Literal['short', 'long'] = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_entity</a>(name: str, type_: str, desc: str, relations: str)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_long_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_quality</a>(text: str, quality_score: float, task_id: Optional[str] = None, iteration: Optional[int] = None, metrics: Optional[Dict[str, float]] = None, memory_type: Literal['short', 'long'] = 'long') -> None</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_short_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_user_memory</a>(user_id: str, text: str, extra: Dict[str, Any] = None)</code>
* <code title="class MemoryConfig">MemoryConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
# Knowledge
Types:
```python
from praisonaiagents import Chunking, ChunkingStrategy, Knowledge, KnowledgeConfig
```
Methods:
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">SUPPORTED_CHUNKERS</a>() -> Dict[str, Any]</code>
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">__call__</a>(text: Union[str, List[str]], **kwargs) -> Union[List[Any], List[List[Any]]]</code>
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">chunk</a>(text: Union[str, List[str]], **kwargs) -> Union[List[Any], List[List[Any]]]</code>
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">chunker</a>()</code>
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">embedding_model</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">add</a>(file_path, user_id = None, agent_id = None, run_id = None, metadata = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">chunker</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">config</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">delete</a>(memory_id)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">delete_all</a>(user_id = None, agent_id = None, run_id = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get</a>(memory_id)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get_all</a>(user_id = None, agent_id = None, run_id = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get_corpus_stats</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">history</a>(memory_id)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">index</a>(path: str, incremental: bool = True, force: bool = False, include_glob: list = None, exclude_glob: list = None, user_id: str = None, agent_id: str = None, run_id: str = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">markdown</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">memory</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">normalize_content</a>(content)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">reset</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">search</a>(query, user_id = None, agent_id = None, run_id = None, rerank = None, **kwargs)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">store</a>(content, user_id = None, agent_id = None, run_id = None, metadata = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">update</a>(memory_id, data)</code>
* <code title="class KnowledgeConfig">KnowledgeConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
# RAG
Types:
```python
from praisonaiagents import CitationsMode, ContextPack, RAG, RAGCitation, RAGConfig, RAGResult, RetrievalConfig, RetrievalPolicy
```
Methods:
* <code title="class ContextPack">ContextPack.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">format_for_prompt</a>(include_sources: bool = True) -> str</code>
* <code title="class ContextPack">ContextPack.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'ContextPack'</code>
* <code title="class ContextPack">ContextPack.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">has_citations</a>() -> bool</code>
* <code title="class ContextPack">ContextPack.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">aquery</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> RAGResult</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">aretrieve</a>(query: str, **kwargs) -> ContextPack</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">astream</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> AsyncIterator[str]</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">get_citations</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> List[Citation]</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">llm</a>()</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">query</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> RAGResult</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">retrieve</a>(query: str, **kwargs) -> ContextPack</code>
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">stream</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> Iterator[str]</code>
* <code title="class RAGCitation">RAGCitation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'Citation'</code>
* <code title="class RAGCitation">RAGCitation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class RAGConfig">RAGConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'RAGConfig'</code>
* <code title="class RAGConfig">RAGConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">format_answer_with_citations</a>() -> str</code>
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'RAGResult'</code>
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">has_citations</a>() -> bool</code>
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">from_dict</a>(data: Dict[str, Any]) -> 'RetrievalConfig'</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">get_strategy</a>(corpus_stats = None)</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">get_token_budget</a>(model_name: Optional[str] = None)</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">should_retrieve</a>(query: str, force: bool = False, skip: bool = False) -> bool</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">to_knowledge_config</a>() -> Dict[str, Any]</code>
* <code title="class RetrievalConfig">RetrievalConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/retrieval_config.py">to_rag_config</a>() -> Dict[str, Any]</code>
# Handoff
Types:
```python
from praisonaiagents import Handoff, RECOMMENDED_PROMPT_PREFIX, handoff, handoff_filters, prompt_with_handoff_instructions
```
Methods:
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">default_tool_description</a>() -> str</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">default_tool_name</a>() -> str</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">execute_async</a>(source_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None) -> HandoffResult</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">execute_programmatic</a>(source_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None) -> HandoffResult</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_tool_function</a>(source_agent: 'Agent') -> Callable</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_description</a>() -> str</code>
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_name</a>() -> str</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None) -> Handoff</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">keep_last_n_messages</a>(n: int) -> Callable[[HandoffInputData], HandoffInputData]</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_all_tools</a>(data: HandoffInputData) -> HandoffInputData</code>
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_system_messages</a>(data: HandoffInputData) -> HandoffInputData</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">prompt_with_handoff_instructions</a>(base_prompt: str, agent: 'Agent') -> str</code>
# Guardrails
Types:
```python
from praisonaiagents import GuardrailAction, GuardrailConfig, LLMGuardrail
```
Methods:
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">__call__</a>(task_output: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]</code>
# Planning
Types:
```python
from praisonaiagents import ApprovalCallback, Plan, PlanStep, PlanStorage, PlanningConfig, READ_ONLY_TOOLS, RESTRICTED_TOOLS, TodoItem, TodoList
```
Methods:
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">__call__</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">always_approve</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">always_reject</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">approve_if_no_dangerous_tools</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">approve_if_small</a>(plan: 'Plan', max_steps: int = 5) -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">async_call</a>(plan: 'Plan') -> bool</code>
* <code title="class ApprovalCallback">ApprovalCallback.<a href="./src/praisonai-agents/praisonaiagents/planning/approval.py">console_approval</a>(plan: 'Plan') -> bool</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">add_step</a>(step: PlanStep) -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">approve</a>() -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">cancel</a>() -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">complete</a>() -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">completed_step_ids</a>() -> List[str]</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">from_dict</a>(data: Dict[str, Any]) -> 'Plan'</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">from_markdown</a>(markdown: str) -> 'Plan'</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">get_next_steps</a>() -> List[PlanStep]</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">get_step</a>(step_id: str) -> Optional[PlanStep]</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">is_complete</a>() -> bool</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">progress</a>() -> float</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">remove_step</a>(step_id: str) -> bool</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">start_execution</a>() -> None</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">to_markdown</a>() -> str</code>
* <code title="class Plan">Plan.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">update_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">from_dict</a>(data: Dict[str, Any]) -> 'PlanStep'</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">is_ready</a>(completed_steps: List[str]) -> bool</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">mark_complete</a>() -> None</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">mark_in_progress</a>() -> None</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">mark_skipped</a>() -> None</code>
* <code title="class PlanStep">PlanStep.<a href="./src/praisonai-agents/praisonaiagents/planning/plan.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">cleanup_old_plans</a>(keep_count: int = 10) -> int</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">delete_plan</a>(plan_id: str) -> bool</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">delete_todo</a>(name: str) -> bool</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">get_latest_plan</a>() -> Optional[Plan]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">list_plans</a>() -> List[Dict[str, Any]]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">list_todos</a>() -> List[str]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">load_plan</a>(plan_id: str) -> Optional[Plan]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">load_plan_from_file</a>(path: str) -> Optional[Plan]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">load_todo</a>(name: str = 'current') -> Optional[TodoList]</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">save_plan</a>(plan: Plan, filename: Optional[str] = None) -> str</code>
* <code title="class PlanStorage">PlanStorage.<a href="./src/praisonai-agents/praisonaiagents/planning/storage.py">save_todo</a>(todo: TodoList, name: str = 'current') -> str</code>
* <code title="class PlanningConfig">PlanningConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">complete</a>() -> None</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_dict</a>(data: Dict[str, Any]) -> 'TodoItem'</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">is_ready</a>(completed_ids: List[str]) -> bool</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">reset</a>() -> None</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">start</a>() -> None</code>
* <code title="class TodoItem">TodoItem.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">add</a>(item: Union[TodoItem, str]) -> TodoItem</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">complete</a>(item_id: str) -> bool</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">completed</a>() -> List[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">completed_ids</a>() -> List[str]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_dict</a>(data: Dict[str, Any]) -> 'TodoList'</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_json</a>(json_str: str) -> 'TodoList'</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_markdown</a>(markdown: str) -> 'TodoList'</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">from_plan</a>(plan: 'Plan') -> 'TodoList'</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">get</a>(item_id: str) -> Optional[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">get_ready_items</a>() -> List[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">in_progress</a>() -> List[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">is_complete</a>() -> bool</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">pending</a>() -> List[TodoItem]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">progress</a>() -> float</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">remove</a>(item_id: str) -> bool</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">start</a>(item_id: str) -> bool</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">sync_with_plan</a>(plan: 'Plan') -> None</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">to_json</a>() -> str</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">to_markdown</a>() -> str</code>
* <code title="class TodoList">TodoList.<a href="./src/praisonai-agents/praisonaiagents/planning/todo.py">update_from_plan</a>(plan: 'Plan') -> None</code>
# Skills
Types:
```python
from praisonaiagents import SkillLoader, SkillManager, SkillMetadata, SkillProperties, SkillsConfig
```
Methods:
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">activate</a>(skill: LoadedSkill) -> bool</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load</a>(skill_path: str, activate: bool = False) -> Optional[LoadedSkill]</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_all_resources</a>(skill: LoadedSkill) -> None</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_assets</a>(skill: LoadedSkill) -> dict</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_metadata</a>(skill_path: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_references</a>(skill: LoadedSkill) -> dict</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_scripts</a>(skill: LoadedSkill) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate</a>(skill: LoadedSkill) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate_by_name</a>(name: str) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill</a>(skill_path: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">clear</a>() -> None</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">discover</a>(skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> int</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_available_skills</a>() -> List[SkillMetadata]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_instructions</a>(name: str) -> Optional[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skill</a>(name: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">load_resources</a>(name: str) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skill_names</a>() -> List[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skills</a>() -> List[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">to_prompt</a>() -> str</code>
* <code title="class SkillMetadata">SkillMetadata.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">from_properties</a>(props: SkillProperties) -> 'SkillMetadata'</code>
* <code title="class SkillProperties">SkillProperties.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">to_dict</a>() -> dict</code>
* <code title="class SkillsConfig">SkillsConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
# Session
Types:
```python
from praisonaiagents import Session
```
Methods:
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">Agent</a>(name: str, role: str = 'Assistant', instructions: Optional[str] = None, tools: Optional[List[Any]] = None, memory: bool = True, knowledge: Optional[List[str]] = None, **kwargs) -> 'Agent'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_knowledge</a>(source: str) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_memory</a>(text: str, memory_type: str = 'long', **metadata) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">chat</a>(message: str, **kwargs) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">clear_memory</a>(memory_type: str = 'all') -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">create_agent</a>(*args, **kwargs) -> 'Agent'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_context</a>(query: str, max_items: int = 3) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_state</a>(key: str, default: Any = None) -> Any</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">increment_state</a>(key: str, increment: int = 1, default: int = 0) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">knowledge</a>() -> 'Knowledge'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">memory</a>() -> 'Memory'</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">restore_state</a>() -> Dict[str, Any]</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">save_state</a>(state_data: Dict[str, Any]) -> None</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_knowledge</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_memory</a>(query: str, memory_type: str = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">send_message</a>(message: str, **kwargs) -> str</code>
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">set_state</a>(key: str, value: Any) -> None</code>
# MCP
Types:
```python
from praisonaiagents import MCP
```
Methods:
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_tools</a>() -> List[Callable]</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">shutdown</a>()</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">to_openai_tool</a>()</code>
# Telemetry
Types:
```python
from praisonaiagents import MinimalTelemetry, TelemetryCollector, cleanup_telemetry_resources, disable_performance_mode, disable_telemetry, enable_performance_mode, enable_telemetry, get_telemetry
```
Methods:
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">flush</a>()</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">get_metrics</a>() -> Dict[str, Any]</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">shutdown</a>()</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_agent_execution</a>(agent_name: str = None, success: bool = True, async_mode: bool = False)</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_error</a>(error_type: str = None)</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_feature_usage</a>(feature_name: str)</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_task_completion</a>(task_name: str = None, success: bool = True)</code>
* <code title="class MinimalTelemetry">MinimalTelemetry.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">track_tool_usage</a>(tool_name: str, success: bool = True, execution_time: float = None)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">get_metrics</a>() -> Dict[str, Any]</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">record_cost</a>(cost: float, model: str = None)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">record_tokens</a>(prompt_tokens: int, completion_tokens: int, model: str = None)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">start</a>()</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">stop</a>()</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">trace_agent_execution</a>(agent_name: str, **attributes)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">trace_llm_call</a>(model: str = None, **attributes)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">trace_task_execution</a>(task_name: str, agent_name: str = None, **attributes)</code>
* <code title="class TelemetryCollector">TelemetryCollector.<a href="./src/praisonai-agents/praisonaiagents/telemetry/telemetry.py">trace_tool_call</a>(tool_name: str, **attributes)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">cleanup_telemetry_resources</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">disable_performance_mode</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">disable_telemetry</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">enable_performance_mode</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">enable_telemetry</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">get_telemetry</a>() -> 'MinimalTelemetry'</code>
# Observability
Types:
```python
from praisonaiagents import FlowDisplay, obs, track_workflow
```
Methods:
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">display</a>()</code>
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">start</a>()</code>
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">stop</a>()</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">track_workflow</a>()</code>
# Context
Types:
```python
from praisonaiagents import ContextManager, ManagerConfig
```
Methods:
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">capture_llm_boundary</a>(messages: List[Dict[str, Any]], tools: List[Dict[str, Any]]) -> SnapshotHookData</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">emergency_truncate</a>(messages: List[Dict[str, Any]], target_tokens: int) -> List[Dict[str, Any]]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">estimate_tokens</a>(text: str, validate: bool = False) -> Tuple[int, Optional[EstimationMetrics]]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_history</a>() -> List[Dict[str, Any]]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_last_snapshot_hook</a>() -> Optional[SnapshotHookData]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_resolved_config</a>() -> Dict[str, Any]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_stats</a>() -> Dict[str, Any]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_tool_budget</a>(tool_name: str) -> int</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">process</a>(messages: List[Dict[str, Any]], system_prompt: str = '', tools: Optional[List[Dict[str, Any]]] = None, trigger: Literal['turn', 'tool_call', 'manual', 'overflow'] = 'turn') -> Dict[str, Any]</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">register_snapshot_callback</a>(callback: Callable[[SnapshotHookData], None]) -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">reset</a>() -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">set_tool_budget</a>(tool_name: str, max_tokens: int, protected: bool = False) -> None</code>
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str) -> str</code>
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">from_env</a>() -> 'ManagerConfig'</code>
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">merge</a>(**overrides) -> 'ManagerConfig'</code>
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">to_dict</a>() -> Dict[str, Any]</code>
# Config
Types:
```python
from praisonaiagents import AutonomyConfig, AutonomyLevel, CachingConfig, ExecutionConfig, ExecutionPreset, GuardrailConfig, HooksConfig, KnowledgeConfig, MemoryConfig, MultiAgentExecutionConfig, MultiAgentHooksConfig, MultiAgentMemoryConfig, MultiAgentOutputConfig, MultiAgentPlanningConfig, OutputConfig, OutputPreset, PlanningConfig, ReflectionConfig, SkillsConfig, TemplateConfig, WebConfig, WebSearchProvider
```
Methods:
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class CachingConfig">CachingConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class HooksConfig">HooksConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class KnowledgeConfig">KnowledgeConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MemoryConfig">MemoryConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentExecutionConfig">MultiAgentExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentHooksConfig">MultiAgentHooksConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentMemoryConfig">MultiAgentMemoryConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentOutputConfig">MultiAgentOutputConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class MultiAgentPlanningConfig">MultiAgentPlanningConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class OutputConfig">OutputConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class PlanningConfig">PlanningConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class ReflectionConfig">ReflectionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class SkillsConfig">SkillsConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class TemplateConfig">TemplateConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class WebConfig">WebConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
# Display
Types:
```python
from praisonaiagents import async_display_callbacks, clean_triple_backticks, display_error, display_generating, display_instruction, display_interaction, display_self_reflection, display_tool_call, error_logs, register_display_callback, sync_display_callbacks
```
Methods:
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">clean_triple_backticks</a>(text: str) -> str</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_error</a>(message: str, console = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_generating</a>(content: str = '', start_time: Optional[float] = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_instruction</a>(message: str, console = None, agent_name: str = None, agent_role: str = None, agent_tools: List[str] = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_interaction</a>(message, response, markdown = True, generation_time = None, console = None, agent_name = None, agent_role = None, agent_tools = None, task_name = None, task_description = None, task_id = None, metrics = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_self_reflection</a>(message: str, console = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_tool_call</a>(message: str, console = None, tool_name: str = None, tool_input: dict = None, tool_output: str = None, elapsed_time: float = None, success: bool = True)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">register_display_callback</a>(display_type: str, callback_fn, is_async: bool = False)</code>
# Utilities
Types:
```python
from praisonaiagents import ArrayMode, is_policy_string, parse_policy_string, resolve, resolve_autonomy, resolve_caching, resolve_context, resolve_execution, resolve_guardrail_policies, resolve_guardrails, resolve_hooks, resolve_knowledge, resolve_memory, resolve_output, resolve_planning, resolve_reflection, resolve_routing, resolve_skills, resolve_web
```
Methods:
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/parse_utils.py">is_policy_string</a>(value: str) -> bool</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/parse_utils.py">parse_policy_string</a>(value: str) -> tuple</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve</a>(value: Any, param_name: str, config_class: Optional[Type] = None, presets: Optional[Dict[str, Any]] = None, default: Any = None, instance_check: Optional[Callable[[Any], bool]] = None, url_schemes: Optional[Dict[str, str]] = None, array_mode: Optional[str] = None, string_mode: Optional[str] = None) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_autonomy</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_caching</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_context</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_execution</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_guardrail_policies</a>(policies: list, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_guardrails</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_hooks</a>(value: Any, config_class: Optional[Type] = None) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_knowledge</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_memory</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_output</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_planning</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_reflection</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_routing</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_skills</a>(value: Any, config_class: Type) -> Any</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/config/param_resolver.py">resolve_web</a>(value: Any, config_class: Type) -> Any</code>
# Other
Types:
```python
from praisonaiagents import EmbeddingResult, Task, aembedding, aembeddings, embedding, embeddings, get_dimensions
from praisonaiagents import AgentManager, EmbeddingResult, Task, aembedding, aembeddings, embedding, embeddings, get_dimensions
```
Methods:
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">context_manager</a>()</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">current_plan</a>()</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">default_completion_checker</a>(task, agent_output)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">delete_state</a>(key: str) -> bool</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">display_token_usage</a>()</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">execute_task</a>(task_id)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_agent_details</a>(agent_name)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_state</a>() -> Dict[str, Any]</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class AgentManager">AgentManager.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback</a>(task_output: TaskOutput) -> None</code>
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback_sync</a>(task_output: TaskOutput) -> None</code>
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">initialize_memory</a>()</code>
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">store_in_memory</a>(content: str, agent_name: str = None, task_id: str = None)</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">embedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">embeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/dimensions.py">get_dimensions</a>(model_name: str) -> int</code>
# Wrapper (praisonai)
Types:
```python
from praisonai import CloudProvider, Deploy, DeployConfig, DeployType, PraisonAI, __version__
from praisonai import AgentApp, CloudProvider, Deploy, DeployConfig, DeployType, PraisonAI, __version__
```
# CLI
@@ -156,6 +750,7 @@ Methods:
* <code title="cli">praisonai agents create <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai agents info <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai agents list <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai app app <a href="./src/praisonai/praisonai/cli/commands/app.py">--help</a></code>
* <code title="cli">praisonai audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.py">--help</a></code>
* <code title="cli">praisonai batch batch-run <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
* <code title="cli">praisonai batch list <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
@@ -169,6 +764,19 @@ Methods:
* <code title="cli">praisonai benchmark profile <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark sdk <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark workflow <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai bot bot-callback <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot discord <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot slack <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot telegram <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai browser browser-callback <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser click <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser navigate <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser open <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser profiles <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser screenshot <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser snapshot <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser status <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser type <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai call call-main <a href="./src/praisonai/praisonai/cli/commands/call.py">--help</a></code>
* <code title="cli">praisonai chat <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai chat chat-main <a href="./src/praisonai/praisonai/cli/commands/chat.py">--help</a></code>
@@ -229,6 +837,8 @@ Methods:
* <code title="cli">praisonai environment doctor <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
* <code title="cli">praisonai environment view <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
* <code title="cli">praisonai eval accuracy <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
* <code title="cli">praisonai eval judge <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
* <code title="cli">praisonai eval list-judges <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
* <code title="cli">praisonai eval performance <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
* <code title="cli">praisonai examples info <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
* <code title="cli">praisonai examples list <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
@@ -243,6 +853,8 @@ Methods:
* <code title="cli">praisonai knowledge index <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
* <code title="cli">praisonai knowledge list <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
* <code title="cli">praisonai knowledge search <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
* <code title="cli">praisonai loop help <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
* <code title="cli">praisonai loop loop-main <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
* <code title="cli">praisonai lsp logs <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai lsp lsp-callback <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai lsp start <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
@@ -268,6 +880,12 @@ Methods:
* <code title="cli">praisonai package install <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai package list <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai package uninstall <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai plugins disable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins doctor <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins enable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins info <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins list <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins plugins-callback <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai profile imports <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
* <code title="cli">praisonai profile optimize <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
* <code title="cli">praisonai profile profile-callback <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
@@ -281,11 +899,24 @@ Methods:
* <code title="cli">praisonai rag query <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
* <code title="cli">praisonai rag serve <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
* <code title="cli">praisonai realtime realtime-main <a href="./src/praisonai/praisonai/cli/commands/realtime.py">--help</a></code>
* <code title="cli">praisonai recipe apply <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe create <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe info <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe install <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe judge <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe list <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe optimize <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe run <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe serve <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai registry list <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
* <code title="cli">praisonai registry serve <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
* <code title="cli">praisonai replay cleanup <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay context <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay dashboard <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay delete <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay flow <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay list <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay show <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai research research-main <a href="./src/praisonai/praisonai/cli/commands/research.py">--help</a></code>
* <code title="cli">praisonai retrieval index <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
* <code title="cli">praisonai retrieval query <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
@@ -295,6 +926,11 @@ Methods:
* <code title="cli">praisonai rules list <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
* <code title="cli">praisonai run <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai run run-main <a href="./src/praisonai/praisonai/cli/commands/run.py">--help</a></code>
* <code title="cli">praisonai sandbox explain <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox list <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox recreate <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox sandbox-callback <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox status <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai schedule delete <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
@@ -304,17 +940,33 @@ Methods:
* <code title="cli">praisonai schedule start <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule stats <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule stop <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai serve a2a <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve a2u <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve acp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve agents <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve docs <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve gateway <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve lsp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve mcp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve rag <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve recipe <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve registry <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve scheduler <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve serve-callback <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve start <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve status <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve stop <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve ui <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve unified <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai session delete <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session export <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session import <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session list <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session resume <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session show <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai skills check <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills create <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills eligible <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills info <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills install <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills list <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
@@ -335,11 +987,16 @@ Methods:
* <code title="cli">praisonai tools info <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
* <code title="cli">praisonai tools list <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
* <code title="cli">praisonai tools test <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
* <code title="cli">praisonai tools validate <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
* <code title="cli">praisonai traces disable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
* <code title="cli">praisonai traces enable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
* <code title="cli">praisonai traces list <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
* <code title="cli">praisonai traces status <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
* <code title="cli">praisonai train train-main <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train agents <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train list <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train llm <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train show <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train train-callback <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai ui chat <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui code <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui gradio <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
@@ -405,7 +1062,8 @@ export type { DbAdapter, DbConfig, DbMessage, DbRun, DbTrace } from "./db";
export { MemoryPostgresAdapter, NeonPostgresAdapter, PostgresSessionStorage, createMemoryPostgres, createNeonPostgres, createPostgresSessionStorage } from "./db/postgres";
export { MemoryRedisAdapter, UpstashRedisAdapter, createMemoryRedis, createUpstashRedis } from "./db/redis";
export { SQLiteAdapter, createSQLiteAdapter } from "./db/sqlite";
export { EvalResults, EvalSuite, Evaluator, accuracyEval, containsKeywordsCriterion, createDefaultEvaluator, createEvalResults, createEvaluator, lengthCriterion, noHarmfulContentCriterion, performanceEval, relevanceCriterion, reliabilityEval } from "./eval";
export { // LLM-as-Judge
Judge, AccuracyJudge, CriteriaJudge, EvalResults, EvalSuite, Evaluator, RecipeJudge, accuracyEval, addJudge, addOptimizationRule, containsKeywordsCriterion, createDefaultEvaluator, createEvalResults, createEvaluator, getJudge, getOptimizationRule, lengthCriterion, listJudges, listOptimizationRules, noHarmfulContentCriterion, parseJudgeResponse, performanceEval, relevanceCriterion, reliabilityEval, removeJudge, removeOptimizationRule } from "./eval";
export { AgentEventBus, AgentEvents, EventEmitterPubSub, PubSub, createEventBus, createPubSub } from "./events";
export { LLMGuardrail, createLLMGuardrail } from "./guardrails/llm-guardrail";
export { DisplayTypes, HooksManager, WorkflowHooksExecutor, clearAllCallbacks, clearApprovalCallback, createHooksManager, createLoggingOperationHooks, createLoggingWorkflowHooks, createTimingWorkflowHooks, createValidationOperationHooks, createWorkflowHooks, executeCallback, executeSyncCallback, getRegisteredDisplayTypes, hasApprovalCallback, registerApprovalCallback, registerDisplayCallback, requestApproval, unregisterDisplayCallback } from "./hooks";
+6
View File
@@ -13,6 +13,12 @@ GOOGLE_API_KEY=your_google_api_key_here
GROQ_API_KEY=your_groq_api_key_here
COHERE_API_KEY=your_cohere_api_key_here
# Bot Tokens (for messaging bot services)
SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
SLACK_APP_TOKEN=xapp-your-slack-app-token
DISCORD_BOT_TOKEN=your-discord-bot-token
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
# Optional: Service Configuration
FLASK_ENV=development
CHAINLIT_HOST=0.0.0.0
+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>=3.10.2" \
"praisonai>=3.12.2" \
"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>=3.10.2" \
"praisonai>=3.12.2" \
"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>=3.10.2" \
"praisonai>=3.12.2" \
"praisonai[ui]" \
"praisonai[crewai]"
+15
View File
@@ -0,0 +1,15 @@
# Bot Environment Variables
# Copy to .env and fill in your values
# Required: LLM API Key
OPENAI_API_KEY=your_openai_api_key_here
# Slack Bot (Socket Mode)
SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
SLACK_APP_TOKEN=xapp-your-slack-app-token
# Discord Bot
DISCORD_BOT_TOKEN=your-discord-bot-token
# Telegram Bot
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
+42
View File
@@ -0,0 +1,42 @@
# Dockerfile for PraisonAI Slack Bot
# Secrets are passed at runtime via environment variables
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
python3-dev \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Create praison config directory
RUN mkdir -p /root/.praison
# Install praisonai with bot dependencies
RUN pip install --no-cache-dir \
praisonai \
slack-bolt \
slack-sdk \
"discord.py>=2.0.0" \
"python-telegram-bot>=20.0"
# Set environment variables for directory management
ENV PRAISON_CONFIG_DIR=/root/.praison
ENV DOCKER_CONTAINER=true
# Bot tokens should be passed at runtime (not baked into image)
# SLACK_BOT_TOKEN=xoxb-...
# SLACK_APP_TOKEN=xapp-...
# Optional: Copy custom agent configuration
# COPY agent.yaml /app/agent.yaml
# Expose no ports - Socket Mode doesn't need them
# For webhook mode, expose port 3000
# Default command - tokens from environment
CMD ["praisonai", "bot", "slack"]
+66
View File
@@ -0,0 +1,66 @@
# PraisonAI Bot Docker Deployment
Deploy Slack, Discord, or Telegram bots using Docker.
## Quick Start
```bash
# 1. Copy environment template
cp .env.template .env
# 2. Edit .env with your tokens
nano .env
# 3. Run specific bot
docker compose up slack-bot -d
# Or run all bots
docker compose up -d
```
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `OPENAI_API_KEY` | Yes | LLM API key |
| `SLACK_BOT_TOKEN` | For Slack | Bot token (xoxb-...) |
| `SLACK_APP_TOKEN` | For Slack | App token (xapp-...) |
| `DISCORD_BOT_TOKEN` | For Discord | Bot token |
| `TELEGRAM_BOT_TOKEN` | For Telegram | Bot token |
## Slack Setup
1. Create app at https://api.slack.com/apps
2. Enable **Socket Mode** → Generate App-Level Token with `connections:write`
3. Add **OAuth Scopes**: `chat:write`, `app_mentions:read`, `im:history`
4. Enable **Event Subscriptions** → Subscribe to `app_mention`, `message.im`
5. **Install to Workspace**
6. Copy Bot Token and App Token to `.env`
## Commands
```bash
# Start Slack bot
docker compose up slack-bot -d
# View logs
docker compose logs -f slack-bot
# Stop bot
docker compose down slack-bot
# Rebuild after updates
docker compose build --no-cache slack-bot
```
## Custom Agent Configuration
Mount your agent config:
```yaml
services:
slack-bot:
volumes:
- ./agent.yaml:/app/agent.yaml
command: ["praisonai", "bot", "slack", "--agent", "/app/agent.yaml"]
```
+53
View File
@@ -0,0 +1,53 @@
version: '3.8'
# PraisonAI Bot Services
# Run specific bots: docker compose up slack-bot
# Run all bots: docker compose up
services:
# Slack Bot
slack-bot:
build:
context: ../..
dockerfile: docker/bots/Dockerfile
volumes:
- praison_data:/root/.praison
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}
SLACK_APP_TOKEN: ${SLACK_APP_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai", "bot", "slack" ]
restart: unless-stopped
# Discord Bot
discord-bot:
build:
context: ../..
dockerfile: docker/bots/Dockerfile
volumes:
- praison_data:/root/.praison
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai", "bot", "discord" ]
restart: unless-stopped
# Telegram Bot
telegram-bot:
build:
context: ../..
dockerfile: docker/bots/Dockerfile
volumes:
- praison_data:/root/.praison
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai", "bot", "telegram" ]
restart: unless-stopped
volumes:
praison_data:
driver: local
+5 -4
View File
@@ -22,7 +22,7 @@ services:
CHAINLIT_DB_DIR: /root/.praison
command: praisonai ui --host 0.0.0.0
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8082"]
test: [ "CMD", "curl", "-f", "http://localhost:8082" ]
interval: 10s
timeout: 5s
retries: 3
@@ -49,7 +49,7 @@ services:
CHAINLIT_DB_DIR: /root/.praison
command: praisonai chat --host 0.0.0.0 --port 8083
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8083"]
test: [ "CMD", "curl", "-f", "http://localhost:8083" ]
interval: 10s
timeout: 5s
retries: 3
@@ -69,7 +69,7 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY}
PRAISON_CONFIG_DIR: /root/.praison
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
test: [ "CMD", "curl", "-f", "http://localhost:8080/health" ]
interval: 10s
timeout: 5s
retries: 3
@@ -89,6 +89,7 @@ services:
PRAISON_CONFIG_DIR: /root/.praison
networks:
- praison_network
# For bot services (Slack, Discord, Telegram), see docker/bots/
volumes:
praison_data:
@@ -96,4 +97,4 @@ volumes:
networks:
praison_network:
driver: bridge
driver: bridge
+17 -14
View File
@@ -1,21 +1,24 @@
"""
Basic Memory Example - Agent-Centric API
Demonstrates the simplest memory usage with consolidated params.
"""
"""Basic memory example - minimal usage."""
from praisonaiagents import Agent
# Basic: Enable memory with preset
# Simple enable (uses default file-based memory)
agent = Agent(
instructions="You are a helpful assistant with memory.",
memory="sqlite", # Preset: sqlite, redis, postgres, file
memory=True,
)
# Run a simple task
response = agent.start("Remember that my favorite color is blue.")
print(response)
# With preset
agent_redis = Agent(
instructions="You are a helpful assistant.",
memory="redis", # Uses redis preset
)
# Follow-up to test memory
response = agent.start("What is my favorite color?")
print(response)
# With URL
agent_postgres = Agent(
instructions="You are a helpful assistant.",
memory="postgresql://localhost/mydb", # URL auto-detected
)
if __name__ == "__main__":
response = agent.chat("Remember that my favorite color is blue.")
print(response)
+16 -20
View File
@@ -1,26 +1,22 @@
"""
Basic Workflow Example - Agent-Centric API
Demonstrates minimal workflow with consolidated params.
"""
"""Basic workflow example with consolidated params."""
from praisonaiagents import Agent
from praisonaiagents.workflows import Workflow, WorkflowStep
# Create a simple workflow
# Create agents
writer = Agent(instructions="You are a content writer.")
editor = Agent(instructions="You are an editor.")
# Create workflow with consolidated params
workflow = Workflow(
name="simple_workflow",
output="verbose", # Workflow-level output preset
name="Content Pipeline",
steps=[
WorkflowStep(name="write", agent=writer, action="Write about {{topic}}"),
WorkflowStep(name="edit", agent=editor, action="Edit the content", context=["write"]),
],
output="verbose",
planning=True,
)
# Add a step
step = WorkflowStep(
name="writer",
action="Write a haiku about programming",
agent=Agent(instructions="You are a creative poet."),
output="result.txt", # Save output to file
)
# Note: This is a structural example - actual execution requires workflow.run()
print("Workflow created with output preset:", workflow.output)
print("Step output file:", step.output_file)
if __name__ == "__main__":
result = workflow.run(variables={"topic": "AI agents"})
print(result)
+20 -15
View File
@@ -2,32 +2,34 @@
"""
Basic Hooks Example for PraisonAI Agents.
This example demonstrates how to use the hooks system to:
This example demonstrates the simplified hook API using add_hook():
1. Log tool calls before execution
2. Block dangerous operations
3. Add context to agent responses
4. Monitor session lifecycle
For the simplest example, see simple_hooks.py
Usage:
python basic_hooks.py
"""
import asyncio
from praisonaiagents import Agent
from praisonaiagents.hooks import (
HookRegistry, HookRunner, HookEvent, HookResult,
add_hook, remove_hook, has_hook, get_default_registry,
HookRunner, HookEvent, HookResult,
BeforeToolInput, AfterToolInput
)
def main():
# Create a hook registry
registry = HookRegistry()
# Clear any existing hooks from previous runs
get_default_registry().clear()
# ==========================================================================
# Hook 1: Log all tool calls
# Hook 1: Log all tool calls (using string event name)
# ==========================================================================
@registry.on(HookEvent.BEFORE_TOOL)
@add_hook('before_tool')
def log_tool_calls(event_data: BeforeToolInput) -> HookResult:
"""Log every tool call before execution."""
print(f"\n📝 [LOG] Tool: {event_data.tool_name}")
@@ -35,9 +37,9 @@ def main():
return HookResult.allow()
# ==========================================================================
# Hook 2: Block dangerous file operations (using function logic)
# Hook 2: Block dangerous file operations
# ==========================================================================
@registry.on(HookEvent.BEFORE_TOOL)
@add_hook('before_tool')
def block_delete_operations(event_data: BeforeToolInput) -> HookResult:
"""Block any delete or remove operations."""
dangerous_keywords = ['delete', 'remove', 'rm', 'unlink']
@@ -53,7 +55,7 @@ def main():
# ==========================================================================
# Hook 3: Require confirmation for write operations
# ==========================================================================
@registry.on(HookEvent.BEFORE_TOOL)
@add_hook('before_tool')
def confirm_write_operations(event_data: BeforeToolInput) -> HookResult:
"""Request confirmation for write operations."""
write_keywords = ['write', 'save', 'create', 'update']
@@ -62,14 +64,13 @@ def main():
if any(kw in tool_lower for kw in write_keywords):
print(f"\n⚠️ [CONFIRM] Write operation: {event_data.tool_name}")
# In a real scenario, you might prompt the user here
# For this example, we'll auto-approve
return HookResult.allow("Auto-approved for demo")
return HookResult.allow()
# ==========================================================================
# Hook 4: Add timing information after tool execution
# ==========================================================================
@registry.on(HookEvent.AFTER_TOOL)
@add_hook('after_tool')
def log_tool_completion(event_data: AfterToolInput) -> HookResult:
"""Log tool completion with timing."""
print(f"\n✅ [DONE] Tool: {event_data.tool_name}")
@@ -82,10 +83,14 @@ def main():
# Test the hooks
# ==========================================================================
print("=" * 60)
print("Testing Hooks System")
print("Testing Hooks System (using add_hook API)")
print("=" * 60)
runner = HookRunner(registry)
# Check hooks are registered
print(f"\nHooks registered for 'before_tool': {has_hook('before_tool')}")
print(f"Hooks registered for 'after_tool': {has_hook('after_tool')}")
runner = HookRunner(get_default_registry())
# Test 1: Normal tool call (should be logged and allowed)
print("\n--- Test 1: Normal read operation ---")
@@ -132,7 +137,7 @@ def main():
print("\n" + "=" * 60)
print("Registered Hooks:")
print("=" * 60)
for event, hooks in registry.list_hooks().items():
for event, hooks in get_default_registry().list_hooks().items():
print(f"\n{event}:")
for hook in hooks:
print(f" - {hook['name']} (matcher: {hook['matcher'] or '*'})")
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
Simple Hooks Example for PraisonAI Agents.
This example shows the SIMPLEST way to use hooks with the add_hook API.
Just a few lines of code to intercept tool calls!
No need to import HookResult - just return None/True to allow,
False to deny, or a string to deny with a reason.
Usage:
python simple_hooks.py
"""
from praisonaiagents.hooks import add_hook, has_hook
# =============================================================================
# Register hooks with simple string event names
# =============================================================================
@add_hook('before_tool')
def log_tools(event_data):
"""Log every tool call. Return nothing (None) to allow."""
print(f"🔧 Tool: {event_data.tool_name}")
# No return needed - defaults to allow
@add_hook('before_tool')
def block_dangerous(event_data):
"""Block delete operations. Return False or string to deny."""
if 'delete' in event_data.tool_name.lower():
print(f"🚫 Blocked: {event_data.tool_name}")
return "Delete operations are not allowed" # String = deny with reason
# No return = allow
@add_hook('after_tool')
def log_completion(event_data):
"""Log tool completion."""
print(f"✅ Done: {event_data.tool_name} ({event_data.execution_time_ms:.0f}ms)")
# =============================================================================
# Test the hooks
# =============================================================================
if __name__ == "__main__":
print("=" * 50)
print("Simple Hooks Example")
print("=" * 50)
# Check hooks are registered
print(f"\nHooks registered for 'before_tool': {has_hook('before_tool')}")
print(f"Hooks registered for 'after_tool': {has_hook('after_tool')}")
# You can now create an Agent and the hooks will be applied
print("\n💡 Hooks are now globally registered!")
print(" Any Agent will automatically use these hooks.")
print("\n Example:")
print(" from praisonaiagents import Agent")
print(" agent = Agent(instructions='You are helpful')")
print(" agent.start('Help me with files')")
print("\n📝 Hook returns:")
print(" - None or True → Allow the operation")
print(" - False → Deny the operation")
print(" - 'reason' → Deny with a custom message")
+76
View File
@@ -0,0 +1,76 @@
"""
Messaging Bot Example - Deploy Agents to Telegram, Discord, Slack
This example demonstrates how to configure bots for different
messaging platforms.
"""
import os
from praisonaiagents import Agent, BotConfig
# Telegram Bot Configuration
telegram_config = BotConfig(
token=os.environ.get("TELEGRAM_BOT_TOKEN", ""),
command_prefix="/",
mention_required=True,
typing_indicator=True,
max_message_length=4096,
metadata={"platform": "telegram"}
)
# Discord Bot Configuration
discord_config = BotConfig(
token=os.environ.get("DISCORD_BOT_TOKEN", ""),
command_prefix="!",
mention_required=True,
typing_indicator=True,
metadata={
"platform": "discord",
"guild_ids": [] # Add your guild IDs here
}
)
# Slack Bot Configuration
slack_config = BotConfig(
token=os.environ.get("SLACK_BOT_TOKEN", ""),
command_prefix="/",
mention_required=True,
typing_indicator=True,
metadata={
"platform": "slack",
"app_token": os.environ.get("SLACK_APP_TOKEN", "")
}
)
# Create the agent that will handle messages
assistant = Agent(
name="assistant",
instructions="""You are a helpful assistant that responds to messages
from users on messaging platforms. Be friendly and concise.""",
llm="gpt-4o-mini"
)
if __name__ == "__main__":
print("Bot Configurations:")
print()
print("Telegram Config:")
print(f" Token configured: {bool(telegram_config.token)}")
print(f" Command prefix: {telegram_config.command_prefix}")
print(f" Mention required: {telegram_config.mention_required}")
print()
print("Discord Config:")
print(f" Token configured: {bool(discord_config.token)}")
print(f" Command prefix: {discord_config.command_prefix}")
print()
print("Slack Config:")
print(f" Token configured: {bool(slack_config.token)}")
print(f" Command prefix: {slack_config.command_prefix}")
print()
print("To start a bot, use the CLI:")
print(" praisonai bot telegram --token $TELEGRAM_BOT_TOKEN")
print(" praisonai bot discord --token $DISCORD_BOT_TOKEN")
print(" praisonai bot slack --token $SLACK_BOT_TOKEN")
@@ -0,0 +1,391 @@
"""
Conditional Branching Example - PraisonAI Agents
This example demonstrates the `if:` pattern for conditional workflow execution:
1. Basic if/then/else branching
2. Numeric condition evaluation (>, <, >=, <=, ==, !=)
3. String condition evaluation (==, !=)
4. Contains condition (in, contains)
5. Combining if: with loop: for per-item decisions
The if: pattern enables dynamic workflow paths based on
runtime conditions and variable values.
"""
from praisonaiagents import Workflow, WorkflowContext, StepResult
from praisonaiagents.workflows import if_, loop
# =============================================================================
# Example 1: Basic If/Then/Else - Score-Based Routing
# =============================================================================
def basic_if_example():
"""
Route to different handlers based on a score threshold.
"""
print("\n" + "="*60)
print("Example 1: Basic If/Then/Else - Score Routing")
print("="*60)
def approve(ctx: WorkflowContext) -> StepResult:
score = ctx.variables.get("score", 0)
print(f" ✅ APPROVED: Score {score} meets threshold")
return StepResult(output="Application approved")
def reject(ctx: WorkflowContext) -> StepResult:
score = ctx.variables.get("score", 0)
print(f" ❌ REJECTED: Score {score} below threshold")
return StepResult(output="Application rejected")
# Test with passing score
workflow_pass = Workflow(
steps=[
if_(
condition="{{score}} >= 70",
then_steps=[approve],
else_steps=[reject]
)
],
variables={"score": 85}
)
print("\nTest 1: Score = 85")
workflow_pass.start("Evaluate application")
# Test with failing score
workflow_fail = Workflow(
steps=[
if_(
condition="{{score}} >= 70",
then_steps=[approve],
else_steps=[reject]
)
],
variables={"score": 55}
)
print("\nTest 2: Score = 55")
workflow_fail.start("Evaluate application")
# =============================================================================
# Example 2: String Condition - Status-Based Processing
# =============================================================================
def string_condition_example():
"""
Branch based on string equality.
"""
print("\n" + "="*60)
print("Example 2: String Condition - Status Processing")
print("="*60)
def process_active(ctx: WorkflowContext) -> StepResult:
print(" 🟢 Processing ACTIVE user")
return StepResult(output="Active user processed")
def process_inactive(ctx: WorkflowContext) -> StepResult:
print(" 🔴 Processing INACTIVE user")
return StepResult(output="Inactive user archived")
# Test with active status
workflow = Workflow(
steps=[
if_(
condition="{{status}} == active",
then_steps=[process_active],
else_steps=[process_inactive]
)
],
variables={"status": "active"}
)
print("\nTest: status = 'active'")
workflow.start("Process user")
# Test with inactive status
workflow2 = Workflow(
steps=[
if_(
condition="{{status}} == active",
then_steps=[process_active],
else_steps=[process_inactive]
)
],
variables={"status": "inactive"}
)
print("\nTest: status = 'inactive'")
workflow2.start("Process user")
# =============================================================================
# Example 3: Contains Condition - Content Filtering
# =============================================================================
def contains_condition_example():
"""
Branch based on whether a string contains a substring.
"""
print("\n" + "="*60)
print("Example 3: Contains Condition - Content Filtering")
print("="*60)
def flag_content(ctx: WorkflowContext) -> StepResult:
print(" 🚨 FLAGGED: Message contains sensitive content")
return StepResult(output="Content flagged for review")
def approve_content(ctx: WorkflowContext) -> StepResult:
print(" ✅ APPROVED: Message is clean")
return StepResult(output="Content approved")
# Test with flagged content
workflow1 = Workflow(
steps=[
if_(
condition="error in {{message}}",
then_steps=[flag_content],
else_steps=[approve_content]
)
],
variables={"message": "System error: connection failed"}
)
print("\nTest 1: Message contains 'error'")
workflow1.start("Filter content")
# Test with clean content
workflow2 = Workflow(
steps=[
if_(
condition="error in {{message}}",
then_steps=[flag_content],
else_steps=[approve_content]
)
],
variables={"message": "Operation completed successfully"}
)
print("\nTest 2: Message is clean")
workflow2.start("Filter content")
# =============================================================================
# Example 4: If Inside Loop - Per-Item Decisions
# =============================================================================
def if_inside_loop_example():
"""
Make decisions for each item in a collection.
Combines iteration with conditional branching.
"""
print("\n" + "="*60)
print("Example 4: If Inside Loop - Per-Item Decisions")
print("="*60)
results = {"premium": [], "standard": []}
def premium_service(ctx: WorkflowContext) -> StepResult:
customer = ctx.variables.get("customer", {})
name = customer.get("name", "Unknown")
results["premium"].append(name)
print(f" ⭐ {name}: Premium service activated")
return StepResult(output=f"Premium service for {name}")
def standard_service(ctx: WorkflowContext) -> StepResult:
customer = ctx.variables.get("customer", {})
name = customer.get("name", "Unknown")
results["standard"].append(name)
print(f" 📦 {name}: Standard service activated")
return StepResult(output=f"Standard service for {name}")
workflow = Workflow(
steps=[
loop(
steps=[
if_(
condition="{{customer.tier}} == premium",
then_steps=[premium_service],
else_steps=[standard_service]
)
],
over="customers",
var_name="customer"
)
],
variables={
"customers": [
{"name": "Alice", "tier": "premium"},
{"name": "Bob", "tier": "standard"},
{"name": "Charlie", "tier": "premium"},
{"name": "Diana", "tier": "standard"}
]
}
)
print("\nProcessing customers:")
workflow.start("Assign service tiers")
print(f"\nSummary:")
print(f" Premium customers: {results['premium']}")
print(f" Standard customers: {results['standard']}")
# =============================================================================
# Example 5: Nested Conditions - Multi-Level Decision Tree
# =============================================================================
def nested_conditions_example():
"""
Demonstrate nested if conditions for complex decision trees.
"""
print("\n" + "="*60)
print("Example 5: Nested Conditions - Decision Tree")
print("="*60)
def vip_treatment(ctx: WorkflowContext) -> StepResult:
print(" 👑 VIP Treatment: Dedicated support + Priority queue")
return StepResult(output="VIP treatment applied") # noqa: ARG001
def regular_premium(ctx: WorkflowContext) -> StepResult:
print(" ⭐ Premium: Priority support")
return StepResult(output="Premium support applied")
def basic_support(ctx: WorkflowContext) -> StepResult:
print(" 📞 Basic: Standard support queue")
return StepResult(output="Basic support applied")
# Outer condition: Is premium?
# Inner condition (if premium): Is VIP?
workflow = Workflow(
steps=[
if_(
condition="{{tier}} == premium",
then_steps=[
if_(
condition="{{spend}} > 10000",
then_steps=[vip_treatment],
else_steps=[regular_premium]
)
],
else_steps=[basic_support]
)
],
variables={"tier": "premium", "spend": 15000}
)
print("\nTest 1: Premium tier, high spend")
workflow.start("Determine support level")
workflow2 = Workflow(
steps=[
if_(
condition="{{tier}} == premium",
then_steps=[
if_(
condition="{{spend}} > 10000",
then_steps=[vip_treatment],
else_steps=[regular_premium]
)
],
else_steps=[basic_support]
)
],
variables={"tier": "premium", "spend": 5000}
)
print("\nTest 2: Premium tier, low spend")
workflow2.start("Determine support level")
workflow3 = Workflow(
steps=[
if_(
condition="{{tier}} == premium",
then_steps=[
if_(
condition="{{spend}} > 10000",
then_steps=[vip_treatment],
else_steps=[regular_premium]
)
],
else_steps=[basic_support]
)
],
variables={"tier": "basic", "spend": 20000}
)
print("\nTest 3: Basic tier (regardless of spend)")
workflow3.start("Determine support level")
# =============================================================================
# Example 6: No Else Branch - Optional Processing
# =============================================================================
def no_else_example():
"""
If condition without else branch - only execute if true.
"""
print("\n" + "="*60)
print("Example 6: No Else Branch - Optional Processing")
print("="*60)
def send_notification(ctx: WorkflowContext) -> StepResult:
print(" 📧 Notification sent!")
return StepResult(output="Notification sent")
def continue_processing(ctx: WorkflowContext) -> StepResult:
print(" ➡️ Continuing with main workflow...")
return StepResult(output="Continued")
# Only send notification if opted in
workflow = Workflow(
steps=[
if_(
condition="{{notify}} == true",
then_steps=[send_notification]
# No else_steps - just skip if false
),
continue_processing
],
variables={"notify": "true"}
)
print("\nTest 1: notify = true")
workflow.start("Process with notification")
workflow2 = Workflow(
steps=[
if_(
condition="{{notify}} == true",
then_steps=[send_notification]
),
continue_processing
],
variables={"notify": "false"}
)
print("\nTest 2: notify = false")
workflow2.start("Process without notification")
# =============================================================================
# Main - Run All Examples
# =============================================================================
if __name__ == "__main__":
print("\n" + "="*60)
print("PraisonAI Conditional Branching Examples")
print("="*60)
basic_if_example()
string_condition_example()
contains_condition_example()
if_inside_loop_example()
nested_conditions_example()
no_else_example()
print("\n" + "="*60)
print("All conditional branching examples completed!")
print("="*60)
@@ -0,0 +1,214 @@
"""Example: Merge Strategies for Multi-Agent Workflows
This example demonstrates how to use and create custom merge strategies
for combining outputs from parallel agent executions.
"""
from praisonaiagents.agents import (
MergeStrategyProtocol,
FirstWinsMerge,
ConcatMerge,
DictMerge,
)
def example_first_wins_merge():
"""Demonstrate FirstWinsMerge strategy."""
print("=" * 50)
print("FirstWinsMerge Examples")
print("=" * 50)
merge = FirstWinsMerge()
# 1. Basic usage - returns first non-None
print("\n1. Basic Usage:")
outputs = [None, "Agent 2 response", "Agent 3 response"]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
# 2. All None returns None
print("\n2. All None:")
outputs = [None, None, None]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
# 3. First value wins
print("\n3. First Value Wins:")
outputs = ["Fast agent", "Slow agent", "Slowest agent"]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
# 4. Works with any type
print("\n4. Various Types:")
print(f" Dict: {merge.merge([None, {'key': 'value'}])}")
print(f" List: {merge.merge([None, [1, 2, 3]])}")
print(f" Number: {merge.merge([None, 42])}")
def example_concat_merge():
"""Demonstrate ConcatMerge strategy."""
print("\n" + "=" * 50)
print("ConcatMerge Examples")
print("=" * 50)
# 1. Default separator (double newline)
print("\n1. Default Separator:")
merge = ConcatMerge()
outputs = ["Part 1: Introduction", "Part 2: Analysis", "Part 3: Conclusion"]
result = merge.merge(outputs)
print(f" Result:\n{result}")
# 2. Custom separator
print("\n2. Custom Separator:")
merge = ConcatMerge(separator=" | ")
outputs = ["Alice", "Bob", "Charlie"]
result = merge.merge(outputs)
print(f" Result: {result}")
# 3. Skips None values
print("\n3. Skips None:")
merge = ConcatMerge(separator=", ")
outputs = ["A", None, "C", None, "E"]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
# 4. Converts non-strings
print("\n4. Converts Non-Strings:")
merge = ConcatMerge(separator=" + ")
outputs = [1, 2, 3]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
def example_dict_merge():
"""Demonstrate DictMerge strategy."""
print("\n" + "=" * 50)
print("DictMerge Examples")
print("=" * 50)
# 1. Basic shallow merge
print("\n1. Shallow Merge (default):")
merge = DictMerge()
outputs = [
{"name": "Alice", "score": 10},
{"name": "Bob", "level": 5},
{"score": 20, "rank": 1}
]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
# 2. Later values override
print("\n2. Override Behavior:")
merge = DictMerge()
outputs = [
{"status": "pending"},
{"status": "processing"},
{"status": "complete"}
]
result = merge.merge(outputs)
print(f" Result: {result}")
# 3. Deep merge for nested dicts
print("\n3. Deep Merge:")
merge = DictMerge(deep=True)
outputs = [
{"config": {"a": 1, "b": 2}},
{"config": {"b": 3, "c": 4}},
{"config": {"d": 5}}
]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
# 4. Skips non-dict values
print("\n4. Skips Non-Dict Values:")
merge = DictMerge()
outputs = [{"a": 1}, "not a dict", None, {"b": 2}]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
def example_custom_merge():
"""Demonstrate creating custom merge strategies."""
print("\n" + "=" * 50)
print("Custom Merge Strategy Examples")
print("=" * 50)
# 1. Longest output merge
class LongestOutputMerge:
"""Returns the longest output."""
def merge(self, outputs, context=None):
valid = [o for o in outputs if o is not None]
if not valid:
return None
return max(valid, key=lambda x: len(str(x)))
print("\n1. LongestOutputMerge:")
merge = LongestOutputMerge()
outputs = ["short", "medium length", "this is the longest response"]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result}")
# 2. Majority vote merge
class MajorityVoteMerge:
"""Returns the most common output."""
def merge(self, outputs, context=None):
from collections import Counter
valid = [o for o in outputs if o is not None]
if not valid:
return None
counts = Counter(valid)
return counts.most_common(1)[0][0]
print("\n2. MajorityVoteMerge:")
merge = MajorityVoteMerge()
outputs = ["yes", "yes", "no", "yes", "no"]
result = merge.merge(outputs)
print(f" Inputs: {outputs}")
print(f" Result: {result} (3 votes)")
# 3. Weighted merge
class WeightedMerge:
"""Merges with agent weights from context."""
def merge(self, outputs, context=None):
weights = context.get("weights", [1] * len(outputs)) if context else [1] * len(outputs)
best_idx = 0
best_weight = 0
for i, (output, weight) in enumerate(zip(outputs, weights)):
if output is not None and weight > best_weight:
best_idx = i
best_weight = weight
return outputs[best_idx] if outputs else None
print("\n3. WeightedMerge:")
merge = WeightedMerge()
outputs = ["junior answer", "senior answer", "expert answer"]
context = {"weights": [1, 5, 10]}
result = merge.merge(outputs, context=context)
print(f" Inputs: {outputs}")
print(f" Weights: {context['weights']}")
print(f" Result: {result}")
# 4. Protocol compliance
print("\n4. Protocol Compliance:")
print(f" LongestOutputMerge: {isinstance(LongestOutputMerge(), MergeStrategyProtocol)}")
print(f" MajorityVoteMerge: {isinstance(MajorityVoteMerge(), MergeStrategyProtocol)}")
print(f" WeightedMerge: {isinstance(WeightedMerge(), MergeStrategyProtocol)}")
if __name__ == "__main__":
example_first_wins_merge()
example_concat_merge()
example_dict_merge()
example_custom_merge()
print("\n" + "=" * 50)
print("All examples completed!")
print("=" * 50)
@@ -0,0 +1,174 @@
"""Example: Retry Policies and Fallback Chains
This example demonstrates how to configure retry behavior with
exponential backoff, jitter, and fallback chains.
"""
from praisonaiagents.tools import (
RetryPolicy,
FallbackChain,
ToolExecutionConfig,
)
def example_retry_policy():
"""Demonstrate RetryPolicy configuration and usage."""
print("=" * 50)
print("RetryPolicy Examples")
print("=" * 50)
# 1. Default policy
print("\n1. Default RetryPolicy:")
policy = RetryPolicy()
print(f" max_attempts: {policy.max_attempts}")
print(f" backoff_factor: {policy.backoff_factor}")
print(f" initial_delay_ms: {policy.initial_delay_ms}")
print(f" max_delay_ms: {policy.max_delay_ms}")
print(f" retry_on: {policy.retry_on}")
# 2. Exponential backoff calculation
print("\n2. Exponential Backoff (no jitter):")
policy = RetryPolicy(
initial_delay_ms=1000,
backoff_factor=2.0,
max_delay_ms=30000,
jitter=False
)
for attempt in range(5):
delay = policy.get_delay_ms(attempt)
print(f" Attempt {attempt}: delay = {delay}ms")
# 3. With jitter for production
print("\n3. With Jitter (production-ready):")
policy = RetryPolicy(
initial_delay_ms=1000,
backoff_factor=2.0,
jitter=True,
jitter_factor=0.25 # ±25%
)
print(" Delays for attempt 0 (10 samples):")
delays = [policy.get_delay_ms(0) for _ in range(10)]
print(f" {delays}")
print(f" Range: {min(delays)} - {max(delays)} (expected: 750-1250)")
# 4. should_retry logic
print("\n4. should_retry Logic:")
policy = RetryPolicy(max_attempts=3, retry_on={"timeout", "rate_limit"})
test_cases = [
("timeout", 0),
("timeout", 2),
("timeout", 3), # At max_attempts
("unknown_error", 0), # Not in retry_on
]
for error_type, attempt in test_cases:
result = policy.should_retry(error_type, attempt)
print(f" should_retry('{error_type}', {attempt}) = {result}")
# 5. Custom error types
print("\n5. Custom Error Types:")
policy = RetryPolicy(retry_on={"api_error", "network_error", "throttled"})
print(f" Retries on: {policy.retry_on}")
print(f" should_retry('api_error', 0) = {policy.should_retry('api_error', 0)}")
print(f" should_retry('timeout', 0) = {policy.should_retry('timeout', 0)}")
def example_fallback_chain():
"""Demonstrate FallbackChain configuration."""
print("\n" + "=" * 50)
print("FallbackChain Examples")
print("=" * 50)
# 1. Basic chain
print("\n1. Basic FallbackChain:")
chain = FallbackChain(
tools=["web_search", "cached_search", "default_response"]
)
print(f" Tools: {chain.tools}")
print(f" Length: {len(chain)}")
print(f" stop_on_success: {chain.stop_on_success}")
# 2. Iteration
print("\n2. Iterating over chain:")
for i, tool in enumerate(chain):
print(f" {i+1}. {tool}")
# 3. Try all (don't stop on success)
print("\n3. Try All Mode:")
chain = FallbackChain(
tools=["validator_1", "validator_2", "validator_3"],
stop_on_success=False
)
print(f" stop_on_success: {chain.stop_on_success}")
print(" (All tools will be tried regardless of success)")
def example_tool_execution_config():
"""Demonstrate ToolExecutionConfig combining retry and fallback."""
print("\n" + "=" * 50)
print("ToolExecutionConfig Examples")
print("=" * 50)
# 1. Default config
print("\n1. Default Config:")
config = ToolExecutionConfig()
print(f" retry_policy: {config.retry_policy}")
print(f" fallback_chain: {config.fallback_chain}")
print(f" timeout_ms: {config.timeout_ms}")
# 2. Using default() factory
print("\n2. Default Factory:")
config = ToolExecutionConfig.default()
print(f" retry_policy.max_attempts: {config.retry_policy.max_attempts}")
# 3. Full configuration
print("\n3. Full Configuration:")
config = ToolExecutionConfig(
retry_policy=RetryPolicy(
max_attempts=5,
backoff_factor=1.5,
initial_delay_ms=500,
jitter=True
),
fallback_chain=FallbackChain(
tools=["primary_api", "backup_api", "cache"]
),
timeout_ms=10000
)
print(f" retry_policy.max_attempts: {config.retry_policy.max_attempts}")
print(f" retry_policy.jitter: {config.retry_policy.jitter}")
print(f" fallback_chain tools: {config.fallback_chain.tools}")
print(f" timeout_ms: {config.timeout_ms}")
def example_validation():
"""Demonstrate validation of RetryPolicy parameters."""
print("\n" + "=" * 50)
print("Validation Examples")
print("=" * 50)
invalid_configs = [
("max_attempts=0", {"max_attempts": 0}),
("backoff_factor=0.5", {"backoff_factor": 0.5}),
("initial_delay_ms=-1", {"initial_delay_ms": -1}),
("max_delay < initial", {"initial_delay_ms": 1000, "max_delay_ms": 500}),
("jitter_factor=1.5", {"jitter_factor": 1.5}),
]
for name, kwargs in invalid_configs:
try:
RetryPolicy(**kwargs)
print(f"\n {name}: No error (unexpected!)")
except ValueError as e:
print(f"\n {name}:")
print(f" → ValueError: {e}")
if __name__ == "__main__":
example_retry_policy()
example_fallback_chain()
example_tool_execution_config()
example_validation()
print("\n" + "=" * 50)
print("All examples completed!")
print("=" * 50)
@@ -0,0 +1,163 @@
"""Example: Tool Validation with Custom Validators
This example demonstrates how to use ValidationResult and implement
custom validators using the ToolValidatorProtocol.
"""
from praisonaiagents.tools import (
ValidationResult,
ToolValidatorProtocol,
PassthroughValidator,
)
def example_validation_result():
"""Demonstrate ValidationResult usage."""
print("=" * 50)
print("ValidationResult Examples")
print("=" * 50)
# 1. Success factory
success = ValidationResult.success()
print(f"\n1. Success: valid={success.valid}, bool={bool(success)}")
# 2. Failure factory
failure = ValidationResult.failure(
errors=["Query cannot be empty", "Missing required field"],
remediation="Provide a non-empty query string"
)
print(f"\n2. Failure: valid={failure.valid}")
print(f" Errors: {failure.errors}")
print(f" Remediation: {failure.remediation}")
# 3. Fluent API - building incrementally
result = ValidationResult(valid=True)
result.add_warning("Query is very long, may be slow")
result.add_warning("Consider using pagination")
print(f"\n3. With warnings: valid={result.valid}, warnings={result.warnings}")
# 4. Adding errors changes validity
result.add_error("Rate limit exceeded")
print(f"\n4. After error: valid={result.valid}, errors={result.errors}")
class InputLengthValidator:
"""Custom validator that checks string input lengths."""
def __init__(self, max_length: int = 100):
self.max_length = max_length
def validate_args(self, tool_name, args, context=None):
"""Validate that string arguments don't exceed max length."""
for key, value in args.items():
if isinstance(value, str) and len(value) > self.max_length:
return ValidationResult.failure(
errors=[f"Argument '{key}' ({len(value)} chars) exceeds limit ({self.max_length})"],
remediation=f"Truncate '{key}' to {self.max_length} characters or less"
)
return ValidationResult.success()
def validate_result(self, tool_name, result, context=None):
"""Validate tool results."""
return ValidationResult.success()
class TypeValidator:
"""Custom validator that checks argument types."""
def __init__(self, type_specs: dict):
"""
Args:
type_specs: Dict mapping arg names to expected types
e.g., {"query": str, "limit": int}
"""
self.type_specs = type_specs
def validate_args(self, tool_name, args, context=None):
result = ValidationResult(valid=True)
for arg_name, expected_type in self.type_specs.items():
if arg_name in args:
actual_value = args[arg_name]
if not isinstance(actual_value, expected_type):
result.add_error(
f"Argument '{arg_name}' must be {expected_type.__name__}, "
f"got {type(actual_value).__name__}"
)
if not result.valid:
result.remediation = "Check argument types and try again"
return result
def validate_result(self, tool_name, result, context=None):
return ValidationResult.success()
def example_custom_validators():
"""Demonstrate custom validator implementations."""
print("\n" + "=" * 50)
print("Custom Validator Examples")
print("=" * 50)
# 1. InputLengthValidator
print("\n1. InputLengthValidator:")
validator = InputLengthValidator(max_length=50)
# Valid input
result = validator.validate_args("search", {"query": "short query"})
print(f" Short query: valid={result.valid}")
# Invalid input
long_query = "x" * 100
result = validator.validate_args("search", {"query": long_query})
print(f" Long query: valid={result.valid}")
print(f" Error: {result.errors[0]}")
print(f" Fix: {result.remediation}")
# 2. TypeValidator
print("\n2. TypeValidator:")
validator = TypeValidator({"query": str, "limit": int, "offset": int})
# Valid types
result = validator.validate_args("search", {"query": "test", "limit": 10})
print(f" Correct types: valid={result.valid}")
# Invalid types
result = validator.validate_args("search", {"query": 123, "limit": "ten"})
print(f" Wrong types: valid={result.valid}")
for error in result.errors:
print(f" - {error}")
# 3. Protocol compliance check
print("\n3. Protocol Compliance:")
print(f" InputLengthValidator: {isinstance(InputLengthValidator(), ToolValidatorProtocol)}")
print(f" TypeValidator: {isinstance(TypeValidator({}), ToolValidatorProtocol)}")
print(f" PassthroughValidator: {isinstance(PassthroughValidator(), ToolValidatorProtocol)}")
def example_passthrough_validator():
"""Demonstrate the default PassthroughValidator."""
print("\n" + "=" * 50)
print("PassthroughValidator Example")
print("=" * 50)
validator = PassthroughValidator()
# Always returns valid
result = validator.validate_args("any_tool", {"any": "args"})
print(f"\nvalidate_args: valid={result.valid}")
result = validator.validate_result("any_tool", {"any": "result"})
print(f"validate_result: valid={result.valid}")
print("\nPassthroughValidator is useful as a default when no validation is needed.")
if __name__ == "__main__":
example_validation_result()
example_custom_validators()
example_passthrough_validator()
print("\n" + "=" * 50)
print("All examples completed!")
print("=" * 50)
+170
View File
@@ -0,0 +1,170 @@
"""
Model Failover Example - Automatic Provider Switching
This example demonstrates how to configure model failover
for reliability and cost optimization.
"""
import os
from praisonaiagents import (
AuthProfile,
FailoverConfig,
FailoverManager,
)
# Create auth profiles for different providers
openai_profile = AuthProfile(
name="openai-primary",
provider="openai",
api_key=os.environ.get("OPENAI_API_KEY", "sk-..."),
priority=1, # Highest priority
rate_limit=100,
)
anthropic_profile = AuthProfile(
name="anthropic-backup",
provider="anthropic",
api_key=os.environ.get("ANTHROPIC_API_KEY", "sk-ant-..."),
priority=2, # Second priority
rate_limit=50,
)
groq_profile = AuthProfile(
name="groq-fallback",
provider="groq",
api_key=os.environ.get("GROQ_API_KEY", "gsk-..."),
priority=3, # Third priority (fast but limited)
rate_limit=30,
)
# Configure failover behavior
failover_config = FailoverConfig(
max_retries=3,
retry_delay=1.0,
exponential_backoff=True,
max_retry_delay=60.0,
failover_on_rate_limit=True,
failover_on_timeout=True,
failover_on_error=True,
)
# Create failover manager
manager = FailoverManager(failover_config)
manager.add_profile(openai_profile)
manager.add_profile(anthropic_profile)
manager.add_profile(groq_profile)
def demonstrate_failover():
"""Demonstrate failover functionality."""
print("Failover Manager Status:")
print()
# Get current status
status = manager.status()
for name, info in status.items():
print(f" {name}:")
print(f" Status: {info.get('status', 'unknown')}")
print(f" Priority: {info.get('priority', 'N/A')}")
print(f" Failures: {info.get('failure_count', 0)}")
print()
# Get next available profile
next_profile = manager.get_next_profile()
if next_profile:
print(f"Next profile to use: {next_profile.name}")
print(f" Provider: {next_profile.provider}")
print(f" Priority: {next_profile.priority}")
print()
# Simulate marking a failure
print("Simulating failure on primary provider...")
manager.mark_failure("openai-primary", "Rate limit exceeded")
# Get next profile after failure
next_profile = manager.get_next_profile()
if next_profile:
print(f"After failure, next profile: {next_profile.name}")
print()
# Get retry delay
delay = manager.get_retry_delay("openai-primary")
print(f"Retry delay for openai-primary: {delay}s")
print()
# Reset all providers
print("Resetting all providers...")
manager.reset_all()
next_profile = manager.get_next_profile()
if next_profile:
print(f"After reset, next profile: {next_profile.name}")
def cost_optimization_example():
"""Example of cost-optimized failover."""
print("\nCost Optimization Example:")
print("-" * 40)
# Create cost-optimized manager
cost_manager = FailoverManager()
# Cheaper model first
cost_manager.add_profile(AuthProfile(
name="gpt-4o-mini",
provider="openai",
api_key=os.environ.get("OPENAI_API_KEY", ""),
priority=1,
metadata={"model": "gpt-4o-mini", "cost_per_1k": 0.00015}
))
# Premium model as fallback
cost_manager.add_profile(AuthProfile(
name="gpt-4o",
provider="openai",
api_key=os.environ.get("OPENAI_API_KEY", ""),
priority=2,
metadata={"model": "gpt-4o", "cost_per_1k": 0.005}
))
# Most expensive as last resort
cost_manager.add_profile(AuthProfile(
name="claude-opus",
provider="anthropic",
api_key=os.environ.get("ANTHROPIC_API_KEY", ""),
priority=3,
metadata={"model": "claude-3-opus", "cost_per_1k": 0.015}
))
print("Cost-optimized failover chain:")
for profile in [
cost_manager.get_profile("gpt-4o-mini"),
cost_manager.get_profile("gpt-4o"),
cost_manager.get_profile("claude-opus"),
]:
if profile:
cost = profile.metadata.get("cost_per_1k", "N/A")
print(f" {profile.priority}. {profile.name} (${cost}/1K tokens)")
if __name__ == "__main__":
print("Model Failover Configuration")
print("=" * 40)
print()
print("Failover Config:")
print(f" Max retries: {failover_config.max_retries}")
print(f" Initial delay: {failover_config.retry_delay}s")
print(f" Exponential backoff: {failover_config.exponential_backoff}")
print(f" Max delay: {failover_config.max_retry_delay}s")
print()
print("Auth Profiles:")
for profile in [openai_profile, anthropic_profile, groq_profile]:
print(f" {profile.name}:")
print(f" Provider: {profile.provider}")
print(f" Priority: {profile.priority}")
print(f" Rate limit: {profile.rate_limit} req/min")
print()
demonstrate_failover()
cost_optimization_example()
+47
View File
@@ -0,0 +1,47 @@
"""
Gateway Example - Multi-Agent Coordination
This example demonstrates how to use the Gateway for coordinating
multiple agents and managing real-time communication.
"""
from praisonaiagents import Agent, GatewayConfig, SessionConfig
# Configure the gateway
gateway_config = GatewayConfig(
host="127.0.0.1",
port=8765,
max_connections=100,
heartbeat_interval=30,
session_config=SessionConfig(
timeout=3600, # 1 hour session timeout
max_messages=500,
)
)
# Create specialized agents
researcher = Agent(
name="researcher",
instructions="You research topics thoroughly and provide detailed information.",
llm="gpt-4o-mini"
)
writer = Agent(
name="writer",
instructions="You write clear, engaging content based on research.",
llm="gpt-4o-mini"
)
# Example: Simple agent interaction
if __name__ == "__main__":
print("Gateway Configuration:")
print(f" Host: {gateway_config.host}")
print(f" Port: {gateway_config.port}")
print(f" WebSocket URL: {gateway_config.ws_url}")
print(f" Max Connections: {gateway_config.max_connections}")
print()
# Test agent
response = researcher.start("What are the key benefits of multi-agent systems?")
print("Researcher Response:")
print(response)
@@ -0,0 +1,72 @@
"""
Example: Memory with Tracing
This example demonstrates how to use memory with context tracing enabled.
When tracing is enabled, memory operations (store/search) emit trace events
that can be used for debugging and analysis with the LLM Judge.
Usage:
python memory_with_tracing_example.py
To judge the trace:
praisonai recipe judge <trace_id> --memory
"""
from praisonaiagents.memory import Memory
from praisonaiagents.trace.context_events import (
ContextTraceEmitter,
ContextListSink,
set_context_emitter,
reset_context_emitter,
)
def main():
# Set up trace emitter to capture events
sink = ContextListSink()
emitter = ContextTraceEmitter(sink=sink, session_id="memory-demo", enabled=True)
token = set_context_emitter(emitter)
try:
# Create memory instance
memory = Memory(config={
"provider": "rag",
"use_embedding": False,
"agent_name": "demo_agent", # Used in trace events
})
# Store some memories
print("Storing memories...")
memory.store_short_term("The user's name is Alice")
memory.store_short_term("Alice likes Python programming")
memory.store_long_term("Important: Alice prefers dark mode in all applications")
# Search memories
print("\nSearching memories...")
results = memory.search_short_term("Alice", limit=5)
print(f"Found {len(results)} short-term memories")
results = memory.search_long_term("preferences", limit=5)
print(f"Found {len(results)} long-term memories")
# Print captured trace events
events = sink.get_events()
print(f"\n--- Captured {len(events)} trace events ---")
for event in events:
print(f" {event.event_type.value}: {event.data}")
# Filter memory-specific events
memory_events = [e for e in events if 'memory' in e.event_type.value]
print(f"\n--- Memory-specific events: {len(memory_events)} ---")
for event in memory_events:
print(f" {event.event_type.value}:")
print(f" agent: {event.agent_name}")
print(f" data: {event.data}")
finally:
reset_context_emitter(token)
print("\nTrace emitter reset.")
if __name__ == "__main__":
main()
+252
View File
@@ -0,0 +1,252 @@
"""
Nested Workflows Example - PraisonAI Agents
This example demonstrates how to use nested workflow patterns:
1. Loop inside Loop (nested iteration)
2. Parallel inside Loop (concurrent processing per item)
3. Route inside Loop (conditional routing per item)
4. Depth limit protection (prevents infinite recursion)
These patterns enable complex data processing pipelines
while maintaining clean, readable workflow definitions.
"""
from praisonaiagents import Workflow, WorkflowContext, StepResult
from praisonaiagents.workflows import loop, parallel, route
# =============================================================================
# Example 1: Nested Loops - Process Matrix Data
# =============================================================================
def nested_loop_example():
"""
Process a 2D matrix using nested loops.
Outer loop iterates rows, inner loop iterates columns.
"""
print("\n" + "="*60)
print("Example 1: Nested Loops - Matrix Processing")
print("="*60)
results = []
def process_cell(ctx: WorkflowContext) -> StepResult:
row = ctx.variables.get("row", {})
col = ctx.variables.get("col", "")
row_name = row.get("name", "?")
results.append(f"{row_name}-{col}")
return StepResult(output=f"Processed cell {row_name}-{col}")
# Inner loop processes columns for each row
inner_loop = loop(
process_cell,
over="columns",
var_name="col"
)
# Outer loop iterates through rows
outer_loop = loop(
steps=[inner_loop],
over="rows",
var_name="row"
)
workflow = Workflow(
steps=[outer_loop],
variables={
"rows": [
{"name": "Row1"},
{"name": "Row2"}
],
"columns": ["A", "B", "C"]
}
)
workflow.start("Process matrix")
print(f"Processed cells: {results}")
# Output: ['Row1-A', 'Row1-B', 'Row1-C', 'Row2-A', 'Row2-B', 'Row2-C']
# =============================================================================
# Example 2: Parallel Inside Loop - Concurrent Task Processing
# =============================================================================
def parallel_inside_loop_example():
"""
For each project, run multiple analysis tasks in parallel.
Combines iteration with concurrent execution.
"""
print("\n" + "="*60)
print("Example 2: Parallel Inside Loop - Concurrent Analysis")
print("="*60)
results = []
def security_scan(ctx: WorkflowContext) -> StepResult:
project = ctx.variables.get("project", "unknown")
results.append(f"security:{project}")
return StepResult(output=f"Security scan complete for {project}")
def performance_test(ctx: WorkflowContext) -> StepResult:
project = ctx.variables.get("project", "unknown")
results.append(f"performance:{project}")
return StepResult(output=f"Performance test complete for {project}")
def code_review(ctx: WorkflowContext) -> StepResult:
project = ctx.variables.get("project", "unknown")
results.append(f"review:{project}")
return StepResult(output=f"Code review complete for {project}")
# Parallel tasks run concurrently for each project
parallel_analysis = parallel([security_scan, performance_test, code_review])
# Loop through projects, running parallel analysis for each
workflow = Workflow(
steps=[
loop(
steps=[parallel_analysis],
over="projects",
var_name="project"
)
],
variables={
"projects": ["frontend", "backend", "api"]
}
)
workflow.start("Analyze all projects")
print(f"Analysis results: {sorted(results)}")
# =============================================================================
# Example 3: Route Inside Loop - Conditional Processing Per Item
# =============================================================================
def route_inside_loop_example():
"""
Process items differently based on their type.
Each item in the loop is routed to the appropriate handler.
"""
print("\n" + "="*60)
print("Example 3: Route Inside Loop - Type-Based Processing")
print("="*60)
results = []
def process_image(ctx: WorkflowContext) -> StepResult:
item = ctx.variables.get("item", {})
results.append(f"image:{item.get('name')}")
return StepResult(output=f"Processed image: {item.get('name')}")
def process_video(ctx: WorkflowContext) -> StepResult:
item = ctx.variables.get("item", {})
results.append(f"video:{item.get('name')}")
return StepResult(output=f"Processed video: {item.get('name')}")
def process_document(ctx: WorkflowContext) -> StepResult:
item = ctx.variables.get("item", {})
results.append(f"document:{item.get('name')}")
return StepResult(output=f"Processed document: {item.get('name')}")
# Route based on item type
type_router = route({
"image": [process_image],
"video": [process_video],
"document": [process_document]
})
# First, determine the type, then route
def get_type(ctx: WorkflowContext) -> StepResult:
item = ctx.variables.get("item", {})
item_type = item.get("type", "unknown")
return StepResult(output=item_type)
workflow = Workflow(
steps=[
loop(
steps=[get_type, type_router],
over="files",
var_name="item"
)
],
variables={
"files": [
{"name": "photo.jpg", "type": "image"},
{"name": "movie.mp4", "type": "video"},
{"name": "report.pdf", "type": "document"},
{"name": "logo.png", "type": "image"}
]
}
)
workflow.start("Process all files")
print(f"Processing results: {results}")
# =============================================================================
# Example 4: Three-Level Nesting - Complex Data Pipeline
# =============================================================================
def three_level_nesting_example():
"""
Demonstrate three levels of nesting:
Department -> Team -> Member
"""
print("\n" + "="*60)
print("Example 4: Three-Level Nesting - Organization Processing")
print("="*60)
results = []
def process_member(ctx: WorkflowContext) -> StepResult:
dept = ctx.variables.get("dept", {}).get("name", "?")
team = ctx.variables.get("team", {}).get("name", "?")
member = ctx.variables.get("member", "?")
results.append(f"{dept}/{team}/{member}")
return StepResult(output=f"Processed {member}")
# Level 3: Process each member
member_loop = loop(process_member, over="members", var_name="member")
# Level 2: Process each team (contains member loop)
team_loop = loop(steps=[member_loop], over="teams", var_name="team")
# Level 1: Process each department (contains team loop)
dept_loop = loop(steps=[team_loop], over="departments", var_name="dept")
workflow = Workflow(
steps=[dept_loop],
variables={
"departments": [
{"name": "Engineering"},
{"name": "Marketing"}
],
"teams": [
{"name": "Frontend", "members": ["Alice", "Bob"]},
{"name": "Backend", "members": ["Charlie"]}
],
"members": ["Member1", "Member2"] # Simplified for demo
}
)
workflow.start("Process organization")
print(f"Processed paths: {results}")
# =============================================================================
# Main - Run All Examples
# =============================================================================
if __name__ == "__main__":
print("\n" + "="*60)
print("PraisonAI Nested Workflows Examples")
print("="*60)
nested_loop_example()
parallel_inside_loop_example()
route_inside_loop_example()
three_level_nesting_example()
print("\n" + "="*60)
print("All nested workflow examples completed!")
print("="*60)
+137
View File
@@ -0,0 +1,137 @@
"""
Sandbox Example - Secure Code Execution
This example demonstrates how to configure and use sandboxes
for safe code execution.
"""
from praisonaiagents import (
SandboxConfig,
ResourceLimits,
SandboxResult,
SandboxStatus
)
from praisonaiagents.sandbox import SecurityPolicy
# Minimal sandbox for untrusted code
minimal_sandbox = SandboxConfig(
sandbox_type="subprocess",
resource_limits=ResourceLimits.minimal(),
security_policy=SecurityPolicy.strict(),
)
# Standard sandbox for general use
standard_sandbox = SandboxConfig(
sandbox_type="subprocess",
resource_limits=ResourceLimits.standard(),
security_policy=SecurityPolicy.standard(),
)
# Docker sandbox for full isolation
docker_sandbox = SandboxConfig.docker(image="python:3.11-slim")
# Custom sandbox configuration
custom_sandbox = SandboxConfig(
sandbox_type="subprocess",
working_dir="/tmp/sandbox",
resource_limits=ResourceLimits(
memory_mb=256,
cpu_percent=50,
timeout_seconds=30,
network_enabled=False,
max_processes=5,
),
security_policy=SecurityPolicy(
allow_network=False,
allow_file_write=True,
allow_subprocess=False,
blocked_imports=["subprocess", "os.system", "eval"],
),
auto_cleanup=True,
)
def demonstrate_result_handling():
"""Show how to handle sandbox results."""
# Simulated successful result
success_result = SandboxResult(
status=SandboxStatus.COMPLETED,
exit_code=0,
stdout="Hello, World!\n",
stderr="",
duration_seconds=0.1,
)
# Simulated timeout result
timeout_result = SandboxResult(
status=SandboxStatus.TIMEOUT,
exit_code=None,
stdout="Partial output...",
stderr="",
error="Execution timed out after 30 seconds",
duration_seconds=30.0,
)
# Simulated error result
error_result = SandboxResult(
status=SandboxStatus.FAILED,
exit_code=1,
stdout="",
stderr="NameError: name 'undefined_var' is not defined",
error="Execution failed",
)
print("Result Handling Examples:")
print()
for name, result in [
("Success", success_result),
("Timeout", timeout_result),
("Error", error_result)
]:
print(f"{name} Result:")
print(f" Status: {result.status.name}")
print(f" Success: {result.success}")
if result.stdout:
print(f" Output: {result.stdout.strip()}")
if result.stderr:
print(f" Error: {result.stderr.strip()}")
if result.error:
print(f" Message: {result.error}")
print()
if __name__ == "__main__":
print("Sandbox Configurations:")
print()
print("Minimal Sandbox (for untrusted code):")
print(f" Type: {minimal_sandbox.sandbox_type}")
print(f" Memory: {minimal_sandbox.resource_limits.memory_mb}MB")
print(f" Timeout: {minimal_sandbox.resource_limits.timeout_seconds}s")
print(f" Network: {minimal_sandbox.resource_limits.network_enabled}")
print()
print("Standard Sandbox:")
print(f" Type: {standard_sandbox.sandbox_type}")
print(f" Memory: {standard_sandbox.resource_limits.memory_mb}MB")
print(f" Timeout: {standard_sandbox.resource_limits.timeout_seconds}s")
print()
print("Docker Sandbox:")
print(f" Type: {docker_sandbox.sandbox_type}")
print(f" Image: {docker_sandbox.image}")
print()
print("Custom Sandbox:")
print(f" Type: {custom_sandbox.sandbox_type}")
print(f" Working Dir: {custom_sandbox.working_dir}")
print(f" Memory: {custom_sandbox.resource_limits.memory_mb}MB")
print(f" Max Processes: {custom_sandbox.resource_limits.max_processes}")
print()
demonstrate_result_handling()
print("To run code in sandbox via CLI:")
print(" praisonai sandbox run \"print('Hello, World!')\"")
print(" praisonai sandbox run --file script.py")
print(" praisonai sandbox shell")
@@ -0,0 +1,22 @@
"""
Example 1: Save Agent Output Using write_file Tool
The agent decides when and what to save based on the task.
"""
from praisonaiagents import Agent
from praisonaiagents.tools import write_file
# Create agent with write_file tool
agent = Agent(
name="ContentWriter",
role="Technical Writer",
goal="Create and save technical documentation",
tools=[write_file]
)
# Agent will use write_file tool to save the content
result = agent.start("Write a short poem about coding and save it to poem.txt")
print("✅ Task completed!")
print(f"Result: {result}")
@@ -0,0 +1,30 @@
"""
Example 2: Save Agent Output Using Task.output_file
Task output is automatically saved to the specified file.
"""
from praisonaiagents import Agent, Task, Agents
# Create agent
writer = Agent(
name="ContentWriter",
role="Writer",
goal="Create engaging content"
)
# Task with output_file - auto-saves result
task = Task(
description="Write a short blog post about the benefits of AI assistants",
expected_output="A well-structured blog post in markdown format",
agent=writer,
output_file="blog_post.md",
create_directory=True
)
# Run
agents = Agents(agents=[writer], tasks=[task])
result = agents.start()
print("✅ Task completed!")
print("Output saved to: blog_post.md")
@@ -0,0 +1,41 @@
"""
Example 3: Save Agent Output Using Workflow output_file
Workflow step output is automatically saved with variable substitution.
"""
from praisonaiagents import Workflow
# Define workflow with output_file
workflow_config = {
"metadata": {
"name": "content-generator",
"version": "1.0"
},
"variables": {
"output_dir": "generated",
"topic": "Python programming"
},
"agents": {
"writer": {
"role": "Content Writer",
"goal": "Create engaging content",
"llm": "gpt-4o-mini"
}
},
"steps": [
{
"agent": "writer",
"action": "Write a short tutorial about Python basics",
"expected_output": "A beginner-friendly tutorial",
"output_file": "{{output_dir}}/tutorial.md"
}
]
}
# Run workflow
workflow = Workflow(config=workflow_config)
result = workflow.run()
print("✅ Workflow completed!")
print("Output saved to: generated/tutorial.md")
@@ -0,0 +1,27 @@
"""
Example 4: Save Agent Output Manually
Capture the response and save it with full control.
"""
from praisonaiagents import Agent
from pathlib import Path
# Create agent
agent = Agent(
name="StoryWriter",
instructions="You are a creative writer who writes short stories."
)
# Get response
response = agent.start("Write a very short story about a robot learning to paint")
# Save manually with error handling
output_path = Path("stories/robot_story.txt")
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
output_path.write_text(response)
print(f"✅ Saved to {output_path}")
except IOError as e:
print(f"❌ Failed to save: {e}")
@@ -0,0 +1,49 @@
"""
Example: Auto-save agent output to file
This example demonstrates how to automatically save agent responses to files.
"""
from praisonaiagents import Agent
from praisonaiagents.config import OutputConfig
# Method 1: Simplest - just pass a file path string
agent = Agent(
instructions="You are a helpful assistant",
output="output/response.txt" # Just a string path!
)
# When you call start(), the response is automatically saved
# result = agent.start("Write a short greeting")
# Prints: ✅ Output saved to output/response.txt
# Method 2: With template formatting using OutputConfig
agent_with_template = Agent(
instructions="You are a blog writer",
output=OutputConfig(
output_file="output/blog_post.md",
template="""# {{title}}
{{content}}
---
*Generated by AI*
"""
)
)
# The agent will format its response according to the template
# result = agent_with_template.start("Write about Python programming")
# Method 3: Combine with other output settings
agent_verbose = Agent(
instructions="You are a researcher",
output=OutputConfig(
verbose=True, # Show rich output in terminal
output_file="output/research.md", # Also save to file
template="## Research Summary\n\n{{content}}"
)
)
print("Examples ready! Uncomment agent.start() calls to run.")
print("Output will be saved to the specified files automatically.")
+35
View File
@@ -0,0 +1,35 @@
# Save Agent Output Examples
Examples demonstrating different methods to save agent output to files.
## Examples
| File | Method | Description |
|------|--------|-------------|
| `01_write_file_tool.py` | write_file Tool | Agent decides when/what to save |
| `02_task_output_file.py` | Task.output_file | Auto-save task result |
| `03_workflow_output_file.py` | Workflow output_file | Save workflow step output |
| `04_manual_save.py` | Manual | Full control over saving |
## Setup
```bash
pip install praisonaiagents
export OPENAI_API_KEY="your-key"
```
## Run Examples
```bash
python 01_write_file_tool.py
python 02_task_output_file.py
python 03_workflow_output_file.py
python 04_manual_save.py
```
## Which Method to Use?
- **write_file Tool**: When agent needs to decide what to save
- **Task.output_file**: For task-based workflows with auto-save
- **Workflow output_file**: For YAML workflows with variable substitution
- **Manual**: When you need full control over the saving process
@@ -0,0 +1,156 @@
"""
Specialized Agents Example
Demonstrates using specialized agent types (AudioAgent, VideoAgent, ImageAgent, OCRAgent)
with the Agents class for multi-agent workflows.
Usage:
python specialized_agents_example.py
"""
# Example 1: Simple TTS with AudioAgent
def example_tts():
"""Text-to-Speech example using AudioAgent."""
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="openai/tts-1")
agent.speech("Hello, welcome to PraisonAI!", output="hello.mp3")
print("Generated: hello.mp3")
# Example 2: Simple STT with AudioAgent
def example_stt():
"""Speech-to-Text example using AudioAgent."""
from praisonaiagents import AudioAgent
agent = AudioAgent(llm="openai/whisper-1")
text = agent.transcribe("hello.mp3")
print(f"Transcribed: {text}")
# Example 3: Image generation with ImageAgent
def example_image():
"""Image generation example using ImageAgent."""
from praisonaiagents import ImageAgent
agent = ImageAgent(llm="openai/dall-e-3")
result = agent.generate("A beautiful sunset over mountains")
print(f"Generated image: {result}")
# Example 4: Multi-agent pipeline with mixed agent types via YAML
def example_multi_agent_pipeline():
"""
Multi-agent pipeline example using YAML workflow:
1. AudioAgent (STT) - Transcribe audio
2. Agent (Research) - Research the topic
3. ImageAgent - Generate image
Note: Specialized agents (AudioAgent, VideoAgent, etc.) are best used
through YAML workflows or directly via their methods (speech, transcribe, etc.)
"""
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
# Define multi-agent pipeline in YAML
yaml_content = """
name: Multi-Agent Pipeline
process: sequential
agents:
transcriber:
agent: AudioAgent
llm: openai/whisper-1
role: Transcriber
goal: Transcribe audio to text
researcher:
role: Research Specialist
goal: Research topics and provide summaries
instructions: You research topics and provide concise summaries.
artist:
agent: ImageAgent
llm: openai/dall-e-3
role: Visual Artist
goal: Create images based on research
steps:
- agent: transcriber
action: transcribe
input: "{{audio_file}}"
- agent: researcher
action: "Research the following topic and provide a summary: {{previous_output}}"
- agent: artist
action: generate
prompt: "Create an image representing: {{previous_output}}"
variables:
audio_file: input.mp3
"""
parser = YAMLWorkflowParser()
workflow = parser.parse_string(yaml_content)
print("Multi-agent pipeline created successfully!")
print(f"Workflow: {workflow.name}")
print(f"Agents: {list(parser._agents.keys())}")
print(f"Agent types: {[a.__class__.__name__ for a in parser._agents.values()]}")
print(f"Steps: {len(workflow.steps)}")
# Example 5: Using YAML workflow with specialized agents
def example_yaml_workflow():
"""Parse YAML workflow with specialized agent types."""
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
yaml_content = """
name: Media Pipeline
process: sequential
agents:
transcriber:
agent: AudioAgent
llm: openai/whisper-1
role: Transcriber
goal: Transcribe audio
researcher:
role: Researcher
goal: Research topics
artist:
agent: ImageAgent
llm: openai/dall-e-3
role: Artist
goal: Create images
steps:
- agent: transcriber
action: transcribe
- agent: researcher
action: "Research: {{previous_output}}"
- agent: artist
action: generate
"""
parser = YAMLWorkflowParser()
workflow = parser.parse_string(yaml_content)
print(f"Workflow: {workflow.name}")
print(f"Agents: {list(parser._agents.keys())}")
print(f"Agent types: {[a.__class__.__name__ for a in parser._agents.values()]}")
if __name__ == "__main__":
print("=" * 60)
print("Specialized Agents Examples")
print("=" * 60)
# Run the YAML workflow example (doesn't require API keys)
print("\n--- Example: YAML Workflow with Specialized Agents ---")
example_yaml_workflow()
print("\n--- Example: Multi-Agent Pipeline ---")
example_multi_agent_pipeline()
print("\n" + "=" * 60)
print("Examples completed!")
print("=" * 60)
@@ -0,0 +1,274 @@
"""
Storage Backends Example for PraisonAI Agents.
Demonstrates how to use different storage backends (File, SQLite, Redis)
for agent data persistence across all supported components.
Supported Backends:
- FileBackend: JSON file-based storage (default, zero dependencies)
- SQLiteBackend: SQLite database storage (zero dependencies)
- RedisBackend: Redis-based storage (requires redis package)
Supported Components:
- BaseJSONStore: Generic JSON storage
- TrainingStorage: Agent training data
- SessionManager: CLI session state
- RunHistory: Recipe run history
- MCPToolIndex: MCP tool schemas
Usage:
python storage_backends_example.py
"""
from praisonaiagents.storage import (
FileBackend,
SQLiteBackend,
BaseJSONStore,
get_backend,
)
def example_file_backend():
"""Example: Using FileBackend for JSON file storage."""
print("\n=== FileBackend Example ===")
# Create file backend
backend = FileBackend(storage_dir="/tmp/praison_example")
# Save data
backend.save("agent_state", {
"agent_name": "Research Assistant",
"messages": ["Hello", "How can I help?"],
"context": {"topic": "AI"}
})
# Load data
data = backend.load("agent_state")
print(f"Loaded: {data}")
# List keys
keys = backend.list_keys()
print(f"Keys: {keys}")
# Check existence
print(f"Exists: {backend.exists('agent_state')}")
# Clean up
backend.delete("agent_state")
print("Deleted agent_state")
def example_sqlite_backend():
"""Example: Using SQLiteBackend for database storage."""
print("\n=== SQLiteBackend Example ===")
# Create SQLite backend (single file, better for concurrent access)
backend = SQLiteBackend(db_path="/tmp/praison_example.db")
# Save multiple sessions
for i in range(3):
backend.save(f"session_{i}", {
"session_id": f"sess-{i}",
"messages": [f"Message {j}" for j in range(i + 1)],
"timestamp": "2024-01-01T00:00:00Z"
})
# List all sessions
keys = backend.list_keys(prefix="session_")
print(f"Sessions: {keys}")
# Load specific session
session = backend.load("session_1")
print(f"Session 1: {session}")
# Clean up
backend.clear()
backend.close()
print("Cleaned up SQLite backend")
def example_backend_factory():
"""Example: Using get_backend factory function."""
print("\n=== Backend Factory Example ===")
# Easy switching between backends
backend_type = "sqlite" # Change to "file" or "redis" as needed
if backend_type == "file":
backend = get_backend("file", storage_dir="/tmp/praison_factory")
elif backend_type == "sqlite":
backend = get_backend("sqlite", db_path="/tmp/praison_factory.db")
# elif backend_type == "redis":
# backend = get_backend("redis", url="redis://localhost:6379")
# Use backend
backend.save("config", {"model": "gpt-4", "temperature": 0.7})
config = backend.load("config")
print(f"Config: {config}")
# Clean up
backend.delete("config")
def example_with_base_json_store():
"""Example: Using BaseJSONStore with custom backend."""
print("\n=== BaseJSONStore with Backend Example ===")
# Create a custom store with SQLite backend
backend = SQLiteBackend(db_path="/tmp/praison_store.db")
store = BaseJSONStore(
storage_path="/tmp/my_store.json", # Used as key name
backend=backend,
)
# Save and load data
store.save({"items": [1, 2, 3], "count": 3})
data = store.load()
print(f"Store data: {data}")
# Clean up
store.delete()
backend.close()
def example_training_storage_with_backend():
"""Example: Using TrainingStorage with SQLite backend."""
print("\n=== TrainingStorage with Backend Example ===")
try:
from praisonai.train.agents.storage import TrainingStorage
# Create SQLite backend for training data
backend = SQLiteBackend(db_path="/tmp/training.db")
# Use with TrainingStorage
storage = TrainingStorage(
session_id="train-example-001",
backend=backend,
)
print(f"Training storage created with session: {storage.session_id}")
print("Training data will be stored in SQLite instead of JSON files")
backend.close()
except ImportError:
print("praisonai package not installed, skipping training example")
def example_session_manager_with_backend():
"""Example: Using SessionManager with SQLite backend."""
print("\n=== SessionManager with Backend Example ===")
try:
from praisonai.cli.state.sessions import SessionManager
# Create SQLite backend for sessions
backend = SQLiteBackend(db_path="/tmp/sessions.db")
# Use with SessionManager
manager = SessionManager(backend=backend)
sessions = manager.list(limit=10)
print(f"SessionManager created with SQLite backend, {len(sessions)} sessions")
print("Sessions will be stored in database instead of files")
backend.close()
except ImportError:
print("praisonai package not installed, skipping session example")
def example_run_history_with_backend():
"""Example: Using RunHistory with SQLite backend."""
print("\n=== RunHistory with Backend Example ===")
try:
from praisonai.recipe.history import RunHistory
# Create SQLite backend for run history
backend = SQLiteBackend(db_path="/tmp/runs.db")
# Use with RunHistory
history = RunHistory(backend=backend)
runs = history.list()
print(f"RunHistory created with SQLite backend, {len(runs)} runs")
print("Recipe runs will be stored in database instead of files")
backend.close()
except ImportError:
print("praisonai package not installed, skipping run history example")
def example_mcp_tool_index_with_backend():
"""Example: Using MCPToolIndex with SQLite backend."""
print("\n=== MCPToolIndex with Backend Example ===")
try:
from praisonai.mcp_server.tool_index import MCPToolIndex
# Create SQLite backend for MCP tool index
backend = SQLiteBackend(db_path="/tmp/mcp.db")
# Use with MCPToolIndex
index = MCPToolIndex(backend=backend)
servers = index.list_servers()
print(f"MCPToolIndex created with SQLite backend, {len(servers)} servers")
print("MCP tool schemas will be stored in database instead of files")
backend.close()
except ImportError:
print("praisonai package not installed, skipping MCP example")
def example_redis_backend():
"""Example: Using RedisBackend for high-speed caching."""
print("\n=== RedisBackend Example ===")
try:
from praisonaiagents.storage import RedisBackend
# Create Redis backend with TTL
backend = RedisBackend(
url="redis://localhost:6379",
prefix="example:",
ttl=3600 # 1 hour TTL
)
# CRUD operations
backend.save("test_key", {"data": "value", "count": 42})
data = backend.load("test_key")
print(f"Loaded from Redis: {data}")
# List keys
keys = backend.list_keys()
print(f"Keys: {keys}")
# Clean up
backend.delete("test_key")
backend.close()
print("RedisBackend example completed")
except ImportError:
print("redis package not installed: pip install redis")
except Exception as e:
print(f"Redis not available: {e}")
if __name__ == "__main__":
print("PraisonAI Storage Backends Examples")
print("=" * 50)
example_file_backend()
example_sqlite_backend()
example_backend_factory()
example_with_base_json_store()
example_training_storage_with_backend()
example_session_manager_with_backend()
example_run_history_with_backend()
example_mcp_tool_index_with_backend()
example_redis_backend()
print("\n" + "=" * 50)
print("All examples completed!")
+211
View File
@@ -0,0 +1,211 @@
"""
Custom Trace Sink Example
Demonstrates how to create custom trace sinks for PraisonAI agents.
The trace system uses a protocol-driven design - implement 3 methods
and your sink works with any agent.
Protocol: ContextTraceSinkProtocol (AGENTS.md naming: XProtocol for interfaces)
- emit(event) - Receive a trace event
- flush() - Flush any buffered events
- close() - Release resources
Examples:
1. HTTP Sink - Send events to a remote server
2. SQLite Sink - Store events in a database
3. Console Sink - Pretty-print events to terminal
"""
from praisonaiagents import (
Agent,
ContextTraceEmitter,
ContextTraceSinkProtocol, # Protocol for type hints (optional)
trace_context,
)
# =============================================================================
# Example 1: HTTP Sink - Send events to a remote server
# =============================================================================
class HTTPSink:
"""Send trace events to a remote HTTP endpoint."""
def __init__(self, url: str, batch_size: int = 10):
self.url = url
self.batch_size = batch_size
self.buffer = []
def emit(self, event):
"""Buffer events and send in batches."""
self.buffer.append(event.to_dict())
if len(self.buffer) >= self.batch_size:
self.flush()
def flush(self):
"""Send buffered events to server."""
if self.buffer:
# In production, use: requests.post(self.url, json=self.buffer)
print(f"[HTTP] Would send {len(self.buffer)} events to {self.url}")
self.buffer.clear()
def close(self):
"""Flush remaining events."""
self.flush()
# =============================================================================
# Example 2: SQLite Sink - Store events in a database
# =============================================================================
class SQLiteSink:
"""Store trace events in SQLite database."""
def __init__(self, db_path: str = "traces.db"):
import sqlite3
self.conn = sqlite3.connect(db_path)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY,
session_id TEXT,
event_type TEXT,
agent_name TEXT,
timestamp REAL,
data TEXT
)
""")
self.conn.commit()
def emit(self, event):
"""Insert event into database."""
import json
self.conn.execute(
"INSERT INTO events (session_id, event_type, agent_name, timestamp, data) VALUES (?, ?, ?, ?, ?)",
(event.session_id, event.event_type.value, event.agent_name, event.timestamp, json.dumps(event.data))
)
def flush(self):
"""Commit pending transactions."""
self.conn.commit()
def close(self):
"""Commit and close connection."""
self.flush()
self.conn.close()
# =============================================================================
# Example 3: Console Sink - Pretty-print events
# =============================================================================
class ConsoleSink:
"""Print trace events to console with colors."""
COLORS = {
"session_start": "\033[92m", # Green
"session_end": "\033[92m",
"agent_start": "\033[94m", # Blue
"agent_end": "\033[94m",
"tool_call_start": "\033[93m", # Yellow
"tool_call_end": "\033[93m",
"llm_request": "\033[95m", # Magenta
"llm_response": "\033[95m",
}
RESET = "\033[0m"
def emit(self, event):
"""Print event with color coding."""
color = self.COLORS.get(event.event_type.value, "")
print(f"{color}[{event.event_type.value}]{self.RESET} {event.agent_name or 'session'}")
def flush(self):
pass
def close(self):
pass
# =============================================================================
# Usage Examples
# =============================================================================
def example_http_sink():
"""Example: Send events to HTTP endpoint."""
print("\n=== HTTP Sink Example ===")
sink = HTTPSink(url="https://my-telemetry.example.com/events")
emitter = ContextTraceEmitter(sink=sink, session_id="http-demo", enabled=True)
with trace_context(emitter):
# Events automatically go to HTTPSink
emitter.session_start({"demo": True})
emitter.agent_start("demo_agent", {"role": "demo"})
emitter.agent_end("demo_agent")
emitter.session_end()
print("HTTP sink example complete")
def example_console_sink():
"""Example: Pretty-print events to console."""
print("\n=== Console Sink Example ===")
sink = ConsoleSink()
emitter = ContextTraceEmitter(sink=sink, session_id="console-demo", enabled=True)
with trace_context(emitter):
emitter.session_start({})
emitter.agent_start("researcher", {"role": "Research Assistant"})
emitter.tool_call_start("researcher", "web_search", {"query": "AI trends"})
emitter.tool_call_end("researcher", "web_search", "Found 10 results", 150.0)
emitter.llm_request("researcher", "gpt-4o-mini", [{"role": "user", "content": "test"}])
emitter.llm_response("researcher", 100, 500, "stop", "Response text")
emitter.agent_end("researcher")
emitter.session_end()
print("Console sink example complete")
def example_with_real_agent():
"""Example: Use custom sink with a real agent.
Note: Requires OPENAI_API_KEY environment variable.
"""
import os
print("\n=== Real Agent with Custom Sink ===")
# Check for API key
if not os.environ.get("OPENAI_API_KEY"):
print("Skipping real agent test (no OPENAI_API_KEY)")
return
# Create custom sink
sink = ConsoleSink()
emitter = ContextTraceEmitter(sink=sink, session_id="real-agent-demo", enabled=True)
# Use trace_context to capture all agent events
with trace_context(emitter):
agent = Agent(
name="demo_agent",
instructions="You are a helpful assistant. Be brief.",
llm="gpt-4o-mini",
)
# This chat will emit events to ConsoleSink
result = agent.chat("Say hello in 5 words or less")
print(f"\nAgent response: {result}")
print("Real agent example complete")
if __name__ == "__main__":
# Run examples
example_http_sink()
example_console_sink()
example_with_real_agent()
print("\n✅ All examples complete!")
print("\nKey points:")
print("1. Implement emit(), flush(), close() - that's it!")
print("2. Use trace_context() for automatic cleanup")
print("3. Zero overhead when tracing is disabled")
+62
View File
@@ -0,0 +1,62 @@
# Nested Workflow Patterns Example
# Demonstrates: nested loops, if conditions, and combined patterns
agents:
data_processor:
role: Data Processor
goal: Process individual data items
backstory: Expert at transforming and validating data
quality_checker:
role: Quality Checker
goal: Verify data quality meets standards
backstory: Meticulous quality assurance specialist
approver:
role: Approver
goal: Approve high-quality items
backstory: Senior reviewer with approval authority
rejector:
role: Rejector
goal: Handle rejected items
backstory: Specialist in rejection workflows
workflow:
name: Nested Data Processing Pipeline
variables:
batches:
- name: batch_1
items: ["item_a", "item_b"]
- name: batch_2
items: ["item_c", "item_d"]
quality_threshold: 80
steps:
# Step 1: Process each batch with nested item loop
- loop:
over: batches
var_name: batch
steps:
# Nested loop: process each item in the batch
- loop:
over: "{{batch.items}}"
var_name: item
steps:
- agent: data_processor
action: "Process item {{item}} from {{batch.name}}"
# Step 2: Quality check with conditional branching
- agent: quality_checker
action: "Check quality of all processed items and return a score"
# Step 3: Conditional approval based on quality score
- if:
condition: "{{quality_score}} >= {{quality_threshold}}"
then:
- agent: approver
action: "Approve the batch - quality score meets threshold"
else:
- agent: rejector
action: "Reject the batch - quality score below threshold"
+280
View File
@@ -0,0 +1,280 @@
# Performance Benchmark CI
# Runs import time and instantiation benchmarks on push/PR
# Fails if import time exceeds 200ms threshold
name: Performance Benchmark
on:
push:
branches: [main, develop]
paths:
- 'praisonaiagents/**'
- 'benchmarks/**'
- '.github/workflows/benchmark.yml'
pull_request:
branches: [main, develop]
paths:
- 'praisonaiagents/**'
- 'benchmarks/**'
- '.github/workflows/benchmark.yml'
workflow_dispatch: # Allow manual trigger
jobs:
benchmark:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install package
run: |
pip install -e .
pip install pytest
- name: Measure Import Time
id: import_time
run: |
# Measure import time (full Agent import)
IMPORT_TIME=$(python -c "
import time
import sys
# Clear any cached modules
for key in list(sys.modules.keys()):
if 'praisonai' in key:
del sys.modules[key]
start = time.perf_counter()
from praisonaiagents import Agent
elapsed = (time.perf_counter() - start) * 1000
print(f'{elapsed:.1f}')
")
echo "import_time=$IMPORT_TIME" >> $GITHUB_OUTPUT
echo "Import time: ${IMPORT_TIME}ms"
- name: Check Import Time Threshold
run: |
IMPORT_TIME=${{ steps.import_time.outputs.import_time }}
THRESHOLD=200
echo "Import time: ${IMPORT_TIME}ms"
echo "Threshold: ${THRESHOLD}ms"
# Compare using bc for floating point
if (( $(echo "$IMPORT_TIME > $THRESHOLD" | bc -l) )); then
echo "❌ FAIL: Import time ${IMPORT_TIME}ms exceeds threshold ${THRESHOLD}ms"
exit 1
else
echo "✅ PASS: Import time ${IMPORT_TIME}ms is within threshold ${THRESHOLD}ms"
fi
- name: Measure Instantiation Time
run: |
python -c "
import time
from praisonaiagents import Agent
# Warmup
for _ in range(5):
Agent(name='Test', output='silent')
# Measure
times = []
for _ in range(50):
start = time.perf_counter()
Agent(name='Test', output='silent')
times.append((time.perf_counter() - start) * 1e6)
avg = sum(times) / len(times)
min_t = min(times)
max_t = max(times)
print(f'Instantiation time:')
print(f' Average: {avg:.2f}μs')
print(f' Min: {min_t:.2f}μs')
print(f' Max: {max_t:.2f}μs')
"
- name: Verify Lazy Imports
run: |
python -c "
import sys
# Clear any cached modules
for key in list(sys.modules.keys()):
if 'praisonai' in key or 'litellm' in key:
del sys.modules[key]
# Import package
import praisonaiagents
# Check heavy deps are NOT loaded
heavy_deps = ['litellm', 'chromadb', 'mem0', 'rich']
loaded = []
for dep in heavy_deps:
if dep in sys.modules:
loaded.append(dep)
if loaded:
print(f'⚠️ WARNING: Heavy deps loaded eagerly: {loaded}')
# Don't fail, just warn - some deps may be needed
else:
print('✅ All heavy deps are lazy loaded')
"
- name: Run Quick Benchmark (if available)
continue-on-error: true
run: |
if [ -f "benchmarks/quick_benchmark.py" ]; then
python benchmarks/quick_benchmark.py --fast || true
else
echo "Quick benchmark not found, skipping"
fi
# Real API benchmark - only runs when API keys are available
# Triggered manually or on main branch with secrets
real-api-benchmark:
runs-on: ubuntu-latest
timeout-minutes: 15
if: github.event_name == 'workflow_dispatch' || (github.ref == 'refs/heads/main' && github.event_name == 'push')
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install package
run: |
pip install -e .
pip install pytest
- name: Check API Key Availability
id: check_keys
run: |
if [ -n "${{ secrets.OPENAI_API_KEY }}" ]; then
echo "has_openai=true" >> $GITHUB_OUTPUT
echo "✅ OpenAI API key available"
else
echo "has_openai=false" >> $GITHUB_OUTPUT
echo "⚠️ OpenAI API key not available, skipping real API tests"
fi
- name: Run Real API Benchmark
if: steps.check_keys.outputs.has_openai == 'true'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
python -c "
import time
import os
# Verify API key is set
if not os.environ.get('OPENAI_API_KEY'):
print('❌ OPENAI_API_KEY not set')
exit(1)
from praisonaiagents import Agent
# Create a simple agent
agent = Agent(
name='BenchmarkAgent',
instructions='You are a helpful assistant. Be concise.',
llm='gpt-4o-mini',
output='silent'
)
# Warmup run
print('Running warmup...')
agent.chat('Say hello in one word')
# Benchmark runs
print('Running benchmark (3 iterations)...')
times = []
for i in range(3):
start = time.perf_counter()
response = agent.chat('What is 2+2? Answer with just the number.')
elapsed = time.perf_counter() - start
times.append(elapsed)
print(f' Run {i+1}: {elapsed:.2f}s')
avg = sum(times) / len(times)
print(f'')
print(f'Real API Benchmark Results:')
print(f' Average response time: {avg:.2f}s')
print(f' Min: {min(times):.2f}s')
print(f' Max: {max(times):.2f}s')
# Verify response quality
if '4' in response:
print('✅ Response quality check passed')
else:
print(f'⚠️ Unexpected response: {response}')
"
- name: Run Multi-Agent Benchmark
if: steps.check_keys.outputs.has_openai == 'true'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
python -c "
import time
import os
from praisonaiagents import Agent, Agents, Task
# Create agents
researcher = Agent(
name='Researcher',
instructions='You research topics. Be brief.',
llm='gpt-4o-mini',
output='silent'
)
writer = Agent(
name='Writer',
instructions='You write summaries. Be brief.',
llm='gpt-4o-mini',
output='silent'
)
# Create tasks
task1 = Task(
description='List 2 facts about Python programming',
agent=researcher,
expected_output='2 brief facts'
)
task2 = Task(
description='Summarize the facts in one sentence',
agent=writer,
expected_output='One sentence summary'
)
# Run workflow
print('Running multi-agent workflow...')
start = time.perf_counter()
agents = Agents(agents=[researcher, writer], tasks=[task1, task2])
result = agents.start()
elapsed = time.perf_counter() - start
print(f'')
print(f'Multi-Agent Benchmark Results:')
print(f' Total workflow time: {elapsed:.2f}s')
print(f' Agents: 2')
print(f' Tasks: 2')
print('✅ Multi-agent workflow completed successfully')
"
@@ -11,7 +11,8 @@ Shows how to monitor agent performance when using external tools.
"""
from praisonaiagents import Agent
from praisonaiagents.tools import duckduckgo, wikipedia_tools
from praisonaiagents import duckduckgo
from praisonai_tools import wikipedia_tools
from praisonaiagents.telemetry import (
monitor_function, track_api_call, get_api_stats,
get_slowest_functions, performance_monitor
+635
View File
@@ -0,0 +1,635 @@
# AGENTS.md - PraisonAI SDK Comprehensive Guide
> **For AI Agents and Developers**: This document provides the complete context needed to work with the PraisonAI ecosystem, including design principles, architecture, repository structure, and implementation guidelines.
---
## 1. What is PraisonAI?
PraisonAI is a **high-performance, agentic AI framework** designed for building production-ready AI agents and multi-agent workflows.
### Core Philosophy
```
Simpler than competitors • More extensible • Faster • Agent-centric
```
| Principle | Description |
|-----------|-------------|
| **Agent-Centric** | Every design decision centers on Agents, workflows, sessions, tools, and memory |
| **Protocol-Driven Core** | Core SDK is lightweight with protocols/hooks/adapters only |
| **Minimal API** | Fewer parameters, sensible defaults, explicit overrides |
| **Performance-First** | Lazy loading, optional dependencies, no hot-path regressions |
| **Production-Ready** | Safe by default, multi-agent safe, async-safe |
Powerful, lightweight, highly reliable and robust
Easy and developer frienldy for non developers
Easy and user friendly
---
## 2. Repository Structure
### 2.1 Canonical Paths
```
/Users/praison/praisonai-package/
├── src/
│ ├── praisonai-agents/ # Core SDK (praisonaiagents)
│ │ ├── praisonaiagents/ # Python package
│ │ ├── tests/ # Unit & integration tests
│ │ ├── examples/ # Package-level examples
│ │ └── pyproject.toml # Package config
│ │
│ ├── praisonai/ # Wrapper (praisonai)
│ │ ├── praisonai/ # CLI, integrations, heavy impls
│ │ └── pyproject.toml
│ │
│ └── praisonai-ts/ # TypeScript SDK
│ └── src/
│
├── examples/ # Main examples directory
│ ├── python/ # Python examples (primary)
│ ├── yaml/ # YAML configuration examples
│ ├── js/ # JavaScript/TypeScript examples
│ └── cookbooks/ # End-to-end recipes
│
/Users/praison/PraisonAI-tools/ # External tools/plugins
/Users/praison/PraisonAIDocs/ # Documentation (Mintlify)
└── docs/
├── agents/
├── api/
├── capabilities/
├── cli/
├── concepts/
├── features/
├── guides/
├── knowledge/
├── memory/
├── models/
├── observability/
├── rag/
├── sdk/
├── tools/
├── tutorials/
└── ... (49+ subdirectories)
```
### 2.2 Package Hierarchy
```
┌─────────────────────────────────────────────────────────────────┐
│ praisonai (Wrapper) │
│ CLI • Integrations • Heavy Implementations • Optional Deps │
├─────────────────────────────────────────────────────────────────┤
│ praisonaiagents (Core SDK) │
│ Protocols • Hooks • Adapters • Base Classes • Decorators │
├─────────────────────────────────────────────────────────────────┤
│ praisonai-tools (External) │
│ Optional Tools • Plugins • Community Extensions │
└─────────────────────────────────────────────────────────────────┘
```
---
## 3. Core SDK Architecture (praisonaiagents)
### 3.1 Key Modules
| Module | Size | Purpose |
|--------|------|---------|
| `agent/` | 1.4M | Agent class, protocols, handoff, autonomy |
| `memory/` | 984K | Memory protocols, adapters, file memory |
| `context/` | 960K | Context management, artifacts, fast context |
| `tools/` | 732K | Tool SDK, decorators, registry, protocols |
| `llm/` | 620K | LLM client, model router |
| `workflows/` | 456K | Workflow engine, patterns (Route, Parallel, Loop) |
| `knowledge/` | 436K | Knowledge/RAG protocols, vector stores |
| `hooks/` | 260K | Hook system, middleware, events |
| `eval/` | 240K | Evaluation framework |
| `rag/` | 264K | RAG protocols, retriever, reranker |
| `plugins/` | 56K | Plugin manager, registry |
| `policy/` | 64K | Policy engine |
| `bus/` | 48K | Event bus |
| `streaming/` | 56K | Streaming events, callbacks |
| `trace/` | 152K | Trace protocols, context events |
### 3.2 Protocol-First Design
Core SDK uses `typing.Protocol` for all extension points:
```
Pattern: Each major module has a protocols.py file
- Protocols define WHAT (interface contract)
- Adapters implement HOW (concrete implementation)
- Naming: XProtocol for interfaces, XAdapter for implementations
- Users can implement any protocol for custom behavior
```
**Discovery:** Find all protocols dynamically:
```bash
grep -r "class.*Protocol" praisonaiagents/ --include="*.py"
```
**Key principle:** When adding new extensibility, create a Protocol first, then implement adapters.
### 3.3 Core Dependencies
```toml
# Required (always loaded) - pyproject.toml
dependencies = [
"pydantic>=2.10.0", # Type validation
"rich", # Terminal UI
"openai>=2.0.0", # OpenAI client
"posthog>=3.0.0", # Telemetry
"aiohttp>=3.8.0" # Async HTTP
]
# Optional (lazy loaded)
[project.optional-dependencies]
memory = ["chromadb>=1.0.0", "litellm>=1.81.0"]
knowledge = ["mem0ai>=0.1.0", "chromadb>=1.0.0", "markitdown[all]>=0.1.0"]
mcp = ["mcp>=1.20.0", "fastapi>=0.115.0", "uvicorn>=0.34.0"]
llm = ["litellm>=1.81.0"]
api = ["fastapi>=0.115.0", "uvicorn>=0.34.0"]
auth = ["PyJWT>=2.8.0", "passlib[bcrypt]>=1.7.4"]
mongodb = ["pymongo>=4.6.3", "motor>=3.4.0"]
```
---
## 4. Core Engineering Principles
### 4.1 Protocol-Driven Core (MUST)
```
Core SDK (praisonaiagents) Wrapper (praisonai)
━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━
✅ Protocols ✅ Heavy implementations
✅ Hooks ✅ CLI commands
✅ Adapters ✅ External integrations
✅ Base classes ✅ Optional deps
✅ Decorators ✅ UI components
✅ Dataclasses ✅ Database adapters
❌ Heavy implementations ❌ Core logic
```
### 4.2 No Performance Impact (MUST)
```python
# ✅ CORRECT: Lazy import
def use_chromadb():
try:
import chromadb
except ImportError:
raise ImportError("Install with: pip install praisonaiagents[memory]")
# ❌ WRONG: Module-level heavy import
import chromadb # Adds 500ms+ to import time
```
**Rules:**
- No module-level imports of optional dependencies
- No heavy initialization in `__init__.py`
- Use `__getattr__` for lazy loading in package roots
- Target: < 200ms package import time
### 4.3 DRY Approach (MUST)
- Reuse existing abstractions; don't duplicate
- Refactor safely when duplication is found
- Check existing protocols before creating new ones
- Use existing EventBus instead of creating new messaging
### 4.4 Agent-Centric Design (MUST)
Every API decision must prioritize:
1. **Agents** - Core execution unit
2. **Multi-agent workflows** - Coordination patterns
3. **Sessions** - State management
4. **Tools** - Agent capabilities
5. **Memory** - Persistence and context
### 4.5 Async-Safe & Multi-Agent Safe (MUST)
```
Async-Safe:
- All I/O operations must have async variants
- Never block the event loop with sync I/O in async context
- Use asyncio primitives for coordination (not threading)
Multi-Agent Safe:
- No shared mutable global state between agents
- Each agent owns its context, memory, and session
- Cross-agent communication via explicit channels (EventBus, handoff)
- Resource isolation by default, explicit sharing when needed
```
### 4.6 Invariants (Never Break)
| Invariant | Meaning |
|-----------|---------|
| Protocol-driven core | Core SDK has NO heavy implementations |
| Lazy imports | Optional deps never imported at module level |
| Backward compatible | Public API changes require deprecation cycle |
| Safe defaults | New features are opt-in, not opt-out |
| Deterministic tests | Tests must not depend on timing or external state |
### 4.7 Concurrency Model
```
Sync/Async Boundaries:
- User-facing: Both sync and async entry points (run/start)
- Internal: Prefer async, wrap for sync callers
- Tools: Support both sync and async execution
- Hooks: Fire in the current execution context (sync or async)
Event Loop Rules:
- One event loop per thread
- Never nest event loops
- Use asyncio.run() only at top level
```
### 4.8 Error Handling & Observability
```
Error Model:
- Fail fast with clear error messages
- Include remediation hints in exceptions
- Propagate context (agent name, tool name, session ID)
- Hook points for error interception (on_error events)
Observability Hooks:
- All key operations emit events via EventBus
- Trace protocols for external observability integration
- Structured logging with correlation IDs
- Metrics collection via optional adapters
```
### 4.9 Naming Conventions
```python
# Registration/Mutation
add_X() # User-facing registration (add_hook, add_policy)
remove_X() # Unregistration
register_X() # Internal/framework registration
# Retrieval
get_X() # Single item by ID (get_tool, get_policy)
search_X() # Multiple by query (search_memory)
list_X() # All items (list_tools, list_plugins)
has_X() # Existence check
# Persistence
save() / load() # Disk persistence (NOT store)
# Configuration
class XConfig: # Configuration dataclass (MemoryConfig, HooksConfig)
# Protocols
class XProtocol(Protocol): # Abstract interface
class XAdapter: # Implementation
# Execution
run() # Synchronous, blocking
start() # Async/long-running
execute() # Internal/low-level
```
---
## 5. API Philosophy
### 5.1 Simple API (User-Facing)
```python
# Basic usage - 3 lines
from praisonaiagents import Agent
agent = Agent(name="assistant", instructions="Be helpful")
response = agent.start("Hello!")
# With tools - still simple
from praisonaiagents import Agent, tool
@tool
def search(query: str) -> list:
"""Search the web."""
return [{"result": query}]
agent = Agent(name="researcher", tools=[search])
```
### 5.2 Progressive Disclosure
```python
# Level 1: Minimal (most users)
agent = Agent(name="assistant")
# Level 2: With common options
agent = Agent(
name="assistant",
llm="gpt-4o-mini",
memory=True
)
# Level 3: Full control (power users)
from praisonaiagents import Agent, MemoryConfig, HooksConfig
agent = Agent(
name="assistant",
llm="gpt-4o-mini",
memory=MemoryConfig(provider="chroma", use_long_term=True),
hooks=HooksConfig(before_tool=[my_validator]),
guardrail=my_guardrail
)
```
---
## 6. Extension Points
### 6.1 Tools
```python
# Decorator style (simplest)
from praisonaiagents import tool
@tool
def my_tool(query: str) -> str:
"""Tool description."""
return f"Result: {query}"
# Class style (more control)
from praisonaiagents.tools import BaseTool
class MyTool(BaseTool):
name = "my_tool"
description = "Does something"
def run(self, query: str) -> str:
return f"Result: {query}"
```
**Base files:**
- `praisonaiagents/tools/base.py` - BaseTool class
- `praisonaiagents/tools/decorator.py` - @tool decorator
- `praisonaiagents/tools/registry.py` - Tool registry
> [!IMPORTANT]
> **Agents only recognize parameters.** When creating tools, always expose all options as function parameters—agents cannot discover env vars or config files.
### 6.2 Hooks & Middleware
```python
from praisonaiagents.hooks import add_hook, before_tool, after_tool
@before_tool
def validate_args(event):
print(f"Calling: {event.tool_name}")
@after_tool
def log_result(event):
print(f"Result: {event.result}")
# Or functional style
hook_id = add_hook("before_tool", my_handler)
```
### 6.3 Memory Adapters
```python
from praisonaiagents.memory.protocols import MemoryProtocol
class MyMemoryAdapter: # Implements MemoryProtocol
def store_short_term(self, text, metadata=None):
...
def search_short_term(self, query, limit=5):
...
```
### 6.4 Database Adapters
```python
# Base files in praisonaiagents/db/
from praisonaiagents.db import DbAdapter, AsyncDbAdapter
class MyDbAdapter(DbAdapter):
def connect(self): ...
def query(self, sql, params): ...
```
---
## 7. CLI Representation
Every feature must have CLI parity:
```bash
# Basic agent
praisonai agent run --name "assistant" --prompt "Hello"
# With tools
praisonai agent run --tools web_search --prompt "Search for Python"
# Multi-agent workflow
praisonai workflow run --file workflow.yaml
# Server mode
praisonai serve --port 8000
```
---
## 8. Documentation Standards
### 8.1 Mintlify Components
Use these Mintlify components for docs:
- `<Accordion>` / `<AccordionGroup>` - Collapsible content
- `<Card>` / `<CardGroup>` / `<Columns>` - Visual containers
- `<CodeGroup>` - Tabbed code blocks
- `<Steps>` / `<Step>` - Sequential instructions
- `<Tabs>` / `<Tab>` - Tabbed content
- `<Note>`, `<Warning>`, `<Info>`, `<Tip>` - Callouts
- `<Frame>` - Image containers
- `<Icon>` - Inline icons
- `<Tooltip>` - Hover text
### 8.2 Mermaid Diagram Standards
```
Two-color scheme:
- Dark Red (#8B0000) - Agents, inputs, outputs
- Teal/Cyan (#189AB4) - Tools
- White text (#fff) for contrast
```
```mermaid
graph LR
A[Input]:::agent --> B[Agent]:::agent
B --> C[Tool]:::tool
C --> D[Output]:::agent
classDef agent fill:#8B0000,color:#fff
classDef tool fill:#189AB4,color:#fff
```
### 8.3 Documentation Principles
| Principle | Description |
|-----------|-------------|
| **Beginner-friendly** | Non-developers should understand core concepts |
| **Copy-paste success** | Code examples must run with minimal setup |
| **Less text, more interaction** | Prefer components over long paragraphs |
| **Show, don't tell** | Use diagrams, tabs, and accordions to explain |
| **Few lines, big impact** | Examples should feel like "only 3 lines to do this" |
### 8.4 Page Title Conventions
```
SDK docs: "Memory Module", "Tools Module", "Hooks Module"
API docs: "Agent API", "Tool API", "Memory API"
Guides: "Single Agent Guide", "Multi-Agent Guide"
Tutorials: "Build a Research Agent", "Create Custom Tools"
```
### 8.5 Example Quality Standards
```
Every code example MUST:
- Run without modification (copy-paste success)
- Include necessary imports
- Use realistic but simple data
- Show expected output in comments
- Be the SHORTEST way to accomplish the task
```
---
## 9. Testing Standards
### 9.1 TDD Mandatory
```python
# 1. Write failing test first
def test_new_feature():
result = new_feature()
assert result == expected
# 2. Implement feature
# 3. Verify test passes
```
### 9.2 Test Structure
```
tests/
├── unit/ # Fast, isolated tests
├── integration/ # Cross-module tests
├── e2e/ # End-to-end tests
└── fixtures/ # Test data
```
### 9.3 Run Tests
```bash
# All tests
pytest tests/
# Specific category
pytest tests/unit/
# With coverage
pytest --cov=praisonaiagents tests/
```
---
## 10. Implementation Checklist
For every feature/change:
- [ ] **Protocol-first**: Add protocol to core if needed
- [ ] **No new deps**: Use optional dependencies only
- [ ] **Lazy imports**: Heavy deps imported inside functions
- [ ] **Naming**: Follow conventions (add_*, get_*, XConfig)
- [ ] **Tests**: TDD - write failing tests first
- [ ] **CLI**: Add corresponding CLI command/option
- [ ] **Docs**: Update Mintlify documentation
- [ ] **Examples**: Add to examples/ directory
- [ ] **Multi-agent safe**: No shared mutable state
- [ ] **Async-safe**: Support async/await patterns
- [ ] **Performance**: No import-time or hot-path regressions
---
## 11. Quick Reference
### 11.1 Core Imports
```python
# Most common
from praisonaiagents import Agent, Agents, Task, tool, Tools
# Configuration
from praisonaiagents import (
MemoryConfig, KnowledgeConfig, PlanningConfig,
OutputConfig, ExecutionConfig, HooksConfig
)
# Hooks
from praisonaiagents.hooks import add_hook, before_tool, after_tool
# Event Bus
from praisonaiagents.bus import EventBus, get_default_bus
# Memory
from praisonaiagents import Memory, Session
# Workflows
from praisonaiagents import Workflow, Route, Parallel, Loop
```
### 11.2 File Locations
| What | Where |
|------|-------|
| Agent class | `praisonaiagents/agent/agent.py` |
| Tool decorator | `praisonaiagents/tools/decorator.py` |
| Base tool | `praisonaiagents/tools/base.py` |
| Hook system | `praisonaiagents/hooks/` |
| Memory protocols | `praisonaiagents/memory/protocols.py` |
| Event bus | `praisonaiagents/bus/bus.py` |
| Workflow engine | `praisonaiagents/workflows/workflows.py` |
| Policy engine | `praisonaiagents/policy/engine.py` |
---
## 12. Design Goals
> Make PraisonAI the **best agent framework in the world**
1. **Simpler** than competitors (fewer concepts, cleaner API)
2. **More extensible** (protocol-driven, plugin-ready)
3. **Faster** (lazy loading, minimal overhead)
4. **Safer** (guardrails, policies, HITL by default)
5. **Production-ready** (observability, checkpoints, replay)
---
## 13. Paid Value Path
The core remains **free and open source**. Clear upgrade paths to paid value:
| Free (Core) | Paid (Cloud/Services) |
|-------------|----------------------|
| All protocols | Managed hosting |
| All base classes | Enterprise support |
| Local execution | Budget management |
| File-based memory | Multi-tenant RBAC |
| Basic observability | Advanced analytics |
| Community support | SLA-backed support |
---
*This document is the source of truth for the PraisonAI SDK architecture and design principles.*
@@ -1,6 +1,6 @@
# PraisonAI Agents - Tools Benchmark Results
**Generated:** 2025-12-18 14:40:35
**Generated:** 2026-01-23 11:50:18
**Iterations:** 100
**Test:** Agent instantiation WITH TOOLS
@@ -8,22 +8,22 @@
| Framework | Avg Time (μs) | Relative |
|-----------|---------------|----------|
| **PraisonAI** | **3.24** | **1.00x (fastest)** |
| Agno | 5.12 | 1.58x |
| PraisonAI (LiteLLM) | 8.59 | 2.65x |
| OpenAI Agents SDK | 279.95 | 86.44x |
| LangGraph | 2,310.82 | 713x |
| CrewAI | 15,773.44 | 4,870x |
| **PraisonAI** | **5.30** | **1.00x (fastest)** |
| Agno | 5.62 | 1.06x |
| PraisonAI (LiteLLM) | 11.08 | 2.09x |
| OpenAI Agents SDK | 315.49 | 59.55x |
| LangGraph | 2,511.95 | 474x |
| CrewAI | 41,349.24 | 7,804x |
## Package Versions
| Package | Version |
|---------|--------|
| PraisonAI | 0.1.5 |
| Agno | 2.3.14 |
| PraisonAI | 0.13.11 |
| Agno | 2.4.2 |
| OpenAI Agents SDK | 0.6.3 |
| LangGraph | 1.0.5 |
| CrewAI | 1.6.1 |
| LangGraph | 1.0.7 |
| CrewAI | 1.8.1 |
## How to Reproduce
+219 -13
View File
@@ -9,8 +9,16 @@ Key Optimizations Applied:
2. Lazy os.getcwd() - fast_context_path resolved only when needed
3. Lazy console initialization - Rich Console created on demand
Advanced Profiling Features:
- Line-by-line profiling of __init__ method
- Call graph analysis
- Memory allocation tracking
- Function call counting
- Attribute access profiling
Usage:
python benchmarks/deep_profiling.py
python benchmarks/deep_profiling.py --deep # More detailed profiling
"""
import time
@@ -136,7 +144,7 @@ def profile_praisonai_detailed():
instructions="Be concise, reply with one sentence.",
llm="gpt-4o-mini",
tools=tools,
verbose=False
output="silent"
)
profiler.end_step()
@@ -156,7 +164,7 @@ def profile_praisonai_detailed():
instructions="Be concise, reply with one sentence.",
llm="gpt-4o-mini",
tools=tools,
verbose=False
output="silent"
)
pr.disable()
@@ -249,14 +257,14 @@ def profile_praisonai_components():
# Test different configurations
configs = [
("Minimal (no tools, no llm)", {"name": "Test", "verbose": False}),
("With name only", {"name": "Test Agent", "verbose": False}),
("With instructions", {"name": "Test", "instructions": "Be concise", "verbose": False}),
("With llm (gpt-4o-mini)", {"name": "Test", "llm": "gpt-4o-mini", "verbose": False}),
("With llm (openai/gpt-4o-mini)", {"name": "Test", "llm": "openai/gpt-4o-mini", "verbose": False}),
("With tools", {"name": "Test", "tools": tools, "verbose": False}),
("Full config (OpenAI SDK)", {"name": "Test", "instructions": "Be concise", "llm": "gpt-4o-mini", "tools": tools, "verbose": False}),
("Full config (LiteLLM)", {"name": "Test", "instructions": "Be concise", "llm": "openai/gpt-4o-mini", "tools": tools, "verbose": False}),
("Minimal (no tools, no llm)", {"name": "Test", "output": "silent"}),
("With name only", {"name": "Test Agent", "output": "silent"}),
("With instructions", {"name": "Test", "instructions": "Be concise", "output": "silent"}),
("With llm (gpt-4o-mini)", {"name": "Test", "llm": "gpt-4o-mini", "output": "silent"}),
("With llm (openai/gpt-4o-mini)", {"name": "Test", "llm": "openai/gpt-4o-mini", "output": "silent"}),
("With tools", {"name": "Test", "tools": tools, "output": "silent"}),
("Full config (OpenAI SDK)", {"name": "Test", "instructions": "Be concise", "llm": "gpt-4o-mini", "tools": tools, "output": "silent"}),
("Full config (LiteLLM)", {"name": "Test", "instructions": "Be concise", "llm": "openai/gpt-4o-mini", "tools": tools, "output": "silent"}),
]
print(f"\n{'Configuration':<40} {'Avg Time (μs)':<15} {'Min (μs)':<15} {'Max (μs)':<15}")
@@ -295,7 +303,7 @@ def compare_specific_operations():
# Operation 1: Bare minimum instantiation
def praisonai_minimal():
return PraisonAgent(name="Test", verbose=False)
return PraisonAgent(name="Test", output="silent")
def agno_minimal():
return AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"))
@@ -306,7 +314,7 @@ def compare_specific_operations():
# Operation 2: With tools
def praisonai_with_tools():
return PraisonAgent(name="Test", tools=tools, verbose=False)
return PraisonAgent(name="Test", tools=tools, output="silent")
def agno_with_tools():
return AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"), tools=tools)
@@ -322,7 +330,7 @@ def compare_specific_operations():
instructions="Be concise",
llm="gpt-4o-mini",
tools=tools,
verbose=False
output="silent"
)
def agno_full():
@@ -344,12 +352,199 @@ def compare_specific_operations():
print(f"{op_name:<30} {praison_time:<18.2f} {agno_time:<18.2f} {ratio:<10.1f}x")
# ============================================================================
# Advanced: Attribute Access Profiling
# ============================================================================
def profile_attribute_access():
"""Profile attribute access patterns in Agent.__init__"""
print("\n" + "="*70)
print("ATTRIBUTE ACCESS PROFILING")
print("="*70)
from praisonaiagents import Agent as PraisonAgent
# Count attribute accesses during init
attr_counts = {}
original_setattr = object.__setattr__
def counting_setattr(obj, name, value):
if isinstance(obj, PraisonAgent):
attr_counts[name] = attr_counts.get(name, 0) + 1
return original_setattr(obj, name, value)
# Temporarily patch
import builtins
old_setattr = builtins.setattr if hasattr(builtins, 'setattr') else None
# Create agent and count
agent = PraisonAgent(name="Test", output="silent")
# Sort by count
sorted_attrs = sorted(attr_counts.items(), key=lambda x: x[1], reverse=True)
print(f"\n{'Attribute':<40} {'Set Count':<15}")
print("-" * 55)
for attr, count in sorted_attrs[:30]:
print(f"{attr:<40} {count:<15}")
print(f"\nTotal attributes set: {len(attr_counts)}")
print(f"Total setattr calls: {sum(attr_counts.values())}")
# ============================================================================
# Advanced: Function Call Analysis
# ============================================================================
def analyze_init_function_calls():
"""Analyze which functions are called during Agent.__init__"""
print("\n" + "="*70)
print("FUNCTION CALL ANALYSIS: PraisonAI Agent.__init__")
print("="*70)
import sys
from praisonaiagents import Agent as PraisonAgent
call_counts = {}
def trace_calls(frame, event, arg):
if event == 'call':
code = frame.f_code
func_name = code.co_name
filename = code.co_filename
# Only track praisonaiagents calls
if 'praisonaiagents' in filename:
key = f"{func_name} ({filename.split('/')[-1]}:{code.co_firstlineno})"
call_counts[key] = call_counts.get(key, 0) + 1
return trace_calls
# Enable tracing
sys.settrace(trace_calls)
try:
agent = PraisonAgent(name="Test", output="silent")
finally:
sys.settrace(None)
# Sort by count
sorted_calls = sorted(call_counts.items(), key=lambda x: x[1], reverse=True)
print(f"\n{'Function':<60} {'Calls':<10}")
print("-" * 70)
for func, count in sorted_calls[:40]:
print(f"{func:<60} {count:<10}")
print(f"\nTotal unique functions called: {len(call_counts)}")
print(f"Total function calls: {sum(call_counts.values())}")
# ============================================================================
# Advanced: Import Time Analysis
# ============================================================================
def analyze_import_overhead():
"""Analyze import overhead for key modules"""
print("\n" + "="*70)
print("IMPORT OVERHEAD ANALYSIS")
print("="*70)
import sys
import importlib
modules_to_test = [
'praisonaiagents',
'praisonaiagents.agent.agent',
'praisonaiagents.config.param_resolver',
'praisonaiagents.config.presets',
'praisonaiagents.config.feature_configs',
]
print(f"\n{'Module':<50} {'Import Time (ms)':<20}")
print("-" * 70)
for module_name in modules_to_test:
# Clear from cache
to_remove = [k for k in sys.modules.keys() if k.startswith('praisonaiagents')]
for k in to_remove:
del sys.modules[k]
start = time.perf_counter()
try:
importlib.import_module(module_name)
elapsed = (time.perf_counter() - start) * 1000
print(f"{module_name:<50} {elapsed:<20.2f}")
except ImportError as e:
print(f"{module_name:<50} FAILED: {e}")
# ============================================================================
# Advanced: Agno vs PraisonAI Architecture Comparison
# ============================================================================
def compare_architecture():
"""Compare architectural differences between Agno and PraisonAI"""
print("\n" + "="*70)
print("ARCHITECTURE COMPARISON: Agno vs PraisonAI")
print("="*70)
from praisonaiagents import Agent as PraisonAgent
from agno.agent import Agent as AgnoAgent
from agno.models.openai import OpenAIChat
# Create instances
praison_agent = PraisonAgent(name="Test", output="silent")
agno_agent = AgnoAgent(model=OpenAIChat(id="gpt-4o-mini"))
print("\n### Class Structure ###")
print(f"\nPraisonAI Agent:")
print(f" - Uses @dataclass: No (regular class)")
print(f" - __slots__: {'Yes' if hasattr(PraisonAgent, '__slots__') else 'No'}")
print(f" - Instance dict size: {len(praison_agent.__dict__)} attributes")
print(f"\nAgno Agent:")
print(f" - Uses @dataclass: Yes (init=False)")
print(f" - __slots__: {'Yes' if hasattr(AgnoAgent, '__slots__') else 'No'}")
print(f" - Instance dict size: {len(agno_agent.__dict__)} attributes")
print("\n### Key Differences ###")
print("\n1. Agno uses @dataclass(init=False) - class attributes pre-declared")
print("2. Agno __init__ is mostly simple attribute assignments")
print("3. PraisonAI __init__ does extensive parameter resolution")
print("4. PraisonAI imports config modules inside __init__")
print("5. PraisonAI calls resolve() 10+ times for feature params")
# Count resolve calls in PraisonAI
print("\n### Parameter Resolution Overhead ###")
print("\nPraisonAI resolve() calls in __init__:")
print(" - output: resolve()")
print(" - execution: resolve()")
print(" - templates: resolve()")
print(" - caching: resolve()")
print(" - hooks: resolve()")
print(" - skills: resolve()")
print(" - memory: resolve()")
print(" - knowledge: resolve()")
print(" - planning: resolve()")
print(" - reflection: resolve()")
print(" - guardrails: resolve()")
print(" - web: resolve()")
print(" Total: 12 resolve() calls per Agent creation")
# ============================================================================
# Main
# ============================================================================
if __name__ == "__main__":
import os
import argparse
parser = argparse.ArgumentParser(description='Deep Profiling Benchmark')
parser.add_argument('--deep', action='store_true', help='Run deep profiling (slower)')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
os.environ.setdefault("OPENAI_API_KEY", "sk-test")
print("="*70)
@@ -363,6 +558,17 @@ if __name__ == "__main__":
profile_praisonai_components()
compare_specific_operations()
# Advanced profiling (optional)
if args.deep:
print("\n" + "="*70)
print("ADVANCED PROFILING (--deep mode)")
print("="*70)
analyze_init_function_calls()
analyze_import_overhead()
# Always run architecture comparison
compare_architecture()
print("\n" + "="*70)
print("PROFILING COMPLETE")
print("="*70)
@@ -0,0 +1,451 @@
#!/usr/bin/env python3
"""
PraisonAI Agents - Detailed Profiling Benchmark
Provides function-level profiling for PraisonAI agent execution.
Shows exactly which functions are called, from which files, and how long each takes.
Usage:
export OPENAI_API_KEY=your_key
python benchmarks/detailed_profiling_benchmark.py
python benchmarks/detailed_profiling_benchmark.py --framework praisonai
python benchmarks/detailed_profiling_benchmark.py --framework agno
"""
import time
import os
import sys
import argparse
import cProfile
import pstats
import io
from functools import wraps
from collections import defaultdict
from datetime import datetime
# Defaults
DEFAULT_MODEL = "gpt-4o-mini"
DEFAULT_PROMPT = "What is 2+2? Reply with just the number."
class DetailedProfiler:
"""Detailed function-level profiler with file path tracking."""
def __init__(self):
self.call_times = defaultdict(list)
self.call_counts = defaultdict(int)
self.profiler = None
def start(self):
"""Start cProfile profiler."""
self.profiler = cProfile.Profile()
self.profiler.enable()
def stop(self):
"""Stop profiler and return stats."""
if self.profiler:
self.profiler.disable()
return self.profiler
return None
def get_stats_string(self, sort_by='cumtime', limit=50):
"""Get formatted stats string."""
if not self.profiler:
return "No profiler data"
s = io.StringIO()
ps = pstats.Stats(self.profiler, stream=s)
ps.sort_stats(sort_by)
ps.print_stats(limit)
return s.getvalue()
def get_detailed_breakdown(self, filter_pattern=None, limit=100):
"""Get detailed breakdown with file paths."""
if not self.profiler:
return []
stats = pstats.Stats(self.profiler)
results = []
for (filename, line, func), (cc, nc, tt, ct, callers) in stats.stats.items():
# Filter by pattern if provided
if filter_pattern and filter_pattern not in filename and filter_pattern not in func:
continue
results.append({
'file': filename,
'line': line,
'function': func,
'calls': nc,
'total_time': tt,
'cumulative_time': ct,
'time_per_call': tt / nc if nc > 0 else 0,
})
# Sort by cumulative time
results.sort(key=lambda x: x['cumulative_time'], reverse=True)
return results[:limit]
def profile_praisonai_detailed(model, prompt):
"""Profile PraisonAI agent execution in detail."""
print("\n" + "=" * 80)
print("DETAILED PROFILING: PraisonAI")
print("=" * 80)
profiler = DetailedProfiler()
# Profile import
print("\n--- Import Phase ---")
import_start = time.perf_counter()
profiler.start()
from praisonaiagents import Agent
profiler.stop()
import_time = time.perf_counter() - import_start
print(f"Import time: {import_time*1000:.2f}ms")
# Show import profile
print("\nTop 20 functions during import (by cumulative time):")
print("-" * 80)
breakdown = profiler.get_detailed_breakdown(filter_pattern='praisonai', limit=20)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in breakdown:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# Profile instantiation
print("\n--- Instantiation Phase ---")
profiler2 = DetailedProfiler()
inst_start = time.perf_counter()
profiler2.start()
agent = Agent(
name="Calculator",
instructions="You are a helpful assistant. Be very brief.",
llm=model
)
profiler2.stop()
inst_time = time.perf_counter() - inst_start
print(f"Instantiation time: {inst_time*1000:.2f}ms")
print("\nTop 30 functions during instantiation (by cumulative time):")
print("-" * 80)
breakdown = profiler2.get_detailed_breakdown(filter_pattern='praisonai', limit=30)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in breakdown:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# Profile execution
print("\n--- Execution Phase (agent.start) ---")
profiler3 = DetailedProfiler()
exec_start = time.perf_counter()
profiler3.start()
response = agent.start(prompt, output="silent")
profiler3.stop()
exec_time = time.perf_counter() - exec_start
print(f"Execution time: {exec_time*1000:.2f}ms ({exec_time:.2f}s)")
print(f"Response: {str(response)[:100]}")
print("\nTop 50 functions during execution (by cumulative time):")
print("-" * 80)
breakdown = profiler3.get_detailed_breakdown(limit=50)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in breakdown:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# PraisonAI-specific functions
print("\n--- PraisonAI-Specific Functions ---")
print("-" * 80)
praisonai_funcs = profiler3.get_detailed_breakdown(filter_pattern='praisonai', limit=50)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in praisonai_funcs:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# Summary
print("\n" + "=" * 80)
print("SUMMARY: PraisonAI")
print("=" * 80)
print(f"Import time: {import_time*1000:>10.2f}ms")
print(f"Instantiation time: {inst_time*1000:>10.2f}ms")
print(f"Execution time: {exec_time*1000:>10.2f}ms")
print(f"Total time: {(import_time+inst_time+exec_time)*1000:>10.2f}ms")
return {
'import_time': import_time,
'instantiation_time': inst_time,
'execution_time': exec_time,
'total_time': import_time + inst_time + exec_time,
}
def profile_agno_detailed(model, prompt):
"""Profile Agno agent execution in detail."""
print("\n" + "=" * 80)
print("DETAILED PROFILING: Agno")
print("=" * 80)
profiler = DetailedProfiler()
# Profile import
print("\n--- Import Phase ---")
import_start = time.perf_counter()
profiler.start()
from agno.agent import Agent
from agno.models.openai import OpenAIChat
profiler.stop()
import_time = time.perf_counter() - import_start
print(f"Import time: {import_time*1000:.2f}ms")
print("\nTop 20 functions during import (by cumulative time):")
print("-" * 80)
breakdown = profiler.get_detailed_breakdown(filter_pattern='agno', limit=20)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in breakdown:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# Profile instantiation
print("\n--- Instantiation Phase ---")
profiler2 = DetailedProfiler()
inst_start = time.perf_counter()
profiler2.start()
agent = Agent(
model=OpenAIChat(id=model),
instructions=["You are a helpful assistant. Be very brief."],
)
profiler2.stop()
inst_time = time.perf_counter() - inst_start
print(f"Instantiation time: {inst_time*1000:.2f}ms")
print("\nTop 30 functions during instantiation (by cumulative time):")
print("-" * 80)
breakdown = profiler2.get_detailed_breakdown(filter_pattern='agno', limit=30)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in breakdown:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# Profile execution
print("\n--- Execution Phase (agent.run) ---")
profiler3 = DetailedProfiler()
exec_start = time.perf_counter()
profiler3.start()
response = agent.run(prompt)
profiler3.stop()
exec_time = time.perf_counter() - exec_start
content = str(response.content)[:100] if hasattr(response, 'content') else str(response)[:100]
print(f"Execution time: {exec_time*1000:.2f}ms ({exec_time:.2f}s)")
print(f"Response: {content}")
print("\nTop 50 functions during execution (by cumulative time):")
print("-" * 80)
breakdown = profiler3.get_detailed_breakdown(limit=50)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in breakdown:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# Agno-specific functions
print("\n--- Agno-Specific Functions ---")
print("-" * 80)
agno_funcs = profiler3.get_detailed_breakdown(filter_pattern='agno', limit=50)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in agno_funcs:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# Summary
print("\n" + "=" * 80)
print("SUMMARY: Agno")
print("=" * 80)
print(f"Import time: {import_time*1000:>10.2f}ms")
print(f"Instantiation time: {inst_time*1000:>10.2f}ms")
print(f"Execution time: {exec_time*1000:>10.2f}ms")
print(f"Total time: {(import_time+inst_time+exec_time)*1000:>10.2f}ms")
return {
'import_time': import_time,
'instantiation_time': inst_time,
'execution_time': exec_time,
'total_time': import_time + inst_time + exec_time,
}
def profile_crewai_detailed(model, prompt):
"""Profile CrewAI agent execution in detail."""
print("\n" + "=" * 80)
print("DETAILED PROFILING: CrewAI")
print("=" * 80)
profiler = DetailedProfiler()
# Profile import
print("\n--- Import Phase ---")
import_start = time.perf_counter()
profiler.start()
from crewai import Agent, Task, Crew
profiler.stop()
import_time = time.perf_counter() - import_start
print(f"Import time: {import_time*1000:.2f}ms")
# Profile instantiation
print("\n--- Instantiation Phase ---")
profiler2 = DetailedProfiler()
inst_start = time.perf_counter()
profiler2.start()
agent = Agent(
role="Calculator",
goal="Answer math questions",
backstory="You are a helpful assistant.",
llm=model,
verbose=False
)
task = Task(
description=prompt,
expected_output="A number",
agent=agent
)
crew = Crew(
agents=[agent],
tasks=[task],
verbose=False
)
profiler2.stop()
inst_time = time.perf_counter() - inst_start
print(f"Instantiation time: {inst_time*1000:.2f}ms")
# Profile execution
print("\n--- Execution Phase (crew.kickoff) ---")
profiler3 = DetailedProfiler()
exec_start = time.perf_counter()
profiler3.start()
response = crew.kickoff()
profiler3.stop()
exec_time = time.perf_counter() - exec_start
print(f"Execution time: {exec_time*1000:.2f}ms ({exec_time:.2f}s)")
print(f"Response: {str(response)[:100]}")
print("\nTop 50 functions during execution (by cumulative time):")
print("-" * 80)
breakdown = profiler3.get_detailed_breakdown(limit=50)
print(f"{'Function':<40} {'File':<50} {'Calls':>8} {'CumTime':>10}")
print("-" * 80)
for item in breakdown:
func = item['function'][:38]
file = item['file'].split('/')[-1][:48] if '/' in item['file'] else item['file'][:48]
print(f"{func:<40} {file:<50} {item['calls']:>8} {item['cumulative_time']*1000:>9.2f}ms")
# Summary
print("\n" + "=" * 80)
print("SUMMARY: CrewAI")
print("=" * 80)
print(f"Import time: {import_time*1000:>10.2f}ms")
print(f"Instantiation time: {inst_time*1000:>10.2f}ms")
print(f"Execution time: {exec_time*1000:>10.2f}ms")
print(f"Total time: {(import_time+inst_time+exec_time)*1000:>10.2f}ms")
return {
'import_time': import_time,
'instantiation_time': inst_time,
'execution_time': exec_time,
'total_time': import_time + inst_time + exec_time,
}
def compare_results(results):
"""Compare results across frameworks."""
print("\n" + "=" * 80)
print("COMPARISON SUMMARY")
print("=" * 80)
print(f"\n{'Framework':<20} {'Import':<12} {'Instantiate':<12} {'Execute':<12} {'Total':<12}")
print("-" * 68)
# Sort by execution time
sorted_results = sorted(results.items(), key=lambda x: x[1]['execution_time'])
fastest_exec = sorted_results[0][1]['execution_time']
for name, times in sorted_results:
import_ms = times['import_time'] * 1000
inst_ms = times['instantiation_time'] * 1000
exec_ms = times['execution_time'] * 1000
total_ms = times['total_time'] * 1000
ratio = times['execution_time'] / fastest_exec
print(f"{name:<20} {import_ms:>10.1f}ms {inst_ms:>10.1f}ms {exec_ms:>10.1f}ms {total_ms:>10.1f}ms ({ratio:.2f}x)")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='PraisonAI Agents - Detailed Profiling Benchmark')
parser.add_argument('--framework', '-f', type=str, default='all',
choices=['all', 'praisonai', 'agno', 'crewai'],
help='Framework to profile (default: all)')
parser.add_argument('--model', '-m', type=str, default=DEFAULT_MODEL,
help=f'Model to use (default: {DEFAULT_MODEL})')
parser.add_argument('--prompt', '-p', type=str, default=DEFAULT_PROMPT,
help=f'Prompt to use')
args = parser.parse_args()
# Check API key
if not os.environ.get('OPENAI_API_KEY'):
print("ERROR: Set OPENAI_API_KEY environment variable")
exit(1)
print("=" * 80)
print("PraisonAI Agents - Detailed Profiling Benchmark")
print("=" * 80)
print(f"\nModel: {args.model}")
print(f"Prompt: \"{args.prompt}\"")
print(f"Framework: {args.framework}")
print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
results = {}
if args.framework in ['all', 'praisonai']:
try:
results['PraisonAI'] = profile_praisonai_detailed(args.model, args.prompt)
except Exception as e:
print(f"PraisonAI error: {e}")
import traceback
traceback.print_exc()
if args.framework in ['all', 'agno']:
try:
results['Agno'] = profile_agno_detailed(args.model, args.prompt)
except Exception as e:
print(f"Agno error: {e}")
import traceback
traceback.print_exc()
if args.framework in ['all', 'crewai']:
try:
results['CrewAI'] = profile_crewai_detailed(args.model, args.prompt)
except Exception as e:
print(f"CrewAI error: {e}")
import traceback
traceback.print_exc()
if len(results) > 1:
compare_results(results)
print("\n" + "=" * 80)
print("Profiling complete!")
print("=" * 80)
@@ -219,7 +219,7 @@ def save_results(results: dict, model: str, iterations: int, prompt: str):
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='PraisonAI Agents - Real Execution Benchmark')
parser.add_argument('--no-save', action='store_true', help='Do not save results to file')
parser.add_argument('--save', action='store_true', help='Save results to file')
parser.add_argument('--model', '-m', type=str, default=DEFAULT_MODEL, help=f'Model to use (default: {DEFAULT_MODEL})')
parser.add_argument('--iterations', '-i', type=int, default=DEFAULT_ITERATIONS, help=f'Number of iterations (default: {DEFAULT_ITERATIONS})')
parser.add_argument('--prompt', '-p', type=str, default=DEFAULT_PROMPT, help=f'Prompt to use (default: "{DEFAULT_PROMPT}")')
@@ -287,8 +287,8 @@ if __name__ == '__main__':
print('\n' + '=' * 60)
# Save results (unless --no-save)
if results and not args.no_save:
# Save results (if --save)
if results and args.save:
save_results(results, model, iterations, prompt)
elif args.no_save:
print('\nResults not saved (--no-save flag used)')
elif not args.save:
print('\nResults not saved (use --save flag to save results to file)')
@@ -16,6 +16,7 @@ CI Gate:
import sys
import time
import statistics
import argparse
def clear_modules():
@@ -145,4 +146,11 @@ def main():
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='PraisonAI Agents Import Time Benchmark')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
if not args.save:
print("\nNote: Results are not saved to file by default. Use --save to save them.")
sys.exit(main())
@@ -15,6 +15,7 @@ CI Gate:
"""
import sys
import tracemalloc
import argparse
def clear_modules():
@@ -86,4 +87,11 @@ def main():
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='PraisonAI Agents Memory Usage Benchmark')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
if not args.save:
print("\nNote: Results are not saved to file by default. Use --save to save them.")
sys.exit(main())
@@ -0,0 +1,330 @@
#!/usr/bin/env python3
"""
PraisonAI Agents - Parameter Impact Benchmark
Profiles each Agent parameter to identify which features have the most
performance impact. Results are ordered by cost (time + memory).
This benchmark helps identify optimization opportunities by measuring
the incremental cost of each feature.
Usage:
python benchmarks/param_impact_benchmark.py
python benchmarks/param_impact_benchmark.py --no-save
python benchmarks/param_impact_benchmark.py --iterations 50
"""
import argparse
import gc
import time
import tracemalloc
import statistics
from typing import Literal, List, Dict, Any, Callable
from dataclasses import dataclass
@dataclass
class ParamResult:
"""Result for a single parameter test."""
param_name: str
param_value: Any
avg_time_us: float # microseconds
min_time_us: float
max_time_us: float
std_dev_us: float
avg_memory_kb: float # kilobytes
delta_time_us: float = 0.0 # difference from baseline
delta_memory_kb: float = 0.0
def measure_instantiation(
create_fn: Callable,
iterations: int = 100,
warmup: int = 10
) -> Dict[str, float]:
"""Measure instantiation time and memory for a function."""
# Warmup
for _ in range(warmup):
create_fn()
# Measure time
times = []
for _ in range(iterations):
gc.collect()
start = time.perf_counter()
create_fn()
elapsed = (time.perf_counter() - start) * 1_000_000 # microseconds
times.append(elapsed)
# Measure memory (separate pass for accuracy)
memory_usages = []
for _ in range(min(iterations, 20)): # Fewer iterations for memory
gc.collect()
tracemalloc.start()
create_fn()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
memory_usages.append(peak / 1024) # KB
return {
'avg_time': statistics.mean(times),
'min_time': min(times),
'max_time': max(times),
'std_dev': statistics.stdev(times) if len(times) > 1 else 0,
'avg_memory': statistics.mean(memory_usages) if memory_usages else 0,
}
def sample_tool(city: Literal['nyc', 'sf']):
"""Sample tool for benchmark testing."""
return 'cloudy' if city == 'nyc' else 'sunny'
def run_benchmark(iterations: int = 100) -> List[ParamResult]:
"""Run parameter impact benchmark."""
from praisonaiagents import Agent
results = []
print('=' * 70)
print('PraisonAI Agents - Parameter Impact Benchmark')
print('=' * 70)
print(f'\nIterations: {iterations}')
print('Measuring incremental cost of each Agent parameter\n')
# Baseline: Minimal agent
print("Testing baseline (minimal agent)...")
baseline_stats = measure_instantiation(
lambda: Agent(name='Test', output='silent'),
iterations
)
baseline_result = ParamResult(
param_name='BASELINE',
param_value='Agent(name="Test", output="silent")',
avg_time_us=baseline_stats['avg_time'],
min_time_us=baseline_stats['min_time'],
max_time_us=baseline_stats['max_time'],
std_dev_us=baseline_stats['std_dev'],
avg_memory_kb=baseline_stats['avg_memory'],
)
results.append(baseline_result)
print(f" Baseline: {baseline_stats['avg_time']:.2f} μs, {baseline_stats['avg_memory']:.2f} KB")
# Define parameters to test
# Format: (param_name, param_value, description)
params_to_test = [
# Core identity
('instructions', 'Be helpful and concise.', 'instructions="..."'),
('role', 'Assistant', 'role="Assistant"'),
('goal', 'Help users', 'goal="Help users"'),
('backstory', 'I am an AI assistant.', 'backstory="..."'),
# LLM configuration
('llm', 'gpt-4o-mini', 'llm="gpt-4o-mini"'),
('llm', 'openai/gpt-4o-mini', 'llm="openai/gpt-4o-mini" (LiteLLM)'),
# Tools
('tools', [sample_tool], 'tools=[sample_tool]'),
# Features (bool flags)
('context', True, 'context=True'),
('context', False, 'context=False'),
('planning', True, 'planning=True'),
('reflection', True, 'reflection=True'),
('memory', True, 'memory=True'),
# Output modes
('output', 'verbose', 'output="verbose"'),
('output', 'actions', 'output="actions"'),
('output', 'stream', 'output="stream"'),
# Execution config
('execution', 'fast', 'execution="fast"'),
('execution', 'thorough', 'execution="thorough"'),
# Caching
('caching', True, 'caching=True'),
('caching', False, 'caching=False'),
]
# Test each parameter
for param_name, param_value, description in params_to_test:
print(f"Testing {description}...")
try:
# Build kwargs
kwargs = {'name': 'Test', 'output': 'silent'}
kwargs[param_name] = param_value
# Special case: if testing output param, don't override with silent
if param_name == 'output':
kwargs = {'name': 'Test'}
kwargs[param_name] = param_value
stats = measure_instantiation(
lambda k=kwargs: Agent(**k),
iterations
)
result = ParamResult(
param_name=param_name,
param_value=description,
avg_time_us=stats['avg_time'],
min_time_us=stats['min_time'],
max_time_us=stats['max_time'],
std_dev_us=stats['std_dev'],
avg_memory_kb=stats['avg_memory'],
delta_time_us=stats['avg_time'] - baseline_stats['avg_time'],
delta_memory_kb=stats['avg_memory'] - baseline_stats['avg_memory'],
)
results.append(result)
delta_sign = '+' if result.delta_time_us >= 0 else ''
print(f" {stats['avg_time']:.2f} μs ({delta_sign}{result.delta_time_us:.2f} μs from baseline)")
except Exception as e:
print(f" SKIPPED: {e}")
return results
def print_results(results: List[ParamResult]):
"""Print results sorted by performance impact."""
print('\n' + '=' * 70)
print('RESULTS - Sorted by Time Impact (highest cost first)')
print('=' * 70)
# Sort by delta time (excluding baseline)
sorted_results = sorted(
[r for r in results if r.param_name != 'BASELINE'],
key=lambda x: x.delta_time_us,
reverse=True
)
# Get baseline
baseline = next((r for r in results if r.param_name == 'BASELINE'), None)
if baseline:
print(f"\nBaseline: {baseline.avg_time_us:.2f} μs, {baseline.avg_memory_kb:.2f} KB")
print(f"\n{'Parameter':<35} {'Avg (μs)':<12} {'Delta (μs)':<12} {'Memory (KB)':<12} {'Impact':<10}")
print('-' * 85)
for r in sorted_results:
# Calculate impact level
if r.delta_time_us > 1000:
impact = '🔴 HIGH'
elif r.delta_time_us > 100:
impact = '🟡 MEDIUM'
elif r.delta_time_us > 10:
impact = '🟢 LOW'
else:
impact = '⚪ MINIMAL'
delta_str = f"+{r.delta_time_us:.2f}" if r.delta_time_us >= 0 else f"{r.delta_time_us:.2f}"
print(f"{r.param_value:<35} {r.avg_time_us:<12.2f} {delta_str:<12} {r.avg_memory_kb:<12.2f} {impact:<10}")
# Summary
print('\n' + '=' * 70)
print('SUMMARY')
print('=' * 70)
high_impact = [r for r in sorted_results if r.delta_time_us > 1000]
medium_impact = [r for r in sorted_results if 100 < r.delta_time_us <= 1000]
low_impact = [r for r in sorted_results if 10 < r.delta_time_us <= 100]
print(f"\nHigh impact (>1ms): {len(high_impact)} parameters")
for r in high_impact:
print(f" - {r.param_value}: +{r.delta_time_us:.2f} μs")
print(f"\nMedium impact (100μs-1ms): {len(medium_impact)} parameters")
for r in medium_impact:
print(f" - {r.param_value}: +{r.delta_time_us:.2f} μs")
print(f"\nLow impact (10-100μs): {len(low_impact)} parameters")
print('\n' + '=' * 70)
def save_results(results: List[ParamResult], filename: str = 'PARAM_IMPACT_RESULTS.md'):
"""Save results to markdown file."""
import os
from datetime import datetime
filepath = os.path.join(os.path.dirname(__file__), filename)
# Sort by delta time
sorted_results = sorted(
[r for r in results if r.param_name != 'BASELINE'],
key=lambda x: x.delta_time_us,
reverse=True
)
baseline = next((r for r in results if r.param_name == 'BASELINE'), None)
with open(filepath, 'w') as f:
f.write('# PraisonAI Agents - Parameter Impact Benchmark Results\n\n')
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n\n')
if baseline:
f.write(f'**Baseline:** {baseline.avg_time_us:.2f} μs, {baseline.avg_memory_kb:.2f} KB\n\n')
f.write('## Results (Sorted by Impact)\n\n')
f.write('| Parameter | Avg Time (μs) | Delta (μs) | Memory (KB) | Impact |\n')
f.write('|-----------|---------------|------------|-------------|--------|\n')
for r in sorted_results:
if r.delta_time_us > 1000:
impact = '🔴 HIGH'
elif r.delta_time_us > 100:
impact = '🟡 MEDIUM'
elif r.delta_time_us > 10:
impact = '🟢 LOW'
else:
impact = '⚪ MINIMAL'
delta_str = f"+{r.delta_time_us:.2f}" if r.delta_time_us >= 0 else f"{r.delta_time_us:.2f}"
f.write(f'| {r.param_value} | {r.avg_time_us:.2f} | {delta_str} | {r.avg_memory_kb:.2f} | {impact} |\n')
f.write('\n## Recommendations\n\n')
high_impact = [r for r in sorted_results if r.delta_time_us > 1000]
if high_impact:
f.write('### High Impact Parameters (>1ms)\n\n')
f.write('These parameters add significant overhead and should be used carefully:\n\n')
for r in high_impact:
f.write(f'- **{r.param_value}**: +{r.delta_time_us:.2f} μs\n')
f.write('\n')
f.write('### Best Practices\n\n')
f.write('1. Use `output="silent"` for production workloads\n')
f.write('2. Only enable `context=True` when needed for long conversations\n')
f.write('3. Use `llm="gpt-4o-mini"` (OpenAI SDK) for fastest instantiation\n')
f.write('4. Avoid `memory=True` unless persistent memory is required\n')
f.write('\n## How to Reproduce\n\n')
f.write('```bash\n')
f.write('cd praisonai-agents\n')
f.write('python benchmarks/param_impact_benchmark.py\n')
f.write('```\n')
print(f'\nResults saved to: {filepath}')
return filepath
def main():
parser = argparse.ArgumentParser(description='PraisonAI Parameter Impact Benchmark')
parser.add_argument('--iterations', type=int, default=100, help='Number of iterations per test')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
results = run_benchmark(iterations=args.iterations)
print_results(results)
if args.save:
save_results(results)
else:
print('\nResults not saved (use --save flag to save results to file)')
if __name__ == '__main__':
main()
@@ -14,6 +14,7 @@ import tracemalloc
import statistics
from typing import Literal, List, Callable
from dataclasses import dataclass, field
import argparse
@dataclass
@@ -186,7 +187,7 @@ def instantiate_praisonai_agent():
instructions="Be concise, reply with one sentence.",
llm="gpt-4o-mini",
tools=tools,
verbose=False
output="silent"
)
@@ -197,7 +198,7 @@ def instantiate_praisonai_litellm_agent():
instructions="Be concise, reply with one sentence.",
llm="openai/gpt-4o-mini",
tools=tools,
verbose=False
output="silent"
)
@@ -364,6 +365,10 @@ def save_benchmark_results(results: dict, baseline):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='PraisonAI Agents - Comprehensive Performance Benchmark')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
print("="*70)
print("PraisonAI Agents - Comprehensive Performance Benchmark")
print("="*70)
@@ -520,4 +525,7 @@ if __name__ == "__main__":
print("\n" + "="*70)
# Save results to files
save_benchmark_results(results, baseline)
if args.save:
save_benchmark_results(results, baseline)
else:
print('\nResults not saved (use --save flag to save results to file)')
@@ -0,0 +1,427 @@
#!/usr/bin/env python3
"""
Comprehensive Quick Benchmark - PraisonAI vs Agno
All-in-one benchmark comparing two frameworks across multiple metrics:
- Import time (cold start)
- Agent instantiation time
- Memory footprint (per-agent)
- Function call count (cProfile)
- Deep profiling (hot spots)
Designed for rapid iteration with minimal iterations.
Usage:
python benchmarks/quick_benchmark.py # All metrics
python benchmarks/quick_benchmark.py --fast # Quick mode (fewer iterations)
python benchmarks/quick_benchmark.py --profile # Include cProfile details
python benchmarks/quick_benchmark.py --memory # Include memory details
python benchmarks/quick_benchmark.py --import # Include import time
python benchmarks/quick_benchmark.py --all # All detailed output
"""
import time
import argparse
import sys
import gc
# ============================================================================
# BENCHMARK FUNCTIONS
# ============================================================================
def measure_import_time(module_name, clear_cache=True, full_agent_import=True):
"""Measure import time for a module.
Args:
module_name: Name of the module to import
clear_cache: Whether to clear module cache before import
full_agent_import: If True, measure full Agent class import (recommended).
If False, measure package import only (misleading for Agno).
Note: Agno's main __init__.py only exports __version__, so measuring
'import agno' gives misleading results (5ms). The actual Agent import
'from agno.agent import Agent' takes ~500ms. This function measures
the full Agent import by default for accurate comparison.
"""
if clear_cache:
# Clear module cache
mods_to_remove = [k for k in sys.modules.keys() if module_name.split('.')[0] in k]
for m in mods_to_remove:
del sys.modules[m]
gc.collect()
start = time.perf_counter()
if module_name == 'praisonaiagents':
if full_agent_import:
from praisonaiagents import Agent # noqa: F401
else:
import praisonaiagents # noqa: F401
elif module_name == 'agno':
if full_agent_import:
from agno.agent import Agent # noqa: F401
else:
import agno # noqa: F401
end = time.perf_counter()
return (end - start) * 1000 # Return in milliseconds
def get_praisonai_factory():
"""Get PraisonAI agent factory function."""
from praisonaiagents import Agent
def create():
return Agent(name="Test", instructions="You are helpful.", output="silent")
return create
def get_agno_factory():
"""Get Agno agent factory function."""
try:
from agno.agent import Agent as AgnoAgent
def create():
return AgnoAgent(name="Test", instructions=["You are helpful."])
return create
except ImportError:
return None
def measure_instantiation(factory, iterations=50, warmup=5):
"""Measure agent instantiation time."""
# Warmup
for _ in range(warmup):
factory()
gc.collect()
# Measure
times = []
for _ in range(iterations):
start = time.perf_counter()
factory()
times.append((time.perf_counter() - start) * 1e6) # microseconds
return {
'avg': sum(times) / len(times),
'min': min(times),
'max': max(times),
'median': sorted(times)[len(times) // 2],
}
def measure_memory(factory, iterations=20):
"""Measure memory footprint per agent."""
import tracemalloc
gc.collect()
tracemalloc.start()
agents = []
for _ in range(iterations):
agents.append(factory())
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
per_agent = current / iterations / 1024 # KB per agent
return {
'per_agent_kb': per_agent,
'total_kb': current / 1024,
'peak_kb': peak / 1024,
}
def measure_function_calls(factory, iterations=50):
"""Count function calls using cProfile."""
import cProfile
import pstats
from io import StringIO
profiler = cProfile.Profile()
profiler.enable()
for _ in range(iterations):
factory()
profiler.disable()
stream = StringIO()
stats = pstats.Stats(profiler, stream=stream)
# Get total function calls
total_calls = stats.total_calls
calls_per_agent = total_calls / iterations
# Get top functions by time
stats.sort_stats('tottime')
top_functions = []
for func, (cc, nc, tt, ct, callers) in list(stats.stats.items())[:10]:
filename, line, name = func
top_functions.append({
'name': name,
'calls': nc,
'tottime': tt * 1e6 / iterations, # μs per agent
})
return {
'total_calls': total_calls,
'calls_per_agent': calls_per_agent,
'top_functions': top_functions,
}
def run_deep_profile(factory, iterations=50):
"""Run detailed cProfile analysis."""
import cProfile
import pstats
from io import StringIO
profiler = cProfile.Profile()
profiler.enable()
for _ in range(iterations):
factory()
profiler.disable()
stream = StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.sort_stats('cumulative')
stats.print_stats(25)
return stream.getvalue()
# ============================================================================
# MAIN
# ============================================================================
def main():
parser = argparse.ArgumentParser(
description="Comprehensive Quick Benchmark - PraisonAI vs Agno",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python benchmarks/quick_benchmark.py # Standard benchmark
python benchmarks/quick_benchmark.py --fast # Quick mode
python benchmarks/quick_benchmark.py --all # All details
"""
)
parser.add_argument("--iterations", "-n", type=int, default=50,
help="Iterations for timing (default: 50)")
parser.add_argument("--fast", "-f", action="store_true",
help="Fast mode (20 iterations)")
parser.add_argument("--profile", "-p", action="store_true",
help="Show detailed cProfile output")
parser.add_argument("--memory", "-m", action="store_true",
help="Show memory details")
parser.add_argument("--import", dest="show_import", action="store_true",
help="Show import time details")
parser.add_argument("--all", "-A", action="store_true",
help="Show all details")
args = parser.parse_args()
if args.fast:
args.iterations = 20
if args.all:
args.profile = True
args.memory = True
args.show_import = True
print("=" * 70)
print("COMPREHENSIVE QUICK BENCHMARK - PraisonAI vs Agno")
print("=" * 70)
print(f"Iterations: {args.iterations}")
print()
results = {'praisonai': {}, 'agno': {}}
# ========================================================================
# 1. IMPORT TIME
# ========================================================================
print("📦 Measuring import time...")
results['praisonai']['import_ms'] = measure_import_time('praisonaiagents')
results['agno']['import_ms'] = measure_import_time('agno')
# ========================================================================
# 2. GET FACTORIES (after import measurement)
# ========================================================================
print("🏭 Loading factories...")
praisonai_factory = get_praisonai_factory()
agno_factory = get_agno_factory()
if agno_factory is None:
print("⚠️ Agno not installed, skipping comparison")
return 1
# ========================================================================
# 3. INSTANTIATION TIME
# ========================================================================
print("⏱️ Measuring instantiation time...")
results['praisonai']['time'] = measure_instantiation(praisonai_factory, args.iterations)
results['agno']['time'] = measure_instantiation(agno_factory, args.iterations)
# ========================================================================
# 4. MEMORY FOOTPRINT
# ========================================================================
print("💾 Measuring memory footprint...")
results['praisonai']['memory'] = measure_memory(praisonai_factory, min(args.iterations, 20))
results['agno']['memory'] = measure_memory(agno_factory, min(args.iterations, 20))
# ========================================================================
# 5. FUNCTION CALLS
# ========================================================================
print("📊 Counting function calls...")
results['praisonai']['calls'] = measure_function_calls(praisonai_factory, args.iterations)
results['agno']['calls'] = measure_function_calls(agno_factory, args.iterations)
# ========================================================================
# RESULTS SUMMARY
# ========================================================================
print()
print("=" * 70)
print("RESULTS SUMMARY")
print("=" * 70)
# Import Time
print()
print("📦 IMPORT TIME (cold start)")
print("-" * 50)
print(f"{'Framework':<15} {'Time (ms)':<15} {'Relative':<15}")
print("-" * 50)
p_import = results['praisonai']['import_ms']
a_import = results['agno']['import_ms']
print(f"{'PraisonAI':<15} {p_import:<15.2f} {'1.00x':<15}")
print(f"{'Agno':<15} {a_import:<15.2f} {a_import/p_import:.2f}x")
# Instantiation Time
print()
print("⏱️ INSTANTIATION TIME (per agent)")
print("-" * 50)
print(f"{'Framework':<15} {'Avg (μs)':<12} {'Min (μs)':<12} {'Median (μs)':<12}")
print("-" * 50)
p_time = results['praisonai']['time']
a_time = results['agno']['time']
print(f"{'PraisonAI':<15} {p_time['avg']:<12.2f} {p_time['min']:<12.2f} {p_time['median']:<12.2f}")
print(f"{'Agno':<15} {a_time['avg']:<12.2f} {a_time['min']:<12.2f} {a_time['median']:<12.2f}")
ratio = p_time['avg'] / a_time['avg']
diff = p_time['avg'] - a_time['avg']
print()
if ratio > 1:
print(f" → PraisonAI is {ratio:.2f}x slower ({diff:.2f}μs more)")
else:
print(f" → PraisonAI is {1/ratio:.2f}x faster ({-diff:.2f}μs less)")
# Memory
print()
print("💾 MEMORY FOOTPRINT (per agent)")
print("-" * 50)
print(f"{'Framework':<15} {'Per Agent (KB)':<15} {'Peak (KB)':<15}")
print("-" * 50)
p_mem = results['praisonai']['memory']
a_mem = results['agno']['memory']
print(f"{'PraisonAI':<15} {p_mem['per_agent_kb']:<15.2f} {p_mem['peak_kb']:<15.2f}")
print(f"{'Agno':<15} {a_mem['per_agent_kb']:<15.2f} {a_mem['peak_kb']:<15.2f}")
# Function Calls
print()
print("📊 FUNCTION CALLS (per agent)")
print("-" * 50)
print(f"{'Framework':<15} {'Calls/Agent':<15} {'Total Calls':<15}")
print("-" * 50)
p_calls = results['praisonai']['calls']
a_calls = results['agno']['calls']
print(f"{'PraisonAI':<15} {p_calls['calls_per_agent']:<15.1f} {p_calls['total_calls']:<15}")
print(f"{'Agno':<15} {a_calls['calls_per_agent']:<15.1f} {a_calls['total_calls']:<15}")
# ========================================================================
# OVERALL SCORE
# ========================================================================
print()
print("=" * 70)
print("OVERALL COMPARISON")
print("=" * 70)
metrics = [
("Import Time", p_import, a_import, "lower"),
("Instantiation", p_time['avg'], a_time['avg'], "lower"),
("Memory/Agent", p_mem['per_agent_kb'], a_mem['per_agent_kb'], "lower"),
("Function Calls", p_calls['calls_per_agent'], a_calls['calls_per_agent'], "lower"),
]
print(f"{'Metric':<20} {'PraisonAI':<15} {'Agno':<15} {'Winner':<15}")
print("-" * 65)
praison_wins = 0
agno_wins = 0
for name, p_val, a_val, better in metrics:
if better == "lower":
winner = "PraisonAI ✓" if p_val <= a_val else "Agno ✓"
if p_val <= a_val:
praison_wins += 1
else:
agno_wins += 1
else:
winner = "PraisonAI ✓" if p_val >= a_val else "Agno ✓"
if p_val >= a_val:
praison_wins += 1
else:
agno_wins += 1
if isinstance(p_val, float):
print(f"{name:<20} {p_val:<15.2f} {a_val:<15.2f} {winner:<15}")
else:
print(f"{name:<20} {p_val:<15} {a_val:<15} {winner:<15}")
print("-" * 65)
print(f"Score: PraisonAI {praison_wins} - Agno {agno_wins}")
print("=" * 70)
# ========================================================================
# DETAILED OUTPUT (if requested)
# ========================================================================
if args.show_import:
print()
print("=" * 70)
print("IMPORT TIME DETAILS")
print("=" * 70)
print(f"PraisonAI: {results['praisonai']['import_ms']:.2f}ms")
print(f"Agno: {results['agno']['import_ms']:.2f}ms")
if args.memory:
print()
print("=" * 70)
print("MEMORY DETAILS")
print("=" * 70)
print("PraisonAI:")
print(f" Per Agent: {p_mem['per_agent_kb']:.2f} KB")
print(f" Total: {p_mem['total_kb']:.2f} KB")
print(f" Peak: {p_mem['peak_kb']:.2f} KB")
print("Agno:")
print(f" Per Agent: {a_mem['per_agent_kb']:.2f} KB")
print(f" Total: {a_mem['total_kb']:.2f} KB")
print(f" Peak: {a_mem['peak_kb']:.2f} KB")
if args.profile:
print()
print("=" * 70)
print("PRAISONAI DEEP PROFILE")
print("=" * 70)
print(run_deep_profile(praisonai_factory, args.iterations))
print()
print("=" * 70)
print("AGNO DEEP PROFILE")
print("=" * 70)
print(run_deep_profile(agno_factory, args.iterations))
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -15,6 +15,7 @@ import os
from typing import Literal, List
from dataclasses import dataclass, field
from importlib.metadata import version as get_version
import argparse
# Ensure API key is set
if not os.environ.get('OPENAI_API_KEY'):
@@ -392,6 +393,10 @@ def save_results(results: dict, versions: dict):
# ============================================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='PraisonAI Agents - Real-World Execution Benchmark')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
print("="*70)
print("PraisonAI Agents - Real-World Execution Benchmark")
print("="*70)
@@ -470,4 +475,7 @@ if __name__ == "__main__":
print("\n" + "="*70)
# Save results
save_results(results, versions)
if args.save:
save_results(results, versions)
else:
print('\nResults not saved (use --save flag to save results to file)')
@@ -61,7 +61,7 @@ def benchmark_praisonai_response(iterations: int = 5) -> ResponseResult:
name="Test Agent",
instructions="Be concise, reply with one sentence.",
llm="gpt-4o-mini",
verbose=False
output="silent"
)
start = time.perf_counter()
@@ -114,7 +114,7 @@ def benchmark_praisonai_with_tool(iterations: int = 5) -> ResponseResult:
instructions="You are a weather assistant. Use the get_weather tool to answer questions.",
llm="gpt-4o-mini",
tools=[get_weather],
verbose=False
output="silent"
)
start = time.perf_counter()
@@ -10,6 +10,7 @@ Usage:
import time
from typing import Literal
import argparse
ITERATIONS = 100
@@ -47,11 +48,11 @@ def run_benchmark():
from praisonaiagents import Agent as PraisonAgent
results['PraisonAI'] = measure_instantiation(
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', verbose=False)
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', output="silent")
)
results['PraisonAI (LiteLLM)'] = measure_instantiation(
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', verbose=False)
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', output="silent")
)
# Other frameworks for comparison
@@ -239,5 +240,13 @@ def update_readme(readme_path: str, table_rows: list):
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='PraisonAI Agents - Performance Benchmark')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
results = run_benchmark()
save_results(results)
if args.save:
save_results(results)
else:
print('\nResults not saved (use --save flag to save results to file)')
@@ -11,6 +11,7 @@ Usage:
import time
from typing import Literal
from importlib.metadata import version as get_version
import argparse
ITERATIONS = 100
@@ -57,11 +58,11 @@ def run_benchmark():
from praisonaiagents import Agent as PraisonAgent
results['PraisonAI'] = measure_instantiation(
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', tools=tools, verbose=False)
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', tools=tools, output="silent")
)
results['PraisonAI (LiteLLM)'] = measure_instantiation(
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', tools=tools, verbose=False)
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', tools=tools, output="silent")
)
# Other frameworks with tools
@@ -209,5 +210,13 @@ def save_results(results: dict, filename: str = 'TOOLS_BENCHMARK_RESULTS.md'):
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='PraisonAI Agents - Tools Benchmark')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
results = run_benchmark()
save_results(results)
if args.save:
save_results(results)
else:
print('\nResults not saved (use --save flag to save results to file)')
@@ -1,24 +0,0 @@
"""Basic memory example - minimal usage."""
from praisonaiagents import Agent
# Simple enable (uses default file-based memory)
agent = Agent(
instructions="You are a helpful assistant with memory.",
memory=True,
)
# With preset
agent_redis = Agent(
instructions="You are a helpful assistant.",
memory="redis", # Uses redis preset
)
# With URL
agent_postgres = Agent(
instructions="You are a helpful assistant.",
memory="postgresql://localhost/mydb", # URL auto-detected
)
if __name__ == "__main__":
response = agent.chat("Remember that my favorite color is blue.")
print(response)
@@ -1,22 +0,0 @@
"""Basic workflow example with consolidated params."""
from praisonaiagents import Agent
from praisonaiagents.workflows import Workflow, WorkflowStep
# Create agents
writer = Agent(instructions="You are a content writer.")
editor = Agent(instructions="You are an editor.")
# Create workflow with consolidated params
workflow = Workflow(
name="Content Pipeline",
steps=[
WorkflowStep(name="write", agent=writer, action="Write about {{topic}}"),
WorkflowStep(name="edit", agent=editor, action="Edit the content", context=["write"]),
],
output="verbose",
planning=True,
)
if __name__ == "__main__":
result = workflow.run(variables={"topic": "AI agents"})
print(result)
+2 -2
View File
@@ -2,7 +2,7 @@ from praisonaiagents import Agent
# Detailed LLM configuration
llm_config = {
"model": "gemini/gemini-1.5-flash-latest", # Model name without provider prefix
"model": "gemini/gemini-3-flash-preview", # Model name without provider prefix
# Core settings
"temperature": 0.7, # Controls randomness (like temperature)
@@ -25,7 +25,7 @@ llm_config = {
# Additional controls
"seed": 42, # For reproducible responses
"stop_phrases": ["##", "END"], # Custom stop sequences
"stop_phrases": ["---END---", "END"], # Custom stop sequences
}
agent = Agent(
+426 -524
View File
@@ -5,6 +5,25 @@ This module uses lazy loading to minimize import time and memory usage.
Heavy dependencies like litellm are only loaded when actually needed.
"""
# =============================================================================
# NAMING CONVENTION GUIDE (Simplified patterns for consistency)
# =============================================================================
# | Pattern | When to Use | Examples |
# |---------------|-------------------------|----------------------------------|
# | add_X | Register something | add_hook, add_tool, add_profile |
# | get_X | Retrieve something | get_tool, get_profile |
# | remove_X | Unregister something | remove_hook, remove_tool |
# | has_X | Check existence | has_hook, has_tool |
# | list_X | List all items | list_tools, list_profiles |
# | enable_X | Turn on feature | enable_telemetry |
# | disable_X | Turn off feature | disable_telemetry |
# | XConfig | Configuration class | MemoryConfig, HooksConfig |
# | @decorator | Decorator | @tool, @add_hook |
# |---------------|-------------------------|----------------------------------|
# | set_default_X | Internal/advanced only | (don't simplify - internal) |
# | create_X | Factory function | (already well-named) |
# =============================================================================
# =============================================================================
# NAMESPACE PACKAGE GUARD
# =============================================================================
@@ -45,559 +64,441 @@ from .tools.tools import Tools
from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
from .tools.decorator import tool, FunctionTool
from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
from .db import db
from .obs import obs
# db and obs are lazy-loaded via __getattr__ for performance
# Sub-packages for organized imports (pa.config, pa.tools, etc.)
# These enable: import praisonaiagents as pa; pa.config.MemoryConfig
from . import config
from . import tools
from . import memory
from . import workflows
# Note: knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
# Note: db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
# Embedding API - explicit import shadows subpackage, enabling:
# from praisonaiagents import embedding, embeddings, EmbeddingResult, get_dimensions
# Note: litellm is still lazy-loaded INSIDE the functions (no performance impact)
from .embedding.embed import embedding, aembedding
from .embedding.result import EmbeddingResult
from .embedding.dimensions import get_dimensions
embeddings = embedding # Plural alias (OpenAI style: client.embeddings.create)
aembeddings = aembedding # Plural alias for async
# Embedding API - LAZY LOADED via __getattr__ for performance
# Supports: embedding, embeddings, aembedding, aembeddings, EmbeddingResult, get_dimensions
# Workflows - lightweight module
from .workflows import (
Workflow, WorkflowStep, WorkflowContext, StepResult,
Route, Parallel, Loop, Repeat,
route, parallel, loop, repeat,
Pipeline # Alias for Workflow
)
from .guardrails import GuardrailResult, LLMGuardrail
# Workflows - LAZY LOADED (moved to __getattr__)
# Workflow, WorkflowStep, WorkflowContext, StepResult, Route, Parallel, Loop, Repeat, etc.
# Handoff - lightweight (unified agent-to-agent transfer)
from .agent.handoff import (
Handoff, handoff, handoff_filters,
RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions,
HandoffConfig, HandoffResult, HandoffInputData,
ContextPolicy, HandoffError, HandoffCycleError, HandoffDepthError, HandoffTimeoutError,
)
# Guardrails - LAZY LOADED (imports main.py which imports rich)
# GuardrailResult and LLMGuardrail moved to __getattr__
# Flow display (optional)
try:
from .flow_display import FlowDisplay, track_workflow
except ImportError:
FlowDisplay = None
track_workflow = None
# Handoff - LAZY LOADED (moved to __getattr__)
# Handoff, handoff, handoff_filters, etc.
# Main display utilities - these are used frequently so import directly
# but they don't pull in litellm
from .main import (
TaskOutput,
ReflectionOutput,
display_interaction,
display_self_reflection,
display_instruction,
display_tool_call,
display_error,
display_generating,
clean_triple_backticks,
error_logs,
register_display_callback,
sync_display_callbacks,
async_display_callbacks,
)
# Flow display - LAZY LOADED (moved to __getattr__)
# FlowDisplay and track_workflow are now lazy loaded
# Module-level caches for lazy-loaded classes
# Using threading.local() ensures thread-safe access to cached values
# Main display utilities - LAZY LOADED to avoid importing rich at startup
# These are only needed when output=verbose, not for silent mode
# Moved to __getattr__ for lazy loading
# ============================================================================
# LAZY LOADING CONFIGURATION
# ============================================================================
# Using centralized _lazy.py utility for DRY, thread-safe lazy loading.
# All heavy dependencies are loaded on-demand to minimize import time.
# ============================================================================
from ._lazy import lazy_import, create_lazy_getattr_with_fallback
# Thread-safe cache for lazy-loaded values
_lazy_cache = {}
# Backward compatibility: _get_lazy_cache function for tests
import threading
_lazy_cache_local = threading.local()
def _get_lazy_cache():
"""Get thread-local lazy cache dict. Thread-safe for concurrent access."""
"""Get thread-local lazy cache dict. Thread-safe for concurrent access.
Note: This is kept for backward compatibility with tests.
The main lazy loading now uses the centralized _lazy.py utility.
"""
if not hasattr(_lazy_cache_local, 'cache'):
_lazy_cache_local.cache = {}
return _lazy_cache_local.cache
# Backward-compatible alias (module-level access still works, now thread-safe)
_lazy_cache = {} # Kept for compatibility, but prefer _get_lazy_cache()
# ============================================================================
# LAZY IMPORT MAPPING
# ============================================================================
# Maps attribute names to (module_path, attr_name) tuples.
# This is the single source of truth for all lazy imports.
# ============================================================================
# Flags for optional modules
_mcp_available = None
_planning_available = None
_telemetry_available = None
_agui_available = None
_a2a_available = None
_LAZY_IMPORTS = {
# Main display utilities (imports rich)
'TaskOutput': ('praisonaiagents.main', 'TaskOutput'),
'ReflectionOutput': ('praisonaiagents.main', 'ReflectionOutput'),
'display_interaction': ('praisonaiagents.main', 'display_interaction'),
'display_self_reflection': ('praisonaiagents.main', 'display_self_reflection'),
'display_instruction': ('praisonaiagents.main', 'display_instruction'),
'display_tool_call': ('praisonaiagents.main', 'display_tool_call'),
'display_error': ('praisonaiagents.main', 'display_error'),
'display_generating': ('praisonaiagents.main', 'display_generating'),
'clean_triple_backticks': ('praisonaiagents.main', 'clean_triple_backticks'),
'error_logs': ('praisonaiagents.main', 'error_logs'),
'register_display_callback': ('praisonaiagents.main', 'register_display_callback'),
'sync_display_callbacks': ('praisonaiagents.main', 'sync_display_callbacks'),
'async_display_callbacks': ('praisonaiagents.main', 'async_display_callbacks'),
# Workflows
'Workflow': ('praisonaiagents.workflows', 'Workflow'),
'WorkflowStep': ('praisonaiagents.workflows', 'WorkflowStep'),
'WorkflowContext': ('praisonaiagents.workflows', 'WorkflowContext'),
'StepResult': ('praisonaiagents.workflows', 'StepResult'),
'Route': ('praisonaiagents.workflows', 'Route'),
'Parallel': ('praisonaiagents.workflows', 'Parallel'),
'Loop': ('praisonaiagents.workflows', 'Loop'),
'Repeat': ('praisonaiagents.workflows', 'Repeat'),
'route': ('praisonaiagents.workflows', 'route'),
'parallel': ('praisonaiagents.workflows', 'parallel'),
'loop': ('praisonaiagents.workflows', 'loop'),
'repeat': ('praisonaiagents.workflows', 'repeat'),
'Pipeline': ('praisonaiagents.workflows', 'Pipeline'),
# Handoff
'Handoff': ('praisonaiagents.agent.handoff', 'Handoff'),
'handoff': ('praisonaiagents.agent.handoff', 'handoff'),
'handoff_filters': ('praisonaiagents.agent.handoff', 'handoff_filters'),
'RECOMMENDED_PROMPT_PREFIX': ('praisonaiagents.agent.handoff', 'RECOMMENDED_PROMPT_PREFIX'),
'prompt_with_handoff_instructions': ('praisonaiagents.agent.handoff', 'prompt_with_handoff_instructions'),
'HandoffConfig': ('praisonaiagents.agent.handoff', 'HandoffConfig'),
'HandoffResult': ('praisonaiagents.agent.handoff', 'HandoffResult'),
'HandoffInputData': ('praisonaiagents.agent.handoff', 'HandoffInputData'),
'ContextPolicy': ('praisonaiagents.agent.handoff', 'ContextPolicy'),
'HandoffError': ('praisonaiagents.agent.handoff', 'HandoffError'),
'HandoffCycleError': ('praisonaiagents.agent.handoff', 'HandoffCycleError'),
'HandoffDepthError': ('praisonaiagents.agent.handoff', 'HandoffDepthError'),
'HandoffTimeoutError': ('praisonaiagents.agent.handoff', 'HandoffTimeoutError'),
# Embedding API
'embedding': ('praisonaiagents.embedding.embed', 'embedding'),
'embeddings': ('praisonaiagents.embedding.embed', 'embedding'),
'aembedding': ('praisonaiagents.embedding.embed', 'aembedding'),
'aembeddings': ('praisonaiagents.embedding.embed', 'aembedding'),
'embed': ('praisonaiagents.embedding.embed', 'embed'),
'aembed': ('praisonaiagents.embedding.embed', 'aembed'),
'EmbeddingResult': ('praisonaiagents.embedding.result', 'EmbeddingResult'),
'get_dimensions': ('praisonaiagents.embedding.dimensions', 'get_dimensions'),
# Guardrails
'GuardrailResult': ('praisonaiagents.guardrails', 'GuardrailResult'),
'LLMGuardrail': ('praisonaiagents.guardrails', 'LLMGuardrail'),
# Flow display
'FlowDisplay': ('praisonaiagents.flow_display', 'FlowDisplay'),
'track_workflow': ('praisonaiagents.flow_display', 'track_workflow'),
# Agent classes
'Agent': ('praisonaiagents.agent.agent', 'Agent'),
'ImageAgent': ('praisonaiagents.agent.image_agent', 'ImageAgent'),
'VideoAgent': ('praisonaiagents.agent.video_agent', 'VideoAgent'),
'VideoConfig': ('praisonaiagents.agent.video_agent', 'VideoConfig'),
'AudioAgent': ('praisonaiagents.agent.audio_agent', 'AudioAgent'),
'AudioConfig': ('praisonaiagents.agent.audio_agent', 'AudioConfig'),
'OCRAgent': ('praisonaiagents.agent.ocr_agent', 'OCRAgent'),
'OCRConfig': ('praisonaiagents.agent.ocr_agent', 'OCRConfig'),
'ContextAgent': ('praisonaiagents.agent.context_agent', 'ContextAgent'),
'create_context_agent': ('praisonaiagents.agent.context_agent', 'create_context_agent'),
'DeepResearchAgent': ('praisonaiagents.agent.deep_research_agent', 'DeepResearchAgent'),
'DeepResearchResponse': ('praisonaiagents.agent.deep_research_agent', 'DeepResearchResponse'),
'ReasoningStep': ('praisonaiagents.agent.deep_research_agent', 'ReasoningStep'),
'WebSearchCall': ('praisonaiagents.agent.deep_research_agent', 'WebSearchCall'),
'CodeExecutionStep': ('praisonaiagents.agent.deep_research_agent', 'CodeExecutionStep'),
'MCPCall': ('praisonaiagents.agent.deep_research_agent', 'MCPCall'),
'FileSearchCall': ('praisonaiagents.agent.deep_research_agent', 'FileSearchCall'),
'Provider': ('praisonaiagents.agent.deep_research_agent', 'Provider'),
'QueryRewriterAgent': ('praisonaiagents.agent.query_rewriter_agent', 'QueryRewriterAgent'),
'RewriteStrategy': ('praisonaiagents.agent.query_rewriter_agent', 'RewriteStrategy'),
'RewriteResult': ('praisonaiagents.agent.query_rewriter_agent', 'RewriteResult'),
'PromptExpanderAgent': ('praisonaiagents.agent.prompt_expander_agent', 'PromptExpanderAgent'),
'ExpandStrategy': ('praisonaiagents.agent.prompt_expander_agent', 'ExpandStrategy'),
'ExpandResult': ('praisonaiagents.agent.prompt_expander_agent', 'ExpandResult'),
'VisionAgent': ('praisonaiagents.agent.vision_agent', 'VisionAgent'),
'VisionConfig': ('praisonaiagents.agent.vision_agent', 'VisionConfig'),
'EmbeddingAgent': ('praisonaiagents.agent.embedding_agent', 'EmbeddingAgent'),
'EmbeddingConfig': ('praisonaiagents.agent.embedding_agent', 'EmbeddingConfig'),
'RealtimeAgent': ('praisonaiagents.agent.realtime_agent', 'RealtimeAgent'),
'RealtimeConfig': ('praisonaiagents.agent.realtime_agent', 'RealtimeConfig'),
'CodeAgent': ('praisonaiagents.agent.code_agent', 'CodeAgent'),
'CodeConfig': ('praisonaiagents.agent.code_agent', 'CodeConfig'),
# Agents / AgentManager
'AgentManager': ('praisonaiagents.agents.agents', 'AgentManager'),
# Note: 'Agents' is handled by _custom_handler for deprecation warning
'Task': ('praisonaiagents.task.task', 'Task'),
'AutoAgents': ('praisonaiagents.agents.autoagents', 'AutoAgents'),
'AutoRagAgent': ('praisonaiagents.agents.auto_rag_agent', 'AutoRagAgent'),
'AutoRagConfig': ('praisonaiagents.agents.auto_rag_agent', 'AutoRagConfig'),
'RagRetrievalPolicy': ('praisonaiagents.agents.auto_rag_agent', 'RetrievalPolicy'),
# Session
'Session': ('praisonaiagents.session', 'Session'),
# App (AgentApp protocol and config)
'AgentAppProtocol': ('praisonaiagents.app.protocols', 'AgentAppProtocol'),
'AgentAppConfig': ('praisonaiagents.app.config', 'AgentAppConfig'),
# MCP (optional)
'MCP': ('praisonaiagents.mcp.mcp', 'MCP'),
# Knowledge
'Knowledge': ('praisonaiagents.knowledge.knowledge', 'Knowledge'),
'Chunking': ('praisonaiagents.knowledge.chunking', 'Chunking'),
# FastContext
'FastContext': ('praisonaiagents.context.fast', 'FastContext'),
'FastContextResult': ('praisonaiagents.context.fast', 'FastContextResult'),
'FileMatch': ('praisonaiagents.context.fast', 'FileMatch'),
'LineRange': ('praisonaiagents.context.fast', 'LineRange'),
# RAG
'RetrievalConfig': ('praisonaiagents.rag.retrieval_config', 'RetrievalConfig'),
'RetrievalPolicy': ('praisonaiagents.rag.retrieval_config', 'RetrievalPolicy'),
'CitationsMode': ('praisonaiagents.rag.retrieval_config', 'CitationsMode'),
'ContextPack': ('praisonaiagents.rag.models', 'ContextPack'),
'RAGResult': ('praisonaiagents.rag', 'RAGResult'),
'Citation': ('praisonaiagents.rag', 'Citation'),
'RAG': ('praisonaiagents.rag', 'RAG'),
'RAGConfig': ('praisonaiagents.rag', 'RAGConfig'),
'RAGCitation': ('praisonaiagents.rag', 'Citation'),
# Skills
'SkillManager': ('praisonaiagents.skills', 'SkillManager'),
'SkillProperties': ('praisonaiagents.skills', 'SkillProperties'),
'SkillMetadata': ('praisonaiagents.skills', 'SkillMetadata'),
'SkillLoader': ('praisonaiagents.skills', 'SkillLoader'),
# Memory
'Memory': ('praisonaiagents.memory.memory', 'Memory'),
# Planning
'Plan': ('praisonaiagents.planning', 'Plan'),
'PlanStep': ('praisonaiagents.planning', 'PlanStep'),
'TodoList': ('praisonaiagents.planning', 'TodoList'),
'TodoItem': ('praisonaiagents.planning', 'TodoItem'),
'PlanStorage': ('praisonaiagents.planning', 'PlanStorage'),
'PlanningAgent': ('praisonaiagents.planning', 'PlanningAgent'),
'ApprovalCallback': ('praisonaiagents.planning', 'ApprovalCallback'),
'READ_ONLY_TOOLS': ('praisonaiagents.planning', 'READ_ONLY_TOOLS'),
'RESTRICTED_TOOLS': ('praisonaiagents.planning', 'RESTRICTED_TOOLS'),
# Trace (protocol-driven, for custom sinks) - AGENTS.md naming: XProtocol
'ContextTraceSinkProtocol': ('praisonaiagents.trace', 'ContextTraceSinkProtocol'),
'ContextTraceSink': ('praisonaiagents.trace', 'ContextTraceSink'), # Backward compat alias
'TraceSinkProtocol': ('praisonaiagents.trace', 'TraceSinkProtocol'),
'TraceSink': ('praisonaiagents.trace', 'TraceSink'), # Backward compat alias
'ContextTraceEmitter': ('praisonaiagents.trace', 'ContextTraceEmitter'),
'ContextEvent': ('praisonaiagents.trace', 'ContextEvent'),
'ContextEventType': ('praisonaiagents.trace', 'ContextEventType'),
'trace_context': ('praisonaiagents.trace', 'trace_context'),
'ContextListSink': ('praisonaiagents.trace', 'ContextListSink'),
'ContextNoOpSink': ('praisonaiagents.trace', 'ContextNoOpSink'),
# Telemetry
'get_telemetry': ('praisonaiagents.telemetry', 'get_telemetry'),
'enable_telemetry': ('praisonaiagents.telemetry', 'enable_telemetry'),
'disable_telemetry': ('praisonaiagents.telemetry', 'disable_telemetry'),
'enable_performance_mode': ('praisonaiagents.telemetry', 'enable_performance_mode'),
'disable_performance_mode': ('praisonaiagents.telemetry', 'disable_performance_mode'),
'cleanup_telemetry_resources': ('praisonaiagents.telemetry', 'cleanup_telemetry_resources'),
'MinimalTelemetry': ('praisonaiagents.telemetry', 'MinimalTelemetry'),
'TelemetryCollector': ('praisonaiagents.telemetry', 'TelemetryCollector'),
# UI (optional)
'AGUI': ('praisonaiagents.ui.agui', 'AGUI'),
'A2A': ('praisonaiagents.ui.a2a', 'A2A'),
# Feature configs
'MemoryConfig': ('praisonaiagents.config.feature_configs', 'MemoryConfig'),
'KnowledgeConfig': ('praisonaiagents.config.feature_configs', 'KnowledgeConfig'),
'PlanningConfig': ('praisonaiagents.config.feature_configs', 'PlanningConfig'),
'ReflectionConfig': ('praisonaiagents.config.feature_configs', 'ReflectionConfig'),
'GuardrailConfig': ('praisonaiagents.config.feature_configs', 'GuardrailConfig'),
'WebConfig': ('praisonaiagents.config.feature_configs', 'WebConfig'),
'OutputConfig': ('praisonaiagents.config.feature_configs', 'OutputConfig'),
'ExecutionConfig': ('praisonaiagents.config.feature_configs', 'ExecutionConfig'),
'TemplateConfig': ('praisonaiagents.config.feature_configs', 'TemplateConfig'),
'CachingConfig': ('praisonaiagents.config.feature_configs', 'CachingConfig'),
'HooksConfig': ('praisonaiagents.config.feature_configs', 'HooksConfig'),
'SkillsConfig': ('praisonaiagents.config.feature_configs', 'SkillsConfig'),
'AutonomyConfig': ('praisonaiagents.config.feature_configs', 'AutonomyConfig'),
'MemoryBackend': ('praisonaiagents.config.feature_configs', 'MemoryBackend'),
'ChunkingStrategy': ('praisonaiagents.config.feature_configs', 'ChunkingStrategy'),
'GuardrailAction': ('praisonaiagents.config.feature_configs', 'GuardrailAction'),
'WebSearchProvider': ('praisonaiagents.config.feature_configs', 'WebSearchProvider'),
'OutputPreset': ('praisonaiagents.config.feature_configs', 'OutputPreset'),
'ExecutionPreset': ('praisonaiagents.config.feature_configs', 'ExecutionPreset'),
'AutonomyLevel': ('praisonaiagents.config.feature_configs', 'AutonomyLevel'),
'MultiAgentHooksConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentHooksConfig'),
'MultiAgentOutputConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentOutputConfig'),
'MultiAgentExecutionConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentExecutionConfig'),
'MultiAgentPlanningConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentPlanningConfig'),
'MultiAgentMemoryConfig': ('praisonaiagents.config.feature_configs', 'MultiAgentMemoryConfig'),
# Parameter resolver
'resolve': ('praisonaiagents.config.param_resolver', 'resolve'),
'ArrayMode': ('praisonaiagents.config.param_resolver', 'ArrayMode'),
'resolve_memory': ('praisonaiagents.config.param_resolver', 'resolve_memory'),
'resolve_knowledge': ('praisonaiagents.config.param_resolver', 'resolve_knowledge'),
'resolve_output': ('praisonaiagents.config.param_resolver', 'resolve_output'),
'resolve_execution': ('praisonaiagents.config.param_resolver', 'resolve_execution'),
'resolve_web': ('praisonaiagents.config.param_resolver', 'resolve_web'),
'resolve_planning': ('praisonaiagents.config.param_resolver', 'resolve_planning'),
'resolve_reflection': ('praisonaiagents.config.param_resolver', 'resolve_reflection'),
'resolve_context': ('praisonaiagents.config.param_resolver', 'resolve_context'),
'resolve_autonomy': ('praisonaiagents.config.param_resolver', 'resolve_autonomy'),
'resolve_caching': ('praisonaiagents.config.param_resolver', 'resolve_caching'),
'resolve_hooks': ('praisonaiagents.config.param_resolver', 'resolve_hooks'),
'resolve_skills': ('praisonaiagents.config.param_resolver', 'resolve_skills'),
'resolve_routing': ('praisonaiagents.config.param_resolver', 'resolve_routing'),
'resolve_guardrails': ('praisonaiagents.config.param_resolver', 'resolve_guardrails'),
'resolve_guardrail_policies': ('praisonaiagents.config.param_resolver', 'resolve_guardrail_policies'),
# Presets
'MEMORY_PRESETS': ('praisonaiagents.config.presets', 'MEMORY_PRESETS'),
'MEMORY_URL_SCHEMES': ('praisonaiagents.config.presets', 'MEMORY_URL_SCHEMES'),
'OUTPUT_PRESETS': ('praisonaiagents.config.presets', 'OUTPUT_PRESETS'),
'EXECUTION_PRESETS': ('praisonaiagents.config.presets', 'EXECUTION_PRESETS'),
'WEB_PRESETS': ('praisonaiagents.config.presets', 'WEB_PRESETS'),
'PLANNING_PRESETS': ('praisonaiagents.config.presets', 'PLANNING_PRESETS'),
'REFLECTION_PRESETS': ('praisonaiagents.config.presets', 'REFLECTION_PRESETS'),
'CONTEXT_PRESETS': ('praisonaiagents.config.presets', 'CONTEXT_PRESETS'),
'AUTONOMY_PRESETS': ('praisonaiagents.config.presets', 'AUTONOMY_PRESETS'),
'CACHING_PRESETS': ('praisonaiagents.config.presets', 'CACHING_PRESETS'),
'MULTI_AGENT_OUTPUT_PRESETS': ('praisonaiagents.config.presets', 'MULTI_AGENT_OUTPUT_PRESETS'),
'MULTI_AGENT_EXECUTION_PRESETS': ('praisonaiagents.config.presets', 'MULTI_AGENT_EXECUTION_PRESETS'),
'GUARDRAIL_PRESETS': ('praisonaiagents.config.presets', 'GUARDRAIL_PRESETS'),
'KNOWLEDGE_PRESETS': ('praisonaiagents.config.presets', 'KNOWLEDGE_PRESETS'),
# Parse utilities
'detect_url_scheme': ('praisonaiagents.config.parse_utils', 'detect_url_scheme'),
'is_path_like': ('praisonaiagents.config.parse_utils', 'is_path_like'),
'suggest_similar': ('praisonaiagents.config.parse_utils', 'suggest_similar'),
'is_policy_string': ('praisonaiagents.config.parse_utils', 'is_policy_string'),
'parse_policy_string': ('praisonaiagents.config.parse_utils', 'parse_policy_string'),
# Context management
'ContextConfig': ('praisonaiagents.context.models', 'ContextConfig'),
'OptimizerStrategy': ('praisonaiagents.context.models', 'OptimizerStrategy'),
'ManagerConfig': ('praisonaiagents.context.manager', 'ManagerConfig'),
'ContextManager': ('praisonaiagents.context.manager', 'ContextManager'),
# db and obs modules
'db': ('praisonaiagents.db', 'db'),
'obs': ('praisonaiagents.obs', 'obs'),
# Gateway protocols and config (implementations in praisonai wrapper)
'GatewayProtocol': ('praisonaiagents.gateway.protocols', 'GatewayProtocol'),
'GatewaySessionProtocol': ('praisonaiagents.gateway.protocols', 'GatewaySessionProtocol'),
'GatewayClientProtocol': ('praisonaiagents.gateway.protocols', 'GatewayClientProtocol'),
'GatewayEvent': ('praisonaiagents.gateway.protocols', 'GatewayEvent'),
'GatewayMessage': ('praisonaiagents.gateway.protocols', 'GatewayMessage'),
'EventType': ('praisonaiagents.gateway.protocols', 'EventType'),
'GatewayConfig': ('praisonaiagents.gateway.config', 'GatewayConfig'),
'SessionConfig': ('praisonaiagents.gateway.config', 'SessionConfig'),
# Bot protocols and config (implementations in praisonai wrapper)
'BotProtocol': ('praisonaiagents.bots.protocols', 'BotProtocol'),
'BotMessage': ('praisonaiagents.bots.protocols', 'BotMessage'),
'BotUser': ('praisonaiagents.bots.protocols', 'BotUser'),
'BotChannel': ('praisonaiagents.bots.protocols', 'BotChannel'),
'MessageType': ('praisonaiagents.bots.protocols', 'MessageType'),
'BotConfig': ('praisonaiagents.bots.config', 'BotConfig'),
# Sandbox protocols and config (implementations in praisonai wrapper)
'SandboxProtocol': ('praisonaiagents.sandbox.protocols', 'SandboxProtocol'),
'SandboxResult': ('praisonaiagents.sandbox.protocols', 'SandboxResult'),
'SandboxStatus': ('praisonaiagents.sandbox.protocols', 'SandboxStatus'),
'ResourceLimits': ('praisonaiagents.sandbox.protocols', 'ResourceLimits'),
'SandboxConfig': ('praisonaiagents.sandbox.config', 'SandboxConfig'),
'SecurityPolicy': ('praisonaiagents.sandbox.config', 'SecurityPolicy'),
# Model failover
'AuthProfile': ('praisonaiagents.llm.failover', 'AuthProfile'),
'ProviderStatus': ('praisonaiagents.llm.failover', 'ProviderStatus'),
'FailoverConfig': ('praisonaiagents.llm.failover', 'FailoverConfig'),
'FailoverManager': ('praisonaiagents.llm.failover', 'FailoverManager'),
}
def __getattr__(name):
"""
Lazy load heavy modules to avoid impacting package load time.
def _custom_handler(name, cache):
"""Handle special cases that need custom logic."""
import warnings
This function is called when an attribute is not found in the module.
It enables deferred loading of heavy dependencies like litellm.
"""
global _mcp_available, _planning_available, _telemetry_available
global _agui_available, _a2a_available
# Return cached values if available
if name in _lazy_cache:
return _lazy_cache[name]
# Agent and related classes (triggers litellm import chain)
if name == "Agent":
from .agent.agent import Agent
_lazy_cache[name] = Agent
return Agent
elif name == "ImageAgent":
from .agent.image_agent import ImageAgent
_lazy_cache[name] = ImageAgent
return ImageAgent
elif name == "VideoAgent":
from .agent.video_agent import VideoAgent
_lazy_cache[name] = VideoAgent
return VideoAgent
elif name == "VideoConfig":
from .agent.video_agent import VideoConfig
_lazy_cache[name] = VideoConfig
return VideoConfig
elif name == "AudioAgent":
from .agent.audio_agent import AudioAgent
_lazy_cache[name] = AudioAgent
return AudioAgent
elif name == "AudioConfig":
from .agent.audio_agent import AudioConfig
_lazy_cache[name] = AudioConfig
return AudioConfig
elif name == "OCRAgent":
from .agent.ocr_agent import OCRAgent
_lazy_cache[name] = OCRAgent
return OCRAgent
elif name == "OCRConfig":
from .agent.ocr_agent import OCRConfig
_lazy_cache[name] = OCRConfig
return OCRConfig
elif name == "ContextAgent":
from .agent.context_agent import ContextAgent
_lazy_cache[name] = ContextAgent
return ContextAgent
elif name == "create_context_agent":
from .agent.context_agent import create_context_agent
_lazy_cache[name] = create_context_agent
return create_context_agent
elif name == "DeepResearchAgent":
from .agent.deep_research_agent import DeepResearchAgent
_lazy_cache[name] = DeepResearchAgent
return DeepResearchAgent
elif name == "DeepResearchResponse":
from .agent.deep_research_agent import DeepResearchResponse
_lazy_cache[name] = DeepResearchResponse
return DeepResearchResponse
elif name == "Citation":
from .agent.deep_research_agent import Citation
_lazy_cache[name] = Citation
return Citation
elif name == "ReasoningStep":
from .agent.deep_research_agent import ReasoningStep
_lazy_cache[name] = ReasoningStep
return ReasoningStep
elif name == "WebSearchCall":
from .agent.deep_research_agent import WebSearchCall
_lazy_cache[name] = WebSearchCall
return WebSearchCall
elif name == "CodeExecutionStep":
from .agent.deep_research_agent import CodeExecutionStep
_lazy_cache[name] = CodeExecutionStep
return CodeExecutionStep
elif name == "MCPCall":
from .agent.deep_research_agent import MCPCall
_lazy_cache[name] = MCPCall
return MCPCall
elif name == "FileSearchCall":
from .agent.deep_research_agent import FileSearchCall
_lazy_cache[name] = FileSearchCall
return FileSearchCall
elif name == "Provider":
from .agent.deep_research_agent import Provider
_lazy_cache[name] = Provider
return Provider
elif name == "QueryRewriterAgent":
from .agent.query_rewriter_agent import QueryRewriterAgent
_lazy_cache[name] = QueryRewriterAgent
return QueryRewriterAgent
elif name == "RewriteStrategy":
from .agent.query_rewriter_agent import RewriteStrategy
_lazy_cache[name] = RewriteStrategy
return RewriteStrategy
elif name == "RewriteResult":
from .agent.query_rewriter_agent import RewriteResult
_lazy_cache[name] = RewriteResult
return RewriteResult
elif name == "PromptExpanderAgent":
from .agent.prompt_expander_agent import PromptExpanderAgent
_lazy_cache[name] = PromptExpanderAgent
return PromptExpanderAgent
elif name == "ExpandStrategy":
from .agent.prompt_expander_agent import ExpandStrategy
_lazy_cache[name] = ExpandStrategy
return ExpandStrategy
elif name == "ExpandResult":
from .agent.prompt_expander_agent import ExpandResult
_lazy_cache[name] = ExpandResult
return ExpandResult
# Agents (canonical) and PraisonAIAgents (backward-compatible alias)
elif name == "Agents":
from .agents.agents import Agents
_lazy_cache[name] = Agents
_lazy_cache["PraisonAIAgents"] = Agents # Also cache alias
return Agents
elif name == "PraisonAIAgents":
import warnings
# Agents deprecation warning (use AgentManager instead)
if name == "Agents":
warnings.warn(
"PraisonAIAgents is deprecated, use Agents instead. "
"Example: from praisonaiagents import Agents",
"Agents is deprecated, use AgentManager instead. "
"Example: from praisonaiagents import AgentManager",
DeprecationWarning,
stacklevel=2
stacklevel=4
)
from .agents.agents import Agents
_lazy_cache["Agents"] = Agents
_lazy_cache[name] = Agents
return Agents
value = lazy_import('praisonaiagents.agents.agents', 'AgentManager', cache)
cache['AgentManager'] = value
cache['Agents'] = value
return value
# Task
elif name == "Task":
from .task.task import Task
_lazy_cache[name] = Task
return Task
# PraisonAIAgents deprecation warning (use AgentManager instead)
if name == "PraisonAIAgents":
warnings.warn(
"PraisonAIAgents is deprecated, use AgentManager instead. "
"Example: from praisonaiagents import AgentManager",
DeprecationWarning,
stacklevel=4
)
value = lazy_import('praisonaiagents.agents.agents', 'AgentManager', cache)
cache['AgentManager'] = value
cache['PraisonAIAgents'] = value
return value
# AutoAgents
elif name == "AutoAgents":
from .agents.autoagents import AutoAgents
_lazy_cache[name] = AutoAgents
return AutoAgents
# WorkflowStep deprecation warning (use Task instead) - Phase 4 Consolidation
if name == "WorkflowStep":
warnings.warn(
"WorkflowStep is deprecated, use Task instead. "
"Task now supports all WorkflowStep features including action, handler, loop_over, etc. "
"Example: from praisonaiagents import Task",
DeprecationWarning,
stacklevel=4
)
value = lazy_import('praisonaiagents.task.task', 'Task', cache)
cache['Task'] = value
cache['WorkflowStep'] = value
return value
# AutoRagAgent (auto RAG retrieval decision)
elif name == "AutoRagAgent":
from .agents.auto_rag_agent import AutoRagAgent
_lazy_cache[name] = AutoRagAgent
return AutoRagAgent
elif name == "AutoRagConfig":
from .agents.auto_rag_agent import AutoRagConfig
_lazy_cache[name] = AutoRagConfig
return AutoRagConfig
elif name == "RagRetrievalPolicy":
from .agents.auto_rag_agent import RetrievalPolicy
_lazy_cache[name] = RetrievalPolicy
return RetrievalPolicy
# Module imports (return the module itself)
if name == 'memory':
import importlib
return importlib.import_module('.memory', 'praisonaiagents')
if name == 'workflows':
import importlib
return importlib.import_module('.workflows', 'praisonaiagents')
# Session (imports requests lazily via session/api.py)
elif name == "Session":
from .session import Session
_lazy_cache[name] = Session
return Session
# MCP support (optional)
elif name == "MCP":
if _mcp_available is None:
try:
from .mcp.mcp import MCP as _MCP
_lazy_cache[name] = _MCP
return _MCP
except ImportError:
return None
return _lazy_cache.get(name)
# Knowledge module (imports chromadb, mem0)
elif name == "Knowledge":
from praisonaiagents.knowledge.knowledge import Knowledge
_lazy_cache[name] = Knowledge
return Knowledge
elif name == "Chunking":
from praisonaiagents.knowledge.chunking import Chunking
_lazy_cache[name] = Chunking
return Chunking
# FastContext support
elif name == "FastContext":
from praisonaiagents.context.fast import FastContext
_lazy_cache[name] = FastContext
return FastContext
elif name == "FastContextResult":
from praisonaiagents.context.fast import FastContextResult
_lazy_cache[name] = FastContextResult
return FastContextResult
elif name == "FileMatch":
from praisonaiagents.context.fast import FileMatch
_lazy_cache[name] = FileMatch
return FileMatch
elif name == "LineRange":
from praisonaiagents.context.fast import LineRange
_lazy_cache[name] = LineRange
return LineRange
# Retrieval configuration (Agent-first unified config)
elif name == "RetrievalConfig":
from praisonaiagents.rag.retrieval_config import RetrievalConfig
_lazy_cache[name] = RetrievalConfig
return RetrievalConfig
elif name == "RetrievalPolicy":
from praisonaiagents.rag.retrieval_config import RetrievalPolicy
_lazy_cache[name] = RetrievalPolicy
return RetrievalPolicy
elif name == "CitationsMode":
from praisonaiagents.rag.retrieval_config import CitationsMode
_lazy_cache[name] = CitationsMode
return CitationsMode
# RAG models (used by Agent.query() and Agent.retrieve())
elif name == "ContextPack":
from praisonaiagents.rag.models import ContextPack
_lazy_cache[name] = ContextPack
return ContextPack
elif name == "RAGResult":
from praisonaiagents.rag import RAGResult
_lazy_cache[name] = RAGResult
return RAGResult
elif name == "Citation":
from praisonaiagents.rag import Citation
_lazy_cache[name] = Citation
return Citation
# RAG internals (advanced use - prefer Agent.query() for normal usage)
elif name == "RAG":
from praisonaiagents.rag import RAG
_lazy_cache[name] = RAG
return RAG
elif name == "RAGConfig":
from praisonaiagents.rag import RAGConfig
_lazy_cache[name] = RAGConfig
return RAGConfig
elif name == "RAGCitation":
from praisonaiagents.rag import Citation
_lazy_cache[name] = Citation
return Citation
# Agent Skills support (lazy loaded for zero performance impact)
elif name == "SkillManager":
from praisonaiagents.skills import SkillManager
_lazy_cache[name] = SkillManager
return SkillManager
elif name == "SkillProperties":
from praisonaiagents.skills import SkillProperties
_lazy_cache[name] = SkillProperties
return SkillProperties
elif name == "SkillMetadata":
from praisonaiagents.skills import SkillMetadata
_lazy_cache[name] = SkillMetadata
return SkillMetadata
elif name == "SkillLoader":
from praisonaiagents.skills import SkillLoader
_lazy_cache[name] = SkillLoader
return SkillLoader
# Memory module (imports chromadb)
elif name == "Memory":
from praisonaiagents.memory.memory import Memory
_lazy_cache[name] = Memory
return Memory
# Embedding module (unified embedding API)
# Note: Must import from .embed submodule to get functions, not the module
elif name == "embedding":
from praisonaiagents.embedding.embed import embedding
_lazy_cache[name] = embedding
return embedding
elif name == "aembedding":
from praisonaiagents.embedding.embed import aembedding
_lazy_cache[name] = aembedding
return aembedding
elif name == "embed":
from praisonaiagents.embedding.embed import embed
_lazy_cache[name] = embed
return embed
elif name == "aembed":
from praisonaiagents.embedding.embed import aembed
_lazy_cache[name] = aembed
return aembed
elif name == "EmbeddingResult":
from praisonaiagents.embedding.result import EmbeddingResult
_lazy_cache[name] = EmbeddingResult
return EmbeddingResult
elif name == "get_dimensions":
from praisonaiagents.embedding.dimensions import get_dimensions
_lazy_cache[name] = get_dimensions
return get_dimensions
# Planning mode support (lazy)
elif name in ("Plan", "PlanStep", "TodoList", "TodoItem", "PlanStorage",
"PlanningAgent", "ApprovalCallback", "READ_ONLY_TOOLS",
"RESTRICTED_TOOLS"):
try:
from . import planning as _planning
result = getattr(_planning, name)
_lazy_cache[name] = result
return result
except ImportError:
if name in ("READ_ONLY_TOOLS", "RESTRICTED_TOOLS"):
return []
return None
# Telemetry support (lazy loaded)
elif name in ("get_telemetry", "enable_telemetry", "disable_telemetry",
"enable_performance_mode", "disable_performance_mode",
"cleanup_telemetry_resources", "MinimalTelemetry", "TelemetryCollector"):
try:
from . import telemetry as _telemetry
result = getattr(_telemetry, name)
_lazy_cache[name] = result
return result
except ImportError:
# Provide stub functions
if name == "get_telemetry":
return lambda: None
elif name == "enable_telemetry":
import logging
def _stub(*args, **kwargs):
logging.warning(
"Telemetry not available. Install with: pip install praisonaiagents[telemetry]"
)
return None
return _stub
elif name in ("disable_telemetry", "enable_performance_mode",
"disable_performance_mode", "cleanup_telemetry_resources"):
return lambda: None
return None
# AG-UI support (optional, lazy loaded)
elif name == "AGUI":
try:
from praisonaiagents.ui.agui import AGUI
_lazy_cache[name] = AGUI
return AGUI
except ImportError:
return None
# A2A support (optional, lazy loaded)
elif name == "A2A":
try:
from praisonaiagents.ui.a2a import A2A
_lazy_cache[name] = A2A
return A2A
except ImportError:
return None
# Feature Config classes (agent-centric API)
elif name in ("MemoryConfig", "KnowledgeConfig", "PlanningConfig",
"ReflectionConfig", "GuardrailConfig", "WebConfig",
"OutputConfig", "ExecutionConfig", "TemplateConfig",
"CachingConfig", "HooksConfig", "SkillsConfig", "AutonomyConfig",
"MemoryBackend", "ChunkingStrategy", "GuardrailAction",
"WebSearchProvider", "OutputPreset", "ExecutionPreset", "AutonomyLevel",
# Multi-Agent config classes
"MultiAgentHooksConfig", "MultiAgentOutputConfig",
"MultiAgentExecutionConfig", "MultiAgentPlanningConfig",
"MultiAgentMemoryConfig"):
from .config import feature_configs
result = getattr(feature_configs, name)
_lazy_cache[name] = result
return result
# Unified parameter resolver (agent-centric API)
elif name in ("resolve", "ArrayMode", "resolve_memory", "resolve_knowledge",
"resolve_output", "resolve_execution", "resolve_web",
"resolve_planning", "resolve_reflection", "resolve_context",
"resolve_autonomy", "resolve_caching", "resolve_hooks",
"resolve_skills", "resolve_routing", "resolve_guardrails",
"resolve_guardrail_policies"):
from .config import param_resolver
result = getattr(param_resolver, name)
_lazy_cache[name] = result
return result
# Preset registries (agent-centric API)
elif name in ("MEMORY_PRESETS", "MEMORY_URL_SCHEMES", "OUTPUT_PRESETS",
"EXECUTION_PRESETS", "WEB_PRESETS", "PLANNING_PRESETS",
"REFLECTION_PRESETS", "CONTEXT_PRESETS", "AUTONOMY_PRESETS",
"CACHING_PRESETS", "MULTI_AGENT_OUTPUT_PRESETS",
"MULTI_AGENT_EXECUTION_PRESETS", "GUARDRAIL_PRESETS",
"KNOWLEDGE_PRESETS"):
from .config import presets
result = getattr(presets, name)
_lazy_cache[name] = result
return result
# Parse utilities (agent-centric API)
elif name in ("detect_url_scheme", "is_path_like", "suggest_similar",
"is_policy_string", "parse_policy_string"):
from .config import parse_utils
result = getattr(parse_utils, name)
_lazy_cache[name] = result
return result
# Context management config
elif name == "ContextConfig":
from .context.models import ContextConfig
_lazy_cache[name] = ContextConfig
return ContextConfig
elif name == "OptimizerStrategy":
from .context.models import OptimizerStrategy
_lazy_cache[name] = OptimizerStrategy
return OptimizerStrategy
elif name == "ManagerConfig":
from .context.manager import ManagerConfig
_lazy_cache[name] = ManagerConfig
return ManagerConfig
elif name == "ContextManager":
from .context.manager import ContextManager
_lazy_cache[name] = ContextManager
return ContextManager
# =========================================================================
# Sub-package fallback: delegate to sub-packages for additional symbols
# This enables: from praisonaiagents import duckduckgo, FileMemory, etc.
# =========================================================================
# Try tools sub-package (110+ tool functions)
try:
from . import tools as _tools_module
if hasattr(_tools_module, name):
result = getattr(_tools_module, name)
_lazy_cache[name] = result
return result
except (ImportError, AttributeError):
pass
# Try memory sub-package (FileMemory, RulesManager, etc.)
try:
from . import memory as _memory_module
if hasattr(_memory_module, name):
result = getattr(_memory_module, name)
_lazy_cache[name] = result
return result
except (ImportError, AttributeError):
pass
# Try config sub-package (already handled above, but catch any stragglers)
try:
from . import config as _config_module
if hasattr(_config_module, name):
result = getattr(_config_module, name)
_lazy_cache[name] = result
return result
except (ImportError, AttributeError):
pass
# Try workflows sub-package (Pipeline, Route, etc. - already imported, catch aliases)
try:
from . import workflows as _workflows_module
if hasattr(_workflows_module, name):
result = getattr(_workflows_module, name)
_lazy_cache[name] = result
return result
except (ImportError, AttributeError):
pass
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
raise AttributeError(f"Not handled by custom_handler: {name}")
# ============================================================================
# SUBPACKAGE FUNCTION OVERRIDES
# ============================================================================
# Some subpackage names conflict with function names we want to export.
# Override them here to return the function instead of the module.
# ============================================================================
# Override 'embedding' to return the function, not the subpackage
from .embedding.embed import embedding as _embedding_func
embedding = _embedding_func
# Also provide embeddings alias
embeddings = _embedding_func
# Create the __getattr__ function using centralized utility
__getattr__ = create_lazy_getattr_with_fallback(
mapping=_LAZY_IMPORTS,
module_name=__name__,
cache=_lazy_cache,
fallback_modules=['tools', 'memory', 'config', 'workflows'],
custom_handler=_custom_handler
)
# Initialize telemetry only if explicitly enabled via config
@@ -698,7 +599,8 @@ def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
__all__ = [
# Core classes - the essentials
'Agent',
'Agents',
'AgentManager', # Primary class for multi-agent coordination (v0.14.16+)
'Agents', # Deprecated alias for AgentManager
'Task',
# Tool essentials
@@ -0,0 +1,297 @@
"""
Centralized lazy loading utility for zero-overhead imports.
This module provides DRY utilities for lazy loading across the praisonaiagents package.
All heavy dependencies should use these utilities to ensure consistent behavior and
thread-safe caching.
Usage:
# Simple lazy import
from praisonaiagents._lazy import lazy_import
HookEvent = lazy_import('praisonaiagents.hooks.types', 'HookEvent')
# Create a __getattr__ function for a module
from praisonaiagents._lazy import create_lazy_getattr
_LAZY_IMPORTS = {
'HookEvent': ('praisonaiagents.hooks.types', 'HookEvent'),
'HookResult': ('praisonaiagents.hooks.types', 'HookResult'),
}
__getattr__ = create_lazy_getattr(_LAZY_IMPORTS, __name__)
"""
import importlib
import threading
from typing import Any, Callable, Dict, Optional, Tuple
# Global cache lock for thread-safe access
_cache_lock = threading.Lock()
# Global module cache
_module_cache: Dict[str, Any] = {}
def lazy_import(
module_path: str,
attr_name: str,
cache: Optional[Dict[str, Any]] = None
) -> Any:
"""
Lazily import an attribute from a module with thread-safe caching.
Args:
module_path: Full module path (e.g., 'praisonaiagents.hooks.types')
attr_name: Attribute name to import (e.g., 'HookEvent')
cache: Optional cache dict (uses global cache if None)
Returns:
The imported attribute
Raises:
ModuleNotFoundError: If the module doesn't exist
AttributeError: If the attribute doesn't exist in the module
Example:
HookEvent = lazy_import('praisonaiagents.hooks.types', 'HookEvent')
"""
cache = cache if cache is not None else _module_cache
cache_key = f"{module_path}.{attr_name}"
# Fast path: check cache without lock
if cache_key in cache:
return cache[cache_key]
# Slow path: acquire lock and import
with _cache_lock:
# Double-check after acquiring lock
if cache_key in cache:
return cache[cache_key]
module = importlib.import_module(module_path)
value = getattr(module, attr_name)
cache[cache_key] = value
return value
class LazyModule:
"""
A lazy module wrapper that defers import until first attribute access.
Useful for creating module-level lazy imports that behave like the real module.
Example:
_hooks_types = LazyModule('praisonaiagents.hooks.types')
# Module not loaded yet
HookEvent = _hooks_types.HookEvent
# Now module is loaded and cached
"""
__slots__ = ('_module_path', '_module', '_lock')
def __init__(self, module_path: str):
object.__setattr__(self, '_module_path', module_path)
object.__setattr__(self, '_module', None)
object.__setattr__(self, '_lock', threading.Lock())
def _load_module(self) -> Any:
"""Load the module if not already loaded."""
if self._module is None:
with self._lock:
if self._module is None:
module = importlib.import_module(self._module_path)
object.__setattr__(self, '_module', module)
return self._module
def __getattr__(self, name: str) -> Any:
module = self._load_module()
return getattr(module, name)
def __repr__(self) -> str:
loaded = "loaded" if self._module is not None else "not loaded"
return f"<LazyModule '{self._module_path}' ({loaded})>"
def create_lazy_getattr(
mapping: Dict[str, Tuple[str, str]],
module_name: str = 'unknown',
cache: Optional[Dict[str, Any]] = None
) -> Callable[[str], Any]:
"""
Create a __getattr__ function for lazy loading module attributes.
This is the DRY way to implement lazy loading in __init__.py files.
Args:
mapping: Dict mapping attribute names to (module_path, attr_name) tuples
module_name: Name of the module (for error messages)
cache: Optional cache dict (creates new one if None)
Returns:
A __getattr__ function suitable for use in a module
Example:
# In __init__.py
from praisonaiagents._lazy import create_lazy_getattr
_LAZY_IMPORTS = {
'HookEvent': ('praisonaiagents.hooks.types', 'HookEvent'),
'HookResult': ('praisonaiagents.hooks.types', 'HookResult'),
}
__getattr__ = create_lazy_getattr(_LAZY_IMPORTS, __name__)
"""
_cache = cache if cache is not None else {}
def __getattr__(name: str) -> Any:
# Check cache first
if name in _cache:
return _cache[name]
# Check if it's in our mapping
if name in mapping:
module_path, attr_name = mapping[name]
value = lazy_import(module_path, attr_name, _cache)
_cache[name] = value
return value
raise AttributeError(f"module {module_name!r} has no attribute {name!r}")
return __getattr__
def create_lazy_getattr_with_fallback(
mapping: Dict[str, Tuple[str, str]],
module_name: str = 'unknown',
cache: Optional[Dict[str, Any]] = None,
fallback_modules: Optional[list] = None,
custom_handler: Optional[Callable[[str, Dict[str, Any]], Any]] = None
) -> Callable[[str], Any]:
"""
Create a __getattr__ function with fallback to sub-packages.
This is useful for main __init__.py files that need to:
1. Check a mapping of known lazy imports
2. Fall back to sub-packages for additional symbols
3. Handle custom logic for optional modules
Args:
mapping: Dict mapping attribute names to (module_path, attr_name) tuples
module_name: Name of the module (for error messages)
cache: Optional cache dict (creates new one if None)
fallback_modules: List of sub-package names to try if not in mapping
custom_handler: Optional function(name, cache) for custom logic before fallback
Returns:
A __getattr__ function suitable for use in a module
"""
_cache = cache if cache is not None else {}
_fallback_modules = fallback_modules or []
def __getattr__(name: str) -> Any:
# Check cache first
if name in _cache:
return _cache[name]
# Check if it's in our mapping
if name in mapping:
module_path, attr_name = mapping[name]
try:
value = lazy_import(module_path, attr_name, _cache)
_cache[name] = value
return value
except (ImportError, AttributeError):
# Optional module not available
_cache[name] = None
return None
# Try custom handler if provided
if custom_handler is not None:
try:
result = custom_handler(name, _cache)
if result is not None:
_cache[name] = result
return result
except AttributeError:
pass # Continue to fallback
# Try fallback modules
for subpkg in _fallback_modules:
try:
submodule = importlib.import_module(f'.{subpkg}', module_name)
if hasattr(submodule, name):
result = getattr(submodule, name)
_cache[name] = result
return result
except (ImportError, AttributeError):
continue
raise AttributeError(f"module {module_name!r} has no attribute {name!r}")
return __getattr__
def create_lazy_getattr_with_groups(
groups: Dict[str, Dict[str, Tuple[str, str]]],
module_name: str = 'unknown',
cache: Optional[Dict[str, Any]] = None
) -> Callable[[str], Any]:
"""
Create a __getattr__ function with grouped lazy imports.
When one attribute from a group is accessed, all attributes in that group
are imported together. This is useful when multiple attributes come from
the same module and are often used together.
Args:
groups: Dict mapping group names to dicts of (attr_name -> (module_path, attr_name))
module_name: Name of the module (for error messages)
cache: Optional cache dict (creates new one if None)
Returns:
A __getattr__ function suitable for use in a module
Example:
_LAZY_GROUPS = {
'types': {
'HookEvent': ('praisonaiagents.hooks.types', 'HookEvent'),
'HookResult': ('praisonaiagents.hooks.types', 'HookResult'),
},
'events': {
'BeforeToolInput': ('praisonaiagents.hooks.events', 'BeforeToolInput'),
'AfterToolInput': ('praisonaiagents.hooks.events', 'AfterToolInput'),
},
}
__getattr__ = create_lazy_getattr_with_groups(_LAZY_GROUPS, __name__)
"""
_cache = cache if cache is not None else {}
# Build reverse mapping: attr_name -> group_name
_attr_to_group: Dict[str, str] = {}
for group_name, attrs in groups.items():
for attr_name in attrs:
_attr_to_group[attr_name] = group_name
def __getattr__(name: str) -> Any:
# Check cache first
if name in _cache:
return _cache[name]
# Check if it's in our groups
if name in _attr_to_group:
group_name = _attr_to_group[name]
group = groups[group_name]
# Import all attributes in the group
for attr_name, (module_path, real_attr_name) in group.items():
if attr_name not in _cache:
value = lazy_import(module_path, real_attr_name)
_cache[attr_name] = value
return _cache[name]
raise AttributeError(f"module {module_name!r} has no attribute {name!r}")
return __getattr__
@@ -122,16 +122,26 @@ _configure_litellm = configure_litellm
# ROOT LOGGER CONFIGURATION
# ========================================================================
def configure_root_logger():
"""Configure the root logger with RichHandler."""
from rich.logging import RichHandler
"""Configure the root logger with optional RichHandler.
Uses standard StreamHandler by default for performance.
RichHandler is only used when LOGLEVEL=DEBUG for better debugging experience.
"""
loglevel = os.environ.get('LOGLEVEL', 'WARNING').upper()
# Only use RichHandler for DEBUG level to avoid importing rich at startup
# This significantly improves import time for silent/normal operation
if loglevel == 'DEBUG':
from rich.logging import RichHandler
handlers = [RichHandler(rich_tracebacks=True)]
else:
handlers = [logging.StreamHandler()]
logging.basicConfig(
level=getattr(logging, loglevel, logging.WARNING),
format="%(asctime)s %(filename)s:%(lineno)d %(levelname)s %(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)],
handlers=handlers,
force=True
)
@@ -1,46 +1,147 @@
"""Agent module for AI agents"""
from .agent import Agent
from .image_agent import ImageAgent
from .video_agent import VideoAgent, VideoConfig
from .audio_agent import AudioAgent, AudioConfig
from .ocr_agent import OCRAgent, OCRConfig
from .context_agent import ContextAgent, create_context_agent
from .handoff import (
Handoff, handoff, handoff_filters,
RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions,
HandoffConfig, HandoffResult, HandoffInputData,
ContextPolicy, HandoffError, HandoffCycleError, HandoffDepthError, HandoffTimeoutError,
)
from .router_agent import RouterAgent
from .deep_research_agent import (
DeepResearchAgent,
DeepResearchResponse,
Citation,
ReasoningStep,
WebSearchCall,
CodeExecutionStep,
MCPCall,
FileSearchCall,
Provider
)
from .query_rewriter_agent import (
QueryRewriterAgent,
RewriteStrategy,
RewriteResult
)
from .prompt_expander_agent import (
PromptExpanderAgent,
ExpandStrategy,
ExpandResult
)
from .protocols import (
AgentProtocol,
RunnableAgentProtocol,
ToolAwareAgentProtocol,
MemoryAwareAgentProtocol,
FullAgentProtocol,
ContextEngineerProtocol,
)
"""Agent module for AI agents - uses lazy loading for performance"""
# Lazy loading cache
_lazy_cache = {}
def __getattr__(name):
"""Lazy load agent classes to avoid importing rich at startup."""
if name in _lazy_cache:
return _lazy_cache[name]
# Core Agent - always needed
if name == 'Agent':
from .agent import Agent
_lazy_cache[name] = Agent
return Agent
# Specialized agents - lazy loaded (import rich)
if name == 'ImageAgent':
from .image_agent import ImageAgent
_lazy_cache[name] = ImageAgent
return ImageAgent
elif name == 'VideoAgent':
from .video_agent import VideoAgent
_lazy_cache[name] = VideoAgent
return VideoAgent
elif name == 'VideoConfig':
from .video_agent import VideoConfig
_lazy_cache[name] = VideoConfig
return VideoConfig
elif name == 'AudioAgent':
from .audio_agent import AudioAgent
_lazy_cache[name] = AudioAgent
return AudioAgent
elif name == 'AudioConfig':
from .audio_agent import AudioConfig
_lazy_cache[name] = AudioConfig
return AudioConfig
elif name == 'OCRAgent':
from .ocr_agent import OCRAgent
_lazy_cache[name] = OCRAgent
return OCRAgent
elif name == 'OCRConfig':
from .ocr_agent import OCRConfig
_lazy_cache[name] = OCRConfig
return OCRConfig
elif name == 'ContextAgent':
from .context_agent import ContextAgent
_lazy_cache[name] = ContextAgent
return ContextAgent
elif name == 'create_context_agent':
from .context_agent import create_context_agent
_lazy_cache[name] = create_context_agent
return create_context_agent
elif name == 'RouterAgent':
from .router_agent import RouterAgent
_lazy_cache[name] = RouterAgent
return RouterAgent
elif name == 'VisionAgent':
from .vision_agent import VisionAgent
_lazy_cache[name] = VisionAgent
return VisionAgent
elif name == 'VisionConfig':
from .vision_agent import VisionConfig
_lazy_cache[name] = VisionConfig
return VisionConfig
elif name == 'EmbeddingAgent':
from .embedding_agent import EmbeddingAgent
_lazy_cache[name] = EmbeddingAgent
return EmbeddingAgent
elif name == 'EmbeddingConfig':
from .embedding_agent import EmbeddingConfig
_lazy_cache[name] = EmbeddingConfig
return EmbeddingConfig
elif name == 'RealtimeAgent':
from .realtime_agent import RealtimeAgent
_lazy_cache[name] = RealtimeAgent
return RealtimeAgent
elif name == 'RealtimeConfig':
from .realtime_agent import RealtimeConfig
_lazy_cache[name] = RealtimeConfig
return RealtimeConfig
elif name == 'CodeAgent':
from .code_agent import CodeAgent
_lazy_cache[name] = CodeAgent
return CodeAgent
elif name == 'CodeConfig':
from .code_agent import CodeConfig
_lazy_cache[name] = CodeConfig
return CodeConfig
# Handoff - lightweight
_handoff_names = {
'Handoff', 'handoff', 'handoff_filters',
'RECOMMENDED_PROMPT_PREFIX', 'prompt_with_handoff_instructions',
'HandoffConfig', 'HandoffResult', 'HandoffInputData',
'ContextPolicy', 'HandoffError', 'HandoffCycleError',
'HandoffDepthError', 'HandoffTimeoutError'
}
if name in _handoff_names:
from . import handoff as _handoff_module
value = getattr(_handoff_module, name)
_lazy_cache[name] = value
return value
# Deep research agent
_deep_research_names = {
'DeepResearchAgent', 'DeepResearchResponse', 'Citation',
'ReasoningStep', 'WebSearchCall', 'CodeExecutionStep',
'MCPCall', 'FileSearchCall', 'Provider'
}
if name in _deep_research_names:
from . import deep_research_agent as _dr_module
value = getattr(_dr_module, name)
_lazy_cache[name] = value
return value
# Query rewriter agent
_query_rewriter_names = {'QueryRewriterAgent', 'RewriteStrategy', 'RewriteResult'}
if name in _query_rewriter_names:
from . import query_rewriter_agent as _qr_module
value = getattr(_qr_module, name)
_lazy_cache[name] = value
return value
# Prompt expander agent
_prompt_expander_names = {'PromptExpanderAgent', 'ExpandStrategy', 'ExpandResult'}
if name in _prompt_expander_names:
from . import prompt_expander_agent as _pe_module
value = getattr(_pe_module, name)
_lazy_cache[name] = value
return value
# Protocols - lightweight
_protocol_names = {
'AgentProtocol', 'RunnableAgentProtocol', 'ToolAwareAgentProtocol',
'MemoryAwareAgentProtocol', 'FullAgentProtocol', 'ContextEngineerProtocol'
}
if name in _protocol_names:
from . import protocols as _protocols_module
value = getattr(_protocols_module, name)
_lazy_cache[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
'Agent',
@@ -51,6 +152,14 @@ __all__ = [
'AudioConfig',
'OCRAgent',
'OCRConfig',
'VisionAgent',
'VisionConfig',
'EmbeddingAgent',
'EmbeddingConfig',
'RealtimeAgent',
'RealtimeConfig',
'CodeAgent',
'CodeConfig',
'ContextAgent',
'create_context_agent',
'Handoff',
File diff suppressed because it is too large. Load diff
@@ -53,12 +53,16 @@ class AutonomyConfig:
doom_loop_threshold: Number of repeated actions to trigger doom loop
auto_escalate: Whether to automatically escalate complexity
observe: Whether to emit observability events
completion_promise: Optional string that signals completion when wrapped in <promise>TEXT</promise>
clear_context: Whether to clear chat history between iterations
"""
enabled: bool = True
max_iterations: int = 20
doom_loop_threshold: int = 3
auto_escalate: bool = True
observe: bool = False
completion_promise: Optional[str] = None
clear_context: bool = False
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "AutonomyConfig":
@@ -69,6 +73,8 @@ class AutonomyConfig:
doom_loop_threshold=data.get("doom_loop_threshold", 3),
auto_escalate=data.get("auto_escalate", True),
observe=data.get("observe", False),
completion_promise=data.get("completion_promise"),
clear_context=data.get("clear_context", False),
)
@@ -209,21 +215,23 @@ class AutonomyResult:
Attributes:
success: Whether the task completed successfully
output: Final output/response
completion_reason: Why execution stopped (goal, timeout, max_iterations, doom_loop, error)
completion_reason: Why execution stopped (goal, timeout, max_iterations, doom_loop, error, promise)
iterations: Number of iterations executed
stage: Final execution stage
actions: List of actions taken
duration_seconds: Total execution time
error: Error message if failed
started_at: ISO 8601 timestamp when execution started
"""
success: bool
output: str = ""
completion_reason: str = "goal" # goal, timeout, max_iterations, doom_loop, error
completion_reason: str = "goal" # goal, timeout, max_iterations, doom_loop, error, promise
iterations: int = 0
stage: str = "direct"
actions: List[Dict[str, Any]] = field(default_factory=list)
duration_seconds: float = 0.0
error: Optional[str] = None
started_at: Optional[str] = None
class DoomLoopTracker:
@@ -395,6 +403,8 @@ class AutonomyMixin:
prompt: str,
max_iterations: Optional[int] = None,
timeout_seconds: Optional[float] = None,
completion_promise: Optional[str] = None,
clear_context: bool = False,
) -> AutonomyResult:
"""Run an autonomous task execution loop.
@@ -403,12 +413,17 @@ class AutonomyMixin:
- Progressive escalation based on task complexity
- Doom loop detection and recovery
- Iteration limits and timeouts
- Completion detection
- Completion detection (keyword-based or promise-based)
- Optional context clearing between iterations
Args:
prompt: The task to execute
max_iterations: Override max iterations (default from config)
timeout_seconds: Timeout in seconds (default: no timeout)
completion_promise: Optional string that signals completion when
wrapped in <promise>TEXT</promise> tags in the response
clear_context: Whether to clear chat history between iterations
(forces agent to rely on external state like files)
Returns:
AutonomyResult with success status, output, and metadata
@@ -418,7 +433,11 @@ class AutonomyMixin:
Example:
agent = Agent(instructions="...", autonomy=True)
result = agent.run_autonomous("Refactor the auth module")
result = agent.run_autonomous(
"Refactor the auth module",
completion_promise="DONE",
clear_context=True
)
if result.success:
print(result.output)
"""
@@ -436,8 +455,17 @@ class AutonomyMixin:
config_max_iter = self.autonomy_config.get("max_iterations", 20)
effective_max_iter = max_iterations if max_iterations is not None else config_max_iter
# Get completion_promise from config if not provided as param
effective_promise = completion_promise
if effective_promise is None:
effective_promise = self.autonomy_config.get("completion_promise")
# Get clear_context from config if not explicitly set
effective_clear_context = clear_context
if not clear_context:
effective_clear_context = self.autonomy_config.get("clear_context", False)
# Analyze prompt and get recommended stage
signals = self.analyze_prompt(prompt)
stage = self.get_recommended_stage(prompt)
# Reset doom loop tracker for new task
@@ -474,9 +502,9 @@ class AutonomyMixin:
)
# Execute one turn using the agent's chat method
# The chat method is expected to be provided by the Agent class
# Always use the original prompt (prompt re-injection)
try:
response = self.chat(prompt if iterations == 1 else "Continue with the task")
response = self.chat(prompt)
except Exception as e:
logger.error(f"Error during autonomous execution: {e}")
return AutonomyResult(
@@ -496,8 +524,24 @@ class AutonomyMixin:
"response": str(response)[:500], # Truncate for storage
})
# Check for completion signals in response
response_lower = str(response).lower()
response_str = str(response)
# Check for completion promise FIRST (structured signal)
if effective_promise:
promise_tag = f"<promise>{effective_promise}</promise>"
if promise_tag in response_str:
return AutonomyResult(
success=True,
output=response_str,
completion_reason="promise",
iterations=iterations,
stage=stage,
actions=actions_taken,
duration_seconds=time.time() - start_time,
)
# Check for keyword-based completion signals (fallback)
response_lower = response_str.lower()
completion_signals = [
"task completed",
"task complete",
@@ -509,7 +553,7 @@ class AutonomyMixin:
if any(signal in response_lower for signal in completion_signals):
return AutonomyResult(
success=True,
output=str(response),
output=response_str,
completion_reason="goal",
iterations=iterations,
stage=stage,
@@ -521,13 +565,17 @@ class AutonomyMixin:
if stage == "direct":
return AutonomyResult(
success=True,
output=str(response),
output=response_str,
completion_reason="goal",
iterations=iterations,
stage=stage,
actions=actions_taken,
duration_seconds=time.time() - start_time,
)
# Clear context between iterations if enabled
if effective_clear_context:
self.clear_history()
# Max iterations reached
return AutonomyResult(
@@ -0,0 +1,500 @@
"""CodeAgent - Code generation, execution, review, and refactoring with sandboxing."""
from dataclasses import dataclass, field, asdict
from typing import Any, Dict, List, Optional, Union
import asyncio
import logging
@dataclass
class CodeConfig:
"""Configuration for CodeAgent.
Attributes:
sandbox: Enable sandboxed execution (default: True for safety)
timeout: Execution timeout in seconds
allowed_languages: List of allowed programming languages
max_output_length: Maximum output length in characters
working_directory: Working directory for code execution
environment: Environment variables for execution
"""
sandbox: bool = True
timeout: int = 30
allowed_languages: List[str] = field(default_factory=lambda: ["python"])
max_output_length: int = 10000
working_directory: Optional[str] = None
environment: Dict[str, str] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert config to dictionary."""
return {k: v for k, v in asdict(self).items() if v is not None}
class CodeAgent:
"""Agent for code generation, execution, review, and refactoring.
This agent provides capabilities for:
- Generating code from natural language descriptions
- Executing code in a sandboxed environment
- Reviewing code for issues and improvements
- Refactoring and fixing code
- Explaining code functionality
Example:
```python
from praisonaiagents import CodeAgent
agent = CodeAgent(name="Coder")
# Generate code
code = agent.generate("Write a function to calculate fibonacci")
# Execute code
result = agent.execute("print('Hello, World!')")
# Review code
review = agent.review(code)
```
"""
def __init__(
self,
name: str = "CodeAgent",
llm: Optional[str] = None,
code: Optional[Union[bool, Dict, CodeConfig]] = None,
instructions: Optional[str] = None,
verbose: bool = True,
**kwargs
):
"""Initialize CodeAgent.
Args:
name: Agent name
llm: LLM model (default: gpt-4o-mini)
code: Code configuration (bool, dict, or CodeConfig)
instructions: System instructions
verbose: Enable verbose output
**kwargs: Additional arguments
"""
self.name = name
self.llm = llm or "gpt-4o-mini"
self.instructions = instructions
self.verbose = verbose
# Resolve configuration (Precedence Ladder)
if code is None or code is True:
self._code_config = CodeConfig()
elif isinstance(code, dict):
self._code_config = CodeConfig(**code)
elif isinstance(code, CodeConfig):
self._code_config = code
else:
self._code_config = CodeConfig()
# Lazy-loaded dependencies
self._litellm = None
self._console = None
# Configure logging
self._logger = logging.getLogger(f"CodeAgent.{name}")
if not verbose:
self._logger.setLevel(logging.WARNING)
@property
def console(self):
"""Lazy load Rich console."""
if self._console is None:
try:
from rich.console import Console
self._console = Console()
except ImportError:
pass
return self._console
@property
def litellm(self):
"""Lazy load litellm."""
if self._litellm is None:
try:
import litellm
self._litellm = litellm
except ImportError:
raise ImportError(
"litellm required for CodeAgent. "
"Install with: pip install litellm"
)
return self._litellm
def _log(self, message: str, style: str = ""):
"""Log message if verbose."""
if self.verbose and self.console:
self.console.print(f"[{style}]{message}[/{style}]" if style else message)
# =========================================================================
# Code Generation Methods
# =========================================================================
def generate(self, prompt: str, language: str = "python", **kwargs) -> str:
"""Generate code from natural language description.
Args:
prompt: Natural language description of desired code
language: Target programming language
**kwargs: Additional arguments for LLM
Returns:
Generated code as string
"""
self._log(f"Generating {language} code...", "cyan")
system_prompt = f"""You are an expert {language} programmer.
Generate clean, well-documented, production-ready code.
Only output the code, no explanations unless asked.
Follow best practices and coding standards."""
if self.instructions:
system_prompt += f"\n\nAdditional instructions: {self.instructions}"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
]
response = self.litellm.completion(
model=kwargs.get("model", self.llm),
messages=messages,
**{k: v for k, v in kwargs.items() if k != "model"}
)
code = response.choices[0].message.content
self._log("Code generated successfully", "green")
return code
def generate_code(self, prompt: str, language: str = "python", **kwargs) -> str:
"""Alias for generate() method."""
return self.generate(prompt, language, **kwargs)
async def agenerate(self, prompt: str, language: str = "python", **kwargs) -> str:
"""Generate code asynchronously.
Args:
prompt: Natural language description
language: Target programming language
**kwargs: Additional arguments
Returns:
Generated code as string
"""
self._log(f"Generating {language} code (async)...", "cyan")
system_prompt = f"""You are an expert {language} programmer.
Generate clean, well-documented, production-ready code.
Only output the code, no explanations unless asked.
Follow best practices and coding standards."""
if self.instructions:
system_prompt += f"\n\nAdditional instructions: {self.instructions}"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
]
response = await self.litellm.acompletion(
model=kwargs.get("model", self.llm),
messages=messages,
**{k: v for k, v in kwargs.items() if k != "model"}
)
code = response.choices[0].message.content
self._log("Code generated successfully", "green")
return code
# =========================================================================
# Code Execution Methods
# =========================================================================
def execute(self, code: str, language: str = "python", **kwargs) -> Dict[str, Any]:
"""Execute code in sandboxed environment.
Args:
code: Code to execute
language: Programming language
**kwargs: Additional execution options
Returns:
Dictionary with stdout, stderr, return_code, and execution_time
"""
if language not in self._code_config.allowed_languages:
return {
"stdout": "",
"stderr": f"Language '{language}' not allowed. Allowed: {self._code_config.allowed_languages}",
"return_code": 1,
"execution_time": 0
}
self._log(f"Executing {language} code...", "cyan")
if language == "python":
return self._execute_python(code, **kwargs)
else:
return {
"stdout": "",
"stderr": f"Execution for '{language}' not implemented",
"return_code": 1,
"execution_time": 0
}
def execute_code(self, code: str, language: str = "python", **kwargs) -> Dict[str, Any]:
"""Alias for execute() method."""
return self.execute(code, language, **kwargs)
def _execute_python(self, code: str, **kwargs) -> Dict[str, Any]:
"""Execute Python code."""
import subprocess
import time
import tempfile
import os
start_time = time.time()
# Write code to temp file
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
# Execute in subprocess (basic sandboxing)
env = os.environ.copy()
env.update(self._code_config.environment)
result = subprocess.run(
["python", temp_file],
capture_output=True,
text=True,
timeout=self._code_config.timeout,
cwd=self._code_config.working_directory,
env=env
)
execution_time = time.time() - start_time
stdout = result.stdout[:self._code_config.max_output_length]
stderr = result.stderr[:self._code_config.max_output_length]
self._log(f"Execution completed in {execution_time:.2f}s", "green")
return {
"stdout": stdout,
"stderr": stderr,
"return_code": result.returncode,
"execution_time": execution_time
}
except subprocess.TimeoutExpired:
return {
"stdout": "",
"stderr": f"Execution timed out after {self._code_config.timeout}s",
"return_code": -1,
"execution_time": self._code_config.timeout
}
except Exception as e:
return {
"stdout": "",
"stderr": str(e),
"return_code": -1,
"execution_time": time.time() - start_time
}
finally:
# Clean up temp file
try:
os.unlink(temp_file)
except:
pass
async def aexecute(self, code: str, language: str = "python", **kwargs) -> Dict[str, Any]:
"""Execute code asynchronously.
Args:
code: Code to execute
language: Programming language
**kwargs: Additional options
Returns:
Execution result dictionary
"""
# Run in thread pool to avoid blocking
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, lambda: self.execute(code, language, **kwargs))
# =========================================================================
# Code Review Methods
# =========================================================================
def review(self, code: str, language: str = "python", **kwargs) -> str:
"""Review code for issues, bugs, and improvements.
Args:
code: Code to review
language: Programming language
**kwargs: Additional arguments
Returns:
Review feedback as string
"""
self._log("Reviewing code...", "cyan")
system_prompt = f"""You are an expert {language} code reviewer.
Analyze the code for:
1. Bugs and potential issues
2. Security vulnerabilities
3. Performance problems
4. Code style and best practices
5. Suggestions for improvement
Provide clear, actionable feedback."""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Review this {language} code:\n\n```{language}\n{code}\n```"}
]
response = self.litellm.completion(
model=kwargs.get("model", self.llm),
messages=messages,
**{k: v for k, v in kwargs.items() if k != "model"}
)
review = response.choices[0].message.content
self._log("Review completed", "green")
return review
# =========================================================================
# Code Explanation Methods
# =========================================================================
def explain(self, code: str, language: str = "python", **kwargs) -> str:
"""Explain what code does in plain language.
Args:
code: Code to explain
language: Programming language
**kwargs: Additional arguments
Returns:
Explanation as string
"""
self._log("Explaining code...", "cyan")
system_prompt = f"""You are an expert {language} programmer and teacher.
Explain the code in clear, simple terms that anyone can understand.
Include:
1. What the code does overall
2. Key components and their purpose
3. How data flows through the code
4. Any important patterns or techniques used"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Explain this {language} code:\n\n```{language}\n{code}\n```"}
]
response = self.litellm.completion(
model=kwargs.get("model", self.llm),
messages=messages,
**{k: v for k, v in kwargs.items() if k != "model"}
)
explanation = response.choices[0].message.content
self._log("Explanation completed", "green")
return explanation
# =========================================================================
# Code Refactoring Methods
# =========================================================================
def refactor(self, code: str, instructions: str = "", language: str = "python", **kwargs) -> str:
"""Refactor code to improve quality.
Args:
code: Code to refactor
instructions: Specific refactoring instructions
language: Programming language
**kwargs: Additional arguments
Returns:
Refactored code as string
"""
self._log("Refactoring code...", "cyan")
system_prompt = f"""You are an expert {language} programmer.
Refactor the code to improve:
1. Readability and maintainability
2. Performance where possible
3. Code organization
4. Following best practices
Only output the refactored code."""
user_prompt = f"Refactor this {language} code:\n\n```{language}\n{code}\n```"
if instructions:
user_prompt += f"\n\nSpecific instructions: {instructions}"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = self.litellm.completion(
model=kwargs.get("model", self.llm),
messages=messages,
**{k: v for k, v in kwargs.items() if k != "model"}
)
refactored = response.choices[0].message.content
self._log("Refactoring completed", "green")
return refactored
# =========================================================================
# Bug Fixing Methods
# =========================================================================
def fix(self, code: str, error: str = "", language: str = "python", **kwargs) -> str:
"""Fix bugs in code.
Args:
code: Code with bugs
error: Error message or description of the bug
language: Programming language
**kwargs: Additional arguments
Returns:
Fixed code as string
"""
self._log("Fixing code...", "cyan")
system_prompt = f"""You are an expert {language} debugger.
Fix the bugs in the code. Only output the fixed code.
Ensure the fix:
1. Addresses the root cause
2. Doesn't introduce new bugs
3. Maintains existing functionality"""
user_prompt = f"Fix this {language} code:\n\n```{language}\n{code}\n```"
if error:
user_prompt += f"\n\nError/Bug description: {error}"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = self.litellm.completion(
model=kwargs.get("model", self.llm),
messages=messages,
**{k: v for k, v in kwargs.items() if k != "model"}
)
fixed = response.choices[0].message.content
self._log("Fix completed", "green")
return fixed
@@ -0,0 +1,403 @@
"""
EmbeddingAgent - A specialized agent for generating text embeddings.
This agent provides embedding capabilities for text using AI embedding models,
with support for batch processing and similarity calculations.
Follows the Agent() class patterns with:
- Lazy loading for heavy dependencies (litellm, rich)
- Precedence Ladder for configuration resolution
- Both sync and async methods
"""
import os
import logging
import math
from dataclasses import dataclass
from typing import Optional, Dict, Any, Union, List
logger = logging.getLogger(__name__)
@dataclass
class EmbeddingConfig:
"""
Configuration for embedding settings.
Follows the Precedence Ladder pattern:
- Instance > Config > Array > Dict > String > Bool > Default
"""
dimensions: Optional[int] = None
encoding_format: str = "float" # float or base64
timeout: int = 60
api_base: Optional[str] = None
api_key: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for LiteLLM calls."""
return {
"dimensions": self.dimensions,
"encoding_format": self.encoding_format,
"timeout": self.timeout,
"api_base": self.api_base,
"api_key": self.api_key,
}
class EmbeddingAgent:
"""
A specialized agent for generating text embeddings.
Provides:
- Single text embedding
- Batch text embedding
- Similarity calculation between texts
Supported Providers:
- OpenAI: `text-embedding-3-small`, `text-embedding-3-large`, `text-embedding-ada-002`
- Azure: `azure/text-embedding-3-small`
- Cohere: `cohere/embed-english-v3.0`, `cohere/embed-multilingual-v3.0`
- Voyage: `voyage/voyage-3`, `voyage/voyage-3-lite`
- Mistral: `mistral/mistral-embed`
Example:
```python
from praisonaiagents import EmbeddingAgent
# Simple usage
agent = EmbeddingAgent()
embedding = agent.embed("Hello world")
print(f"Embedding dimension: {len(embedding)}")
# Batch embedding
embeddings = agent.embed_batch([
"First text",
"Second text",
"Third text"
])
# Calculate similarity
score = agent.similarity("Hello", "Hi there")
print(f"Similarity: {score:.2f}")
```
"""
DEFAULT_MODEL = "text-embedding-3-small"
def __init__(
self,
name: Optional[str] = None,
instructions: Optional[str] = None,
llm: Optional[str] = None,
model: Optional[str] = None,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
embedding: Optional[Union[bool, Dict, "EmbeddingConfig"]] = None,
verbose: Union[bool, int] = True,
):
"""Initialize EmbeddingAgent.
Args:
name: Agent name for identification
instructions: Optional instructions (unused for embeddings)
llm: Model name (e.g., "text-embedding-3-small")
model: Alias for llm= parameter
base_url: Custom API endpoint URL
api_key: API key for the provider
embedding: Embedding configuration. Accepts:
- bool: True enables with defaults
- dict: {"dimensions": 1536}
- EmbeddingConfig: Full configuration object
verbose: Verbosity level for output
"""
if llm is None and model is not None:
llm = model
self.name = name or "EmbeddingAgent"
self.instructions = instructions
self.llm = llm or os.getenv('PRAISONAI_EMBEDDING_MODEL', self.DEFAULT_MODEL)
self.base_url = base_url
self.api_key = api_key
self._embedding_config = self._resolve_embedding_config(embedding)
self.verbose = verbose
self._litellm = None
self._console = None
self._configure_logging(verbose)
def _resolve_embedding_config(self, embedding: Optional[Union[bool, Dict, EmbeddingConfig]]) -> EmbeddingConfig:
"""Resolve embedding parameter using Precedence Ladder."""
if embedding is None or embedding is True or embedding is False:
return EmbeddingConfig()
elif isinstance(embedding, EmbeddingConfig):
return embedding
elif isinstance(embedding, dict):
return EmbeddingConfig(**embedding)
return EmbeddingConfig()
@property
def console(self):
"""Lazily initialize Rich Console."""
if self._console is None:
from rich.console import Console
self._console = Console()
return self._console
@property
def litellm(self):
"""Lazy load litellm module when needed."""
if self._litellm is None:
try:
import litellm
litellm.telemetry = False
litellm.success_callback = []
self._litellm = litellm
except ImportError:
raise ImportError(
"litellm is required for embedding generation. "
"Please install with: pip install litellm"
)
return self._litellm
def _configure_logging(self, verbose: Union[bool, int]) -> None:
"""Configure logging levels."""
if isinstance(verbose, int) and verbose >= 10:
logging.getLogger("litellm").setLevel(logging.DEBUG)
else:
logging.getLogger("litellm").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
def _get_model_params(self, model: Optional[str] = None) -> Dict[str, Any]:
"""Build parameters for LiteLLM calls."""
params = {"model": model or self.llm}
if self.api_key:
params["api_key"] = self.api_key
if self.base_url:
params["api_base"] = self.base_url
if self._embedding_config.dimensions:
params["dimensions"] = self._embedding_config.dimensions
if self._embedding_config.encoding_format != "float":
params["encoding_format"] = self._embedding_config.encoding_format
return params
def embed(
self,
text: str,
model: Optional[str] = None,
**kwargs
) -> List[float]:
"""
Generate embedding for a single text.
Args:
text: Text to embed
model: Override model for this call
**kwargs: Additional provider-specific parameters
Returns:
List of floats representing the embedding vector
Example:
```python
agent = EmbeddingAgent()
embedding = agent.embed("Hello world")
print(f"Dimension: {len(embedding)}")
```
"""
params = self._get_model_params(model)
params["input"] = [text]
params.update(kwargs)
if self.verbose:
self.console.print(f"[cyan]Generating embedding with {params['model']}...[/cyan]")
response = self.litellm.embedding(**params)
result = response.data[0]["embedding"]
if self.verbose:
self.console.print(f"[green]✓ Embedding generated (dim={len(result)})[/green]")
return result
def embed_batch(
self,
texts: List[str],
model: Optional[str] = None,
**kwargs
) -> List[List[float]]:
"""
Generate embeddings for multiple texts.
Args:
texts: List of texts to embed
model: Override model for this call
**kwargs: Additional provider-specific parameters
Returns:
List of embedding vectors
Example:
```python
agent = EmbeddingAgent()
embeddings = agent.embed_batch(["Hello", "World"])
print(f"Generated {len(embeddings)} embeddings")
```
"""
if not texts:
return []
params = self._get_model_params(model)
params["input"] = texts
params.update(kwargs)
if self.verbose:
self.console.print(f"[cyan]Generating {len(texts)} embeddings with {params['model']}...[/cyan]")
response = self.litellm.embedding(**params)
results = [item["embedding"] for item in response.data]
if self.verbose:
self.console.print(f"[green]✓ {len(results)} embeddings generated[/green]")
return results
def similarity(
self,
text1: str,
text2: str,
model: Optional[str] = None,
**kwargs
) -> float:
"""
Calculate cosine similarity between two texts.
Args:
text1: First text
text2: Second text
model: Override model for this call
**kwargs: Additional parameters
Returns:
Cosine similarity score (0.0 to 1.0)
Example:
```python
agent = EmbeddingAgent()
score = agent.similarity("Hello", "Hi there")
print(f"Similarity: {score:.2f}")
```
"""
embeddings = self.embed_batch([text1, text2], model=model, **kwargs)
return self._cosine_similarity(embeddings[0], embeddings[1])
def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
"""Calculate cosine similarity between two vectors."""
if len(vec1) != len(vec2):
raise ValueError("Vectors must have the same dimension")
dot_product = sum(a * b for a, b in zip(vec1, vec2))
norm1 = math.sqrt(sum(a * a for a in vec1))
norm2 = math.sqrt(sum(b * b for b in vec2))
if norm1 == 0 or norm2 == 0:
return 0.0
return dot_product / (norm1 * norm2)
def find_most_similar(
self,
query: str,
candidates: List[str],
top_k: int = 5,
model: Optional[str] = None,
**kwargs
) -> List[Dict[str, Any]]:
"""
Find the most similar texts to a query.
Args:
query: Query text
candidates: List of candidate texts to compare
top_k: Number of top results to return
model: Override model for this call
**kwargs: Additional parameters
Returns:
List of dicts with 'text', 'score', and 'index' keys
Example:
```python
agent = EmbeddingAgent()
results = agent.find_most_similar(
"What is AI?",
["Artificial intelligence is...", "Machine learning...", "Deep learning..."],
top_k=2
)
for r in results:
print(f"{r['score']:.2f}: {r['text'][:50]}...")
```
"""
if not candidates:
return []
all_texts = [query] + candidates
embeddings = self.embed_batch(all_texts, model=model, **kwargs)
query_embedding = embeddings[0]
candidate_embeddings = embeddings[1:]
scores = []
for i, emb in enumerate(candidate_embeddings):
score = self._cosine_similarity(query_embedding, emb)
scores.append({
"text": candidates[i],
"score": score,
"index": i
})
scores.sort(key=lambda x: x["score"], reverse=True)
return scores[:top_k]
async def aembed(
self,
text: str,
model: Optional[str] = None,
**kwargs
) -> List[float]:
"""Async version of embed()."""
params = self._get_model_params(model)
params["input"] = [text]
params.update(kwargs)
response = await self.litellm.aembedding(**params)
return response.data[0]["embedding"]
async def aembed_batch(
self,
texts: List[str],
model: Optional[str] = None,
**kwargs
) -> List[List[float]]:
"""Async version of embed_batch()."""
if not texts:
return []
params = self._get_model_params(model)
params["input"] = texts
params.update(kwargs)
response = await self.litellm.aembedding(**params)
return [item["embedding"] for item in response.data]
async def asimilarity(
self,
text1: str,
text2: str,
model: Optional[str] = None,
**kwargs
) -> float:
"""Async version of similarity()."""
embeddings = await self.aembed_batch([text1, text2], model=model, **kwargs)
return self._cosine_similarity(embeddings[0], embeddings[1])
@@ -0,0 +1,431 @@
"""RealtimeAgent - Real-time voice conversations using OpenAI Realtime API."""
from dataclasses import dataclass, field, asdict
from typing import Any, Callable, Dict, List, Optional, Union
import asyncio
import logging
@dataclass
class RealtimeConfig:
"""Configuration for RealtimeAgent.
Attributes:
voice: Voice to use for audio output (alloy, echo, fable, onyx, nova, shimmer)
modalities: List of modalities to use (text, audio)
turn_detection: Turn detection mode (server_vad, none)
input_audio_format: Input audio format (pcm16, g711_ulaw, g711_alaw)
output_audio_format: Output audio format (pcm16, g711_ulaw, g711_alaw)
temperature: Sampling temperature (0.0 to 2.0)
max_response_output_tokens: Maximum tokens for response
instructions: System instructions for the session
"""
voice: str = "alloy"
modalities: List[str] = field(default_factory=lambda: ["text", "audio"])
turn_detection: str = "server_vad"
input_audio_format: str = "pcm16"
output_audio_format: str = "pcm16"
temperature: float = 0.8
max_response_output_tokens: Optional[int] = None
instructions: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert config to dictionary."""
return {k: v for k, v in asdict(self).items() if v is not None}
class RealtimeAgent:
"""Agent for real-time voice conversations using OpenAI Realtime API.
This agent enables bidirectional audio streaming for voice conversations,
supporting both text and audio modalities with configurable voice settings.
Example:
```python
from praisonaiagents import RealtimeAgent
agent = RealtimeAgent(
name="VoiceAssistant",
realtime={"voice": "nova"}
)
# Connect and start conversation
await agent.aconnect()
await agent.send_text("Hello!")
```
"""
def __init__(
self,
name: str = "RealtimeAgent",
llm: Optional[str] = None,
realtime: Optional[Union[bool, Dict, RealtimeConfig]] = None,
instructions: Optional[str] = None,
verbose: bool = True,
**kwargs
):
"""Initialize RealtimeAgent.
Args:
name: Agent name
llm: LLM model (default: gpt-4o-realtime-preview)
realtime: Realtime configuration (bool, dict, or RealtimeConfig)
instructions: System instructions
verbose: Enable verbose output
**kwargs: Additional arguments
"""
self.name = name
self.llm = llm or "gpt-4o-realtime-preview"
self.instructions = instructions
self.verbose = verbose
# Resolve configuration (Precedence Ladder)
if realtime is None or realtime is True:
self._realtime_config = RealtimeConfig()
elif isinstance(realtime, dict):
self._realtime_config = RealtimeConfig(**realtime)
elif isinstance(realtime, RealtimeConfig):
self._realtime_config = realtime
else:
self._realtime_config = RealtimeConfig()
# Override instructions if provided
if instructions:
self._realtime_config.instructions = instructions
# Lazy-loaded dependencies
self._client = None
self._console = None
self._ws = None
# Callbacks
self._on_message_callbacks: List[Callable] = []
self._on_audio_callbacks: List[Callable] = []
self._on_error_callbacks: List[Callable] = []
# Connection state
self._connected = False
# Configure logging
self._logger = logging.getLogger(f"RealtimeAgent.{name}")
if not verbose:
self._logger.setLevel(logging.WARNING)
@property
def console(self):
"""Lazy load Rich console."""
if self._console is None:
try:
from rich.console import Console
self._console = Console()
except ImportError:
pass
return self._console
def _log(self, message: str, style: str = ""):
"""Log message if verbose."""
if self.verbose and self.console:
self.console.print(f"[{style}]{message}[/{style}]" if style else message)
# =========================================================================
# Connection Methods
# =========================================================================
def connect(self, **kwargs) -> bool:
"""Connect to the Realtime API (sync wrapper).
Returns:
True if connected successfully
"""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
# If already in async context, can't use run_until_complete
self._log("Use aconnect() in async context", "yellow")
return False
return loop.run_until_complete(self.aconnect(**kwargs))
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(self.aconnect(**kwargs))
async def aconnect(self, **kwargs) -> bool:
"""Connect to the Realtime API asynchronously.
Returns:
True if connected successfully
"""
if self._connected:
self._log("Already connected", "yellow")
return True
try:
# Lazy import websockets
try:
import websockets
except ImportError:
raise ImportError(
"websockets required for RealtimeAgent. "
"Install with: pip install websockets"
)
# Get API key
import os
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY environment variable required")
# Build WebSocket URL
url = f"wss://api.openai.com/v1/realtime?model={self.llm}"
headers = {
"Authorization": f"Bearer {api_key}",
"OpenAI-Beta": "realtime=v1"
}
self._ws = await websockets.connect(url, extra_headers=headers)
self._connected = True
# Send session configuration
config = self._realtime_config.to_dict()
await self._send_event({
"type": "session.update",
"session": config
})
self._log(f"Connected to Realtime API with model {self.llm}", "green")
return True
except Exception as e:
self._log(f"Connection failed: {e}", "red")
for callback in self._on_error_callbacks:
callback(e)
return False
def disconnect(self) -> bool:
"""Disconnect from the Realtime API (sync wrapper).
Returns:
True if disconnected successfully
"""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
self._log("Use adisconnect() in async context", "yellow")
return False
return loop.run_until_complete(self.adisconnect())
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(self.adisconnect())
async def adisconnect(self) -> bool:
"""Disconnect from the Realtime API asynchronously.
Returns:
True if disconnected successfully
"""
if not self._connected or not self._ws:
return True
try:
await self._ws.close()
self._connected = False
self._ws = None
self._log("Disconnected from Realtime API", "yellow")
return True
except Exception as e:
self._log(f"Disconnect error: {e}", "red")
return False
# =========================================================================
# Send Methods
# =========================================================================
async def _send_event(self, event: Dict[str, Any]) -> bool:
"""Send an event to the Realtime API.
Args:
event: Event dictionary to send
Returns:
True if sent successfully
"""
if not self._connected or not self._ws:
self._log("Not connected", "red")
return False
try:
import json
await self._ws.send(json.dumps(event))
return True
except Exception as e:
self._log(f"Send error: {e}", "red")
return False
def send_text(self, text: str) -> bool:
"""Send text message (sync wrapper).
Args:
text: Text message to send
Returns:
True if sent successfully
"""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
self._log("Use async send in async context", "yellow")
return False
return loop.run_until_complete(self.asend_text(text))
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(self.asend_text(text))
async def asend_text(self, text: str) -> bool:
"""Send text message asynchronously.
Args:
text: Text message to send
Returns:
True if sent successfully
"""
event = {
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": text}]
}
}
if await self._send_event(event):
# Request response
return await self._send_event({"type": "response.create"})
return False
def send_audio(self, audio_data: bytes) -> bool:
"""Send audio data (sync wrapper).
Args:
audio_data: Raw audio bytes (PCM16 format)
Returns:
True if sent successfully
"""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
return False
return loop.run_until_complete(self.asend_audio(audio_data))
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(self.asend_audio(audio_data))
async def asend_audio(self, audio_data: bytes) -> bool:
"""Send audio data asynchronously.
Args:
audio_data: Raw audio bytes (PCM16 format)
Returns:
True if sent successfully
"""
import base64
encoded = base64.b64encode(audio_data).decode("utf-8")
event = {
"type": "input_audio_buffer.append",
"audio": encoded
}
return await self._send_event(event)
# =========================================================================
# Callback Registration
# =========================================================================
def on_message(self, callback: Callable[[str], None]) -> None:
"""Register callback for text messages.
Args:
callback: Function to call with message text
"""
self._on_message_callbacks.append(callback)
def on_audio(self, callback: Callable[[bytes], None]) -> None:
"""Register callback for audio data.
Args:
callback: Function to call with audio bytes
"""
self._on_audio_callbacks.append(callback)
def on_error(self, callback: Callable[[Exception], None]) -> None:
"""Register callback for errors.
Args:
callback: Function to call with exception
"""
self._on_error_callbacks.append(callback)
# =========================================================================
# Receive Methods
# =========================================================================
async def receive_loop(self) -> None:
"""Main receive loop for processing incoming events."""
if not self._connected or not self._ws:
return
import json
import base64
try:
async for message in self._ws:
try:
event = json.loads(message)
event_type = event.get("type", "")
# Handle text responses
if event_type == "response.text.delta":
text = event.get("delta", "")
for callback in self._on_message_callbacks:
callback(text)
# Handle audio responses
elif event_type == "response.audio.delta":
audio_b64 = event.get("delta", "")
if audio_b64:
audio_bytes = base64.b64decode(audio_b64)
for callback in self._on_audio_callbacks:
callback(audio_bytes)
# Handle errors
elif event_type == "error":
error = event.get("error", {})
self._log(f"API Error: {error}", "red")
for callback in self._on_error_callbacks:
callback(Exception(str(error)))
except json.JSONDecodeError:
pass
except Exception as e:
self._log(f"Receive loop error: {e}", "red")
for callback in self._on_error_callbacks:
callback(e)
# =========================================================================
# Context Manager
# =========================================================================
async def __aenter__(self):
"""Async context manager entry."""
await self.aconnect()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit."""
await self.adisconnect()
@@ -0,0 +1,448 @@
"""
VisionAgent - A specialized agent for image analysis and understanding.
This agent provides vision capabilities for analyzing, describing, and
extracting information from images using AI vision models.
Follows the Agent() class patterns with:
- Lazy loading for heavy dependencies (litellm, rich)
- Precedence Ladder for configuration resolution
- Both sync and async methods
"""
import os
import logging
from dataclasses import dataclass
from typing import Optional, Dict, Any, Union, List
from pathlib import Path
logger = logging.getLogger(__name__)
@dataclass
class VisionConfig:
"""
Configuration for vision processing settings.
Follows the Precedence Ladder pattern:
- Instance > Config > Array > Dict > String > Bool > Default
"""
detail: str = "auto" # low, high, auto
max_tokens: int = 4096
timeout: int = 60
api_base: Optional[str] = None
api_key: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for LiteLLM calls."""
return {
"detail": self.detail,
"max_tokens": self.max_tokens,
"timeout": self.timeout,
"api_base": self.api_base,
"api_key": self.api_key,
}
class VisionAgent:
"""
A specialized agent for image analysis and understanding.
Provides:
- Image analysis and description
- Multi-image comparison
- Text extraction from images
Supported Providers:
- OpenAI: `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo`
- Anthropic: `claude-3-5-sonnet-20241022`, `claude-3-opus-20240229`
- Google: `gemini/gemini-1.5-pro`, `gemini/gemini-1.5-flash`
Example:
```python
from praisonaiagents import VisionAgent
# Simple usage
agent = VisionAgent()
description = agent.describe("https://example.com/image.jpg")
print(description)
# Analyze with custom prompt
result = agent.analyze(
"https://example.com/chart.png",
prompt="What data does this chart show?"
)
# Compare images
comparison = agent.compare([
"image1.jpg",
"image2.jpg"
])
# Extract text
text = agent.extract_text("document.png")
```
"""
DEFAULT_MODEL = "gpt-4o"
def __init__(
self,
name: Optional[str] = None,
instructions: Optional[str] = None,
llm: Optional[str] = None,
model: Optional[str] = None,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
vision: Optional[Union[bool, Dict, "VisionConfig"]] = None,
verbose: Union[bool, int] = True,
):
"""Initialize VisionAgent.
Args:
name: Agent name for identification
instructions: Optional instructions for the agent
llm: Model name (e.g., "gpt-4o", "claude-3-5-sonnet-20241022")
model: Alias for llm= parameter
base_url: Custom API endpoint URL
api_key: API key for the provider
vision: Vision configuration. Accepts:
- bool: True enables with defaults
- dict: {"detail": "high", "max_tokens": 8192}
- VisionConfig: Full configuration object
verbose: Verbosity level for output
"""
if llm is None and model is not None:
llm = model
self.name = name or "VisionAgent"
self.instructions = instructions
self.llm = llm or os.getenv('PRAISONAI_VISION_MODEL', self.DEFAULT_MODEL)
self.base_url = base_url
self.api_key = api_key
self._vision_config = self._resolve_vision_config(vision)
self.verbose = verbose
self._litellm = None
self._console = None
self._configure_logging(verbose)
def _resolve_vision_config(self, vision: Optional[Union[bool, Dict, VisionConfig]]) -> VisionConfig:
"""Resolve vision parameter using Precedence Ladder."""
if vision is None or vision is True or vision is False:
return VisionConfig()
elif isinstance(vision, VisionConfig):
return vision
elif isinstance(vision, dict):
return VisionConfig(**vision)
return VisionConfig()
@property
def console(self):
"""Lazily initialize Rich Console."""
if self._console is None:
from rich.console import Console
self._console = Console()
return self._console
@property
def litellm(self):
"""Lazy load litellm module when needed."""
if self._litellm is None:
try:
import litellm
litellm.telemetry = False
litellm.success_callback = []
self._litellm = litellm
except ImportError:
raise ImportError(
"litellm is required for vision processing. "
"Please install with: pip install litellm"
)
return self._litellm
def _configure_logging(self, verbose: Union[bool, int]) -> None:
"""Configure logging levels."""
if isinstance(verbose, int) and verbose >= 10:
logging.getLogger("litellm").setLevel(logging.DEBUG)
else:
logging.getLogger("litellm").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
def _get_model_params(self, model: Optional[str] = None) -> Dict[str, Any]:
"""Build parameters for LiteLLM calls."""
params = {"model": model or self.llm}
if self.api_key:
params["api_key"] = self.api_key
if self.base_url:
params["api_base"] = self.base_url
return params
def _build_image_content(self, image: str, detail: Optional[str] = None) -> Dict[str, Any]:
"""Build image content for vision API."""
detail = detail or self._vision_config.detail
if image.startswith(('http://', 'https://')):
return {
"type": "image_url",
"image_url": {"url": image, "detail": detail}
}
else:
import base64
path = Path(image)
if path.exists():
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode("utf-8")
ext = path.suffix.lower().lstrip('.')
mime = f"image/{ext}" if ext in ('png', 'jpg', 'jpeg', 'gif', 'webp') else "image/png"
return {
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{data}", "detail": detail}
}
else:
return {
"type": "image_url",
"image_url": {"url": image, "detail": detail}
}
def analyze(
self,
image: str,
prompt: Optional[str] = None,
detail: Optional[str] = None,
model: Optional[str] = None,
**kwargs
) -> str:
"""
Analyze an image and return analysis.
Args:
image: Image URL or local file path
prompt: Custom prompt for analysis (default: general analysis)
detail: Detail level (low, high, auto)
model: Override model for this call
**kwargs: Additional provider-specific parameters
Returns:
Analysis text
Example:
```python
agent = VisionAgent()
result = agent.analyze(
"https://example.com/chart.png",
prompt="What trends does this chart show?"
)
```
"""
prompt = prompt or "Analyze this image in detail. Describe what you see, including objects, colors, composition, and any text or data present."
params = self._get_model_params(model)
params["messages"] = [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
self._build_image_content(image, detail)
]
}
]
params["max_tokens"] = kwargs.pop("max_tokens", self._vision_config.max_tokens)
params.update(kwargs)
if self.verbose:
self.console.print(f"[cyan]Analyzing image with {params['model']}...[/cyan]")
response = self.litellm.completion(**params)
result = response.choices[0].message.content
if self.verbose:
self.console.print("[green]✓ Analysis complete[/green]")
return result
def describe(
self,
image: str,
detail: Optional[str] = None,
model: Optional[str] = None,
**kwargs
) -> str:
"""
Generate a detailed description of an image.
Args:
image: Image URL or local file path
detail: Detail level (low, high, auto)
model: Override model for this call
**kwargs: Additional parameters
Returns:
Detailed description text
Example:
```python
agent = VisionAgent()
description = agent.describe("photo.jpg")
print(description)
```
"""
prompt = """Provide a comprehensive description of this image. Include:
1. Main subject and composition
2. Colors, lighting, and mood
3. Background and setting
4. Any text, symbols, or notable details
5. Overall impression and context"""
return self.analyze(image, prompt=prompt, detail=detail, model=model, **kwargs)
def compare(
self,
images: List[str],
prompt: Optional[str] = None,
detail: Optional[str] = None,
model: Optional[str] = None,
**kwargs
) -> str:
"""
Compare multiple images.
Args:
images: List of image URLs or file paths
prompt: Custom comparison prompt
detail: Detail level (low, high, auto)
model: Override model for this call
**kwargs: Additional parameters
Returns:
Comparison analysis text
Example:
```python
agent = VisionAgent()
comparison = agent.compare([
"before.jpg",
"after.jpg"
], prompt="What changed between these images?")
```
"""
if len(images) < 2:
raise ValueError("At least 2 images are required for comparison")
prompt = prompt or f"Compare these {len(images)} images. Describe their similarities and differences in detail."
content = [{"type": "text", "text": prompt}]
for i, img in enumerate(images):
content.append(self._build_image_content(img, detail))
params = self._get_model_params(model)
params["messages"] = [{"role": "user", "content": content}]
params["max_tokens"] = kwargs.pop("max_tokens", self._vision_config.max_tokens)
params.update(kwargs)
if self.verbose:
self.console.print(f"[cyan]Comparing {len(images)} images with {params['model']}...[/cyan]")
response = self.litellm.completion(**params)
result = response.choices[0].message.content
if self.verbose:
self.console.print("[green]✓ Comparison complete[/green]")
return result
def extract_text(
self,
image: str,
detail: Optional[str] = None,
model: Optional[str] = None,
**kwargs
) -> str:
"""
Extract text from an image (OCR-like functionality).
Args:
image: Image URL or local file path
detail: Detail level (low, high, auto) - recommend "high" for text
model: Override model for this call
**kwargs: Additional parameters
Returns:
Extracted text
Example:
```python
agent = VisionAgent()
text = agent.extract_text("document.png", detail="high")
print(text)
```
"""
detail = detail or "high"
prompt = """Extract ALL text visible in this image.
Preserve the original formatting and structure as much as possible.
Include headers, paragraphs, lists, tables, captions, and any other text.
If text is unclear, indicate with [unclear].
Return ONLY the extracted text, no additional commentary."""
return self.analyze(image, prompt=prompt, detail=detail, model=model, **kwargs)
async def aanalyze(
self,
image: str,
prompt: Optional[str] = None,
detail: Optional[str] = None,
model: Optional[str] = None,
**kwargs
) -> str:
"""Async version of analyze()."""
prompt = prompt or "Analyze this image in detail."
params = self._get_model_params(model)
params["messages"] = [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
self._build_image_content(image, detail)
]
}
]
params["max_tokens"] = kwargs.pop("max_tokens", self._vision_config.max_tokens)
params.update(kwargs)
response = await self.litellm.acompletion(**params)
return response.choices[0].message.content
async def adescribe(self, image: str, **kwargs) -> str:
"""Async version of describe()."""
prompt = "Provide a comprehensive description of this image."
return await self.aanalyze(image, prompt=prompt, **kwargs)
async def acompare(self, images: List[str], **kwargs) -> str:
"""Async version of compare()."""
if len(images) < 2:
raise ValueError("At least 2 images are required for comparison")
prompt = kwargs.pop("prompt", None) or f"Compare these {len(images)} images."
detail = kwargs.pop("detail", None)
model = kwargs.pop("model", None)
content = [{"type": "text", "text": prompt}]
for img in images:
content.append(self._build_image_content(img, detail))
params = self._get_model_params(model)
params["messages"] = [{"role": "user", "content": content}]
params["max_tokens"] = kwargs.pop("max_tokens", self._vision_config.max_tokens)
params.update(kwargs)
response = await self.litellm.acompletion(**params)
return response.choices[0].message.content
async def aextract_text(self, image: str, **kwargs) -> str:
"""Async version of extract_text()."""
kwargs.setdefault("detail", "high")
prompt = "Extract ALL text visible in this image. Preserve formatting."
return await self.aanalyze(image, prompt=prompt, **kwargs)
@@ -1,6 +1,18 @@
"""Agents module for managing multiple AI agents"""
from .agents import Agents, PraisonAIAgents
"""Agents module for managing multiple AI agents.
AgentManager is the primary class for multi-agent coordination.
Agents is a deprecated alias for backward compatibility.
"""
from .agents import AgentManager, Agents, PraisonAIAgents
from .autoagents import AutoAgents
from .auto_rag_agent import AutoRagAgent, AutoRagConfig, RetrievalPolicy
from .protocols import MergeStrategyProtocol, FirstWinsMerge, ConcatMerge, DictMerge
__all__ = ['Agents', 'PraisonAIAgents', 'AutoAgents', 'AutoRagAgent', 'AutoRagConfig', 'RetrievalPolicy']
__all__ = [
'AgentManager', # Primary class (v0.14.16+)
'Agents', # Deprecated alias for AgentManager
'PraisonAIAgents', # Deprecated alias for AgentManager
'AutoAgents',
'AutoRagAgent', 'AutoRagConfig', 'RetrievalPolicy',
'MergeStrategyProtocol', 'FirstWinsMerge', 'ConcatMerge', 'DictMerge',
]
@@ -169,7 +169,34 @@ def process_task_context(context_item, verbose=0, user_id=None):
else:
return str(context_item) # Fallback for unknown types
class Agents:
class AgentManager:
"""
Multi-agent coordinator that manages and delegates work to multiple agents.
AgentManager orchestrates the execution of tasks across multiple Agent instances,
supporting sequential, parallel, and hierarchical execution patterns.
Example:
from praisonaiagents import Agent, AgentManager, Task
researcher = Agent(role="Researcher", instructions="Research topics")
writer = Agent(role="Writer", instructions="Write content")
task1 = Task(description="Research AI trends", agent=researcher)
task2 = Task(description="Write article", agent=writer)
manager = AgentManager(
agents=[researcher, writer],
tasks=[task1, task2],
process="sequential"
)
result = manager.start()
Note:
The class was renamed from `Agents` to `AgentManager` in v0.14.16.
`Agents` remains as a deprecated alias for backward compatibility.
"""
def __init__(
self,
agents,
@@ -194,7 +221,7 @@ class Agents:
caching: Optional[Any] = None, # Union[bool, CachingConfig] - caching
):
"""
Initialize PraisonAIAgents with consolidated feature parameters.
Initialize AgentManager with consolidated feature parameters.
Args:
agents: List of Agent instances
@@ -798,8 +825,10 @@ Context:
tasks_to_run = []
# Run sync task in an executor to avoid blocking the event loop
# Use copy_context_to_callable to propagate contextvars (needed for trace emission)
from ..trace.context_events import copy_context_to_callable
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, self.run_task, task_id)
await loop.run_in_executor(None, copy_context_to_callable(lambda tid=task_id: self.run_task(tid)))
if tasks_to_run:
await asyncio.gather(*tasks_to_run)
@@ -822,8 +851,10 @@ Context:
# Before running a sync task, execute all pending async tasks
await flush_async_tasks()
# Run sync task in an executor to avoid blocking the event loop
# Use copy_context_to_callable to propagate contextvars (needed for trace emission)
from ..trace.context_events import copy_context_to_callable
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, self.run_task, task_id)
await loop.run_in_executor(None, copy_context_to_callable(lambda tid=task_id: self.run_task(tid)))
# Execute any remaining async tasks at the end
await flush_async_tasks()
@@ -835,8 +866,10 @@ Context:
await self.arun_task(task_id)
else:
# Run sync task in an executor to avoid blocking the event loop
# Use copy_context_to_callable to propagate contextvars (needed for trace emission)
from ..trace.context_events import copy_context_to_callable
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, self.run_task, task_id)
await loop.run_in_executor(None, copy_context_to_callable(lambda tid=task_id: self.run_task(tid)))
async def astart(self, content=None, return_dict=False, **kwargs):
"""Async version of start method.
@@ -1678,9 +1711,11 @@ Context:
response = await agent_instance.achat(current_input, task_name=None, task_description=None, task_id=None)
else:
# Run sync function in a thread to avoid blocking
# Use copy_context_to_callable to propagate contextvars (needed for trace emission)
from ..trace.context_events import copy_context_to_callable
loop = asyncio.get_running_loop()
# Correctly pass current_input to the lambda for closure
response = await loop.run_in_executor(None, lambda ci=current_input: agent_instance.chat(ci))
response = await loop.run_in_executor(None, copy_context_to_callable(lambda ci=current_input: agent_instance.chat(ci)))
# Store this agent's result
results.append({
@@ -1752,8 +1787,10 @@ Context:
if asyncio.iscoroutinefunction(agent.chat):
response = await agent.achat(query)
else:
# Use copy_context_to_callable to propagate contextvars (needed for trace emission)
from ..trace.context_events import copy_context_to_callable
loop = asyncio.get_running_loop()
response = await loop.run_in_executor(None, lambda q=query: agent.chat(q))
response = await loop.run_in_executor(None, copy_context_to_callable(lambda q=query: agent.chat(q)))
return {
"agent": agent.display_name,
@@ -1888,8 +1925,10 @@ Context:
if hasattr(agent_instance, 'achat') and asyncio.iscoroutinefunction(agent_instance.achat):
response = await agent_instance.achat(current_input, tools=agent_instance.tools, task_name=None, task_description=None, task_id=None)
elif hasattr(agent_instance, 'chat'): # Fallback to sync chat if achat not suitable
# Use copy_context_to_callable to propagate contextvars (needed for trace emission)
from ..trace.context_events import copy_context_to_callable
loop = asyncio.get_running_loop()
response = await loop.run_in_executor(None, lambda ci=current_input: agent_instance.chat(ci, tools=agent_instance.tools))
response = await loop.run_in_executor(None, copy_context_to_callable(lambda ci=current_input: agent_instance.chat(ci, tools=agent_instance.tools)))
else:
logging.warning(f"Agent {agent_instance.display_name} has no suitable chat or achat method.")
response = f"Error: Agent {agent_instance.display_name} has no callable chat method."
@@ -2361,6 +2400,7 @@ Context:
# Keep plan tasks for results but note original tasks are preserved in _plan_tasks
# Backward compatibility alias - Agents points to Agents
Agents = Agents
PraisonAIAgents = Agents
# Backward compatibility aliases
# Agents is deprecated in favor of AgentManager (v0.14.16+)
Agents = AgentManager
PraisonAIAgents = AgentManager
@@ -261,6 +261,23 @@ def register_profile(profile: AgentProfile) -> None:
BUILTIN_PROFILES[profile.name] = profile
# Simplified alias
add_profile = register_profile
def has_profile(name: str) -> bool:
"""Check if a profile exists."""
return name in BUILTIN_PROFILES
def remove_profile(name: str) -> bool:
"""Remove a profile by name. Returns True if found and removed."""
if name in BUILTIN_PROFILES:
del BUILTIN_PROFILES[name]
return True
return False
def get_profiles_by_mode(mode: AgentMode) -> List[AgentProfile]:
"""Get all profiles that can be used in a specific mode."""
return [
@@ -0,0 +1,115 @@
"""Agent protocols for extensibility."""
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
@runtime_checkable
class MergeStrategyProtocol(Protocol):
"""Protocol for merging outputs from multiple agents.
Used in parallel execution workflows where multiple agents
produce outputs that need to be combined.
Example:
class CustomMerge:
def merge(self, outputs, context=None):
return max(outputs, key=len) # Return longest output
# Check protocol compliance
assert isinstance(CustomMerge(), MergeStrategyProtocol)
"""
def merge(
self,
outputs: List[Any],
context: Optional[Dict[str, Any]] = None
) -> Any:
"""Merge multiple agent outputs into one.
Args:
outputs: List of outputs from agents
context: Optional context including agent names, etc.
Returns:
Merged output
"""
...
class FirstWinsMerge:
"""Returns the first non-None output."""
def merge(
self,
outputs: List[Any],
context: Optional[Dict[str, Any]] = None
) -> Any:
for output in outputs:
if output is not None:
return output
return None
class ConcatMerge:
"""Concatenates all string outputs with a separator."""
def __init__(self, separator: str = "\n\n"):
self.separator = separator
def merge(
self,
outputs: List[Any],
context: Optional[Dict[str, Any]] = None
) -> str:
string_outputs = [str(o) for o in outputs if o is not None]
return self.separator.join(string_outputs)
class DictMerge:
"""Merges dictionary outputs by combining keys.
Useful when agents return structured data that needs to be combined.
Later values override earlier ones for duplicate keys.
Example:
merge = DictMerge()
result = merge.merge([
{"name": "Alice", "score": 10},
{"name": "Bob", "level": 5}
])
# Result: {"name": "Bob", "score": 10, "level": 5}
"""
def __init__(self, deep: bool = False):
"""Initialize DictMerge.
Args:
deep: If True, recursively merge nested dicts
"""
self.deep = deep
def merge(
self,
outputs: List[Any],
context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
result: Dict[str, Any] = {}
for output in outputs:
if output is None:
continue
if not isinstance(output, dict):
continue
if self.deep:
result = self._deep_merge(result, output)
else:
result.update(output)
return result
def _deep_merge(self, base: Dict, update: Dict) -> Dict:
"""Recursively merge dictionaries."""
result = base.copy()
for key, value in update.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = self._deep_merge(result[key], value)
else:
result[key] = value
return result
@@ -0,0 +1,30 @@
"""
App module for production deployment of AI agents.
This module provides the protocol and configuration for AgentApp,
a production platform for deploying agents as web services.
The protocol is defined here in the core SDK (lightweight).
The implementation lives in the praisonai wrapper (heavy deps like FastAPI).
Example:
# Protocol is importable from core SDK
from praisonaiagents import AgentAppProtocol, AgentAppConfig
# Implementation is in wrapper
from praisonai import AgentApp
app = AgentApp(
name="My App",
agents=[researcher, writer],
)
app.serve(port=8000)
"""
from .protocols import AgentAppProtocol
from .config import AgentAppConfig
__all__ = [
'AgentAppProtocol',
'AgentAppConfig',
]
@@ -0,0 +1,55 @@
"""
AgentApp configuration.
This module defines the configuration dataclass for AgentApp.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class AgentAppConfig:
"""
Configuration for AgentApp.
This dataclass holds all configuration options for an AgentApp instance.
It follows the PraisonAI principle of sensible defaults with explicit overrides.
Attributes:
name: Name of the application (default: "PraisonAI App")
host: Host address to bind to (default: "0.0.0.0")
port: Port number to listen on (default: 8000)
reload: Enable auto-reload for development (default: False)
cors_origins: List of allowed CORS origins (default: ["*"])
api_prefix: API route prefix (default: "/api")
docs_url: URL for API documentation (default: "/docs")
openapi_url: URL for OpenAPI schema (default: "/openapi.json")
debug: Enable debug mode (default: False)
log_level: Logging level (default: "info")
workers: Number of worker processes (default: 1)
timeout: Request timeout in seconds (default: 60)
metadata: Additional metadata for the app
Example:
config = AgentAppConfig(
name="My AI App",
port=9000,
reload=True,
debug=True
)
"""
name: str = "PraisonAI App"
host: str = "0.0.0.0"
port: int = 8000
reload: bool = False
cors_origins: List[str] = field(default_factory=lambda: ["*"])
api_prefix: str = "/api"
docs_url: str = "/docs"
openapi_url: str = "/openapi.json"
debug: bool = False
log_level: str = "info"
workers: int = 1
timeout: int = 60
metadata: Dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,76 @@
"""
AgentApp protocol definitions.
This module defines the protocol (interface) for AgentApp implementations.
The protocol is lightweight and lives in the core SDK.
Implementations live in the praisonai wrapper.
"""
from typing import TYPE_CHECKING, Any, List, Optional, Protocol, runtime_checkable
if TYPE_CHECKING:
from ..agent.agent import Agent
from ..agents.agents import AgentManager
from ..workflows.workflows import Workflow
@runtime_checkable
class AgentAppProtocol(Protocol):
"""
Protocol for AgentApp implementations.
AgentApp is a production platform for deploying agents as web services.
It wraps agents, managers, and workflows into a unified API server.
Implementations should:
- Provide FastAPI-based HTTP endpoints
- Support WebSocket connections for real-time communication
- Handle agent lifecycle management
- Provide health checks and monitoring endpoints
Example implementation (in praisonai wrapper):
class AgentApp:
def __init__(
self,
name: str = "PraisonAI App",
agents: List[Agent] = None,
managers: List[AgentManager] = None,
workflows: List[Workflow] = None,
):
...
def serve(self, host: str = "0.0.0.0", port: int = 8000):
'''Start the FastAPI server.'''
...
def get_app(self):
'''Get the FastAPI app instance for custom mounting.'''
...
"""
def serve(
self,
host: Optional[str] = None,
port: Optional[int] = None,
reload: bool = False,
**kwargs: Any
) -> None:
"""
Start the AgentApp server.
Args:
host: Host address to bind to (default: "0.0.0.0")
port: Port number to listen on (default: 8000)
reload: Enable auto-reload for development (default: False)
**kwargs: Additional server configuration
"""
...
def get_app(self) -> Any:
"""
Get the underlying web application instance.
Returns:
The FastAPI application instance for custom mounting or configuration.
"""
...
@@ -11,6 +11,7 @@ import json
import os
from typing import Dict, Set, Optional, Callable, Any, Literal, List
from functools import wraps
import contextvars
from contextvars import ContextVar
from dataclasses import dataclass, field
@@ -56,6 +57,9 @@ approval_callback: Optional[Callable] = None
# Context variable to track if we're in an approved execution context
_approved_context: ContextVar[Set[str]] = ContextVar('approved_context', default=set())
# Context variable for YAML-defined auto-approved tools (from agents.yaml approve field)
_yaml_approved_tools: ContextVar[Set[str]] = ContextVar('yaml_approved_tools', default=set())
# Global permission allowlist
_permission_allowlist: Optional["PermissionAllowlist"] = None
@@ -257,6 +261,41 @@ def is_already_approved(tool_name: str) -> bool:
approved = _approved_context.get(set())
return tool_name in approved
def is_yaml_approved(tool_name: str) -> bool:
"""Check if a tool is auto-approved via YAML approve field."""
try:
yaml_approved = _yaml_approved_tools.get()
return tool_name in yaml_approved
except LookupError:
return False
def is_env_auto_approve() -> bool:
"""Check if PRAISONAI_AUTO_APPROVE environment variable is set."""
return os.environ.get("PRAISONAI_AUTO_APPROVE", "").lower() in ("true", "1", "yes")
def set_yaml_approved_tools(tools: List[str]) -> contextvars.Token:
"""
Set the list of YAML-approved tools for the current context.
This is called by the workflow runner when parsing agents.yaml with approve field.
Args:
tools: List of tool names to auto-approve
Returns:
Token that can be used to reset the context
"""
return _yaml_approved_tools.set(set(tools))
def reset_yaml_approved_tools(token: contextvars.Token) -> None:
"""Reset YAML-approved tools to previous state."""
_yaml_approved_tools.reset(token)
def clear_approval_context():
"""Clear the approval context."""
_approved_context.set(set())
@@ -278,6 +317,16 @@ def require_approval(risk_level: RiskLevel = "high"):
if is_already_approved(tool_name):
return func(*args, **kwargs)
# Skip approval if tool is auto-approved via YAML approve field (primary)
if is_yaml_approved(tool_name):
mark_approved(tool_name)
return func(*args, **kwargs)
# Skip approval if PRAISONAI_AUTO_APPROVE env var is set (secondary)
if is_env_auto_approve():
mark_approved(tool_name)
return func(*args, **kwargs)
# Request approval before executing the function
try:
# Try to check if we're in an async context
@@ -310,6 +359,16 @@ def require_approval(risk_level: RiskLevel = "high"):
if is_already_approved(tool_name):
return await func(*args, **kwargs)
# Skip approval if tool is auto-approved via YAML approve field (primary)
if is_yaml_approved(tool_name):
mark_approved(tool_name)
return await func(*args, **kwargs)
# Skip approval if PRAISONAI_AUTO_APPROVE env var is set (secondary)
if is_env_auto_approve():
mark_approved(tool_name)
return await func(*args, **kwargs)
# Request approval before executing the function
decision = await request_approval(tool_name, kwargs)
if not decision.approved:
@@ -0,0 +1,33 @@
"""
Bot Protocols for PraisonAI Agents.
Provides protocols and base classes for building messaging bot implementations
(Telegram, Discord, Slack, etc.) that connect agents to messaging platforms.
This module contains only protocols and lightweight utilities.
Heavy implementations live in the praisonai wrapper package.
"""
from .protocols import (
BotProtocol,
BotMessageProtocol,
BotUserProtocol,
BotChannelProtocol,
BotMessage,
BotUser,
BotChannel,
MessageType,
)
from .config import BotConfig
__all__ = [
"BotProtocol",
"BotMessageProtocol",
"BotUserProtocol",
"BotChannelProtocol",
"BotMessage",
"BotUser",
"BotChannel",
"BotConfig",
"MessageType",
]
@@ -0,0 +1,91 @@
"""
Bot Configuration for PraisonAI Agents.
Provides configuration dataclasses for bot settings.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class BotConfig:
"""Configuration for messaging bots.
Attributes:
token: Bot authentication token
webhook_url: URL for webhook mode (optional)
webhook_path: Path for webhook endpoint
polling_interval: Interval for polling mode (seconds)
allowed_users: List of allowed user IDs (empty = all allowed)
allowed_channels: List of allowed channel IDs (empty = all allowed)
command_prefix: Prefix for commands (default: "/")
mention_required: Whether bot mention is required in groups
typing_indicator: Whether to show typing indicator
max_message_length: Maximum message length before splitting
retry_attempts: Number of retry attempts for failed operations
timeout: Request timeout in seconds
metadata: Additional platform-specific configuration
"""
token: str = ""
webhook_url: Optional[str] = None
webhook_path: str = "/webhook"
polling_interval: float = 1.0
allowed_users: List[str] = field(default_factory=list)
allowed_channels: List[str] = field(default_factory=list)
command_prefix: str = "/"
mention_required: bool = True
typing_indicator: bool = True
max_message_length: int = 4096
retry_attempts: int = 3
timeout: int = 30
reply_in_thread: bool = False # Default to inline, set True for thread replies
thread_threshold: int = 500 # Auto-thread responses longer than this (0 = disabled)
# Default tools enabled for all bots
default_tools: List[str] = field(default_factory=lambda: ["execute_command", "search_web"])
# Auto-approve tool calls (useful for trusted environments)
auto_approve_tools: bool = False # If True, skip confirmation for tool execution
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary (hides sensitive data)."""
return {
"token": "***" if self.token else None,
"webhook_url": self.webhook_url,
"webhook_path": self.webhook_path,
"polling_interval": self.polling_interval,
"allowed_users": self.allowed_users,
"allowed_channels": self.allowed_channels,
"command_prefix": self.command_prefix,
"mention_required": self.mention_required,
"typing_indicator": self.typing_indicator,
"max_message_length": self.max_message_length,
"retry_attempts": self.retry_attempts,
"timeout": self.timeout,
"reply_in_thread": self.reply_in_thread,
"thread_threshold": self.thread_threshold,
"default_tools": self.default_tools,
"auto_approve_tools": self.auto_approve_tools,
"metadata": self.metadata,
}
@property
def is_webhook_mode(self) -> bool:
"""Whether bot is configured for webhook mode."""
return bool(self.webhook_url)
def is_user_allowed(self, user_id: str) -> bool:
"""Check if a user is allowed to interact with the bot."""
if not self.allowed_users:
return True
return user_id in self.allowed_users
def is_channel_allowed(self, channel_id: str) -> bool:
"""Check if a channel is allowed for bot interaction."""
if not self.allowed_channels:
return True
return channel_id in self.allowed_channels
@@ -0,0 +1,453 @@
"""
Bot Protocols for PraisonAI Agents.
Defines the interfaces for messaging bot implementations.
These protocols enable agents to communicate through messaging platforms
like Telegram, Discord, Slack, etc.
All implementations should live in the praisonai wrapper package.
"""
from __future__ import annotations
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Protocol,
Union,
runtime_checkable,
)
if TYPE_CHECKING:
from ..agent import Agent
class MessageType(str, Enum):
"""Types of bot messages."""
TEXT = "text"
IMAGE = "image"
AUDIO = "audio"
VIDEO = "video"
FILE = "file"
LOCATION = "location"
STICKER = "sticker"
COMMAND = "command"
CALLBACK = "callback"
REACTION = "reaction"
REPLY = "reply"
EDIT = "edit"
DELETE = "delete"
@dataclass
class BotUser:
"""Represents a user in a messaging platform.
Attributes:
user_id: Platform-specific user identifier
username: User's username (if available)
display_name: User's display name
is_bot: Whether this user is a bot
metadata: Additional platform-specific metadata
"""
user_id: str
username: Optional[str] = None
display_name: Optional[str] = None
is_bot: bool = False
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"user_id": self.user_id,
"username": self.username,
"display_name": self.display_name,
"is_bot": self.is_bot,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BotUser":
"""Create from dictionary."""
return cls(
user_id=data.get("user_id", "unknown"),
username=data.get("username"),
display_name=data.get("display_name"),
is_bot=data.get("is_bot", False),
metadata=data.get("metadata", {}),
)
@dataclass
class BotChannel:
"""Represents a channel/chat in a messaging platform.
Attributes:
channel_id: Platform-specific channel identifier
name: Channel name (if available)
channel_type: Type of channel (dm, group, channel, thread)
metadata: Additional platform-specific metadata
"""
channel_id: str
name: Optional[str] = None
channel_type: str = "dm" # dm, group, channel, thread
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"channel_id": self.channel_id,
"name": self.name,
"channel_type": self.channel_type,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BotChannel":
"""Create from dictionary."""
return cls(
channel_id=data.get("channel_id", "unknown"),
name=data.get("name"),
channel_type=data.get("channel_type", "dm"),
metadata=data.get("metadata", {}),
)
@dataclass
class BotMessage:
"""Represents a message in a messaging platform.
Attributes:
message_id: Platform-specific message identifier
content: Message content (text or structured data)
message_type: Type of message
sender: User who sent the message
channel: Channel where the message was sent
timestamp: Message timestamp
reply_to: ID of message being replied to
thread_id: Thread identifier (for threaded conversations)
attachments: List of attachment URLs or data
metadata: Additional platform-specific metadata
"""
message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
content: Union[str, Dict[str, Any]] = ""
message_type: MessageType = MessageType.TEXT
sender: Optional[BotUser] = None
channel: Optional[BotChannel] = None
timestamp: float = field(default_factory=time.time)
reply_to: Optional[str] = None
thread_id: Optional[str] = None
attachments: List[Dict[str, Any]] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"message_id": self.message_id,
"content": self.content,
"message_type": self.message_type.value if isinstance(self.message_type, MessageType) else self.message_type,
"sender": self.sender.to_dict() if self.sender else None,
"channel": self.channel.to_dict() if self.channel else None,
"timestamp": self.timestamp,
"reply_to": self.reply_to,
"thread_id": self.thread_id,
"attachments": self.attachments,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BotMessage":
"""Create from dictionary."""
msg_type = data.get("message_type", "text")
try:
msg_type = MessageType(msg_type)
except ValueError:
msg_type = MessageType.TEXT
sender_data = data.get("sender")
channel_data = data.get("channel")
return cls(
message_id=data.get("message_id", str(uuid.uuid4())),
content=data.get("content", ""),
message_type=msg_type,
sender=BotUser.from_dict(sender_data) if sender_data else None,
channel=BotChannel.from_dict(channel_data) if channel_data else None,
timestamp=data.get("timestamp", time.time()),
reply_to=data.get("reply_to"),
thread_id=data.get("thread_id"),
attachments=data.get("attachments", []),
metadata=data.get("metadata", {}),
)
@property
def text(self) -> str:
"""Get message text content."""
if isinstance(self.content, str):
return self.content
return self.content.get("text", "")
@property
def is_command(self) -> bool:
"""Check if message is a command."""
return self.message_type == MessageType.COMMAND or (
isinstance(self.content, str) and self.content.startswith("/")
)
@property
def command(self) -> Optional[str]:
"""Extract command name if this is a command message."""
if not self.is_command:
return None
text = self.text
if text.startswith("/"):
return text.split()[0][1:] # Remove "/" and get first word
return None
@property
def command_args(self) -> List[str]:
"""Extract command arguments if this is a command message."""
if not self.is_command:
return []
text = self.text
parts = text.split()
return parts[1:] if len(parts) > 1 else []
@runtime_checkable
class BotMessageProtocol(Protocol):
"""Protocol for bot message handling."""
@property
def message_id(self) -> str:
"""Unique message identifier."""
...
@property
def content(self) -> Union[str, Dict[str, Any]]:
"""Message content."""
...
@property
def sender(self) -> Optional[BotUser]:
"""Message sender."""
...
@property
def channel(self) -> Optional[BotChannel]:
"""Message channel."""
...
@runtime_checkable
class BotUserProtocol(Protocol):
"""Protocol for bot user representation."""
@property
def user_id(self) -> str:
"""Unique user identifier."""
...
@property
def username(self) -> Optional[str]:
"""User's username."""
...
@property
def is_bot(self) -> bool:
"""Whether this user is a bot."""
...
@runtime_checkable
class BotChannelProtocol(Protocol):
"""Protocol for bot channel representation."""
@property
def channel_id(self) -> str:
"""Unique channel identifier."""
...
@property
def channel_type(self) -> str:
"""Type of channel (dm, group, channel, thread)."""
...
@runtime_checkable
class BotProtocol(Protocol):
"""Protocol for messaging bot implementations.
Bots connect agents to messaging platforms, handling:
- Message receiving and sending
- Command handling
- Webhook/polling management
- User and channel management
Example usage (implementation in praisonai wrapper):
from praisonai.bots import TelegramBot
bot = TelegramBot(token="...", agent=my_agent)
await bot.start()
"""
@property
def is_running(self) -> bool:
"""Whether the bot is currently running."""
...
@property
def platform(self) -> str:
"""Platform name (telegram, discord, slack, etc.)."""
...
@property
def bot_user(self) -> Optional[BotUser]:
"""The bot's user information."""
...
# Lifecycle methods
async def start(self) -> None:
"""Start the bot (begin receiving messages)."""
...
async def stop(self) -> None:
"""Stop the bot."""
...
# Agent management
def set_agent(self, agent: "Agent") -> None:
"""Set the agent that handles messages.
Args:
agent: The agent to handle incoming messages
"""
...
def get_agent(self) -> Optional["Agent"]:
"""Get the current agent."""
...
# Message handling
async def send_message(
self,
channel_id: str,
content: Union[str, Dict[str, Any]],
reply_to: Optional[str] = None,
thread_id: Optional[str] = None,
) -> BotMessage:
"""Send a message to a channel.
Args:
channel_id: Target channel ID
content: Message content
reply_to: Optional message ID to reply to
thread_id: Optional thread ID for threaded replies
Returns:
The sent message
"""
...
async def edit_message(
self,
channel_id: str,
message_id: str,
content: Union[str, Dict[str, Any]],
) -> BotMessage:
"""Edit an existing message.
Args:
channel_id: Channel containing the message
message_id: ID of message to edit
content: New message content
Returns:
The edited message
"""
...
async def delete_message(
self,
channel_id: str,
message_id: str,
) -> bool:
"""Delete a message.
Args:
channel_id: Channel containing the message
message_id: ID of message to delete
Returns:
True if deleted successfully
"""
...
# Event handlers
def on_message(self, handler: Callable[[BotMessage], Any]) -> Callable:
"""Register a message handler.
Args:
handler: Function to call when a message is received
Returns:
The handler function (for decorator use)
"""
...
def on_command(self, command: str) -> Callable:
"""Decorator to register a command handler.
Args:
command: Command name (without /)
Example:
@bot.on_command("help")
async def handle_help(message: BotMessage):
await bot.send_message(message.channel.channel_id, "Help text...")
"""
...
# Typing indicator
async def send_typing(self, channel_id: str) -> None:
"""Send typing indicator to a channel.
Args:
channel_id: Target channel ID
"""
...
# User/channel info
async def get_user(self, user_id: str) -> Optional[BotUser]:
"""Get user information.
Args:
user_id: User ID to look up
Returns:
User information or None if not found
"""
...
async def get_channel(self, channel_id: str) -> Optional[BotChannel]:
"""Get channel information.
Args:
channel_id: Channel ID to look up
Returns:
Channel information or None if not found
"""
...
@@ -252,11 +252,14 @@ class ContextCompactor:
role = msg.get("role", "")
content = msg.get("content", "")
# If this is a tool result, truncate it
# If this is a tool result, truncate it (use smart format)
if role == "tool" or msg.get("tool_call_id"):
if isinstance(content, str) and len(content) > 500:
pruned_msg = msg.copy()
pruned_msg["content"] = content[:200] + "\n...[output truncated]..."
tail_size = min(100, len(content) // 5)
head = content[:200]
tail = content[-tail_size:] if tail_size > 0 else ""
pruned_msg["content"] = f"{head}\n...[{len(content):,} chars, showing first/last portions]...\n{tail}"
result.append(pruned_msg)
else:
result.append(msg)
@@ -29,6 +29,7 @@ __all__ = [
"MemoryConfig",
"KnowledgeConfig",
"PlanningConfig",
"MultiAgentPlanningConfig",
"ReflectionConfig",
"GuardrailConfig",
"WebConfig",
@@ -91,6 +92,7 @@ _MODULE_MAP = {
"MemoryConfig": "feature_configs",
"KnowledgeConfig": "feature_configs",
"PlanningConfig": "feature_configs",
"MultiAgentPlanningConfig": "feature_configs",
"ReflectionConfig": "feature_configs",
"GuardrailConfig": "feature_configs",
"WebConfig": "feature_configs",
@@ -557,6 +557,15 @@ class OutputConfig:
# Shows: [timestamp] Calling LLM..., Executing tool..., Response: ...
status_trace: bool = False
# Output file - save agent response to file automatically
# When set, saves the response to the specified file path
output_file: Optional[str] = None
# Output template - format template for the response
# Agent will be instructed to follow this template when generating response
# Example: "# {{title}}\n\n{{content}}\n\n---\nGenerated by AI"
template: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
@@ -570,6 +579,8 @@ class OutputConfig:
"simple_output": self.simple_output,
"show_parameters": self.show_parameters,
"status_trace": self.status_trace,
"output_file": self.output_file,
"template": self.template,
}
@@ -671,6 +671,119 @@ def resolve_guardrails(value: Any, config_class: Type) -> Any:
)
# =============================================================================
# Batch Resolution for Performance
# =============================================================================
def resolve_batch(
params: dict,
presets_map: dict,
config_classes: dict,
defaults: dict = None,
) -> dict:
"""
Resolve multiple parameters in a single batch call for performance.
This reduces function call overhead by processing all parameters together
instead of making 11+ separate resolve() calls.
Args:
params: Dict of {param_name: value} to resolve
presets_map: Dict of {param_name: presets_dict}
config_classes: Dict of {param_name: config_class}
defaults: Dict of {param_name: default_value}
Returns:
Dict of {param_name: resolved_value}
Example:
results = resolve_batch(
params={
'output': 'silent',
'execution': None,
'memory': True,
},
presets_map={
'output': OUTPUT_PRESETS,
'execution': EXECUTION_PRESETS,
'memory': MEMORY_PRESETS,
},
config_classes={
'output': OutputConfig,
'execution': ExecutionConfig,
'memory': MemoryConfig,
},
defaults={
'output': OutputConfig(),
'execution': ExecutionConfig(),
'memory': None,
}
)
"""
defaults = defaults or {}
results = {}
for param_name, value in params.items():
config_class = config_classes.get(param_name)
presets = presets_map.get(param_name)
default = defaults.get(param_name)
# Fast path: None -> default
if value is None:
results[param_name] = default
continue
# Fast path: already a config instance
if config_class and isinstance(value, config_class):
results[param_name] = value
continue
# Fast path: bool handling
if isinstance(value, bool):
if value:
results[param_name] = config_class() if config_class else True
else:
results[param_name] = None
continue
# Fast path: string preset lookup
if isinstance(value, str) and presets:
value_lower = value.lower()
for preset_key in presets:
if preset_key.lower() == value_lower:
preset_value = presets[preset_key]
if preset_value is None:
results[param_name] = None
elif config_class and isinstance(preset_value, config_class):
results[param_name] = preset_value
elif isinstance(preset_value, dict) and config_class:
results[param_name] = config_class(**preset_value)
else:
results[param_name] = preset_value
break
else:
# Not a preset - use full resolve for error handling
results[param_name] = resolve(
value=value,
param_name=param_name,
config_class=config_class,
presets=presets,
default=default,
)
continue
# Fallback to full resolve for complex cases
results[param_name] = resolve(
value=value,
param_name=param_name,
config_class=config_class,
presets=presets,
default=default,
)
return results
def resolve_guardrail_policies(
policies: list,
config_class: Type,
@@ -99,6 +99,7 @@ __all__ = [
"PruneToolsOptimizer",
"NonDestructiveOptimizer",
"SummarizeOptimizer",
"LLMSummarizeOptimizer",
"SmartOptimizer",
"get_optimizer",
"get_effective_history",
@@ -125,6 +126,7 @@ __all__ = [
"EstimationMetrics",
"PerToolBudget",
"SnapshotHookData",
"SessionDeduplicationCache",
# Protocols (NEW)
"ContextView",
"ContextMutator",
@@ -203,7 +205,7 @@ def __getattr__(name: str):
# Optimization
if name in ("BaseOptimizer", "TruncateOptimizer", "SlidingWindowOptimizer",
"PruneToolsOptimizer", "NonDestructiveOptimizer", "SummarizeOptimizer",
"SmartOptimizer", "get_optimizer"):
"LLMSummarizeOptimizer", "SmartOptimizer", "get_optimizer"):
from . import optimizer
return getattr(optimizer, name)
@@ -218,7 +220,8 @@ def __getattr__(name: str):
if name in ("ContextManager", "MultiAgentContextManager", "create_context_manager",
"ManagerConfig", "ContextPolicy", "EstimationMode", "ContextShareMode",
"ToolShareMode", "OptimizationEvent", "OptimizationEventType",
"EstimationMetrics", "PerToolBudget", "SnapshotHookData"):
"EstimationMetrics", "PerToolBudget", "SnapshotHookData",
"SessionDeduplicationCache"):
from . import manager
return getattr(manager, name)
@@ -182,7 +182,11 @@ class ContextComposer:
# Truncate to max tokens (rough: 4 chars per token)
max_chars = self.max_tool_output_tokens * 4
truncated = content[:max_chars] + "\n...[output truncated]..."
# Use smart truncation format that judge recognizes as OK
tail_chars = min(max_chars // 5, 1000) # Keep ~20% or 1000 chars from end
head = content[:max_chars - tail_chars]
tail = content[-tail_chars:] if tail_chars > 0 else ""
truncated = f"{head}\n...[{len(content):,} chars, showing first/last portions]...\n{tail}"
result = msg.copy()
result["content"] = truncated
@@ -46,7 +46,11 @@ class ContextMetrics:
# Token tracking
tokens_processed: int = 0
tokens_saved: int = 0
tokens_saved_by_summarization: int = 0 # Tokens saved by LLM summarization
tokens_saved_by_truncation: int = 0 # Tokens saved by truncation
compactions_triggered: int = 0
tool_outputs_summarized: int = 0 # Count of tool outputs summarized
tool_outputs_truncated: int = 0 # Count of tool outputs truncated
# Cache stats
cache_hits: int = 0
@@ -76,8 +80,14 @@ class ContextMetrics:
"tokens": {
"processed": self.tokens_processed,
"saved": self.tokens_saved,
"saved_by_summarization": self.tokens_saved_by_summarization,
"saved_by_truncation": self.tokens_saved_by_truncation,
"compactions": self.compactions_triggered,
},
"tool_outputs": {
"summarized": self.tool_outputs_summarized,
"truncated": self.tool_outputs_truncated,
},
"cache": {
"hits": self.cache_hits,
"misses": self.cache_misses,
@@ -26,13 +26,133 @@ from enum import Enum
from .models import (
ContextLedger, BudgetAllocation, OptimizerStrategy, OptimizationResult
)
from .tokens import estimate_tokens_heuristic, estimate_messages_tokens
from .tokens import estimate_tokens_heuristic, estimate_messages_tokens, estimate_message_tokens
from .budgeter import ContextBudgeter
from .ledger import ContextLedgerManager, MultiAgentLedger
from .optimizer import get_optimizer
from .monitor import ContextMonitor, MultiAgentMonitor
class SessionDeduplicationCache:
"""
Thread-safe session-level content deduplication cache.
Tracks content hashes across all agents in a workflow session
to prevent duplicate content from being sent to LLM.
"""
def __init__(self, max_size: int = 1000):
"""
Initialize session deduplication cache.
Args:
max_size: Maximum number of hashes to cache (LRU eviction)
"""
self._cache: Dict[str, str] = {} # hash -> agent_name
self._lock = threading.Lock()
self._max_size = max_size
self._stats = {"duplicates_prevented": 0, "tokens_saved": 0}
def check_and_add(self, content_hash: str, agent_name: str, tokens: int = 0) -> bool:
"""
Check if content hash exists and add if new.
Args:
content_hash: Hash of the content
agent_name: Name of the agent adding this content
tokens: Estimated tokens in this content
Returns:
True if duplicate (already exists), False if new
"""
with self._lock:
if content_hash in self._cache:
self._stats["duplicates_prevented"] += 1
self._stats["tokens_saved"] += tokens
return True # Duplicate
# LRU eviction if at capacity
if len(self._cache) >= self._max_size:
# Remove oldest entry
oldest_key = next(iter(self._cache))
del self._cache[oldest_key]
self._cache[content_hash] = agent_name
return False # New content
def get_stats(self) -> Dict[str, int]:
"""Get deduplication statistics."""
with self._lock:
return self._stats.copy()
def clear(self) -> None:
"""Clear the cache."""
with self._lock:
self._cache.clear()
self._stats = {"duplicates_prevented": 0, "tokens_saved": 0}
def deduplicate_topics(topics: list, key: str = "title", similarity_threshold: float = 0.8) -> list:
"""
Programmatic deduplication of topics/items before agent processing.
This helps prevent duplicate content from being passed to downstream agents,
reducing token waste and improving quality.
Args:
topics: List of topic dicts or strings
key: Key to use for comparison if topics are dicts (default: "title")
similarity_threshold: Similarity threshold for fuzzy matching (0.0-1.0)
Returns:
Deduplicated list of topics
"""
if not topics:
return topics
seen_hashes = set()
seen_normalized = set()
unique_topics = []
for topic in topics:
# Get the content to compare
if isinstance(topic, dict):
content = str(topic.get(key, topic.get("content", str(topic))))
else:
content = str(topic)
# Normalize for comparison
normalized = content.lower().strip()
# Remove common words for better matching
normalized = " ".join(w for w in normalized.split() if len(w) > 3)
# Check exact hash match
content_hash = hashlib.md5(normalized.encode()).hexdigest()
if content_hash in seen_hashes:
continue
# Check fuzzy match using simple word overlap
is_duplicate = False
for seen in seen_normalized:
# Calculate Jaccard similarity
words1 = set(normalized.split())
words2 = set(seen.split())
if words1 and words2:
intersection = len(words1 & words2)
union = len(words1 | words2)
similarity = intersection / union if union > 0 else 0
if similarity >= similarity_threshold:
is_duplicate = True
break
if not is_duplicate:
seen_hashes.add(content_hash)
seen_normalized.add(normalized)
unique_topics.append(topic)
return unique_topics
class EstimationMode(str, Enum):
"""Token estimation modes."""
HEURISTIC = "heuristic"
@@ -194,6 +314,13 @@ class ManagerConfig:
tool_budgets: Dict[str, PerToolBudget] = field(default_factory=dict)
protected_tools: List[str] = field(default_factory=list)
# LLM-powered summarization
llm_summarize: bool = False # Enable LLM-powered summarization
# Smart tool output summarization
smart_tool_summarize: bool = True # Summarize large tool outputs using LLM before truncating
tool_summarize_limits: Dict[str, int] = field(default_factory=dict) # Per-tool min_chars_to_summarize
# Estimation
estimation_mode: EstimationMode = EstimationMode.HEURISTIC
log_estimation_mismatch: bool = False
@@ -243,6 +370,9 @@ class ManagerConfig:
"allow_absolute_paths": self.allow_absolute_paths,
"prune_after_tokens": self.prune_after_tokens,
"keep_recent_turns": self.keep_recent_turns,
"llm_summarize": self.llm_summarize,
"smart_tool_summarize": self.smart_tool_summarize,
"tool_summarize_limits": self.tool_summarize_limits,
"source": self.source,
}
return result
@@ -364,6 +494,8 @@ class ContextManager:
config: Optional[ManagerConfig] = None,
session_id: str = "",
agent_name: str = "",
session_cache: Optional[SessionDeduplicationCache] = None,
llm_summarize_fn: Optional[Callable] = None,
):
"""
Initialize context manager.
@@ -373,12 +505,20 @@ class ContextManager:
config: Manager configuration
session_id: Session identifier
agent_name: Agent name for monitoring
session_cache: Shared session cache for cross-agent deduplication
llm_summarize_fn: Optional LLM function for intelligent summarization
"""
self.model = model
self.config = config or ManagerConfig.from_env()
self.session_id = session_id
self.agent_name = agent_name
# LLM summarization function (auto-wired from agent when llm_summarize=True)
self._llm_summarize_fn = llm_summarize_fn
# Session-level deduplication cache (shared across agents in workflow)
self._session_cache = session_cache
# Core components
self._budgeter = ContextBudgeter(
model=model,
@@ -475,7 +615,21 @@ class ContextManager:
details={"utilization": utilization, "threshold": self.config.compact_threshold},
)
# Auto-compact if needed
# Step 1: Deduplicate messages before optimization
deduped_messages = self._deduplicate_messages(messages)
dedup_saved = estimate_messages_tokens(messages) - estimate_messages_tokens(deduped_messages)
if dedup_saved > 0:
self._add_history_event(
OptimizationEventType.CAP_OUTPUTS,
tokens_before=estimate_messages_tokens(messages),
tokens_after=estimate_messages_tokens(deduped_messages),
tokens_saved=dedup_saved,
details={"reason": "deduplication", "messages_removed": len(messages) - len(deduped_messages)},
)
messages = deduped_messages
result["tokens_before"] = estimate_messages_tokens(messages)
# Step 2: Auto-compact if needed
if self.config.auto_compact and needs_optimization:
optimized_messages, opt_result = self._optimize_with_benefit_check(
messages,
@@ -524,11 +678,14 @@ class ContextManager:
if original_tokens <= target_tokens:
return messages, None
# Get optimizer
# Get optimizer with LLM summarization if configured
optimizer = get_optimizer(
self.config.strategy,
preserve_recent=self.config.keep_recent_turns,
protected_tools=self.config.protected_tools,
llm_summarize_fn=self._llm_summarize_fn if self.config.llm_summarize else None,
smart_tool_summarize=self.config.smart_tool_summarize,
tool_summarize_limits=self.config.tool_summarize_limits,
)
# Try optimization
@@ -573,6 +730,70 @@ class ContextManager:
return optimized, result
def _deduplicate_messages(
self,
messages: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
"""
Remove duplicate content from messages.
Detects and removes messages with identical content hashes,
keeping only the first occurrence. Preserves message order.
Uses session-level cache for cross-agent deduplication if available.
Args:
messages: List of messages to deduplicate
Returns:
Deduplicated list of messages
"""
if not messages:
return messages
seen_hashes: set = set()
result: List[Dict[str, Any]] = []
for msg in messages:
role = msg.get("role", "")
content = msg.get("content", "")
# Always keep system messages (usually unique)
if role == "system":
result.append(msg)
continue
# Always keep assistant messages with tool_calls (they're actions)
if role == "assistant" and msg.get("tool_calls"):
result.append(msg)
continue
# For tool results and user/assistant content, check for duplicates
if isinstance(content, str) and len(content) > 100:
# Hash the content (first 2000 chars to avoid hashing huge content)
content_key = content[:2000]
content_hash = hashlib.md5(content_key.encode()).hexdigest()[:16]
# Check local seen hashes first
if content_hash in seen_hashes:
import logging
logging.debug(f"[Context] Dedup: skipping local duplicate (hash={content_hash[:8]}, agent={self.agent_name})")
continue
# Check session-level cache for cross-agent deduplication
if self._session_cache:
tokens = estimate_message_tokens(msg)
if self._session_cache.check_and_add(content_hash, self.agent_name, tokens):
# Duplicate found in session cache - skip
import logging
logging.debug(f"[Context] Dedup: skipping session duplicate (hash={content_hash[:8]}, agent={self.agent_name}, tokens={tokens})")
continue
seen_hashes.add(content_hash)
result.append(msg)
return result
def _take_snapshot(
self,
messages: List[Dict[str, Any]],
@@ -623,8 +844,9 @@ class ContextManager:
message_hash = hashlib.sha256(messages_json.encode()).hexdigest()[:16]
tools_hash = hashlib.sha256(tools_json.encode()).hexdigest()[:16]
from datetime import timezone
hook_data = SnapshotHookData(
timestamp=datetime.utcnow().isoformat() + "Z",
timestamp=datetime.now(tz=timezone.utc).isoformat().replace('+00:00', 'Z'),
messages=messages,
tools=tools,
message_hash=message_hash,
@@ -751,7 +973,11 @@ class ContextManager:
ratio = max_tokens / current_tokens
max_chars = int(len(output) * ratio * 0.9) # 10% safety margin
truncated = output[:max_chars] + "\n...[output truncated]..."
# Use smart truncation format that judge recognizes as OK
tail_size = min(max_chars // 5, 1000)
head = output[:max_chars - tail_size]
tail = output[-tail_size:] if tail_size > 0 else ""
truncated = f"{head}\n...[{len(output):,} chars, showing first/last portions]...\n{tail}"
self._add_history_event(
OptimizationEventType.CAP_OUTPUTS,
@@ -773,8 +999,9 @@ class ContextManager:
details: Optional[Dict[str, Any]] = None,
) -> None:
"""Add an event to optimization history."""
from datetime import timezone
event = OptimizationEvent(
timestamp=datetime.utcnow().isoformat() + "Z",
timestamp=datetime.now(tz=timezone.utc).isoformat().replace('+00:00', 'Z'),
event_type=event_type,
strategy=strategy,
tokens_before=tokens_before,
@@ -809,6 +1036,87 @@ class ContextManager:
"monitor_stats": self._monitor.get_stats(),
}
def emergency_truncate(
self,
messages: List[Dict[str, Any]],
target_tokens: int,
) -> List[Dict[str, Any]]:
"""
Emergency truncation when optimization isn't enough.
Aggressively removes messages to fit within target tokens.
Preserves system messages and most recent turns.
Args:
messages: Messages to truncate
target_tokens: Target token count
Returns:
Truncated messages list
"""
if not messages:
return messages
# Estimate current tokens
current_tokens = estimate_messages_tokens(messages)
if current_tokens <= target_tokens:
return messages
# Separate system and non-system messages
system_msgs = [m for m in messages if m.get("role") == "system"]
other_msgs = [m for m in messages if m.get("role") != "system"]
# Keep system messages (they're usually small)
result = list(system_msgs)
system_tokens = estimate_messages_tokens(system_msgs)
remaining_budget = target_tokens - system_tokens
if remaining_budget <= 0:
# Even system messages exceed budget - truncate system content
for msg in result:
if isinstance(msg.get("content"), str) and len(msg["content"]) > 500:
content = msg["content"]
tail_size = min(50, len(content) // 10)
msg["content"] = f"{content[:450]}\n...[{len(content):,} chars, showing first/last portions]...\n{content[-tail_size:] if tail_size > 0 else ''}"
return result
# Keep most recent messages that fit
kept_msgs = []
tokens_used = 0
for msg in reversed(other_msgs):
msg_tokens = estimate_message_tokens(msg)
if tokens_used + msg_tokens <= remaining_budget:
kept_msgs.insert(0, msg)
tokens_used += msg_tokens
else:
# Try to fit a truncated version
if msg.get("role") == "user" and isinstance(msg.get("content"), str):
# Keep user messages but truncate content
available = remaining_budget - tokens_used
if available > 50:
truncated_msg = msg.copy()
max_chars = available * 4 # ~4 chars per token
content = msg["content"]
tail_size = min(max_chars // 10, 100)
truncated_msg["content"] = f"{content[:max_chars - tail_size]}\n...[{len(content):,} chars, showing first/last portions]...\n{content[-tail_size:] if tail_size > 0 else ''}"
kept_msgs.insert(0, truncated_msg)
break
result.extend(kept_msgs)
# Log the emergency truncation
self._add_history_event(
OptimizationEventType.CAP_OUTPUTS,
tokens_before=current_tokens,
tokens_after=estimate_messages_tokens(result),
tokens_saved=current_tokens - estimate_messages_tokens(result),
messages_affected=len(messages) - len(result),
details={"reason": "emergency_truncation", "target_tokens": target_tokens},
)
return result
def get_resolved_config(self) -> Dict[str, Any]:
"""Get the fully resolved configuration with source info."""
return {
@@ -847,6 +1155,7 @@ class MultiAgentContextManager:
self,
config: Optional[ManagerConfig] = None,
default_policy: Optional[ContextPolicy] = None,
session_cache: Optional[SessionDeduplicationCache] = None,
):
"""
Initialize multi-agent context manager.
@@ -854,10 +1163,14 @@ class MultiAgentContextManager:
Args:
config: Base configuration
default_policy: Default context sharing policy
session_cache: Shared session deduplication cache
"""
self.config = config or ManagerConfig.from_env()
self.default_policy = default_policy or ContextPolicy()
# Session-level deduplication cache (shared across all agents)
self._session_cache = session_cache or SessionDeduplicationCache()
self._agents: Dict[str, ContextManager] = {}
self._multi_ledger = MultiAgentLedger()
self._multi_monitor = MultiAgentMonitor(
@@ -884,9 +1197,14 @@ class MultiAgentContextManager:
model=model,
config=self.config,
agent_name=agent_id,
session_cache=self._session_cache, # Share session cache
)
return self._agents[agent_id]
def get_session_cache(self) -> SessionDeduplicationCache:
"""Get the session deduplication cache."""
return self._session_cache
def set_agent_policy(self, agent_id: str, policy: ContextPolicy) -> None:
"""Set context policy for an agent."""
self._policies[agent_id] = policy
@@ -978,6 +1296,7 @@ class MultiAgentContextManager:
agent_id: policy.to_dict()
for agent_id, policy in self._policies.items()
},
"session_dedup": self._session_cache.get_stats() if self._session_cache else {},
}
@@ -214,6 +214,9 @@ class OptimizationResult:
messages_removed: int = 0
messages_tagged: int = 0
tool_outputs_pruned: int = 0
tool_outputs_summarized: int = 0 # Count of tool outputs summarized via LLM
tokens_saved_by_summarization: int = 0 # Tokens saved specifically by LLM summarization
tokens_saved_by_truncation: int = 0 # Tokens saved specifically by truncation
summary_added: bool = False
@property
@@ -274,12 +277,21 @@ class ContextConfig:
prune_after_tokens: int = 40000
protected_tools: List[str] = field(default_factory=list)
# Per-tool output limits (tool_name -> max_chars)
tool_limits: Dict[str, int] = field(default_factory=dict)
# Monitoring
monitor: MonitorConfig = field(default_factory=MonitorConfig)
# Sliding window
keep_recent_turns: int = 5
# LLM-powered summarization
llm_summarize: bool = False # Enable LLM-powered summarization (uses agent's LLM)
# Smart tool output summarization (summarize before truncating)
smart_tool_summarize: bool = True # Summarize large tool outputs using LLM before truncating
# Session tracking (Agno pattern)
session_tracking: bool = False # Enable goal/plan/progress tracking
track_summary: bool = True # Auto-extract conversation summary
@@ -301,6 +313,31 @@ class ContextConfig:
result = asdict(self)
result['strategy'] = self.strategy.value
return result
@classmethod
def for_recipe(cls) -> "ContextConfig":
"""
Preset for recipe/workflow use cases with many tool calls.
Optimized for:
- Preventing context overflow in multi-step workflows
- Preserving important tool outputs
- Triggering compaction earlier (70% vs 80%)
- Limiting tool outputs to prevent explosion
Returns:
ContextConfig with recipe-optimized settings
"""
return cls(
auto_compact=True,
compact_threshold=0.7, # Trigger at 70% (earlier than default 80%)
strategy=OptimizerStrategy.SMART,
tool_output_max=2000, # Limit each tool output to ~2000 tokens
keep_recent_turns=3, # Keep last 3 turns intact
prune_after_tokens=50000, # Start pruning after 50K tokens
output_reserve=8000,
history_ratio=0.6,
)
@runtime_checkable
@@ -151,12 +151,14 @@ class PruneToolsOptimizer(BaseOptimizer):
def __init__(
self,
preserve_recent: int = 5,
max_output_chars: int = 500,
max_output_chars: int = 4000, # Increased from 500 to allow full page content
protected_tools: Optional[List[str]] = None,
tool_limits: Optional[Dict[str, int]] = None, # Per-tool limits
):
self.preserve_recent = preserve_recent
self.max_output_chars = max_output_chars
self.protected_tools = protected_tools or []
self.tool_limits = tool_limits or {} # {tool_name: max_chars}
def optimize(
self,
@@ -192,11 +194,18 @@ class PruneToolsOptimizer(BaseOptimizer):
result.append(msg)
continue
# Truncate if too long
if isinstance(content, str) and len(content) > self.max_output_chars:
# Get per-tool limit or use default
limit = self.tool_limits.get(tool_name, self.max_output_chars)
# Truncate if too long (use smart format)
if isinstance(content, str) and len(content) > limit:
pruned_msg = msg.copy()
pruned_msg["content"] = content[:self.max_output_chars] + "\n...[output truncated]..."
tail_size = min(limit // 5, 500)
head = content[:limit - tail_size]
tail = content[-tail_size:] if tail_size > 0 else ""
pruned_msg["content"] = f"{head}\n...[{len(content):,} chars, showing first/last portions]...\n{tail}"
pruned_msg["_pruned"] = True
pruned_msg["_original_length"] = len(content)
result.append(pruned_msg)
pruned_count += 1
else:
@@ -287,15 +296,32 @@ class NonDestructiveOptimizer(BaseOptimizer):
class SummarizeOptimizer(BaseOptimizer):
"""
Summarize older messages using LLM.
Summarize older messages using LLM or fallback to truncation.
Note: This is a placeholder that creates a structured summary.
Actual LLM summarization should be done at the agent level.
When an LLM summarize function is provided, uses it to create
intelligent summaries. Otherwise falls back to truncation.
"""
def __init__(self, preserve_recent: int = 5, max_summary_items: int = 10):
def __init__(
self,
preserve_recent: int = 5,
max_summary_items: int = 10,
llm_summarize_fn: Optional[callable] = None,
max_summary_tokens: int = 500,
):
"""
Initialize summarize optimizer.
Args:
preserve_recent: Number of recent messages to keep intact
max_summary_items: Max items to include in fallback summary
llm_summarize_fn: Optional function(messages) -> str for LLM summarization
max_summary_tokens: Target token count for LLM summary
"""
self.preserve_recent = preserve_recent
self.max_summary_items = max_summary_items
self.llm_summarize_fn = llm_summarize_fn
self.max_summary_tokens = max_summary_tokens
def optimize(
self,
@@ -324,24 +350,16 @@ class SummarizeOptimizer(BaseOptimizer):
older = other_msgs[:-self.preserve_recent] if len(other_msgs) > self.preserve_recent else []
if older:
# Create summary
summary_parts = []
for msg in older[:self.max_summary_items]:
role = msg.get("role", "unknown")
content = msg.get("content", "")
if isinstance(content, str) and content:
summary_parts.append(f"[{role}]: {content[:150]}...")
# Try LLM summarization first
summary = self._create_summary(older)
if summary_parts:
summary = (
"[Previous conversation summary]\n"
+ "\n".join(summary_parts)
)
if summary:
result.append({
"role": "system",
"content": summary,
"_summary": True,
"_original_count": len(older),
"_llm_summarized": self.llm_summarize_fn is not None,
})
result.extend(recent)
@@ -356,6 +374,206 @@ class SummarizeOptimizer(BaseOptimizer):
messages_removed=len(older),
summary_added=bool(older),
)
def _create_summary(self, messages: List[Dict[str, Any]]) -> str:
"""Create summary using LLM or fallback to truncation."""
if self.llm_summarize_fn:
try:
# Use LLM to create intelligent summary
return self.llm_summarize_fn(messages, self.max_summary_tokens)
except Exception:
# Fallback to truncation on error
pass
# Fallback: Create truncated summary
summary_parts = []
for msg in messages[:self.max_summary_items]:
role = msg.get("role", "unknown")
content = msg.get("content", "")
if isinstance(content, str) and content:
summary_parts.append(f"[{role}]: {content[:150]}...")
if summary_parts:
return "[Previous conversation summary]\n" + "\n".join(summary_parts)
return ""
class SummarizeToolOutputsOptimizer(BaseOptimizer):
"""
Summarize large tool outputs using LLM before truncation.
This optimizer specifically targets tool role messages with large content,
using an LLM to create intelligent summaries that preserve key information.
Falls back to keeping original content if LLM is unavailable or fails.
"""
def __init__(
self,
llm_summarize_fn: Optional[callable] = None,
max_output_tokens: int = 1000,
min_chars_to_summarize: int = 2000,
preserve_recent: int = 2,
tool_summarize_limits: Optional[Dict[str, int]] = None,
):
"""
Initialize tool output summarizer.
Args:
llm_summarize_fn: Function(content, max_tokens) -> summary string
max_output_tokens: Target token count for summarized output
min_chars_to_summarize: Default minimum chars before summarization triggers
preserve_recent: Number of recent tool outputs to preserve intact
tool_summarize_limits: Per-tool min_chars_to_summarize limits {tool_name: min_chars}
"""
self.llm_summarize_fn = llm_summarize_fn
self.max_output_tokens = max_output_tokens
self.min_chars_to_summarize = min_chars_to_summarize
self.preserve_recent = preserve_recent
self.tool_summarize_limits = tool_summarize_limits or {}
def optimize(
self,
messages: List[Dict[str, Any]],
target_tokens: int,
ledger: Optional[ContextLedger] = None,
) -> tuple:
original_tokens = estimate_messages_tokens(messages)
# If already under budget or no LLM function, return as-is
if original_tokens <= target_tokens or not self.llm_summarize_fn:
return messages, OptimizationResult(
original_tokens=original_tokens,
optimized_tokens=original_tokens,
tokens_saved=0,
strategy_used=OptimizerStrategy.SMART,
)
result = []
summarized_count = 0
# Find tool messages and their indices
tool_indices = [i for i, m in enumerate(messages) if m.get("role") == "tool"]
# Preserve recent tool outputs (only if there are more than preserve_recent tools)
if tool_indices and self.preserve_recent > 0 and len(tool_indices) > self.preserve_recent:
recent_tool_indices = set(tool_indices[-self.preserve_recent:])
else:
recent_tool_indices = set() # Summarize all if few tools or preserve_recent=0
for i, msg in enumerate(messages):
role = msg.get("role", "")
content = msg.get("content", "")
# Only process tool messages with large content
if role == "tool" and i not in recent_tool_indices:
# Get per-tool limit or use default
tool_name = msg.get("name", "")
min_chars = self.tool_summarize_limits.get(tool_name, self.min_chars_to_summarize)
if isinstance(content, str) and len(content) >= min_chars:
# Try to summarize
try:
summary = self.llm_summarize_fn(content, self.max_output_tokens)
if summary and len(summary) < len(content):
summarized_msg = msg.copy()
summarized_msg["content"] = summary
summarized_msg["_summarized"] = True
summarized_msg["_original_length"] = len(content)
result.append(summarized_msg)
summarized_count += 1
continue
except Exception:
# Fallback to original on error
pass
result.append(msg)
optimized_tokens = estimate_messages_tokens(result)
tokens_saved = original_tokens - optimized_tokens
return result, OptimizationResult(
original_tokens=original_tokens,
optimized_tokens=optimized_tokens,
tokens_saved=tokens_saved,
strategy_used=OptimizerStrategy.SMART,
tool_outputs_summarized=summarized_count,
tokens_saved_by_summarization=tokens_saved, # All savings from summarization
)
class LLMSummarizeOptimizer(SummarizeOptimizer):
"""
LLM-powered summarization optimizer.
Uses the agent's LLM to create intelligent summaries of older messages,
preserving key information while reducing token count.
"""
def __init__(
self,
preserve_recent: int = 5,
llm_client: Optional[Any] = None,
model: str = "gpt-4o-mini",
max_summary_tokens: int = 500,
):
"""
Initialize LLM summarize optimizer.
Args:
preserve_recent: Number of recent messages to keep intact
llm_client: OpenAI-compatible client for summarization
model: Model to use for summarization
max_summary_tokens: Target token count for summary
"""
self.llm_client = llm_client
self.model = model
# Create LLM summarize function if client provided
llm_fn = self._llm_summarize if llm_client else None
super().__init__(
preserve_recent=preserve_recent,
llm_summarize_fn=llm_fn,
max_summary_tokens=max_summary_tokens,
)
def _llm_summarize(self, messages: List[Dict[str, Any]], max_tokens: int) -> str:
"""Use LLM to create intelligent summary."""
if not self.llm_client:
return ""
# Build content to summarize
content_parts = []
for msg in messages:
role = msg.get("role", "unknown")
content = msg.get("content", "")
if isinstance(content, str) and content:
# Truncate very long content for summarization input
truncated = content[:2000] if len(content) > 2000 else content
content_parts.append(f"[{role}]: {truncated}")
if not content_parts:
return ""
# Create summarization prompt
summarize_prompt = f"""Summarize the following conversation history concisely, preserving key facts, decisions, and context. Keep the summary under {max_tokens} tokens.
Conversation:
{chr(10).join(content_parts)}
Summary:"""
try:
response = self.llm_client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": summarize_prompt}],
max_tokens=max_tokens,
temperature=0.3,
)
summary = response.choices[0].message.content
return f"[AI Summary of {len(messages)} previous messages]\n{summary}"
except Exception:
return ""
class SmartOptimizer(BaseOptimizer):
@@ -363,25 +581,43 @@ class SmartOptimizer(BaseOptimizer):
Smart optimization combining multiple strategies.
Applies strategies in order:
1. Prune tool outputs
2. Sliding window
3. Summarize if still over
1. Summarize tool outputs (if LLM available and smart_tool_summarize=True)
2. Prune tool outputs (fallback truncation)
3. Sliding window
4. Summarize conversation if still over
"""
def __init__(
self,
preserve_recent: int = 5,
protected_tools: Optional[List[str]] = None,
tool_limits: Optional[Dict[str, int]] = None,
llm_summarize_fn: Optional[callable] = None,
smart_tool_summarize: bool = True,
tool_summarize_limits: Optional[Dict[str, int]] = None,
):
self.preserve_recent = preserve_recent
self.protected_tools = protected_tools or []
self.tool_limits = tool_limits or {}
self.smart_tool_summarize = smart_tool_summarize
self.tool_summarize_limits = tool_summarize_limits or {}
# Tool output summarization (LLM-powered, applied first when available)
self._summarize_tools = SummarizeToolOutputsOptimizer(
llm_summarize_fn=llm_summarize_fn if smart_tool_summarize else None,
preserve_recent=preserve_recent,
tool_summarize_limits=tool_summarize_limits,
)
self._prune = PruneToolsOptimizer(
preserve_recent=preserve_recent,
protected_tools=protected_tools,
tool_limits=tool_limits,
)
self._window = SlidingWindowOptimizer()
self._summarize = SummarizeOptimizer(preserve_recent=preserve_recent)
self._summarize = SummarizeOptimizer(
preserve_recent=preserve_recent,
llm_summarize_fn=llm_summarize_fn,
)
def optimize(
self,
@@ -399,8 +635,29 @@ class SmartOptimizer(BaseOptimizer):
strategy_used=OptimizerStrategy.SMART,
)
# Step 1: Prune tool outputs
result, prune_result = self._prune.optimize(messages, target_tokens, ledger)
# Step 1: Summarize tool outputs (LLM-powered, if available)
tool_summarized_count = 0
tokens_saved_by_summarization = 0
if self._summarize_tools.llm_summarize_fn:
result, tool_summary_result = self._summarize_tools.optimize(messages, target_tokens, ledger)
tool_summarized_count = tool_summary_result.tool_outputs_summarized
tokens_saved_by_summarization = tool_summary_result.tokens_saved_by_summarization
if estimate_messages_tokens(result) <= target_tokens:
return result, OptimizationResult(
original_tokens=original_tokens,
optimized_tokens=tool_summary_result.optimized_tokens,
tokens_saved=original_tokens - tool_summary_result.optimized_tokens,
strategy_used=OptimizerStrategy.SMART,
tool_outputs_summarized=tool_summarized_count,
tokens_saved_by_summarization=tokens_saved_by_summarization,
)
else:
result = messages
# Step 2: Prune tool outputs (fallback truncation)
result, prune_result = self._prune.optimize(result, target_tokens, ledger)
tokens_saved_by_truncation = prune_result.tokens_saved
if estimate_messages_tokens(result) <= target_tokens:
return result, OptimizationResult(
@@ -408,10 +665,13 @@ class SmartOptimizer(BaseOptimizer):
optimized_tokens=prune_result.optimized_tokens,
tokens_saved=original_tokens - prune_result.optimized_tokens,
strategy_used=OptimizerStrategy.SMART,
tool_outputs_summarized=tool_summarized_count,
tool_outputs_pruned=prune_result.tool_outputs_pruned,
tokens_saved_by_summarization=tokens_saved_by_summarization,
tokens_saved_by_truncation=tokens_saved_by_truncation,
)
# Step 2: Sliding window
# Step 3: Sliding window
result, window_result = self._window.optimize(result, target_tokens, ledger)
if estimate_messages_tokens(result) <= target_tokens:
@@ -424,7 +684,7 @@ class SmartOptimizer(BaseOptimizer):
messages_removed=window_result.messages_removed,
)
# Step 3: Summarize
# Step 4: Summarize conversation
result, summary_result = self._summarize.optimize(result, target_tokens, ledger)
optimized_tokens = estimate_messages_tokens(result)
@@ -434,7 +694,10 @@ class SmartOptimizer(BaseOptimizer):
optimized_tokens=optimized_tokens,
tokens_saved=original_tokens - optimized_tokens,
strategy_used=OptimizerStrategy.SMART,
tool_outputs_summarized=tool_summarized_count,
tool_outputs_pruned=prune_result.tool_outputs_pruned,
tokens_saved_by_summarization=tokens_saved_by_summarization,
tokens_saved_by_truncation=tokens_saved_by_truncation,
messages_removed=window_result.messages_removed + summary_result.messages_removed,
summary_added=summary_result.summary_added,
)
@@ -450,6 +713,9 @@ OPTIMIZER_REGISTRY: Dict[OptimizerStrategy, type] = {
OptimizerStrategy.SMART: SmartOptimizer,
}
# LLM summarizer available separately (not in registry as it needs client)
LLM_SUMMARIZE_OPTIMIZER = LLMSummarizeOptimizer
def get_optimizer(
strategy: OptimizerStrategy,
@@ -26,14 +26,56 @@ __all__ = [
"PerformanceEvaluator",
"ReliabilityEvaluator",
"CriteriaEvaluator",
"MediaEvaluator",
"AccuracyResult",
"PerformanceResult",
"ReliabilityResult",
"CriteriaResult",
"MediaEvaluationResult",
"EvaluationScore",
"PerformanceMetrics",
"ToolCallResult",
"CriteriaScore",
# DRY: Common grading base classes and protocols
"BaseLLMGrader",
"GradeResult",
"GraderProtocol",
"GradeResultProtocol",
"ScoredResultProtocol",
"AsyncGraderProtocol",
"parse_score_reasoning",
# Eval package types
"EvalCase",
"EvalResult",
"EvalReport",
"EvalPackage",
"EvalRunnerProtocol",
# Unified Judge API (follows add_X/get_X/list_X naming)
"Judge",
"JudgeConfig",
"JudgeResult",
"JudgeProtocol",
"JudgeResultProtocol",
"AccuracyJudge",
"CriteriaJudge",
"RecipeJudge",
"add_judge",
"get_judge",
"list_judges",
"remove_judge",
# Dynamic judge configuration (domain-agnostic)
"JudgeCriteriaConfig",
"OptimizationRuleProtocol",
"add_optimization_rule",
"get_optimization_rule",
"list_optimization_rules",
"remove_optimization_rule",
# Token utilities
"estimate_tokens",
"get_context_length",
"count_tokens",
"needs_chunking",
"get_recommended_chunk_size",
]
_LAZY_IMPORTS = {
@@ -42,14 +84,56 @@ _LAZY_IMPORTS = {
"PerformanceEvaluator": ("performance", "PerformanceEvaluator"),
"ReliabilityEvaluator": ("reliability", "ReliabilityEvaluator"),
"CriteriaEvaluator": ("criteria", "CriteriaEvaluator"),
"MediaEvaluator": ("media", "MediaEvaluator"),
"AccuracyResult": ("results", "AccuracyResult"),
"PerformanceResult": ("results", "PerformanceResult"),
"ReliabilityResult": ("results", "ReliabilityResult"),
"CriteriaResult": ("results", "CriteriaResult"),
"MediaEvaluationResult": ("media", "MediaEvaluationResult"),
"EvaluationScore": ("results", "EvaluationScore"),
"PerformanceMetrics": ("results", "PerformanceMetrics"),
"ToolCallResult": ("results", "ToolCallResult"),
"CriteriaScore": ("results", "CriteriaScore"),
# DRY: Common grading base classes and protocols
"BaseLLMGrader": ("grader", "BaseLLMGrader"),
"GradeResult": ("grader", "GradeResult"),
"GraderProtocol": ("protocols", "GraderProtocol"),
"GradeResultProtocol": ("protocols", "GradeResultProtocol"),
"ScoredResultProtocol": ("protocols", "ScoredResultProtocol"),
"AsyncGraderProtocol": ("protocols", "AsyncGraderProtocol"),
"parse_score_reasoning": ("grader", "parse_score_reasoning"),
# Eval package types
"EvalCase": ("package", "EvalCase"),
"EvalResult": ("package", "EvalResult"),
"EvalReport": ("package", "EvalReport"),
"EvalPackage": ("package", "EvalPackage"),
"EvalRunnerProtocol": ("package", "EvalRunnerProtocol"),
# Unified Judge API (follows add_X/get_X/list_X naming)
"Judge": ("judge", "Judge"),
"JudgeConfig": ("judge", "JudgeConfig"),
"JudgeResult": ("results", "JudgeResult"),
"JudgeProtocol": ("protocols", "JudgeProtocol"),
"JudgeResultProtocol": ("protocols", "JudgeResultProtocol"),
"AccuracyJudge": ("judge", "AccuracyJudge"),
"CriteriaJudge": ("judge", "CriteriaJudge"),
"RecipeJudge": ("judge", "RecipeJudge"),
"add_judge": ("judge", "add_judge"),
"get_judge": ("judge", "get_judge"),
"list_judges": ("judge", "list_judges"),
"remove_judge": ("judge", "remove_judge"),
# Dynamic judge configuration (domain-agnostic)
"JudgeCriteriaConfig": ("judge", "JudgeCriteriaConfig"),
"OptimizationRuleProtocol": ("protocols", "OptimizationRuleProtocol"),
"add_optimization_rule": ("judge", "add_optimization_rule"),
"get_optimization_rule": ("judge", "get_optimization_rule"),
"list_optimization_rules": ("judge", "list_optimization_rules"),
"remove_optimization_rule": ("judge", "remove_optimization_rule"),
# Token utilities
"estimate_tokens": ("tokens", "estimate_tokens"),
"get_context_length": ("tokens", "get_context_length"),
"count_tokens": ("tokens", "count_tokens"),
"needs_chunking": ("tokens", "needs_chunking"),
"get_recommended_chunk_size": ("tokens", "get_recommended_chunk_size"),
}
@@ -6,10 +6,11 @@ Evaluates agent output accuracy by comparing against expected output using LLM-a
import os
import logging
from typing import Any, Callable, List, Optional, Union, TYPE_CHECKING
from typing import Callable, Optional, Union, TYPE_CHECKING
from .base import BaseEvaluator
from .results import AccuracyResult, EvaluationScore
from .grader import parse_score_reasoning
if TYPE_CHECKING:
from ..agent.agent import Agent
@@ -123,19 +124,8 @@ REASONING: [brief explanation]"""
response_text = response.choices[0].message.content or ""
score = 5.0
reasoning = "Unable to parse response"
lines = response_text.strip().split('\n')
for line in lines:
if line.startswith('SCORE:'):
try:
score = float(line.replace('SCORE:', '').strip())
score = max(1.0, min(10.0, score))
except ValueError:
pass
elif line.startswith('REASONING:'):
reasoning = line.replace('REASONING:', '').strip()
# DRY: Use common parsing function from grader module
score, reasoning = parse_score_reasoning(response_text)
return EvaluationScore(
score=score,
@@ -6,10 +6,11 @@ Evaluates agent outputs against custom criteria using LLM-as-judge.
import os
import logging
from typing import Callable, List, Literal, Optional, Union, TYPE_CHECKING
from typing import Callable, Literal, Optional, Union, TYPE_CHECKING
from .base import BaseEvaluator
from .results import CriteriaResult, CriteriaScore
from .grader import parse_score_reasoning
if TYPE_CHECKING:
from ..agent.agent import Agent
@@ -146,28 +147,24 @@ REASONING: [brief explanation]"""
response_text = response.choices[0].message.content or ""
score = 5.0
passed = False
reasoning = "Unable to parse response"
lines = response_text.strip().split('\n')
for line in lines:
if self.scoring_type == "binary":
# DRY: Use common parsing for numeric mode, custom for binary
if self.scoring_type == "binary":
score = 5.0
passed = False
reasoning = "Unable to parse response"
lines = response_text.strip().split('\n')
for line in lines:
if line.startswith('RESULT:'):
result_str = line.replace('RESULT:', '').strip().upper()
passed = result_str == 'PASS'
score = 10.0 if passed else 0.0
else:
if line.startswith('SCORE:'):
try:
score = float(line.replace('SCORE:', '').strip())
score = max(1.0, min(10.0, score))
passed = score >= self.threshold
except ValueError:
pass
if line.startswith('REASONING:'):
reasoning = line.replace('REASONING:', '').strip()
elif line.startswith('REASONING:'):
reasoning = line.replace('REASONING:', '').strip()
else:
# DRY: Use common parsing function from grader module
score, reasoning = parse_score_reasoning(response_text)
passed = score >= self.threshold
return CriteriaScore(
score=score,
@@ -0,0 +1,378 @@
"""
Base LLM Grader for PraisonAI Agents.
Provides a common base class for LLM-as-judge grading to eliminate duplication
across AccuracyEvaluator, CriteriaEvaluator, and TrainingGrader.
DRY: This module extracts the common prompt building and response parsing logic
that was duplicated across multiple evaluators.
"""
import os
import logging
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
@dataclass
class GradeResult:
"""
Result from grading an agent output.
This is the common result type used by all graders.
Attributes:
score: Quality score (1-10)
reasoning: Explanation for the score
suggestions: List of improvement suggestions (optional)
input_text: The input that was given
output: The output that was graded
expected_output: Optional expected output
timestamp: When grading occurred
"""
score: float
reasoning: str
suggestions: List[str] = field(default_factory=list)
input_text: str = ""
output: str = ""
expected_output: Optional[str] = None
timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "GradeResult":
"""Create from dictionary."""
return cls(
score=data.get("score", 5.0),
reasoning=data.get("reasoning", ""),
suggestions=data.get("suggestions", []),
input_text=data.get("input_text", ""),
output=data.get("output", ""),
expected_output=data.get("expected_output"),
timestamp=data.get("timestamp", datetime.utcnow().isoformat()),
)
class BaseLLMGrader:
"""
Base class for LLM-as-judge grading.
Provides common functionality for:
- Building grading prompts
- Parsing LLM responses (SCORE/REASONING/SUGGESTIONS format)
- Lazy litellm import
- Sync and async grading
Subclasses can override:
- _build_prompt() for custom prompt templates
- _parse_response() for custom response parsing
Usage:
grader = BaseLLMGrader()
result = grader.grade(
input_text="What is Python?",
output="Python is a programming language",
expected_output="Python is a high-level programming language"
)
print(f"Score: {result.score}/10")
"""
# Default prompt template - can be overridden by subclasses
PROMPT_TEMPLATE = """You are an expert evaluator for AI agent outputs. Your task is to grade the quality of an agent's response.
INPUT (what the agent was asked):
{input_text}
AGENT OUTPUT (what the agent responded):
{output}
{expected_section}
GRADING CRITERIA:
- Accuracy: Is the response factually correct?
- Completeness: Does it fully address the input?
- Clarity: Is it well-written and easy to understand?
- Relevance: Does it stay on topic?
SCORING GUIDELINES:
- 10: Perfect - Excellent in all criteria
- 8-9: Very Good - Minor improvements possible
- 6-7: Good - Some issues but mostly correct
- 4-5: Fair - Significant issues
- 2-3: Poor - Major problems
- 1: Very Poor - Completely wrong or irrelevant
Respond in this EXACT format:
SCORE: [number 1-10]
REASONING: [brief explanation of the score]
SUGGESTIONS:
- [first suggestion for improvement]
- [second suggestion if applicable]
- [third suggestion if applicable]
If no suggestions are needed, write "SUGGESTIONS: None"
"""
def __init__(
self,
model: Optional[str] = None,
temperature: float = 0.1,
max_tokens: int = 500,
):
"""
Initialize the grader.
Args:
model: LLM model to use for grading (default: gpt-4o-mini)
temperature: Temperature for LLM calls (default: 0.1 for consistency)
max_tokens: Maximum tokens for LLM response
"""
self.model = model or os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini")
self.temperature = temperature
self.max_tokens = max_tokens
def _build_prompt(
self,
input_text: str,
output: str,
expected_output: Optional[str] = None,
) -> str:
"""
Build the grading prompt.
Override this method in subclasses for custom prompt templates.
Args:
input_text: The input given to the agent
output: The agent's output to grade
expected_output: Optional expected output for comparison
Returns:
The prompt string for the LLM
"""
expected_section = ""
if expected_output:
expected_section = f"""
EXPECTED OUTPUT (ideal response):
{expected_output}
"""
return self.PROMPT_TEMPLATE.format(
input_text=input_text,
output=output,
expected_section=expected_section,
)
def _parse_response(
self,
response_text: str,
input_text: str,
output: str,
expected_output: Optional[str],
) -> GradeResult:
"""
Parse the LLM response into a GradeResult.
Override this method in subclasses for custom parsing logic.
Args:
response_text: Raw LLM response
input_text: Original input
output: Original output
expected_output: Original expected output
Returns:
Parsed GradeResult
"""
score = 5.0 # Default
reasoning = "Unable to parse response"
suggestions: List[str] = []
lines = response_text.strip().split('\n')
in_suggestions = False
for line in lines:
line = line.strip()
if line.startswith('SCORE:'):
try:
score_str = line.replace('SCORE:', '').strip()
score = float(score_str)
# Clamp to valid range
score = max(1.0, min(10.0, score))
except ValueError:
pass
elif line.startswith('REASONING:'):
reasoning = line.replace('REASONING:', '').strip()
elif line.startswith('SUGGESTIONS:'):
in_suggestions = True
rest = line.replace('SUGGESTIONS:', '').strip()
if rest.lower() != 'none' and rest:
suggestions.append(rest)
elif in_suggestions and line.startswith('-'):
suggestion = line.lstrip('- ').strip()
if suggestion and suggestion.lower() != 'none':
suggestions.append(suggestion)
return GradeResult(
score=score,
reasoning=reasoning,
suggestions=suggestions,
input_text=input_text,
output=output,
expected_output=expected_output,
)
def _get_litellm(self):
"""
Lazy import litellm.
Returns:
The litellm module
Raises:
ImportError: If litellm is not installed
"""
try:
import litellm
return litellm
except ImportError:
raise ImportError(
"litellm is required for LLM grading. "
"Install with: pip install litellm"
)
def grade(
self,
input_text: str,
output: str,
expected_output: Optional[str] = None,
) -> GradeResult:
"""
Grade an agent output.
Args:
input_text: The input given to the agent
output: The agent's output to grade
expected_output: Optional expected output for comparison
Returns:
GradeResult with score, reasoning, and suggestions
"""
litellm = self._get_litellm()
prompt = self._build_prompt(input_text, output, expected_output)
try:
response = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=self.temperature,
max_tokens=self.max_tokens,
)
response_text = response.choices[0].message.content or ""
return self._parse_response(response_text, input_text, output, expected_output)
except Exception as e:
logger.warning(f"Grading failed: {e}")
return GradeResult(
score=5.0,
reasoning=f"Grading error: {str(e)}",
suggestions=[],
input_text=input_text,
output=output,
expected_output=expected_output,
)
async def grade_async(
self,
input_text: str,
output: str,
expected_output: Optional[str] = None,
) -> GradeResult:
"""
Grade an agent output asynchronously.
Args:
input_text: The input given to the agent
output: The agent's output to grade
expected_output: Optional expected output for comparison
Returns:
GradeResult with score, reasoning, and suggestions
"""
litellm = self._get_litellm()
prompt = self._build_prompt(input_text, output, expected_output)
try:
response = await litellm.acompletion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=self.temperature,
max_tokens=self.max_tokens,
)
response_text = response.choices[0].message.content or ""
return self._parse_response(response_text, input_text, output, expected_output)
except Exception as e:
logger.warning(f"Async grading failed: {e}")
return GradeResult(
score=5.0,
reasoning=f"Grading error: {str(e)}",
suggestions=[],
input_text=input_text,
output=output,
expected_output=expected_output,
)
def parse_score_reasoning(response_text: str) -> tuple:
"""
Parse SCORE and REASONING from LLM response.
DRY: Static utility function for parsing LLM-as-judge responses.
Used by AccuracyEvaluator, CriteriaEvaluator, and BaseLLMGrader.
Args:
response_text: Raw LLM response text
Returns:
Tuple of (score: float, reasoning: str)
"""
score = 5.0 # Default
reasoning = "Unable to parse response"
lines = response_text.strip().split('\n')
for line in lines:
line = line.strip()
if line.startswith('SCORE:'):
try:
score_str = line.replace('SCORE:', '').strip()
score = float(score_str)
# Clamp to valid range
score = max(1.0, min(10.0, score))
except ValueError:
pass
elif line.startswith('REASONING:'):
reasoning = line.replace('REASONING:', '').strip()
return score, reasoning
__all__ = [
'GradeResult',
'BaseLLMGrader',
'parse_score_reasoning',
]
Loaded 100 of 381 files, more files were not shown because too many files have changed in this diff. Show more