Compare commits

...
67 Commits
Author SHA1 Message Date
Mervin Praison cbe8beeca6 Release v2.3.80 2025-12-23 07:54:07 +00:00
Mervin Praison f7b66a52b7 Release v2.3.79 2025-12-23 07:27:35 +00:00
Mervin Praison 6dbf194312 Release v2.3.78 2025-12-23 06:46:15 +00:00
Mervin Praison 335e8d7e47 Release v2.3.77 2025-12-23 06:28:26 +00:00
Mervin Praison d65840e092 Release v2.3.76 2025-12-23 05:40:52 +00:00
Mervin Praison 5a13571fdd Release v2.3.75 2025-12-22 19:11:32 +00:00
Mervin Praison e3b48c2526 Release v2.3.74 2025-12-22 17:01:50 +00:00
Mervin Praison 8d32238b4c Release v2.3.73 2025-12-22 15:47:51 +00:00
Mervin Praison c17b1298bb Reorganize README feature table with improved categorization and hierarchy 2025-12-22 14:50:37 +00:00
Mervin Praison ec6c150315 Release v2.3.72 2025-12-22 13:44:48 +00:00
Mervin Praison 3d26cedd5d Release v2.3.71 2025-12-22 11:59:12 +00:00
Mervin Praison 8c735a60b2 Release v2.3.70 2025-12-22 10:43:48 +00:00
Mervin Praison 2f5f2f1c13 Release v2.3.69 2025-12-22 10:40:53 +00:00
Mervin Praison 0253073c68 Release v2.3.68 2025-12-22 10:35:45 +00:00
Mervin Praison 7f0e334e8c Release v2.3.67 2025-12-22 09:14:33 +00:00
Mervin Praison cacf1097ef Release v2.3.66 2025-12-22 08:50:02 +00:00
Mervin Praison 09f01919d2 Fix duplicate --max-retries argument conflict in CLI parser 2025-12-21 05:19:08 +00:00
Mervin Praison 92397615f8 Release v2.3.65 2025-12-20 21:49:04 +00:00
Mervin Praison 4b21943a53 Release v2.3.64 2025-12-20 19:57:39 +00:00
Mervin Praison af56a753bd Release v2.3.63 2025-12-20 12:02:44 +00:00
Mervin Praison 15c2559a3c Release v2.3.62 2025-12-20 11:59:46 +00:00
Mervin Praison 3215bdc0a6 Release v2.3.61 2025-12-20 11:15:22 +00:00
Mervin Praison 0ff0d83744 fix: Preserve real API keys for integration tests in conftest.py
- Check for @pytest.mark.integration marker
- Check if test file is in integration/ directory
- This prevents conftest from overriding real API keys with 'test-key'
2025-12-20 03:58:13 +00:00
Mervin Praison 8f956f91dd fix: Improve API key validation in integration test skip conditions
- Check for 'sk-' prefix and minimum length (40 chars) to detect invalid keys
- This ensures tests are skipped when using placeholder/test API keys in CI
2025-12-20 03:54:58 +00:00
Mervin Praison 954f6ac33d fix: Skip integration tests when using test API key
- test_auto_generator_integration.py: Skip when OPENAI_API_KEY contains 'test'
- test_model_routing_real.py: Skip test_real_openai_call when using test key
- test_cli_integrations.py: Skip all tests when using test key

These tests require real API credentials and should not run in CI
when only a placeholder test key is available.
2025-12-20 03:51:22 +00:00
Mervin Praison 264d11d8d9 fix: Fix failing unit tests for ClaudeCodeSDK and QueryRewriter CLI
- test_use_sdk_option: Updated test to match actual behavior where use_sdk
  is only True when SDK is available (use_sdk and CLAUDE_SDK_AVAILABLE)
- test_applies_rewrite_before_agent: Fixed mocking to properly mock
  _run_with_status_display and praisonaiagents submodules
2025-12-20 03:47:28 +00:00
Mervin Praison 88da2bdd7b Release v2.3.60 2025-12-20 03:35:28 +00:00
Mervin Praison b84e6c2460 Release v2.3.59 2025-12-20 03:28:48 +00:00
Mervin Praison 0434b23ada Release v2.3.58 2025-12-20 03:08:31 +00:00
Mervin Praison a9951e0694 Release v2.3.57 2025-12-19 16:47:09 +00:00
Mervin Praison 073062cd96 Release v2.3.56 2025-12-19 16:14:58 +00:00
Mervin Praison 936b422876 Release v2.3.55 2025-12-19 16:05:38 +00:00
Mervin Praison 99ab421ce2 Release v2.3.54 2025-12-19 16:04:43 +00:00
Mervin Praison c1f334905e Release v2.3.53 2025-12-19 15:38:30 +00:00
Mervin Praison 6d3ba7cdf8 Release v2.3.52 2025-12-19 15:34:19 +00:00
Mervin Praison 96616498d2 Release v2.3.51 2025-12-19 15:25:30 +00:00
Mervin Praison 1cad345415 Release v2.3.50 2025-12-19 15:19:29 +00:00
Mervin Praison 802e7e52f9 fix(readme): correct CLI docs URLs to include /docs/ prefix
Changed all https://docs.praison.ai/cli/* URLs to https://docs.praison.ai/docs/cli/*
Fixed 39 URLs total
2025-12-19 13:01:53 +00:00
Mervin Praison 53704f234c Release v2.3.49 2025-12-19 12:49:12 +00:00
Mervin Praison b575994d91 docs(readme): update AI commit CLI with security scanning and auto mode
- Add full auto mode (-a) with stage, security check, commit, push
- Document security scanning features
- Add --no-verify flag documentation
- List detected sensitive patterns
2025-12-19 12:35:49 +00:00
Mervin Praison 7ed8596261 Release v2.3.48 2025-12-19 12:35:02 +00:00
Mervin Praison deef65c4bb Release v2.3.47 2025-12-19 12:23:05 +00:00
Mervin Praison d51327effd Release v2.3.46 2025-12-19 12:22:29 +00:00
Mervin Praison 30ce8857e8 fix: suppress crewai.cli.config INFO log at startup 2025-12-19 12:18:23 +00:00
Mervin Praison 397874ccfc Release v2.3.45 2025-12-19 12:16:12 +00:00
Mervin Praison 2016902e9a docs: Add examples README with quick links to docs 2025-12-19 11:14:24 +00:00
Mervin Praison d2d19511da docs: Update README with Model Router info and fix links
- Rename 'Router Agent' to 'Model Router' for consistency
- Fix docs link to point to /features/model-router
- Add model-routing template CLI command
- Add link to Model Router documentation
2025-12-19 11:13:48 +00:00
Mervin Praison bc9579b497 Add Code category with individual examples for external CLI integrations
- Add Code category in Key Features table (like Workflows)
- Add individual examples for each external agent:
  - claude_code_example.py
  - gemini_cli_example.py
  - codex_cli_example.py
  - cursor_cli_example.py
  - external_agents_example.py (all integrations)
  - code_editing_example.py
- Link to individual docs pages for each integration
- Organize code-related features under Code category
2025-12-19 11:07:25 +00:00
Mervin Praison 3161389341 Add External Agents CLI documentation to README
- Add External Agents feature to Key Features table
- Update Code Editing docs link to /code/editing
- Add External Agents CLI section with examples
- Include Python API usage examples
- Add environment variables documentation
- Link to detailed docs at docs.praison.ai/code/external-agents
2025-12-19 11:01:24 +00:00
Mervin Praison b01eb2b8fa Release v2.3.44 2025-12-19 10:50:33 +00:00
Mervin Praison e609fb7425 Release v2.3.43 2025-12-19 10:32:33 +00:00
Mervin Praison 168ef98473 feat: Implement all remaining AutoAgents improvements (TDD)
TODO 2: Single-Agent Option
- Add SingleAgentStructure Pydantic model
- Add single_agent parameter to AutoGenerator and WorkflowAutoGenerator

TODO 3: LLM-Based Pattern Recommendation
- Add PatternRecommendation Pydantic model (pattern, reasoning, confidence)
- Add recommend_pattern_llm() method to WorkflowAutoGenerator

TODO 4: Validation Gates
- Add ValidationGate Pydantic model (criteria, pass_action, fail_action)
- Add optional gates field to WorkflowStructure

TODO 6: Pattern Support for AutoGenerator
- Add pattern parameter to AutoGenerator
- Add recommend_pattern() method to AutoGenerator
- Add pattern-specific guidance in get_user_content()

TODO 7: Merge Support for WorkflowAutoGenerator
- Add merge parameter to generate() method
- Add merge_with_existing_workflow() method

TODO 10: Framework Support for WorkflowAutoGenerator
- Add framework parameter to WorkflowAutoGenerator
- Default framework is 'praisonai'

Test Coverage:
- 51 tests pass (up from 34)
- 17 new tests added using TDD approach
- All tests verify new functionality
2025-12-19 10:29:40 +00:00
Mervin Praison ce2c2926f9 refactor: Add BaseAutoGenerator class and improve WorkflowAutoGenerator
- Create BaseAutoGenerator with shared functionality (DRY principle)
  - Lazy-loaded instructor client (LiteLLM or OpenAI fallback)
  - Environment variable handling for model/API configuration
  - analyze_complexity() static method for task complexity detection
  - get_available_tools() static method for tool list
  - AVAILABLE_TOOLS constant shared between generators

- Update AutoGenerator to inherit from BaseAutoGenerator
  - Removes ~40 lines of duplicate code
  - Maintains all existing functionality

- Update WorkflowAutoGenerator to inherit from BaseAutoGenerator
  - Removes ~40 lines of duplicate code
  - Add dynamic agent count based on task complexity
  - Add tools list to workflow prompts
  - Improved prompt with 3-step analysis

- Add 8 new unit tests for new functionality
  - TestBaseAutoGenerator: complexity analysis, tools list
  - TestWorkflowDynamicAgentCount: prompt improvements

All 34 tests pass with no performance impact.
2025-12-19 10:23:29 +00:00
Mervin Praison 0a569e2b46 docs: Add AutoAgents workflow patterns examples and update README
- Add autoagents_workflow_patterns.py with all 6 workflow patterns
- Update auto_agents_example.py with new features and CLI examples
- Add orchestrator-workers and evaluator-optimizer patterns to README CLI section
- Update pattern options table with all 6 patterns
- Add documentation links to examples
2025-12-19 09:54:05 +00:00
Mervin Praison 054aea8f66 Add MCP Registry badge and ignore MCP tokens 2025-12-18 22:18:33 +00:00
Mervin Praison 34f1293cfc Release v2.3.42 2025-12-18 22:13:08 +00:00
Mervin Praison 7c1e32afc0 upate 2025-12-18 22:11:02 +00:00
Mervin Praison 1125c835ec Release v2.3.41 2025-12-18 22:09:58 +00:00
Mervin Praison 889d04a843 Release v2.3.40 2025-12-18 22:07:54 +00:00
Mervin Praison fba811c477 Release v2.3.39 2025-12-18 21:54:19 +00:00
Mervin Praison df687d86c3 Release v2.3.38 2025-12-18 21:26:34 +00:00
Mervin Praison afa37b0782 Release v2.3.37 2025-12-18 17:41:16 +00:00
Mervin Praison 43a15e6049 Release v2.3.36 2025-12-18 16:42:58 +00:00
Mervin Praison 8bf5a08246 Release v2.3.35 2025-12-18 16:28:02 +00:00
Mervin Praison aa81094ac3 Release v2.3.34 2025-12-18 14:55:33 +00:00
Mervin Praison c74fd7e925 Release v2.3.33 2025-12-18 14:41:36 +00:00
Mervin Praison 249acf7965 docs: Add performance benchmarks and auto-update functionality
Add performance comparison table to README showing PraisonAI as fastest framework for agent instantiation. Include benchmarks for LangGraph and CrewAI frameworks. Implement auto-save functionality to generate BENCHMARK_RESULTS.md and update README.md with latest benchmark results.
2025-12-18 13:30:06 +00:00
259 changed files with 52543 additions and 762 deletions

No files matched your search

+4 -1
View File
@@ -77,4 +77,7 @@ agents/praisonaiagents/praisonaiagents.egg-info
.qodo
CopilotKit*
.gemini
issue_details.json
issue_details.json
# MCP Registry tokens
.mcpregistry_*
+383 -98
View File
@@ -6,10 +6,13 @@
</picture>
</p>
<!-- mcp-name: io.github.MervinPraison/praisonai -->
<p align="center">
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://static.pepy.tech/badge/PraisonAI" alt="Total Downloads" /></a>
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://img.shields.io/github/v/release/MervinPraison/PraisonAI" alt="Latest Stable Version" /></a>
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License" /></a>
<a href="https://registry.modelcontextprotocol.io/servers/io.github.MervinPraison/praisonai"><img src="https://img.shields.io/badge/MCP-Registry-blue" alt="MCP Registry" /></a>
</p>
<div align="center">
@@ -34,6 +37,7 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
## 📑 Table of Contents
- [⚡ Performance](#-performance)
- [🚀 Quick Start](#-quick-start)
- [📦 Installation](#-installation)
- [Python SDK](#python-sdk)
@@ -59,6 +63,7 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
- [Planning & Workflows](#planning--workflows)
- [Specialised Agents](#specialised-agents)
- [MCP Protocol](#mcp-protocol)
- [A2A & A2UI Protocols](#a2a--a2ui-protocols)
- [Safety & Control](#safety--control)
- [Developer Tools](#developer-tools)
- [📊 Process Types & Patterns](#-process-types--patterns)
@@ -73,6 +78,32 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
---
## ⚡ Performance
PraisonAI Agents is the **fastest AI agent framework** for agent instantiation.
| Framework | Avg Time (μs) | Relative |
|-----------|---------------|----------|
| **PraisonAI** | **3.77** | **1.00x (fastest)** |
| OpenAI Agents SDK | 5.26 | 1.39x |
| Agno | 5.64 | 1.49x |
| 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>
```bash
cd praisonai-agents
python benchmarks/simple_benchmark.py
```
</details>
---
## 🚀 Quick Start
Get started with PraisonAI in under 1 minute:
@@ -140,12 +171,13 @@ npm install praisonai
| Feature | Code | Docs |
|---------|:----:|:----:|
| 🚀 Single Agent | [Example](examples/python/agents/single-agent.py) | [📖](https://docs.praison.ai/agents/single) |
| 🤝 Multi Agents | [Example](examples/python/general/mini_agents_example.py) | [📖](https://docs.praison.ai/concepts/agents) |
| 🤖 Auto Agents | [Example](examples/python/general/auto_agents_example.py) | [📖](https://docs.praison.ai/features/autoagents) |
| 🔄 Self Reflection AI Agents | [Example](examples/python/concepts/self-reflection-details.py) | [📖](https://docs.praison.ai/features/selfreflection) |
| 🧠 Reasoning AI Agents | [Example](examples/python/concepts/reasoning-extraction.py) | [📖](https://docs.praison.ai/features/reasoning) |
| 👁️ Multi Modal AI Agents | [Example](examples/python/general/multimodal.py) | [📖](https://docs.praison.ai/features/multimodal) |
| **🤖 Core Agents** | | |
| ↳ Single Agent | [Example](examples/python/agents/single-agent.py) | [📖](https://docs.praison.ai/agents/single) |
| ↳ Multi Agents | [Example](examples/python/general/mini_agents_example.py) | [📖](https://docs.praison.ai/concepts/agents) |
| ↳ Auto Agents | [Example](examples/python/general/auto_agents_example.py) | [📖](https://docs.praison.ai/features/autoagents) |
| ↳ Self Reflection AI Agents | [Example](examples/python/concepts/self-reflection-details.py) | [📖](https://docs.praison.ai/features/selfreflection) |
| ↳ Reasoning AI Agents | [Example](examples/python/concepts/reasoning-extraction.py) | [📖](https://docs.praison.ai/features/reasoning) |
| ↳ Multi Modal AI Agents | [Example](examples/python/general/multimodal.py) | [📖](https://docs.praison.ai/features/multimodal) |
| **🔄 Workflows** | | |
| ↳ Simple Workflow | [Example](examples/python/workflows/simple_workflow.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow with Agents | [Example](examples/python/workflows/workflow_with_agents.py) | [📖](https://docs.praison.ai/features/workflows) |
@@ -157,68 +189,105 @@ npm install praisonai
| ↳ Workflow Branching | [Example](examples/python/workflows/workflow_branching.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Early Stop | [Example](examples/python/workflows/workflow_early_stop.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Checkpoints | [Example](examples/python/workflows/workflow_checkpoints.py) | [📖](https://docs.praison.ai/features/workflows) |
| 📚 Add Custom Knowledge | [Example](examples/python/concepts/knowledge-agents.py) | [📖](https://docs.praison.ai/features/knowledge) |
| 🧠 Memory (Short & Long Term) | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| 📄 Chat with PDF Agents | [Example](examples/python/concepts/chat-with-pdf.py) | [📖](https://docs.praison.ai/features/chat-with-pdf) |
| 💻 Code Interpreter Agents | [Example](examples/python/agents/code-agent.py) | [📖](https://docs.praison.ai/features/codeagent) |
| 📚 RAG Agents | [Example](examples/python/concepts/rag-agents.py) | [📖](https://docs.praison.ai/features/rag) |
| 🤔 Async & Parallel Processing | [Example](examples/python/general/async_example.py) | [📖](https://docs.praison.ai/features/async) |
| 🔢 Math Agents | [Example](examples/python/agents/math-agent.py) | [📖](https://docs.praison.ai/features/mathagent) |
| 🎯 Structured Output Agents | [Example](examples/python/general/structured_agents_example.py) | [📖](https://docs.praison.ai/features/structured) |
| 🔗 LangChain Integrated Agents | [Example](examples/python/general/langchain_example.py) | [📖](https://docs.praison.ai/features/langchain) |
| 📞 Callback Agents | [Example](examples/python/general/advanced-callback-systems.py) | [📖](https://docs.praison.ai/features/callbacks) |
| 🛠️ 100+ Custom Tools | [Example](examples/python/general/tools_example.py) | [📖](https://docs.praison.ai/tools/tools) |
| 📄 YAML Configuration | [Example](examples/cookbooks/yaml/secondary_market_research_agents.yaml) | [📖](https://docs.praison.ai/developers/agents-playbook) |
| 💯 100+ LLM Support | [Example](examples/python/providers/openai/openai_gpt4_example.py) | [📖](https://docs.praison.ai/models) |
| 🔬 Deep Research Agents | [Example](examples/python/agents/research-agent.py) | [📖](https://docs.praison.ai/agents/deep-research) |
| 🔄 Query Rewriter Agent | [Example](#5-query-rewriter-agent) | [📖](https://docs.praison.ai/agents/query-rewriter) |
| 🌐 Native Web Search | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/agents/websearch) |
| 📥 Web Fetch (Anthropic) | [Example](#web-search-web-fetch--prompt-caching) | [📖](https://docs.praison.ai/features/model-capabilities) |
| 💾 Prompt Caching | [Example](#web-search-web-fetch--prompt-caching) | [📖](https://docs.praison.ai/features/model-capabilities) |
| 🧠 Claude Memory Tool | [Example](#claude-memory-tool-cli) | [📖](https://docs.praison.ai/features/claude-memory-tool) |
| 💾 File-Based Memory | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| 🔍 Built-in Search Tools | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/tools/tavily) |
| 📋 Planning Mode | [Example](examples/python/agents/planning-agent.py) | [📖](https://docs.praison.ai/features/planning-mode) |
| 🔧 Planning Tools | [Example](#3-agent-with-planning-mode) | [📖](https://docs.praison.ai/features/planning-mode) |
| 🧠 Planning Reasoning | [Example](#3-agent-with-planning-mode) | [📖](https://docs.praison.ai/features/planning-mode) |
| 🔌 MCP Transports | [Example](examples/python/mcp/mcp-transports-overview.py) | [📖](https://docs.praison.ai/mcp/transports) |
| 🌐 WebSocket MCP | [Example](examples/python/mcp/websocket-mcp.py) | [📖](https://docs.praison.ai/mcp/sse-transport) |
| 🔐 MCP Security | [Example](examples/python/mcp/mcp-security.py) | [📖](https://docs.praison.ai/mcp/transports) |
| 🔄 MCP Resumability | [Example](examples/python/mcp/mcp-resumability.py) | [📖](https://docs.praison.ai/mcp/sse-transport) |
| ⚡ Fast Context | [Example](examples/context/00_agent_fast_context_basic.py) | [📖](https://docs.praison.ai/features/fast-context) |
| 🖼️ Image Generation Agent | [Example](examples/python/image/image-agent.py) | [📖](https://docs.praison.ai/features/image-generation) |
| 📷 Image to Text Agent | [Example](examples/python/agents/image-to-text-agent.py) | [📖](https://docs.praison.ai/agents/image-to-text) |
| 🎬 Video Agent | [Example](examples/python/agents/video-agent.py) | [📖](https://docs.praison.ai/agents/video) |
| 📊 Data Analyst Agent | [Example](examples/python/agents/data-analyst-agent.py) | [📖](https://docs.praison.ai/agents/data-analyst) |
| 💰 Finance Agent | [Example](examples/python/agents/finance-agent.py) | [📖](https://docs.praison.ai/agents/finance) |
| 🛒 Shopping Agent | [Example](examples/python/agents/shopping-agent.py) | [📖](https://docs.praison.ai/agents/shopping) |
| ⭐ Recommendation Agent | [Example](examples/python/agents/recommendation-agent.py) | [📖](https://docs.praison.ai/agents/recommendation) |
| 📖 Wikipedia Agent | [Example](examples/python/agents/wikipedia-agent.py) | [📖](https://docs.praison.ai/agents/wikipedia) |
| 💻 Programming Agent | [Example](examples/python/agents/programming-agent.py) | [📖](https://docs.praison.ai/agents/programming) |
| 📝 Markdown Agent | [Example](examples/python/agents/markdown-agent.py) | [📖](https://docs.praison.ai/agents/markdown) |
| 📝 Prompt Expander Agent | [Example](#prompt-expansion) | [📖](https://docs.praison.ai/agents/prompt-expander) |
| 🔀 Router Agent | [Example](examples/python/agents/router-agent-cost-optimization.py) | [📖](https://docs.praison.ai/features/routing) |
| ⛓️ Prompt Chaining | [Example](examples/python/general/prompt_chaining.py) | [📖](https://docs.praison.ai/features/promptchaining) |
| 🔍 Evaluator Optimiser | [Example](examples/python/general/evaluator-optimiser.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| 👷 Orchestrator Workers | [Example](examples/python/general/orchestrator-workers.py) | [📖](https://docs.praison.ai/features/orchestrator-worker) |
| ⚡ Parallelisation | [Example](examples/python/general/parallelisation.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| 🔁 Repetitive Agents | [Example](examples/python/concepts/repetitive-agents.py) | [📖](https://docs.praison.ai/features/repetitive) |
| 🤝 Agent Handoffs | [Example](examples/python/handoff/handoff_basic.py) | [📖](https://docs.praison.ai/features/handoffs) |
| 🛡️ Guardrails | [Example](examples/python/guardrails/comprehensive-guardrails-example.py) | [📖](https://docs.praison.ai/features/guardrails) |
| 💬 Sessions Management | [Example](examples/python/sessions/comprehensive-session-management.py) | [📖](https://docs.praison.ai/features/sessions) |
| ✅ Human Approval | [Example](examples/python/general/human_approval_example.py) | [📖](https://docs.praison.ai/features/approval) |
| 🔄 Stateful Agents | [Example](examples/python/stateful/workflow-state-example.py) | [📖](https://docs.praison.ai/features/stateful-agents) |
| 🤖 Autonomous Workflow | [Example](examples/python/general/autonomous-agent.py) | [📖](https://docs.praison.ai/features/autonomous-workflow) |
| 📜 Rules & Instructions | [Example](#6-rules--instructions) | [📖](https://docs.praison.ai/features/rules) |
| 🪝 Hooks | [Example](#9-hooks) | [📖](https://docs.praison.ai/features/hooks) |
| 📈 Telemetry | [Example](examples/python/telemetry/production-telemetry-example.py) | [📖](https://docs.praison.ai/features/telemetry) |
| 📹 Camera Integration | [Example](examples/python/camera/) | [📖](https://docs.praison.ai/features/camera-integration) |
| 📄 Project Docs (.praison/docs/) | [Example](#docs-cli) | [📖](https://docs.praison.ai/cli/docs) |
| 🔌 MCP Config Management | [Example](#mcp-config-cli) | [📖](https://docs.praison.ai/cli/mcp) |
| 💬 AI Commit Messages | [Example](#ai-commit-cli) | [📖](https://docs.praison.ai/cli/commit) |
| @ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/cli/mentions) |
| 💾 Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| 📜 History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| **💻 Code & Development** | | |
| ↳ Code Interpreter Agents | [Example](examples/python/agents/code-agent.py) | [📖](https://docs.praison.ai/features/codeagent) |
| ↳ AI Code Editing Tools | [Example](examples/python/code/code_editing_example.py) | [📖](https://docs.praison.ai/code/editing) |
| ↳ External Agents (All) | [Example](examples/python/code/external_agents_example.py) | [📖](https://docs.praison.ai/code/external-agents) |
| ↳ Claude Code CLI | [Example](examples/python/code/claude_code_example.py) | [📖](https://docs.praison.ai/code/claude-code) |
| ↳ Gemini CLI | [Example](examples/python/code/gemini_cli_example.py) | [📖](https://docs.praison.ai/code/gemini-cli) |
| ↳ Codex CLI | [Example](examples/python/code/codex_cli_example.py) | [📖](https://docs.praison.ai/code/codex-cli) |
| ↳ Cursor CLI | [Example](examples/python/code/cursor_cli_example.py) | [📖](https://docs.praison.ai/code/cursor-cli) |
| **🧠 Memory & Knowledge** | | |
| ↳ Memory (Short & Long Term) | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| ↳ File-Based Memory | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| ↳ Claude Memory Tool | [Example](#claude-memory-tool-cli) | [📖](https://docs.praison.ai/features/claude-memory-tool) |
| ↳ Add Custom Knowledge | [Example](examples/python/concepts/knowledge-agents.py) | [📖](https://docs.praison.ai/features/knowledge) |
| ↳ RAG Agents | [Example](examples/python/concepts/rag-agents.py) | [📖](https://docs.praison.ai/features/rag) |
| ↳ Chat with PDF Agents | [Example](examples/python/concepts/chat-with-pdf.py) | [📖](https://docs.praison.ai/features/chat-with-pdf) |
| **🔬 Research & Intelligence** | | |
| ↳ Deep Research Agents | [Example](examples/python/agents/research-agent.py) | [📖](https://docs.praison.ai/agents/deep-research) |
| ↳ Query Rewriter Agent | [Example](#5-query-rewriter-agent) | [📖](https://docs.praison.ai/agents/query-rewriter) |
| ↳ Native Web Search | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/agents/websearch) |
| ↳ Built-in Search Tools | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/tools/tavily) |
| ↳ Unified Web Search | [Example](src/praisonai-agents/examples/web_search_example.py) | [📖](https://docs.praison.ai/tools/web-search) |
| ↳ Web Fetch (Anthropic) | [Example](#web-search-web-fetch--prompt-caching) | [📖](https://docs.praison.ai/features/model-capabilities) |
| **📋 Planning & Execution** | | |
| ↳ Planning Mode | [Example](examples/python/agents/planning-agent.py) | [📖](https://docs.praison.ai/features/planning-mode) |
| ↳ Planning Tools | [Example](#3-agent-with-planning-mode) | [📖](https://docs.praison.ai/features/planning-mode) |
| ↳ Planning Reasoning | [Example](#3-agent-with-planning-mode) | [📖](https://docs.praison.ai/features/planning-mode) |
| ↳ Prompt Chaining | [Example](examples/python/general/prompt_chaining.py) | [📖](https://docs.praison.ai/features/promptchaining) |
| ↳ Evaluator Optimiser | [Example](examples/python/general/evaluator-optimiser.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| ↳ Orchestrator Workers | [Example](examples/python/general/orchestrator-workers.py) | [📖](https://docs.praison.ai/features/orchestrator-worker) |
| **👥 Specialized Agents** | | |
| ↳ Data Analyst Agent | [Example](examples/python/agents/data-analyst-agent.py) | [📖](https://docs.praison.ai/agents/data-analyst) |
| ↳ Finance Agent | [Example](examples/python/agents/finance-agent.py) | [📖](https://docs.praison.ai/agents/finance) |
| ↳ Shopping Agent | [Example](examples/python/agents/shopping-agent.py) | [📖](https://docs.praison.ai/agents/shopping) |
| ↳ Recommendation Agent | [Example](examples/python/agents/recommendation-agent.py) | [📖](https://docs.praison.ai/agents/recommendation) |
| ↳ Wikipedia Agent | [Example](examples/python/agents/wikipedia-agent.py) | [📖](https://docs.praison.ai/agents/wikipedia) |
| ↳ Programming Agent | [Example](examples/python/agents/programming-agent.py) | [📖](https://docs.praison.ai/agents/programming) |
| ↳ Math Agents | [Example](examples/python/agents/math-agent.py) | [📖](https://docs.praison.ai/features/mathagent) |
| ↳ Markdown Agent | [Example](examples/python/agents/markdown-agent.py) | [📖](https://docs.praison.ai/agents/markdown) |
| ↳ Prompt Expander Agent | [Example](#prompt-expansion) | [📖](https://docs.praison.ai/agents/prompt-expander) |
| **🎨 Media & Multimodal** | | |
| ↳ Image Generation Agent | [Example](examples/python/image/image-agent.py) | [📖](https://docs.praison.ai/features/image-generation) |
| ↳ Image to Text Agent | [Example](examples/python/agents/image-to-text-agent.py) | [📖](https://docs.praison.ai/agents/image-to-text) |
| ↳ Video Agent | [Example](examples/python/agents/video-agent.py) | [📖](https://docs.praison.ai/agents/video) |
| ↳ Camera Integration | [Example](examples/python/camera/) | [📖](https://docs.praison.ai/features/camera-integration) |
| **🔌 Protocols & Integration** | | |
| ↳ MCP Transports | [Example](examples/python/mcp/mcp-transports-overview.py) | [📖](https://docs.praison.ai/mcp/transports) |
| ↳ WebSocket MCP | [Example](examples/python/mcp/websocket-mcp.py) | [📖](https://docs.praison.ai/mcp/sse-transport) |
| ↳ MCP Security | [Example](examples/python/mcp/mcp-security.py) | [📖](https://docs.praison.ai/mcp/transports) |
| ↳ MCP Resumability | [Example](examples/python/mcp/mcp-resumability.py) | [📖](https://docs.praison.ai/mcp/sse-transport) |
| ↳ MCP Config Management | [Example](#mcp-config-cli) | [📖](https://docs.praison.ai/docs/cli/mcp) |
| ↳ LangChain Integrated Agents | [Example](examples/python/general/langchain_example.py) | [📖](https://docs.praison.ai/features/langchain) |
| **🛡️ Safety & Control** | | |
| ↳ Guardrails | [Example](examples/python/guardrails/comprehensive-guardrails-example.py) | [📖](https://docs.praison.ai/features/guardrails) |
| ↳ Human Approval | [Example](examples/python/general/human_approval_example.py) | [📖](https://docs.praison.ai/features/approval) |
| ↳ Rules & Instructions | [Example](#6-rules--instructions) | [📖](https://docs.praison.ai/features/rules) |
| **⚙️ Advanced Features** | | |
| ↳ Async & Parallel Processing | [Example](examples/python/general/async_example.py) | [📖](https://docs.praison.ai/features/async) |
| ↳ Parallelisation | [Example](examples/python/general/parallelisation.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| ↳ Repetitive Agents | [Example](examples/python/concepts/repetitive-agents.py) | [📖](https://docs.praison.ai/features/repetitive) |
| ↳ Agent Handoffs | [Example](examples/python/handoff/handoff_basic.py) | [📖](https://docs.praison.ai/features/handoffs) |
| ↳ Stateful Agents | [Example](examples/python/stateful/workflow-state-example.py) | [📖](https://docs.praison.ai/features/stateful-agents) |
| ↳ Autonomous Workflow | [Example](examples/python/general/autonomous-agent.py) | [📖](https://docs.praison.ai/features/autonomous-workflow) |
| ↳ Structured Output Agents | [Example](examples/python/general/structured_agents_example.py) | [📖](https://docs.praison.ai/features/structured) |
| ↳ Model Router | [Example](examples/python/agents/router-agent-cost-optimization.py) | [📖](https://docs.praison.ai/features/model-router) |
| ↳ Prompt Caching | [Example](#web-search-web-fetch--prompt-caching) | [📖](https://docs.praison.ai/features/model-capabilities) |
| ↳ Fast Context | [Example](examples/context/00_agent_fast_context_basic.py) | [📖](https://docs.praison.ai/features/fast-context) |
| **🛠️ Tools & Configuration** | | |
| ↳ 100+ Custom Tools | [Example](examples/python/general/tools_example.py) | [📖](https://docs.praison.ai/tools/tools) |
| ↳ YAML Configuration | [Example](examples/cookbooks/yaml/secondary_market_research_agents.yaml) | [📖](https://docs.praison.ai/developers/agents-playbook) |
| ↳ 100+ LLM Support | [Example](examples/python/providers/openai/openai_gpt4_example.py) | [📖](https://docs.praison.ai/models) |
| ↳ Callback Agents | [Example](examples/python/general/advanced-callback-systems.py) | [📖](https://docs.praison.ai/features/callbacks) |
| ↳ Hooks | [Example](#9-hooks) | [📖](https://docs.praison.ai/features/hooks) |
| **📊 Monitoring & Management** | | |
| ↳ Sessions Management | [Example](examples/python/sessions/comprehensive-session-management.py) | [📖](https://docs.praison.ai/features/sessions) |
| ↳ Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| ↳ History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| ↳ Telemetry | [Example](examples/python/telemetry/production-telemetry-example.py) | [📖](https://docs.praison.ai/features/telemetry) |
| ↳ Project Docs (.praison/docs/) | [Example](#docs-cli) | [📖](https://docs.praison.ai/docs/cli/docs) |
| ↳ AI Commit Messages | [Example](#ai-commit-cli) | [📖](https://docs.praison.ai/docs/cli/commit) |
| ↳ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/docs/cli/mentions) |
| **🖥️ CLI Features** | | |
| ↳ Slash Commands | [Example](examples/python/cli/slash_commands_example.py) | [📖](https://docs.praison.ai/docs/cli/slash-commands) |
| ↳ Autonomy Modes | [Example](examples/python/cli/autonomy_modes_example.py) | [📖](https://docs.praison.ai/docs/cli/autonomy-modes) |
| ↳ Cost Tracking | [Example](examples/python/cli/cost_tracking_example.py) | [📖](https://docs.praison.ai/docs/cli/cost-tracking) |
| ↳ Repository Map | [Example](examples/python/cli/repo_map_example.py) | [📖](https://docs.praison.ai/docs/cli/repo-map) |
| ↳ Interactive TUI | [Example](examples/python/cli/interactive_tui_example.py) | [📖](https://docs.praison.ai/docs/cli/interactive-tui) |
| ↳ Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/docs/cli/git-integration) |
| ↳ Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/docs/cli/sandbox-execution) |
| ↳ CLI Compare | [Example](examples/compare/cli_compare_basic.py) | [📖](https://docs.praison.ai/docs/cli/compare) |
| **🧪 Evaluation** | | |
| ↳ Accuracy Evaluation | [Example](examples/eval/accuracy_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| ↳ Performance Evaluation | [Example](examples/eval/performance_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| ↳ Reliability Evaluation | [Example](examples/eval/reliability_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| ↳ Criteria Evaluation | [Example](examples/eval/criteria_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| **🎯 Agent Skills** | | |
| ↳ Skills Management | [Example](examples/skills/basic_skill_usage.py) | [📖](https://docs.praison.ai/features/skills) |
| ↳ Custom Skills | [Example](examples/skills/custom_skill_example.py) | [📖](https://docs.praison.ai/features/skills) |
| **⏰ 24/7 Scheduling** | | |
| ↳ Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/docs/cli/scheduler) |
---
@@ -925,6 +994,32 @@ export OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
praisonai --auto create a movie script about Robots in Mars
```
### Interactive Mode CLI:
```bash
# Start interactive terminal mode (inspired by Gemini CLI, Codex CLI, Claude Code)
praisonai --interactive
praisonai -i
# Features:
# - Streaming responses (no boxes)
# - Built-in tools: read_file, write_file, list_files, execute_command, internet_search
# - Slash commands: /help, /exit, /tools, /clear
# Chat mode - single prompt with interactive style (for testing/scripting)
# Use --chat (or --chat-mode for backward compatibility)
praisonai "list files in current folder" --chat
praisonai "search the web for AI news" --chat
praisonai "read README.md" --chat
```
### Chat UI (Web Interface):
```bash
# Start web-based Chainlit chat interface (requires praisonai[chat])
pip install "praisonai[chat]"
praisonai chat
# Opens browser at http://localhost:8084
```
### Query Rewriting (works with any command):
```bash
# Rewrite query for better results (uses QueryRewriterAgent)
@@ -985,6 +1080,20 @@ praisonai "Complex analysis task" --planning --planning-reasoning
praisonai "Task" --planning --auto-approve-plan
```
### Tool Approval CLI:
```bash
# Auto-approve ALL tool executions (use with caution!)
praisonai "run ls command" --trust
# Auto-approve tools up to a risk level (prompt for higher)
# Levels: low, medium, high, critical
praisonai "write to file" --approve-level high # Prompts for critical tools only
praisonai "task" --approve-level medium # Prompts for high and critical
# Default behavior (no flags): prompts for all dangerous tools
praisonai "run shell command" # Will prompt for approval
```
### Memory CLI:
```bash
# Enable memory for agent (persists across sessions)
@@ -1091,6 +1200,15 @@ praisonai workflow auto "Research AI trends" --pattern parallel
# Generate routing workflow (classifier routes to specialists)
praisonai workflow auto "Build a chatbot" --pattern routing
# Generate orchestrator-workers workflow (central orchestrator delegates)
praisonai workflow auto "Comprehensive market analysis" --pattern orchestrator-workers
# Generate evaluator-optimizer workflow (iterative refinement)
praisonai workflow auto "Write and refine article" --pattern evaluator-optimizer
# Specify output file
praisonai workflow auto "Build a chatbot" --pattern routing
# Specify output file
praisonai workflow auto "Research AI" --pattern sequential --output my_workflow.yaml
```
@@ -1100,7 +1218,7 @@ praisonai workflow auto "Research AI" --pattern sequential --output my_workflow.
|------|-------------|
| `--workflow-var key=value` | Set workflow variable (can be repeated) |
| `--var key=value` | Set variable for YAML workflows |
| `--pattern <pattern>` | Pattern for auto-generation (sequential, parallel, routing, loop) |
| `--pattern <pattern>` | Pattern for auto-generation (sequential, parallel, routing, loop, orchestrator-workers, evaluator-optimizer) |
| `--output <file>` | Output file for auto-generation |
| `--llm <model>` | LLM model (e.g., openai/gpt-4o-mini) |
| `--tools <tools>` | Tools (comma-separated, e.g., tavily) |
@@ -1146,6 +1264,21 @@ praisonai "Analyze this data" --metrics
praisonai "Complex task" --metrics --planning
```
### Scheduler CLI:
```bash
praisonai schedule start <name> "task" --interval hourly
praisonai schedule list
praisonai schedule logs <name> [--follow]
praisonai schedule stop <name>
praisonai schedule restart <name>
praisonai schedule delete <name>
praisonai schedule describe <name>
praisonai schedule save <name> [file.yaml]
praisonai schedule "task" --interval hourly # foreground mode
praisonai schedule agents.yaml # foreground mode
```
### Image Processing CLI:
```bash
# Process images with vision-based tasks
@@ -1332,8 +1465,13 @@ praisonai "Complex analysis" --router --router-provider anthropic
# Router automatically selects:
# - Simple tasks → gpt-4o-mini, claude-3-haiku
# - Complex tasks → gpt-4-turbo, claude-3-opus
# Create workflow with model routing template
praisonai workflow create --template model-routing --output my_workflow.yaml
```
Custom models can be configured in `agents.yaml`. See [Model Router Docs](https://docs.praison.ai/features/model-router) for details.
### Flow Display CLI:
```bash
# Enable visual workflow tracking
@@ -1385,13 +1523,110 @@ praisonai mcp help
### AI Commit CLI:
```bash
# Generate AI commit message for staged changes
# Full auto mode: stage all, security check, commit, and push
praisonai commit -a
# Interactive mode (requires git add first)
praisonai commit
# Generate, commit, and push
# Interactive with auto-push
praisonai commit --push
# Skip security check (not recommended)
praisonai commit -a --no-verify
```
**Features:**
- 🤖 AI-generated conventional commit messages
- 🔒 Built-in security scanning (API keys, passwords, secrets, sensitive files)
- 📦 Auto-staging with `-a` flag
- 🚀 Auto-push in full auto mode
- ✏️ Edit message before commit in interactive mode
**Security Detection:**
- API keys, secrets, tokens (AWS, GitHub, GitLab, Slack)
- Passwords and private keys
- Sensitive files (`.env`, `id_rsa`, `.pem`, `.key`, etc.)
### Serve CLI (API Server):
```bash
# Start API server for agents defined in YAML
praisonai serve agents.yaml
# With custom port and host
praisonai serve agents.yaml --port 8005 --host 0.0.0.0
# Alternative flag style
praisonai agents.yaml --serve
# The server provides:
# POST /agents - Run all agents sequentially
# POST /agents/{name} - Run specific agent (e.g., /agents/researcher)
# GET /agents/list - List available agents
```
### n8n Integration CLI:
```bash
# Export workflow to n8n and open in browser
praisonai agents.yaml --n8n
# With custom n8n URL
praisonai agents.yaml --n8n --n8n-url http://localhost:5678
# Set N8N_API_KEY for auto-import
export N8N_API_KEY="your-api-key"
praisonai agents.yaml --n8n
```
### External Agents CLI:
Use external AI coding CLI tools (Claude Code, Gemini CLI, Codex CLI, Cursor CLI) as agent tools:
```bash
# Use Claude Code for coding tasks
praisonai "Refactor the auth module" --external-agent claude
# Use Gemini CLI for code analysis
praisonai "Analyze codebase architecture" --external-agent gemini
# Use OpenAI Codex CLI
praisonai "Fix all bugs in src/" --external-agent codex
# Use Cursor CLI
praisonai "Add comprehensive tests" --external-agent cursor
```
**Python API:**
```python
from praisonai.integrations import (
ClaudeCodeIntegration,
GeminiCLIIntegration,
CodexCLIIntegration,
CursorCLIIntegration
)
# Create integration
claude = ClaudeCodeIntegration(workspace="/project")
# Execute a coding task
result = await claude.execute("Refactor the auth module")
# Use as agent tool
from praisonai import Agent
tool = claude.as_tool()
agent = Agent(tools=[tool])
```
**Environment Variables:**
```bash
export ANTHROPIC_API_KEY=your-key # Claude Code
export GEMINI_API_KEY=your-key # Gemini CLI
export OPENAI_API_KEY=your-key # Codex CLI
export CURSOR_API_KEY=your-key # Cursor CLI
```
See [External Agents Documentation](https://docs.praison.ai/code/external-agents) for more details.
### @Mentions in Prompts:
```bash
# Include file content in prompt
@@ -1571,36 +1806,78 @@ server.run() # stdio for Claude Desktop
| Security | Origin validation, DNS rebinding prevention |
| WebSocket | Auto-reconnect with exponential backoff |
## A2A (Agent2Agent Protocol)
PraisonAI supports the [A2A Protocol](https://a2a-protocol.org) for agent-to-agent communication, enabling your agents to be discovered and collaborate with other AI agents.
### A2A Server (Expose Agent as A2A Server)
```python
from praisonaiagents import Agent, A2A
from fastapi import FastAPI
# Create an agent with tools
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Results for: {query}"
agent = Agent(
name="Research Assistant",
role="Research Analyst",
goal="Help users research topics",
tools=[search_web]
)
# Expose as A2A Server
a2a = A2A(agent=agent, url="http://localhost:8000/a2a")
app = FastAPI()
app.include_router(a2a.get_router())
# Run: uvicorn app:app --reload
# Agent Card: GET /.well-known/agent.json
# Status: GET /status
```
### A2A Features
| Feature | Description |
|---------|-------------|
| Agent Card | JSON metadata for agent discovery |
| Skills Extraction | Auto-generate skills from tools |
| Task Management | Stateful task lifecycle |
| Streaming | SSE streaming for real-time updates |
> **Documentation**: [docs.praison.ai/a2a](https://docs.praison.ai/a2a) | **Examples**: [examples/python/a2a](https://github.com/MervinPraison/PraisonAI/tree/main/examples/python/a2a)
## CLI Features
| Feature | Docs |
|---------|:----:|
| 🔄 Query Rewrite - RAG optimization | [📖](https://docs.praison.ai/cli/query-rewrite) |
| 🔬 Deep Research - Automated research | [📖](https://docs.praison.ai/cli/deep-research) |
| 📋 Planning - Step-by-step execution | [📖](https://docs.praison.ai/cli/planning) |
| 💾 Memory - Persistent agent memory | [📖](https://docs.praison.ai/cli/memory) |
| 📜 Rules - Auto-discovered instructions | [📖](https://docs.praison.ai/cli/rules) |
| 🔄 Workflow - Multi-step workflows | [📖](https://docs.praison.ai/cli/workflow) |
| 🪝 Hooks - Event-driven actions | [📖](https://docs.praison.ai/cli/hooks) |
| 🧠 Claude Memory - Anthropic memory tool | [📖](https://docs.praison.ai/cli/claude-memory) |
| 🛡️ Guardrail - Output validation | [📖](https://docs.praison.ai/cli/guardrail) |
| 📊 Metrics - Token usage tracking | [📖](https://docs.praison.ai/cli/metrics) |
| 🖼️ Image - Vision processing | [📖](https://docs.praison.ai/cli/image) |
| 📡 Telemetry - Usage monitoring | [📖](https://docs.praison.ai/cli/telemetry) |
| 🔌 MCP - Model Context Protocol | [📖](https://docs.praison.ai/cli/mcp) |
| ⚡ Fast Context - Codebase search | [📖](https://docs.praison.ai/cli/fast-context) |
| 📚 Knowledge - RAG management | [📖](https://docs.praison.ai/cli/knowledge) |
| 💬 Session - Conversation management | [📖](https://docs.praison.ai/cli/session) |
| 🔧 Tools - Tool discovery | [📖](https://docs.praison.ai/cli/tools) |
| 🤝 Handoff - Agent delegation | [📖](https://docs.praison.ai/cli/handoff) |
| 🧠 Auto Memory - Memory extraction | [📖](https://docs.praison.ai/cli/auto-memory) |
| 📋 Todo - Task management | [📖](https://docs.praison.ai/cli/todo) |
| 🎯 Router - Smart model selection | [📖](https://docs.praison.ai/cli/router) |
| 📈 Flow Display - Visual workflow | [📖](https://docs.praison.ai/cli/flow-display) |
| ✨ Prompt Expansion - Detailed prompts | [📖](https://docs.praison.ai/cli/prompt-expansion) |
| 🌐 Web Search - Real-time search | [📖](https://docs.praison.ai/cli/web-search) |
| 📥 Web Fetch - URL content retrieval | [📖](https://docs.praison.ai/cli/web-fetch) |
| 💾 Prompt Caching - Cost reduction | [📖](https://docs.praison.ai/cli/prompt-caching) |
| 🔄 Query Rewrite - RAG optimization | [📖](https://docs.praison.ai/docs/cli/query-rewrite) |
| 🔬 Deep Research - Automated research | [📖](https://docs.praison.ai/docs/cli/deep-research) |
| 📋 Planning - Step-by-step execution | [📖](https://docs.praison.ai/docs/cli/planning) |
| 💾 Memory - Persistent agent memory | [📖](https://docs.praison.ai/docs/cli/memory) |
| 📜 Rules - Auto-discovered instructions | [📖](https://docs.praison.ai/docs/cli/rules) |
| 🔄 Workflow - Multi-step workflows | [📖](https://docs.praison.ai/docs/cli/workflow) |
| 🪝 Hooks - Event-driven actions | [📖](https://docs.praison.ai/docs/cli/hooks) |
| 🧠 Claude Memory - Anthropic memory tool | [📖](https://docs.praison.ai/docs/cli/claude-memory) |
| 🛡️ Guardrail - Output validation | [📖](https://docs.praison.ai/docs/cli/guardrail) |
| 📊 Metrics - Token usage tracking | [📖](https://docs.praison.ai/docs/cli/metrics) |
| 🖼️ Image - Vision processing | [📖](https://docs.praison.ai/docs/cli/image) |
| 📡 Telemetry - Usage monitoring | [📖](https://docs.praison.ai/docs/cli/telemetry) |
| 🔌 MCP - Model Context Protocol | [📖](https://docs.praison.ai/docs/cli/mcp) |
| ⚡ Fast Context - Codebase search | [📖](https://docs.praison.ai/docs/cli/fast-context) |
| 📚 Knowledge - RAG management | [📖](https://docs.praison.ai/docs/cli/knowledge) |
| 💬 Session - Conversation management | [📖](https://docs.praison.ai/docs/cli/session) |
| 🔧 Tools - Tool discovery | [📖](https://docs.praison.ai/docs/cli/tools) |
| 🤝 Handoff - Agent delegation | [📖](https://docs.praison.ai/docs/cli/handoff) |
| 🧠 Auto Memory - Memory extraction | [📖](https://docs.praison.ai/docs/cli/auto-memory) |
| 📋 Todo - Task management | [📖](https://docs.praison.ai/docs/cli/todo) |
| 🎯 Router - Smart model selection | [📖](https://docs.praison.ai/docs/cli/router) |
| 📈 Flow Display - Visual workflow | [📖](https://docs.praison.ai/docs/cli/flow-display) |
| ✨ Prompt Expansion - Detailed prompts | [📖](https://docs.praison.ai/docs/cli/prompt-expansion) |
| 🌐 Web Search - Real-time search | [📖](https://docs.praison.ai/docs/cli/web-search) |
| 📥 Web Fetch - URL content retrieval | [📖](https://docs.praison.ai/docs/cli/web-fetch) |
| 💾 Prompt Caching - Cost reduction | [📖](https://docs.praison.ai/docs/cli/prompt-caching) |
---
@@ -2152,7 +2429,7 @@ PraisonAI provides zero-dependency persistent memory for agents. For detailed ex
- 📖 **Wikipedia Agent** - Search and extract Wikipedia information
- 💻 **Programming Agent** - Code development and analysis
- 📝 **Markdown Agent** - Generate and format Markdown content
- 🔀 **Router Agent** - Dynamic task routing with cost optimisation
- 🔀 **Model Router** - Smart model selection based on task complexity
### MCP Protocol
@@ -2161,11 +2438,19 @@ PraisonAI provides zero-dependency persistent memory for agents. For detailed ex
- 🔐 **MCP Security** - Origin validation, DNS rebinding prevention, secure sessions
- 🔄 **MCP Resumability** - SSE stream recovery via Last-Event-ID
### A2A & A2UI Protocols
- 🔗 **A2A Protocol** - Agent-to-Agent communication for inter-agent collaboration
- 🖼️ **A2UI Protocol** - Agent-to-User Interface for generating rich UIs from agents
- 📋 **UI Templates** - ChatTemplate, ListTemplate, FormTemplate, DashboardTemplate
- 🔧 **Surface Builder** - Fluent API for building declarative UIs
### Safety & Control
- 🤝 **Agent Handoffs** - Transfer context between specialised agents
- 🛡️ **Guardrails** - Input/output validation and safety checks
- ✅ **Human Approval** - Require human confirmation for critical actions
- 🔐 **Tool Approval CLI** - `--trust` (auto-approve all) and `--approve-level` (risk-based approval)
- 💬 **Sessions Management** - Isolated conversation contexts
- 🔄 **Stateful Agents** - Maintain state across interactions
+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>=2.3.32" \
"praisonai>=2.3.80" \
"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>=2.3.32" \
"praisonai>=2.3.80" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+25
View File
@@ -0,0 +1,25 @@
# Dockerfile for PraisonAI Serve API
# Deploy to Railway, Render, Fly.io, or any container platform
#
# Build: docker build -f Dockerfile.serve -t praisonai-serve .
# Run: docker run -p 8005:8005 -e OPENAI_API_KEY=sk-xxx praisonai-serve
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
RUN pip install --no-cache-dir praisonai praisonaiagents
# Copy agents configuration
COPY agents.yaml .
# Expose the API port
EXPOSE 8005
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8005/health || exit 1
# Start the API server
CMD ["praisonai", "serve", "agents.yaml", "--host", "0.0.0.0", "--port", "8005"]
+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>=2.3.32" \
"praisonai>=2.3.80" \
"praisonai[ui]" \
"praisonai[crewai]"
+49
View File
@@ -0,0 +1,49 @@
# PraisonAI Examples
This folder contains examples for PraisonAI. For detailed documentation, visit [docs.praison.ai](https://docs.praison.ai).
## Structure
```
examples/
├── python/ # Python examples
│ ├── agents/ # Agent examples (single, multi, router, etc.)
│ ├── workflows/ # Workflow patterns (routing, parallel, loop)
│ ├── tools/ # Custom tools examples
│ ├── mcp/ # MCP protocol examples
│ ├── memory/ # Memory and sessions
│ ├── code/ # Code editing and external CLI tools
│ └── ...
├── yaml/ # YAML workflow examples
└── cookbooks/ # Complete use-case examples
```
## Quick Links
| Category | Examples | Docs |
|----------|----------|------|
| **Agents** | [python/agents/](python/agents/) | [📖](https://docs.praison.ai/concepts/agents) |
| **Workflows** | [python/workflows/](python/workflows/) | [📖](https://docs.praison.ai/features/workflows) |
| **Model Router** | [python/agents/router-agent-cost-optimization.py](python/agents/router-agent-cost-optimization.py) | [📖](https://docs.praison.ai/features/model-router) |
| **MCP** | [python/mcp/](python/mcp/) | [📖](https://docs.praison.ai/mcp) |
| **Memory** | [python/memory/](python/memory/) | [📖](https://docs.praison.ai/concepts/memory) |
| **Tools** | [python/tools/](python/tools/) | [📖](https://docs.praison.ai/tools) |
| **Code** | [python/code/](python/code/) | [📖](https://docs.praison.ai/code) |
| **YAML** | [yaml/](yaml/) | [📖](https://docs.praison.ai/features/yaml-workflows) |
## Running Examples
```bash
# Install PraisonAI
pip install praisonai
# Set API key
export OPENAI_API_KEY=your_key_here
# Run an example
python examples/python/agents/single-agent.py
```
## CLI Commands
See the main [README.md](../README.md#-cli--no-code-interface) for all CLI commands.
+85
View File
@@ -0,0 +1,85 @@
"""
CLI Compare Example - Advanced Usage
This example demonstrates advanced comparison features including
custom mode configurations and result analysis.
"""
from praisonai.cli.features.compare import (
CompareHandler,
CompareResult,
ModeResult,
get_mode_config,
save_compare_result,
format_comparison_table,
)
# Create custom mode configurations
custom_modes = {
"fast": {"llm": "gpt-4o-mini"},
"accurate": {"llm": "gpt-4o", "planning": True},
"research": {"research": True, "web_search": True},
}
# Create handler with verbose output
handler = CompareHandler(verbose=True)
# Example: Manually create comparison results for analysis
# (useful for testing without API calls)
mock_result = CompareResult(
query="What are the latest AI trends?",
comparisons=[
ModeResult(
mode="basic",
output="AI trends include LLMs, multimodal models, and agents.",
execution_time_ms=1234.5,
model_used="gpt-4o-mini",
tools_used=[]
),
ModeResult(
mode="tools",
output="Based on recent search results, AI trends in 2024 include...",
execution_time_ms=2567.8,
model_used="gpt-4o-mini",
tools_used=["internet_search"]
),
ModeResult(
mode="research",
output="Comprehensive research shows that AI trends...",
execution_time_ms=5432.1,
model_used="gpt-4o-mini",
tools_used=["internet_search", "web_scraper"]
),
]
)
# Get summary statistics
summary = mock_result.get_summary()
print("Comparison Summary:")
print(f" Fastest mode: {summary['fastest']} ({summary['fastest_time_ms']:.1f}ms)")
print(f" Slowest mode: {summary['slowest']} ({summary['slowest_time_ms']:.1f}ms)")
# Format as table
print("\n" + format_comparison_table(mock_result))
# Convert to JSON for storage
print("\nJSON Output:")
print(mock_result.to_json())
# Save to file
# save_compare_result(mock_result, "advanced_comparison.json")
print("\n--- CLI Usage Examples ---")
print("""
# Basic comparison
praisonai "What is AI?" --compare "basic,tools"
# Compare with specific model
praisonai "Explain quantum computing" --compare "basic,planning,research" --model gpt-4o
# Save results to file
praisonai "Latest tech news" --compare "basic,tools,web_search" --compare-output results.json
# Verbose output
praisonai "Write a poem" --compare "basic,planning" --verbose
""")
+45
View File
@@ -0,0 +1,45 @@
"""
CLI Compare Example - Basic Usage
This example demonstrates how to compare different CLI modes programmatically.
"""
from praisonai.cli.features.compare import (
CompareHandler,
get_mode_config,
list_available_modes,
parse_modes,
)
# List all available comparison modes
print("Available modes:")
for mode in list_available_modes():
config = get_mode_config(mode)
print(f" - {mode}: {config}")
# Parse modes from comma-separated string
modes = parse_modes("basic,tools,planning")
print(f"\nParsed modes: {modes}")
# Create compare handler
handler = CompareHandler(verbose=True)
# Compare modes (this will run actual agent calls)
# Uncomment to run:
# result = handler.compare(
# query="What is artificial intelligence?",
# modes=["basic", "tools"],
# model="gpt-4o-mini"
# )
# handler.print_result(result)
# Save results to file
# result = handler.execute(
# query="Explain machine learning",
# modes_str="basic,planning",
# output_path="comparison_results.json"
# )
print("\nTo run comparison via CLI:")
print(' praisonai "What is AI?" --compare "basic,tools,planning"')
print(' praisonai "What is AI?" --compare "basic,research" --compare-output results.json')
@@ -0,0 +1,23 @@
# Example: Data Collection Agent with 30-minute Schedule
# Collects and analyzes data every 30 minutes
framework: praisonai
agents:
- name: "Data Collector"
role: "Data Analysis Specialist"
goal: "Collect and analyze market data at regular intervals"
instructions: |
You are a data analysis specialist focused on collecting and analyzing
market trends and metrics. Provide actionable insights from the data.
tools:
- search_tool
verbose: false
task: "Collect latest market data and identify key trends"
# Schedule: Every 30 minutes with immediate execution
schedule:
interval: "*/30m"
max_retries: 5
run_immediately: true
@@ -0,0 +1,53 @@
# Example: 24/7 AI News Monitor with Scheduler
# This agent runs continuously at regular intervals to monitor AI news
framework: praisonai
agents:
- name: "AI News Monitor"
role: "Technology News Analyst"
goal: "Monitor and summarize the latest AI developments"
instructions: |
You are an expert technology news analyst specializing in AI and machine learning.
Your job is to search for the latest AI news, identify the most important developments,
and provide clear, concise summaries with sources.
Focus on:
- Major AI product launches
- Research breakthroughs
- Industry trends
- Policy and regulation updates
tools:
- search_tool
verbose: true
# Task to execute on each scheduled run
task: |
Search for the latest AI news from the past 24 hours.
Provide the top 3 most important stories with:
1. Headline
2. Brief summary (2-3 sentences)
3. Source URL
4. Why it's significant
# Schedule configuration
schedule:
# Interval options:
# - "hourly" - Every hour
# - "daily" - Every 24 hours
# - "*/30m" - Every 30 minutes
# - "*/6h" - Every 6 hours
# - "3600" - Custom seconds
interval: "hourly"
# Maximum retry attempts if execution fails
max_retries: 3
# Run immediately when scheduler starts (before first scheduled interval)
run_immediately: true
# Optional: Variables for customization
variables:
news_category: "AI and Machine Learning"
region: "Global"
time_range: "24 hours"
+69
View File
@@ -0,0 +1,69 @@
"""
Accuracy Evaluation Example
This example demonstrates how to evaluate agent output accuracy
by comparing against expected outputs using LLM-as-judge.
"""
import os
from praisonaiagents import Agent
from praisonaiagents.eval import AccuracyEvaluator
# Check if we have an API key
has_api_key = os.getenv("OPENAI_API_KEY") is not None
if has_api_key:
# Create a simple math tutor agent
agent = Agent(
instructions="You are a math tutor. Answer math questions concisely with just the number."
)
# Create accuracy evaluator
evaluator = AccuracyEvaluator(
agent=agent,
input_text="What is 2 + 2?",
expected_output="4",
num_iterations=3, # Run 3 times for statistical significance
verbose=True
)
# Run evaluation
result = evaluator.run(print_summary=True)
# Check results
print(f"\nAverage Score: {result.avg_score}/10")
print(f"Passed: {result.passed}")
else:
print("⚠️ No OPENAI_API_KEY found. Using mock evaluation...")
# Create evaluator without agent for mock testing
evaluator = AccuracyEvaluator(
func=lambda x: "4", # Mock function
input_text="What is 2 + 2?",
expected_output="4",
num_iterations=1,
verbose=True
)
# Test with mock output
try:
result = evaluator.run(print_summary=True)
print(f"\nMock Average Score: {result.avg_score}/10")
print(f"Mock Passed: {result.passed}")
except Exception as e:
print(f"Mock evaluation failed: {e}")
print("This is expected without an API key")
# You can also evaluate pre-generated outputs
print("\n--- Testing pre-generated output evaluation ---")
output = "The answer is 4"
if has_api_key:
try:
result2 = evaluator.evaluate_output(output, print_summary=True)
print(f"Pre-generated output score: {result2.avg_score}/10")
except Exception as e:
print(f"Pre-generated evaluation failed: {e}")
else:
print("⚠️ Skipping pre-generated evaluation (no API key)")
print("To run full evaluation, set OPENAI_API_KEY environment variable")
+105
View File
@@ -0,0 +1,105 @@
"""
Batch Evaluation Example
This example demonstrates how to run batch evaluations
from a JSON test file.
"""
import os
import json
import tempfile
from praisonaiagents.eval import AccuracyEvaluator
# Check if we have an API key
has_api_key = os.getenv("OPENAI_API_KEY") is not None
# Create a test file with multiple test cases
test_cases = [
{
"input": "What is 2 + 2?",
"expected": "4"
},
{
"input": "What is the capital of France?",
"expected": "Paris"
},
{
"input": "What color is the sky?",
"expected": "Blue"
}
]
# Save test cases to a temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump(test_cases, f)
test_file = f.name
print(f"Test file created: {test_file}")
print(f"Running {len(test_cases)} test cases...")
if has_api_key:
# You can use the CLI for batch evaluation:
# praisonai eval batch --agent agents.yaml --test-file tests.json --batch-type accuracy
# Or programmatically:
from praisonaiagents import Agent
agent = Agent(
instructions="You are a helpful assistant. Answer questions concisely."
)
results = []
for i, test_case in enumerate(test_cases):
print(f"\nTest {i + 1}: {test_case['input']}")
evaluator = AccuracyEvaluator(
agent=agent,
input_text=test_case["input"],
expected_output=test_case["expected"],
num_iterations=1
)
result = evaluator.run()
results.append({
"input": test_case["input"],
"expected": test_case["expected"],
"score": result.avg_score,
"passed": result.passed
})
print(f" Score: {result.avg_score}/10")
print(f" Passed: {result.passed}")
# Summary
passed = sum(1 for r in results if r["passed"])
print(f"\n{'='*50}")
print(f"Summary: {passed}/{len(results)} tests passed")
print(f"Pass Rate: {passed/len(results):.1%}")
else:
print("⚠️ No OPENAI_API_KEY found. Running mock batch evaluation...")
# Mock evaluation without API key
results = []
for i, test_case in enumerate(test_cases):
print(f"\nTest {i + 1}: {test_case['input']}")
# Mock result (all would fail without API key)
mock_score = 0.0
mock_passed = False
results.append({
"input": test_case["input"],
"expected": test_case["expected"],
"score": mock_score,
"passed": mock_passed
})
print(f" Score: {mock_score}/10 (mock)")
print(f" Passed: {mock_passed} (expected without API key)")
# Summary
passed = sum(1 for r in results if r["passed"])
print(f"\n{'='*50}")
print(f"Summary: {passed}/{len(results)} tests passed (mock evaluation)")
print(f"Pass Rate: {passed/len(results):.1%}")
print("\nTo run real batch evaluation, set OPENAI_API_KEY environment variable")
+82
View File
@@ -0,0 +1,82 @@
"""
Criteria Evaluation Example
This example demonstrates how to evaluate agent outputs
against custom criteria using LLM-as-judge.
"""
import os
from praisonaiagents import Agent
from praisonaiagents.eval import CriteriaEvaluator
# Check if we have an API key
has_api_key = os.getenv("OPENAI_API_KEY") is not None
if has_api_key:
print("--- Testing Criteria Evaluation with Agent ---")
# Create a customer service agent
agent = Agent(
instructions="You are a friendly customer service agent. Be helpful and empathetic."
)
# Create criteria evaluator with numeric scoring
evaluator = CriteriaEvaluator(
criteria="Response is helpful, empathetic, and provides a clear solution",
agent=agent,
input_text="I'm frustrated because my order hasn't arrived yet.",
scoring_type="numeric", # Score 1-10
threshold=7.0, # Pass if score >= 7
num_iterations=2,
verbose=True
)
# Run evaluation
result = evaluator.run(print_summary=True)
print("\nNumeric Scoring Results:")
print(f" Average Score: {result.avg_score}/10")
print(f" Pass Rate: {result.pass_rate:.1%}")
print(f" Passed: {result.passed}")
# Binary scoring example (pass/fail)
print("\n--- Testing Binary Scoring ---")
binary_evaluator = CriteriaEvaluator(
criteria="Response does not contain any offensive language",
agent=agent,
input_text="Tell me a joke",
scoring_type="binary", # Pass or Fail
verbose=True
)
binary_result = binary_evaluator.run(print_summary=True)
# With on_fail callback
print("\n--- Testing Failure Callback ---")
def handle_failure(score):
print(f"ALERT: Evaluation failed with score {score.score}")
print(f"Reasoning: {score.reasoning}")
callback_evaluator = CriteriaEvaluator(
criteria="Response is professional and helpful",
agent=agent,
input_text="Help me",
on_fail=handle_failure,
threshold=8.0
)
callback_evaluator.run()
else:
print("⚠️ No OPENAI_API_KEY found. Skipping agent-based criteria evaluation...")
# Test pre-generated output evaluation (doesn't need agent)
print("\n--- Testing Pre-generated Output Evaluation ---")
if has_api_key:
output = "I understand your frustration. Let me check on your order right away."
try:
result2 = evaluator.evaluate_output(output, print_summary=True)
print(f"Pre-generated output score: {result2.avg_score}/10")
except Exception as e:
print(f"Pre-generated evaluation failed: {e}")
else:
print("⚠️ Skipping pre-generated evaluation (no API key)")
print("To run full evaluation, set OPENAI_API_KEY environment variable")
+59
View File
@@ -0,0 +1,59 @@
"""
Performance Evaluation Example
This example demonstrates how to benchmark agent performance
by measuring runtime and memory usage across multiple iterations.
"""
import os
from praisonaiagents import Agent
from praisonaiagents.eval import PerformanceEvaluator
# Check if we have an API key
has_api_key = os.getenv("OPENAI_API_KEY") is not None
if has_api_key:
print("--- Testing Agent Performance ---")
# Create a simple agent
agent = Agent(
instructions="You are a helpful assistant. Answer questions briefly."
)
# Create performance evaluator
evaluator = PerformanceEvaluator(
agent=agent,
input_text="What is the capital of France?",
num_iterations=10, # Run 10 benchmark iterations
warmup_runs=2, # 2 warmup runs before measurement
track_memory=True, # Track memory usage
verbose=True
)
# Run evaluation
result = evaluator.run(print_summary=True)
# Access detailed metrics
print("\nAgent Performance Results:")
print(f" Average Time: {result.avg_run_time:.4f}s")
print(f" Min Time: {result.min_run_time:.4f}s")
print(f" Max Time: {result.max_run_time:.4f}s")
print(f" Median Time: {result.median_run_time:.4f}s")
print(f" P95 Time: {result.p95_run_time:.4f}s")
print(f" Avg Memory: {result.avg_memory:.2f} MB")
else:
print("⚠️ No OPENAI_API_KEY found. Skipping agent performance test...")
# You can also benchmark any function
def my_function():
import time
time.sleep(0.1)
return "done"
func_evaluator = PerformanceEvaluator(
func=my_function,
num_iterations=5,
warmup_runs=1,
track_memory=False
)
func_result = func_evaluator.run(print_summary=True)
+72
View File
@@ -0,0 +1,72 @@
"""
Reliability Evaluation Example
This example demonstrates how to verify that agents call
the expected tools during execution.
"""
import os
from praisonaiagents import Agent
from praisonaiagents.eval import ReliabilityEvaluator
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Search results for: {query}"
def calculate(expression: str) -> str:
"""Calculate a math expression."""
return str(eval(expression))
# Check if we have an API key
has_api_key = os.getenv("OPENAI_API_KEY") is not None
if has_api_key:
print("--- Testing Agent Tool Call Reliability ---")
# Create an agent with tools
agent = Agent(
instructions="You are a helpful assistant with access to search and calculator tools.",
tools=[search_web, calculate]
)
# Create reliability evaluator
evaluator = ReliabilityEvaluator(
agent=agent,
input_text="Search for the weather in Paris and calculate 25 * 4",
expected_tools=["search_web", "calculate"], # Tools that should be called
forbidden_tools=["delete_file"], # Tools that should NOT be called
verbose=True
)
# Run evaluation
result = evaluator.run(print_summary=True)
# Check results
print("\nAgent Reliability Results:")
print(f" Passed: {result.passed}")
print(f" Pass Rate: {result.pass_rate:.1%}")
else:
print("⚠️ No OPENAI_API_KEY found. Skipping agent reliability test...")
print("Agent would fail to call tools without API key (expected behavior)")
# You can also evaluate pre-recorded tool calls (doesn't need API key)
print("\n--- Testing Pre-recorded Tool Call Evaluation ---")
# Create a mock agent for tool call evaluation
mock_agent = Agent(instructions="Mock agent")
evaluator = ReliabilityEvaluator(
agent=mock_agent,
expected_tools=["search_web", "calculate"], # Tools that should be called
forbidden_tools=["delete_file"], # Tools that should NOT be called
verbose=True
)
result2 = evaluator.evaluate_tool_calls(
actual_tools=["search_web", "calculate"],
print_summary=True
)
print("\nPre-recorded Tool Call Results:")
print(f" Passed: {result2.passed}")
print(f" Pass Rate: {result2.pass_rate:.1%}")
+53
View File
@@ -0,0 +1,53 @@
"""
PraisonAI A2A Server Example
Expose a PraisonAI Agent as an A2A (Agent2Agent) Server.
This enables other AI agents to discover and communicate with your agent.
Run:
uvicorn a2a-server:app --reload
Endpoints:
GET /.well-known/agent.json - Agent Card for discovery
GET /status - Server status
"""
from praisonaiagents import Agent, A2A
from fastapi import FastAPI
# Create an agent with tools
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Search results for: {query}"
def calculate(expression: str) -> str:
"""Calculate a mathematical expression."""
try:
return f"Result: {eval(expression)}"
except Exception:
return "Invalid expression"
agent = Agent(
name="Research Assistant",
role="Research Analyst",
goal="Help users research topics and answer questions",
tools=[search_web, calculate]
)
# Expose as A2A Server
a2a = A2A(
agent=agent,
url="http://localhost:8000/a2a",
version="1.0.0"
)
# Create FastAPI app
app = FastAPI(
title="PraisonAI A2A Server",
description="A2A-compatible agent server"
)
app.include_router(a2a.get_router())
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -0,0 +1,132 @@
"""
AutoAgents Workflow Patterns Example
This example demonstrates the new workflow auto-generation features:
- Dynamic agent count based on task complexity
- Multiple workflow patterns (sequential, parallel, routing, orchestrator-workers, evaluator-optimizer)
- Pattern recommendation based on task keywords
Documentation: https://docs.praison.ai/features/autoagents
CLI Reference: https://docs.praison.ai/nocode/auto
"""
from praisonai.auto import AutoGenerator, WorkflowAutoGenerator
import os
import tempfile
# Ensure API key is set
if not os.environ.get("OPENAI_API_KEY"):
print("Please set OPENAI_API_KEY environment variable")
exit(1)
print("=" * 60)
print("AutoAgents Workflow Patterns Demo")
print("=" * 60)
# =============================================================================
# Example 1: Dynamic Agent Count (Simple vs Complex Tasks)
# =============================================================================
print("\n📊 Example 1: Dynamic Agent Count")
print("-" * 40)
# Simple task - should create 1-2 agents
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
simple_file = f.name
simple_generator = AutoGenerator(
topic="Write a haiku about spring",
agent_file=simple_file,
framework="praisonai"
)
print("Simple task: 'Write a haiku about spring'")
result = simple_generator.generate()
print(f"Generated: {result}")
# Complex task - should create 3-4 agents
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
complex_file = f.name
complex_generator = AutoGenerator(
topic="Research AI trends, analyze market data, write a comprehensive report with executive summary",
agent_file=complex_file,
framework="praisonai"
)
print("\nComplex task: 'Research AI trends, analyze market data...'")
result = complex_generator.generate()
print(f"Generated: {result}")
# =============================================================================
# Example 2: Pattern Recommendation
# =============================================================================
print("\n🎯 Example 2: Pattern Recommendation")
print("-" * 40)
test_tasks = [
("Write a blog post", "sequential"),
("Research from multiple sources simultaneously", "parallel"),
("Classify and route customer requests", "routing"),
("Comprehensive analysis and break down the problem", "orchestrator-workers"),
("Refine and improve content quality", "evaluator-optimizer"),
]
for task, expected in test_tasks:
generator = WorkflowAutoGenerator(topic=task)
recommended = generator.recommend_pattern()
status = "✅" if recommended == expected else "⚠️"
print(f"{status} Task: '{task[:40]}...'")
print(f" Recommended: {recommended}")
# =============================================================================
# Example 3: Generate Workflow with Specific Pattern
# =============================================================================
print("\n🔄 Example 3: Generate Workflows with Patterns")
print("-" * 40)
patterns = ["sequential", "parallel", "orchestrator-workers", "evaluator-optimizer"]
for pattern in patterns:
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
workflow_file = f.name
generator = WorkflowAutoGenerator(
topic="Create a market analysis report",
workflow_file=workflow_file
)
print(f"\n📋 Pattern: {pattern}")
result = generator.generate(pattern=pattern)
print(f" Generated: {result}")
# Read and show structure
import yaml
with open(result, 'r') as f:
data = yaml.safe_load(f)
print(f" Agents: {list(data.get('agents', {}).keys())}")
print(f" Steps: {len(data.get('steps', []))}")
# =============================================================================
# Example 4: CLI Usage (for reference)
# =============================================================================
print("\n💻 Example 4: CLI Commands (for reference)")
print("-" * 40)
print("""
# Auto-generate agents (dynamic count)
praisonai --auto "Write a haiku about spring"
# Auto-generate workflow with pattern
praisonai workflow auto "Research and write a report" --pattern sequential
praisonai workflow auto "Research from multiple sources" --pattern parallel
praisonai workflow auto "Comprehensive market analysis" --pattern orchestrator-workers
praisonai workflow auto "Refine content quality" --pattern evaluator-optimizer
# With output file
praisonai workflow auto "Customer support routing" --pattern routing --output support.yaml
""")
print("\n" + "=" * 60)
print("✅ AutoAgents Workflow Patterns Demo Complete!")
print("=" * 60)
print("\nDocumentation: https://docs.praison.ai/features/autoagents")
print("CLI Reference: https://docs.praison.ai/nocode/auto")
@@ -0,0 +1,38 @@
"""
Autonomy Modes Example for PraisonAI CLI.
Control AI autonomy: suggest, auto_edit, full_auto.
Docs: https://docs.praison.ai/cli/autonomy-modes
"""
from praisonai.cli.features import AutonomyModeHandler
from praisonai.cli.features.autonomy_mode import ActionRequest, ActionType
# Initialize with suggest mode (safest)
handler = AutonomyModeHandler()
manager = handler.initialize(mode="suggest")
print(f"Current mode: {handler.get_mode()}")
print("Available modes: suggest, auto_edit, full_auto")
# Create an action request
action = ActionRequest(
action_type=ActionType.FILE_WRITE,
description="Edit src/main.py to add logging",
details={"file": "src/main.py"}
)
# In suggest mode, this would require approval
# For demo, we'll just show the action
print(f"\nAction: {action.description}")
print(f"Type: {action.action_type.value}")
print("Would require approval in 'suggest' mode")
# Change to auto_edit mode
handler.set_mode("auto_edit")
print(f"\nChanged to: {handler.get_mode()}")
print("File edits now auto-approved, shell commands still require approval")
# Get statistics
stats = handler.get_stats()
print(f"\nStats: {stats}")
@@ -0,0 +1,34 @@
"""
Cost Tracking Example for PraisonAI CLI.
Real-time token usage and cost monitoring.
Docs: https://docs.praison.ai/cli/cost-tracking
"""
from praisonai.cli.features import CostTrackerHandler
# Initialize tracker
handler = CostTrackerHandler()
tracker = handler.initialize(session_id="demo-session")
# Track some requests
handler.track_request("gpt-4o", input_tokens=1000, output_tokens=500)
handler.track_request("gpt-4o-mini", input_tokens=2000, output_tokens=800)
handler.track_request("claude-3-5-sonnet", input_tokens=1500, output_tokens=600)
# Get totals
print(f"Total tokens: {handler.get_tokens():,}")
print(f"Total cost: ${handler.get_cost():.4f}")
# Get detailed summary
summary = handler.get_summary()
print("\n=== Session Summary ===")
print(f"Session ID: {summary['session_id']}")
print(f"Total requests: {summary['total_requests']}")
print(f"Input tokens: {summary['total_input_tokens']:,}")
print(f"Output tokens: {summary['total_output_tokens']:,}")
print(f"Total cost: ${summary['total_cost']:.4f}")
print(f"Avg cost/request: ${summary['avg_cost_per_request']:.4f}")
# Display formatted (uses Rich if available)
handler.display_summary()
@@ -0,0 +1,45 @@
"""
Git Integration Example for PraisonAI CLI.
Auto-commit with AI messages, diff viewing, undo.
Docs: https://docs.praison.ai/cli/git-integration
"""
from praisonai.cli.features import GitIntegrationHandler
# Initialize
handler = GitIntegrationHandler(verbose=True)
git = handler.initialize(repo_path=".")
# Check if it's a git repo
if not git.is_repo:
print("Not a git repository!")
exit(1)
# Show status
print("=== Git Status ===")
status = handler.show_status()
print(f"Branch: {status.branch}")
print(f"Staged: {len(status.staged_files)}")
print(f"Modified: {len(status.modified_files)}")
print(f"Untracked: {len(status.untracked_files)}")
# Show diff (if any changes)
if status.has_changes:
print("\n=== Diff ===")
diff = handler.show_diff()
if diff:
print(diff[:300] + "..." if len(diff) > 300 else diff)
# Show recent commits
print("\n=== Recent Commits ===")
commits = handler.show_log(count=5)
for c in commits:
print(f" {c.short_hash} {c.message[:50]}")
# To commit with AI message:
# commit = handler.commit() # Auto-generates message
# commit = handler.commit(message="Custom message")
# To undo last commit:
# handler.undo(soft=True) # Keeps changes staged
@@ -0,0 +1,49 @@
"""
Interactive TUI Example for PraisonAI CLI.
Rich terminal interface with completions and history.
Docs: https://docs.praison.ai/cli/interactive-tui
"""
from praisonai.cli.features import InteractiveTUIHandler
from praisonai.cli.features.interactive_tui import InteractiveConfig
# Custom configuration
config = InteractiveConfig(
prompt="ai> ",
multiline=True,
enable_completions=True,
show_status_bar=True
)
# Define callbacks
def on_input(text):
"""Handle regular input."""
return f"You said: {text}"
def on_command(cmd):
"""Handle slash commands."""
if cmd == "/exit":
return {"type": "exit"}
return {"type": "command", "message": f"Executed: {cmd}"}
# Initialize
handler = InteractiveTUIHandler()
session = handler.initialize(
config=config,
on_input=on_input,
on_command=on_command
)
# Add commands for completion
session.add_commands(["help", "exit", "cost", "model", "plan"])
# Add symbols from codebase
session.add_symbols(["MyClass", "my_function"])
print("Interactive TUI initialized!")
print("Commands available: /help, /exit, /cost, /model, /plan")
print("Run handler.run() to start interactive session")
# Uncomment to run interactive session:
# handler.run()
+24
View File
@@ -0,0 +1,24 @@
"""
Repository Map Example for PraisonAI CLI.
Intelligent codebase mapping with symbol extraction.
Docs: https://docs.praison.ai/cli/repo-map
"""
from praisonai.cli.features import RepoMapHandler
# Initialize with current directory
handler = RepoMapHandler(verbose=True)
repo_map = handler.initialize(root=".")
# Get the repository map
print("=== Repository Map ===")
map_str = handler.get_map()
print(map_str[:500] if len(map_str) > 500 else map_str)
print("..." if len(map_str) > 500 else "")
# Refresh after file changes
handler.refresh()
# Get symbols for a specific file (if exists)
# symbols = handler.get_context("MyClass")
@@ -0,0 +1,42 @@
"""
Sandbox Execution Example for PraisonAI CLI.
Secure isolated command execution (--sandbox flag).
Docs: https://docs.praison.ai/cli/sandbox-execution
"""
from praisonai.cli.features import SandboxExecutorHandler
from praisonai.cli.features.sandbox_executor import SandboxMode
# Initialize with basic sandbox
handler = SandboxExecutorHandler(verbose=True)
sandbox = handler.initialize(mode="basic")
print(f"Sandbox mode: {handler.get_mode()}")
print(f"Sandbox enabled: {handler.is_enabled}")
# Execute a safe command
print("\n=== Safe Command ===")
result = handler.execute("echo 'Hello from sandbox!'")
print(f"Success: {result.success}")
print(f"Output: {result.stdout.strip()}")
print(f"Sandboxed: {result.was_sandboxed}")
# Try a blocked command
print("\n=== Blocked Command ===")
result = handler.execute("rm -rf /")
print(f"Success: {result.success}")
print(f"Violations: {result.policy_violations}")
# Validate command before execution
print("\n=== Command Validation ===")
violations = handler.validate_command("sudo apt install something")
if violations:
print(f"Command blocked: {violations}")
else:
print("Command allowed")
# Available modes
print("\n=== Available Modes ===")
for mode in SandboxMode:
print(f" {mode.value}")
@@ -0,0 +1,51 @@
"""
Slash Commands Example for PraisonAI CLI.
Demonstrates interactive slash commands like /help, /cost, /model.
Docs: https://docs.praison.ai/cli/slash-commands
"""
from praisonai.cli.features import SlashCommandHandler
from praisonai.cli.features.slash_commands import SlashCommand, CommandKind
# Create handler
handler = SlashCommandHandler()
# Execute commands
print("=== /help ===")
result = handler.execute("/help")
print(result.get("message", ""))
print("\n=== /cost ===")
result = handler.execute("/cost")
print(result.get("message", ""))
print("\n=== /model ===")
result = handler.execute("/model")
print(result.get("message", ""))
# Check if input is a command
print("\n=== Command Detection ===")
print(f"/help is command: {handler.is_command('/help')}")
print(f"hello is command: {handler.is_command('hello')}")
# Get completions for auto-complete
print("\n=== Completions for '/he' ===")
completions = handler.get_completions("/he")
print(completions)
# Register custom command
def my_custom_handler(args, context):
return {"type": "custom", "message": f"Custom command with args: {args}"}
custom_cmd = SlashCommand(
name="mycommand",
description="My custom command",
handler=my_custom_handler,
kind=CommandKind.ACTION,
aliases=["mc"]
)
handler.register(custom_cmd)
result = handler.execute("/mycommand arg1 arg2")
print(f"\n=== Custom Command ===\n{result.get('message', '')}")
@@ -0,0 +1,34 @@
"""
Claude Code CLI Integration Example
Demonstrates how to use Claude Code CLI as an agent tool in PraisonAI.
"""
import asyncio
from praisonai.integrations import ClaudeCodeIntegration
async def main():
# Create Claude Code integration
claude = ClaudeCodeIntegration(
workspace=".",
output_format="json",
skip_permissions=True,
system_prompt="Be concise and follow best practices",
allowed_tools=["Read", "Write", "Bash"]
)
# Check availability
print(f"Claude CLI available: {claude.is_available}")
print(f"Claude SDK available: {claude.sdk_available}")
# Execute a coding task
result = await claude.execute("List all Python files in the current directory")
print(f"Result: {result}")
# Stream output
print("\nStreaming output:")
async for event in claude.stream("Explain the project structure"):
print(event)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,60 @@
"""
Code Editing Tools Example
Demonstrates how to use PraisonAI's code editing tools with SEARCH/REPLACE diffs.
"""
from praisonai.code import (
set_workspace,
code_read_file,
code_write_file,
code_apply_diff,
code_list_files,
code_execute_command,
CODE_TOOLS
)
# Set workspace
set_workspace(".")
# List files
print("Files in current directory:")
files = code_list_files(".", recursive=False)
for f in files:
print(f" {f}")
# Read a file
print("\nReading example file:")
try:
content = code_read_file("README.md", start_line=1, end_line=10)
print(content)
except Exception as e:
print(f" File not found: {e}")
# Write a file
print("\nWriting test file:")
code_write_file("test_output.txt", "Hello from PraisonAI Code Tools!")
print(" Created test_output.txt")
# Apply a diff
print("\nApplying diff:")
diff = """
<<<<<<< SEARCH
Hello from PraisonAI Code Tools!
=======
Hello from PraisonAI Code Tools!
This line was added by a diff.
>>>>>>> REPLACE
"""
result = code_apply_diff("test_output.txt", diff)
print(f" {result}")
# Execute command
print("\nExecuting command:")
output = code_execute_command("cat test_output.txt")
print(f" {output}")
# Cleanup
import os
os.remove("test_output.txt")
print("\nCleanup complete.")
+36
View File
@@ -0,0 +1,36 @@
"""
Codex CLI Integration Example
Demonstrates how to use OpenAI Codex CLI as an agent tool in PraisonAI.
"""
import asyncio
from praisonai.integrations import CodexCLIIntegration
async def main():
# Create Codex CLI integration
codex = CodexCLIIntegration(
workspace=".",
full_auto=True,
sandbox="danger-full-access",
json_output=True
)
# Check availability
print(f"Codex CLI available: {codex.is_available}")
print(f"Full Auto: {codex.full_auto}")
print(f"Sandbox: {codex.sandbox}")
# Execute a coding task
result = await codex.execute("List files in the current directory")
print(f"Result: {result}")
# Stream JSON events
print("\nStreaming events:")
async for event in codex.stream("Explain the project"):
event_type = event.get("type", "")
if event_type == "item.completed":
print(f" {event}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,37 @@
"""
Cursor CLI Integration Example
Demonstrates how to use Cursor CLI as an agent tool in PraisonAI.
"""
import asyncio
from praisonai.integrations import CursorCLIIntegration
async def main():
# Create Cursor CLI integration
cursor = CursorCLIIntegration(
workspace=".",
output_format="json",
force=True,
model="gpt-5",
stream_partial=True
)
# Check availability
print(f"Cursor CLI available: {cursor.is_available}")
print(f"Force mode: {cursor.force}")
print(f"Model: {cursor.model}")
# Execute a coding task
result = await cursor.execute("List files in the current directory")
print(f"Result: {result}")
# Stream output
print("\nStreaming output:")
async for event in cursor.stream("Explain the project"):
content = event.get("content", "")
if content:
print(content, end="", flush=True)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,58 @@
"""
External Agents Integration Example
Demonstrates how to use all external CLI integrations as agent tools.
"""
import asyncio
from praisonai.integrations import (
ClaudeCodeIntegration,
GeminiCLIIntegration,
CodexCLIIntegration,
CursorCLIIntegration,
get_available_integrations
)
async def main():
# Check availability of all integrations
print("=== External CLI Integrations ===\n")
availability = get_available_integrations()
print("Availability:")
for name, avail in availability.items():
status = "✅" if avail else "❌"
print(f" {status} {name}")
# Create integrations
print("\n--- Claude Code ---")
claude = ClaudeCodeIntegration(workspace=".", skip_permissions=True)
print(f"CLI: {claude.cli_command}")
print(f"Available: {claude.is_available}")
tool = claude.as_tool()
print(f"Tool: {tool.__name__}")
print("\n--- Gemini CLI ---")
gemini = GeminiCLIIntegration(workspace=".", model="gemini-2.5-flash")
print(f"CLI: {gemini.cli_command}")
print(f"Available: {gemini.is_available}")
tool = gemini.as_tool()
print(f"Tool: {tool.__name__}")
print("\n--- Codex CLI ---")
codex = CodexCLIIntegration(workspace=".", full_auto=True)
print(f"CLI: {codex.cli_command}")
print(f"Available: {codex.is_available}")
tool = codex.as_tool()
print(f"Tool: {tool.__name__}")
print("\n--- Cursor CLI ---")
cursor = CursorCLIIntegration(workspace=".", force=True)
print(f"CLI: {cursor.cli_command}")
print(f"Available: {cursor.is_available}")
tool = cursor.as_tool()
print(f"Tool: {tool.__name__}")
print("\n=== All integrations ready ===")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,33 @@
"""
Gemini CLI Integration Example
Demonstrates how to use Gemini CLI as an agent tool in PraisonAI.
"""
import asyncio
from praisonai.integrations import GeminiCLIIntegration
async def main():
# Create Gemini CLI integration
gemini = GeminiCLIIntegration(
workspace=".",
model="gemini-2.5-flash",
output_format="json",
include_directories=["../lib", "../docs"]
)
# Check availability
print(f"Gemini CLI available: {gemini.is_available}")
print(f"Model: {gemini.model}")
# Execute a coding task
result = await gemini.execute("Analyze the code structure")
print(f"Result: {result}")
# Execute with stats
result, stats = await gemini.execute_with_stats("Explain main.py")
print(f"Result: {result}")
print(f"Stats: {stats}")
if __name__ == "__main__":
asyncio.run(main())
+40 -3
View File
@@ -1,11 +1,48 @@
"""
AutoAgents Example
AutoAgents automatically creates and manages AI agents based on high-level instructions.
It features dynamic agent count based on task complexity.
Documentation: https://docs.praison.ai/features/autoagents
CLI Reference: https://docs.praison.ai/nocode/auto
New Features:
- Dynamic agent count (1-4 based on task complexity)
- Workflow patterns: sequential, parallel, routing, orchestrator-workers, evaluator-optimizer
- Pattern recommendation based on task keywords
- Tool preservation from LLM suggestions
"""
from praisonaiagents import AutoAgents
from praisonaiagents.tools import duckduckgo
# Basic usage - AutoAgents analyzes complexity and creates optimal agents
agents = AutoAgents(
instructions="Search for information about AI Agents",
tools=[duckduckgo],
process="sequential",
verbose=True
process="sequential", # or "hierarchical"
verbose=True,
max_agents=3 # Maximum number of agents to create
)
agents.start()
result = agents.start()
print(result)
# =============================================================================
# CLI Usage Examples (for reference)
# =============================================================================
#
# # Auto-generate agents (dynamic count based on complexity)
# praisonai --auto "Write a haiku about spring"
# praisonai --auto "Research AI trends, analyze data, write report"
#
# # Auto-generate workflow with specific pattern
# praisonai workflow auto "Research and write" --pattern sequential
# praisonai workflow auto "Research from multiple sources" --pattern parallel
# praisonai workflow auto "Comprehensive analysis" --pattern orchestrator-workers
# praisonai workflow auto "Refine content quality" --pattern evaluator-optimizer
#
# # With framework selection
# praisonai --framework crewai --auto "Create a movie script"
# praisonai --framework autogen --auto "Create a movie script"
@@ -0,0 +1,115 @@
"""
24/7 News Checker Agent Example
This example demonstrates how to create a continuously running agent
that checks for AI news every hour and prints the results.
Usage:
python news_checker_agent.py
The agent will:
1. Run immediately on start
2. Check for latest AI news every 1 hour
3. Print results to console
4. Continue running until stopped (Ctrl+C)
"""
import os
import sys
from datetime import datetime
# Add parent directory to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..'))
from praisonaiagents import Agent
from praisonai.agent_scheduler import AgentScheduler
from tools import search_tool
def on_success(result):
"""Callback function called when agent execution succeeds."""
print("\n" + "="*80)
print(f"✅ SUCCESS - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("="*80)
print(result)
print("="*80 + "\n")
def on_failure(error):
"""Callback function called when agent execution fails."""
print("\n" + "="*80)
print(f"❌ FAILURE - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("="*80)
print(f"Error: {error}")
print("="*80 + "\n")
def main():
"""Main function to set up and run the scheduled news checker agent."""
print("\n🤖 Starting 24/7 News Checker Agent")
print("="*80)
# Create the agent with web search capability
agent = Agent(
name="NewsChecker",
role="AI News Monitor",
goal="Monitor and summarize the latest AI and technology news",
backstory="""You are an expert AI news analyst who stays up-to-date
with the latest developments in artificial intelligence, machine learning,
and technology. You provide concise, accurate summaries of important news.""",
tools=[search_tool],
verbose=True
)
# Define the task
task = """Search for the latest AI news from today.
Focus on major developments, breakthroughs, or announcements.
Provide a summary of the top 3-5 most important news items.
Include the source and key details for each item."""
# Create scheduler with callbacks
scheduler = AgentScheduler(
agent=agent,
task=task,
on_success=on_success,
on_failure=on_failure
)
print(f"Agent: {agent.name}")
print(f"Task: {task}")
print(f"Schedule: Every 1 hour")
print("="*80)
print("\n⏰ Starting scheduler... (Press Ctrl+C to stop)\n")
# Start the scheduler
# - "hourly" or "*/1h" = every 1 hour
# - run_immediately=True = run once before starting the schedule
scheduler.start(
schedule_expr="hourly", # Run every hour
max_retries=3, # Retry up to 3 times on failure
run_immediately=True # Run immediately on start
)
try:
# Keep the main thread alive
while scheduler.is_running:
import time
time.sleep(1)
except KeyboardInterrupt:
print("\n\n🛑 Stopping scheduler...")
scheduler.stop()
# Print final statistics
stats = scheduler.get_stats()
print("\n📊 Final Statistics:")
print(f" Total Executions: {stats['total_executions']}")
print(f" Successful: {stats['successful_executions']}")
print(f" Failed: {stats['failed_executions']}")
print(f" Success Rate: {stats['success_rate']:.1f}%")
print("\n✅ Agent stopped successfully\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,143 @@
"""
Live News Checker Agent - 24/7 Scheduled Agent
This example demonstrates a real working agent that checks AI news
every hour using the AgentScheduler with actual API calls.
Requirements:
- OPENAI_API_KEY environment variable set
- praisonaiagents package installed
Usage:
python news_checker_live.py
"""
import os
import sys
from datetime import datetime
# Add parent directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../..'))
from praisonaiagents import Agent
from praisonai.scheduler import AgentScheduler
# Import search_tool from root tools.py
try:
from tools import search_tool
except ImportError:
# Fallback: create inline search tool
from duckduckgo_search import DDGS
def search_tool(query: str) -> list:
"""Search the web using DuckDuckGo."""
try:
results = []
ddgs = DDGS()
for result in ddgs.text(keywords=query, max_results=5):
results.append({
"title": result.get("title", ""),
"url": result.get("href", ""),
"snippet": result.get("body", ""),
})
return results
except Exception as e:
print(f"Search error: {e}")
return []
def on_success(result):
"""Callback when agent execution succeeds."""
print("\n" + "="*80)
print(f"✅ SUCCESS - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("="*80)
print(result)
print("="*80 + "\n")
def on_failure(error):
"""Callback when agent execution fails."""
print("\n" + "="*80)
print(f"❌ FAILURE - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("="*80)
print(f"Error: {error}")
print("="*80 + "\n")
def main():
"""Main function to run the scheduled news checker."""
# Check for API key
if not os.getenv("OPENAI_API_KEY"):
print("❌ Error: OPENAI_API_KEY environment variable not set")
print("Please set it with: export OPENAI_API_KEY=your_key_here")
sys.exit(1)
print("\n🤖 Starting 24/7 AI News Checker Agent")
print("="*80)
# Create the agent with web search capability
agent = Agent(
name="AI News Monitor",
role="Technology News Analyst",
goal="Monitor and summarize the latest AI and technology news",
backstory="""You are an expert AI news analyst who stays up-to-date
with the latest developments in artificial intelligence, machine learning,
and technology. You provide concise, accurate summaries of important news.""",
tools=[search_tool],
verbose=True,
self_reflect=False # Disable reflection for faster execution
)
# Define the task
task = """Search for the latest AI news from today.
Focus on major developments, breakthroughs, or announcements.
Provide a summary of the top 3 most important news items.
Include the source and key details for each item."""
# Create scheduler with callbacks
scheduler = AgentScheduler(
agent=agent,
task=task,
on_success=on_success,
on_failure=on_failure
)
print(f"Agent: {agent.name}")
print(f"Task: {task}")
print(f"Schedule: Every 1 hour")
print("="*80)
print("\n⏰ Starting scheduler... (Press Ctrl+C to stop)\n")
# Start the scheduler
# For testing: use "*/2m" (every 2 minutes) instead of "hourly"
# For production: use "hourly"
scheduler.start(
schedule_expr="*/2m", # Every 2 minutes for testing
max_retries=3,
run_immediately=True # Run once immediately
)
try:
# Keep the main thread alive
while scheduler.is_running:
import time
time.sleep(1)
except KeyboardInterrupt:
print("\n\n🛑 Stopping scheduler...")
scheduler.stop()
# Print final statistics
stats = scheduler.get_stats()
print("\n📊 Final Statistics:")
print(f" Total Executions: {stats['total_executions']}")
print(f" Successful: {stats['successful_executions']}")
print(f" Failed: {stats['failed_executions']}")
print(f" Success Rate: {stats['success_rate']:.1f}%")
print("\n✅ Agent stopped successfully\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,61 @@
"""
Simple test script for 24/7 Agent Scheduler
This script demonstrates the basic usage of AgentScheduler
for running agents periodically.
Usage:
python simple_test.py
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..'))
from unittest.mock import Mock
from praisonai.scheduler import AgentScheduler
# Create a mock agent for testing
mock_agent = Mock()
mock_agent.name = "TestAgent"
mock_agent.start = Mock(return_value="Test execution successful!")
# Create scheduler
scheduler = AgentScheduler(
agent=mock_agent,
task="Test task execution"
)
print("🤖 Testing AgentScheduler")
print("=" * 60)
# Test 1: Execute once
print("\n✅ Test 1: Execute once")
result = scheduler.execute_once()
print(f"Result: {result}")
# Test 2: Get stats
print("\n✅ Test 2: Get statistics")
stats = scheduler.get_stats()
print(f"Stats: {stats}")
# Test 3: Start scheduler (will run every 5 seconds)
print("\n✅ Test 3: Start scheduler (*/5s)")
scheduler.start("*/5s", run_immediately=True)
import time
print("Running for 12 seconds...")
time.sleep(12)
# Stop scheduler
print("\n✅ Test 4: Stop scheduler")
scheduler.stop()
# Final stats
print("\n📊 Final Statistics:")
final_stats = scheduler.get_stats()
for key, value in final_stats.items():
print(f" {key}: {value}")
print("\n✅ All tests passed!")
print("=" * 60)
@@ -0,0 +1,14 @@
framework: praisonai
agents:
- name: "5 Second Timer"
role: "Timer Agent"
instructions: "Display the current time"
verbose: true
task: "Show current time"
schedule:
interval: "*/5s"
max_retries: 3
run_immediately: true
+14
View File
@@ -0,0 +1,14 @@
framework: praisonai
agents:
- name: "Counter Agent"
role: "Counter"
instructions: "Count from 1 to 5, incrementing by 1 each time. Just output the numbers clearly."
verbose: true
task: "Print numbers 1, 2, 3, 4, 5 (one number per line)"
schedule:
interval: "*/5s"
max_retries: 3
run_immediately: true
@@ -0,0 +1,16 @@
framework: praisonai
agents:
- name: "Counter Agent with Limits"
role: "Counter"
instructions: "Count from 1 to 5, incrementing by 1 each time. Just output the numbers clearly."
verbose: true
task: "Print numbers 1, 2, 3, 4, 5 (one number per line)"
schedule:
interval: "*/5s" # Run every 5 seconds
max_retries: 3 # Retry up to 3 times on failure
run_immediately: true # Run once immediately on start
timeout: 30 # Max 30 seconds per execution
max_cost: 0.01 # Stop after spending $0.01 USD
@@ -0,0 +1,15 @@
framework: praisonai
agents:
- name: "Budget Test Agent"
role: "Tester"
instructions: "Count from 1 to 3"
verbose: true
task: "Print numbers 1, 2, 3"
schedule:
interval: "*/3s"
max_retries: 2
run_immediately: true
# No max_cost specified - should default to $1.00
+73
View File
@@ -0,0 +1,73 @@
"""
Basic Agent Skills Usage Example
This example demonstrates how to use Agent Skills with PraisonAI Agents.
Skills are modular capabilities that can be loaded and used by agents.
"""
from praisonaiagents import Agent, SkillManager
# Example 1: Using SkillManager directly
print("=" * 50)
print("Example 1: Using SkillManager directly")
print("=" * 50)
# Create a skill manager
manager = SkillManager()
# Discover skills from a directory
# This scans the directory for subdirectories containing SKILL.md files
skill_count = manager.discover(["./pdf-processing"], include_defaults=False)
print(f"Discovered {skill_count} skill(s)")
# List available skills
for skill in manager:
print(f" - {skill.properties.name}: {skill.properties.description[:50]}...")
# Generate prompt XML for system prompt injection
prompt_xml = manager.to_prompt()
print("\nGenerated prompt XML:")
print(prompt_xml)
# Example 2: Using skills with an Agent
print("\n" + "=" * 50)
print("Example 2: Using skills with an Agent")
print("=" * 50)
# Create an agent with skills
# Skills are lazy-loaded only when accessed
agent = Agent(
name="PDF Assistant",
instructions="You are a helpful assistant that can process PDF documents.",
skills=["./pdf-processing"], # Direct skill paths
)
# Access the skill manager through the agent
if agent.skill_manager:
print(f"Agent has {len(agent.skill_manager)} skill(s) loaded")
# Get the skills prompt for system prompt injection
skills_prompt = agent.get_skills_prompt()
print("\nAgent skills prompt:")
print(skills_prompt)
# Example 3: Discovering skills from multiple directories
print("\n" + "=" * 50)
print("Example 3: Using skills_dirs for discovery")
print("=" * 50)
# Create an agent that discovers skills from directories
agent_with_discovery = Agent(
name="Multi-Skill Agent",
instructions="You are a versatile assistant with multiple skills.",
skills_dirs=["./"], # Scan current directory for skill subdirectories
)
if agent_with_discovery.skill_manager:
print(f"Discovered {len(agent_with_discovery.skill_manager)} skill(s)")
for name in agent_with_discovery.skill_manager.skill_names:
print(f" - {name}")
print("\n" + "=" * 50)
print("Examples complete!")
print("=" * 50)
+163
View File
@@ -0,0 +1,163 @@
"""
Custom Skill Creation Example
This example demonstrates how to programmatically create and validate
custom Agent Skills using PraisonAI Agents.
"""
import tempfile
from pathlib import Path
from praisonaiagents.skills import (
SkillManager,
SkillLoader,
validate,
read_properties,
)
def create_custom_skill(
name: str,
description: str,
instructions: str,
base_dir: str = None
) -> Path:
"""Create a custom skill programmatically.
Args:
name: Skill name (kebab-case)
description: What the skill does
instructions: Markdown instructions for the skill
base_dir: Directory to create skill in (default: temp dir)
Returns:
Path to the created skill directory
"""
if base_dir is None:
base_dir = tempfile.mkdtemp()
skill_dir = Path(base_dir) / name
skill_dir.mkdir(parents=True, exist_ok=True)
# Create SKILL.md
skill_md_content = f"""---
name: {name}
description: {description}
license: Apache-2.0
metadata:
author: custom
version: "1.0"
---
{instructions}
"""
(skill_dir / "SKILL.md").write_text(skill_md_content)
# Create optional directories
(skill_dir / "scripts").mkdir(exist_ok=True)
(skill_dir / "references").mkdir(exist_ok=True)
(skill_dir / "assets").mkdir(exist_ok=True)
return skill_dir
def main():
print("=" * 60)
print("Custom Skill Creation Example")
print("=" * 60)
# Create a custom skill
print("\n1. Creating a custom skill...")
skill_path = create_custom_skill(
name="code-review",
description="Review code for best practices, bugs, and improvements. Use when asked to review or analyze code quality.",
instructions="""
# Code Review Skill
## Overview
This skill enables thorough code review with focus on:
- Code quality and readability
- Potential bugs and edge cases
- Performance considerations
- Security vulnerabilities
- Best practices adherence
## Instructions
When reviewing code:
1. **Read the code carefully** - Understand the purpose and flow
2. **Check for bugs** - Look for null checks, edge cases, error handling
3. **Evaluate readability** - Variable names, comments, structure
4. **Consider performance** - Time/space complexity, unnecessary operations
5. **Security review** - Input validation, injection risks, data exposure
6. **Suggest improvements** - Provide actionable recommendations
## Output Format
Structure your review as:
- Summary: Brief overview of the code
- Issues: List of problems found (severity: high/medium/low)
- Suggestions: Recommended improvements
- Positive aspects: What's done well
"""
)
print(f" Created skill at: {skill_path}")
# Validate the skill
print("\n2. Validating the skill...")
errors = validate(skill_path)
if errors:
print(" Validation errors:")
for error in errors:
print(f" - {error}")
else:
print(" ✓ Skill is valid!")
# Load and inspect the skill
print("\n3. Loading skill properties...")
props = read_properties(skill_path)
print(f" Name: {props.name}")
print(f" Description: {props.description[:60]}...")
print(f" License: {props.license}")
# Use with SkillLoader for progressive loading
print("\n4. Using SkillLoader for progressive disclosure...")
loader = SkillLoader()
# Level 1: Load metadata only
skill = loader.load_metadata(str(skill_path))
print(f" Level 1 - Metadata loaded: {skill.properties.name}")
print(f" Instructions loaded: {skill.is_activated}")
# Level 2: Activate (load instructions)
loader.activate(skill)
print(f" Level 2 - Activated: {skill.is_activated}")
print(f" Instructions preview: {skill.instructions[:100]}...")
# Use with SkillManager
print("\n5. Using with SkillManager...")
manager = SkillManager()
manager.add_skill(str(skill_path))
# Generate prompt XML
prompt = manager.to_prompt()
print(" Generated prompt XML:")
print(prompt)
# Clean up
print("\n6. Cleanup...")
import shutil
shutil.rmtree(skill_path.parent)
print(" Temporary files removed.")
print("\n" + "=" * 60)
print("Example complete!")
print("=" * 60)
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
---
name: pdf-processing
description: Process and extract information from PDF documents. Use this skill when the user asks to read, analyze, or extract data from PDF files.
license: Apache-2.0
compatibility: Works with PraisonAI Agents
metadata:
author: praisonai
version: "1.0"
---
# PDF Processing Skill
## Overview
This skill enables agents to process PDF documents, extract text content, and analyze the information within them.
## When to Use
Activate this skill when:
- User asks to read or analyze a PDF file
- User needs to extract text from a PDF
- User wants to summarize a PDF document
- User needs to search for information within a PDF
## Instructions
1. First, verify the PDF file exists at the specified path
2. Use appropriate tools to read the PDF content
3. Extract text while preserving structure where possible
4. For large PDFs, process in chunks to manage context
5. Summarize or analyze based on user's specific request
## Best Practices
- Always confirm the file path before processing
- Handle encrypted PDFs gracefully with appropriate error messages
- For scanned PDFs, note that OCR may be required
- Preserve important formatting like tables and lists
@@ -0,0 +1,35 @@
# PraisonAI Agents - Benchmark Results
**Generated:** 2025-12-18 14:40:22
**Iterations:** 100
**Test:** Agent instantiation (without tools)
## Results
| Framework | Avg Time (μs) | Relative |
|-----------|---------------|----------|
| **PraisonAI** | **3.77** | **1.00x (fastest)** |
| OpenAI Agents SDK | 5.26 | 1.39x |
| Agno | 5.64 | 1.49x |
| PraisonAI (LiteLLM) | 7.56 | 2.00x |
| PydanticAI | 226.94 | 60.16x |
| LangGraph | 4,558.71 | 1,209x |
| CrewAI | 15,607.92 | 4,138x |
## Package Versions
| Package | Version |
|---------|--------|
| PraisonAI | 0.1.5 |
| Agno | 2.3.14 |
| PydanticAI | 1.35.0 |
| OpenAI Agents SDK | 0.6.3 |
| LangGraph | 1.0.5 |
| CrewAI | 1.6.1 |
## How to Reproduce
```bash
cd praisonai-agents
python benchmarks/simple_benchmark.py
```
@@ -0,0 +1,31 @@
# PraisonAI Agents - Real Execution Benchmark
**Generated:** 2025-12-18 14:43:52
**Model:** gpt-4o-mini
**Iterations:** 3
**Prompt:** "What is 2+2? Reply with just the number."
## Results
| Framework | Method | Avg Time | Relative |
|-----------|--------|----------|----------|
| **PraisonAI** | `agent.start()` | **0.45s** | **1.00x (fastest)** |
| PraisonAI (LiteLLM) | `agent.start()` | 0.55s | 1.22x |
| CrewAI | `crew.kickoff()` | 0.58s | 1.28x |
| Agno | `agent.run()` | 0.92s | 2.05x |
## Package Versions
| Package | Version |
|---------|--------|
| PraisonAI | 0.1.5 |
| Agno | 2.3.14 |
| CrewAI | 1.6.1 |
## How to Reproduce
```bash
export OPENAI_API_KEY=your_key
cd praisonai-agents
python benchmarks/execution_benchmark.py
```
@@ -0,0 +1,72 @@
# PraisonAI Agents - Real-World Execution Benchmark
**Generated:** 2025-12-18 14:33:49
**Model:** gpt-4o-mini
**Iterations:** 3 per test
## Results Summary
| Test | PraisonAI (avg) | Agno (avg) | Difference |
|------|-----------------|------------|------------|
| Single Agent | 2.04s | 3.18s | PraisonAI 1.56x faster |
| 2 Agents | 2.49s | 20.49s | PraisonAI 8.21x faster |
| 3 Agents | 6.52s | 13.97s | PraisonAI 2.14x faster |
## Detailed Results
### praisonai_single
- Average: 2.04s
- Min: 1.95s
- Max: 2.19s
- Std Dev: 0.13s
- Times: ['2.19s', '1.95s', '2.00s']
### agno_single
- Average: 3.18s
- Min: 3.00s
- Max: 3.51s
- Std Dev: 0.28s
- Times: ['3.51s', '3.00s', '3.04s']
### praisonai_two_agents
- Average: 2.49s
- Min: 2.22s
- Max: 2.65s
- Std Dev: 0.24s
- Times: ['2.65s', '2.22s', '2.62s']
### agno_two_agents
- Average: 20.49s
- Min: 9.96s
- Max: 35.05s
- Std Dev: 13.02s
- Times: ['16.46s', '9.96s', '35.05s']
### praisonai_three_agents
- Average: 6.52s
- Min: 5.62s
- Max: 7.39s
- Std Dev: 0.88s
- Times: ['5.62s', '6.54s', '7.39s']
### agno_three_agents
- Average: 13.97s
- Min: 11.69s
- Max: 15.32s
- Std Dev: 1.99s
- Times: ['14.91s', '11.69s', '15.32s']
## Package Versions
| Package | Version |
|---------|--------|
| PraisonAI | 0.1.5 |
| Agno | 2.3.14 |
## How to Reproduce
```bash
export OPENAI_API_KEY=your_key
cd praisonai-agents
python benchmarks/real_benchmark.py
```
@@ -0,0 +1,22 @@
# PraisonAI Agents - Benchmark Results
**Generated:** 2025-12-18 13:23:43
## Agent Instantiation Time
| Framework | Avg Time (μs) | Relative |
|-----------|---------------|----------|
| **PraisonAI** | **4.16** | **1.00x (fastest)** |
| OpenAI Agents SDK | 5.55 | 1.33x |
| Agno | 5.58 | 1.34x |
| PraisonAI (LiteLLM) | 8.52 | 2.05x |
| PydanticAI | 225.87 | 54.25x |
| LangGraph | 4,349.54 | 1,045x |
| CrewAI | 15,911.11 | 3,821x |
## How to Reproduce
```bash
cd praisonai-agents
python benchmarks/simple_benchmark.py
```
@@ -0,0 +1,33 @@
# PraisonAI Agents - Tools Benchmark Results
**Generated:** 2025-12-18 14:40:35
**Iterations:** 100
**Test:** Agent instantiation WITH TOOLS
## Results
| 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 |
## Package Versions
| Package | Version |
|---------|--------|
| PraisonAI | 0.1.5 |
| Agno | 2.3.14 |
| OpenAI Agents SDK | 0.6.3 |
| LangGraph | 1.0.5 |
| CrewAI | 1.6.1 |
## How to Reproduce
```bash
cd praisonai-agents
python benchmarks/tools_benchmark.py
```
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""
PraisonAI Agents - Real Execution Benchmark
Benchmarks ACTUAL agent execution with LLM API calls.
Tests single agent running a simple task across frameworks.
Usage:
export OPENAI_API_KEY=your_key
python benchmarks/execution_benchmark.py
python benchmarks/execution_benchmark.py --no-save
python benchmarks/execution_benchmark.py --iterations 5
python benchmarks/execution_benchmark.py --model gpt-4o --iterations 10
"""
import time
import os
import argparse
from importlib.metadata import version as get_version
# Defaults (can be overridden via CLI)
DEFAULT_MODEL = "gpt-4o-mini"
DEFAULT_ITERATIONS = 3
DEFAULT_PROMPT = "What is 2+2? Reply with just the number."
def get_package_versions():
"""Get version numbers for benchmarked packages."""
packages = {
'PraisonAI': 'praisonaiagents',
'Agno': 'agno',
'CrewAI': 'crewai',
}
versions = {}
for name, pkg in packages.items():
try:
versions[name] = get_version(pkg)
except:
versions[name] = 'not installed'
return versions
def benchmark_praisonai(model, iterations, prompt):
"""Benchmark PraisonAI agent execution using agent.start()"""
from praisonaiagents import Agent
print("\n--- PraisonAI ---")
print("Method: agent.start()")
times = []
for i in range(iterations):
agent = Agent(
name="Calculator",
instructions="You are a helpful assistant. Be very brief.",
llm=model,
verbose=False
)
start = time.perf_counter()
response = agent.start(prompt)
elapsed = time.perf_counter() - start
times.append(elapsed)
print(f" Run {i+1}: {elapsed:.2f}s - Response: {str(response)[:50]}")
avg = sum(times) / len(times)
print(f" Average: {avg:.2f}s")
return avg
def benchmark_praisonai_litellm(model, iterations, prompt):
"""Benchmark PraisonAI with LiteLLM backend using agent.start()"""
from praisonaiagents import Agent
print("\n--- PraisonAI (LiteLLM) ---")
print("Method: agent.start()")
times = []
for i in range(iterations):
agent = Agent(
name="Calculator",
instructions="You are a helpful assistant. Be very brief.",
llm=f"openai/{model}",
verbose=False
)
start = time.perf_counter()
response = agent.start(prompt)
elapsed = time.perf_counter() - start
times.append(elapsed)
print(f" Run {i+1}: {elapsed:.2f}s - Response: {str(response)[:50]}")
avg = sum(times) / len(times)
print(f" Average: {avg:.2f}s")
return avg
def benchmark_agno(model, iterations, prompt):
"""Benchmark Agno agent execution using agent.run()"""
from agno.agent import Agent
from agno.models.openai import OpenAIChat
print("\n--- Agno ---")
print("Method: agent.run()")
times = []
for i in range(iterations):
agent = Agent(
model=OpenAIChat(id=model),
instructions=["You are a helpful assistant. Be very brief."],
)
start = time.perf_counter()
response = agent.run(prompt)
elapsed = time.perf_counter() - start
times.append(elapsed)
# Extract content from response
content = str(response.content)[:50] if hasattr(response, 'content') else str(response)[:50]
print(f" Run {i+1}: {elapsed:.2f}s - Response: {content}")
avg = sum(times) / len(times)
print(f" Average: {avg:.2f}s")
return avg
def benchmark_crewai(model, iterations, prompt):
"""Benchmark CrewAI agent execution using crew.kickoff()"""
from crewai import Agent, Task, Crew
print("\n--- CrewAI ---")
print("Method: crew.kickoff()")
times = []
for i in range(iterations):
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
)
start = time.perf_counter()
response = crew.kickoff()
elapsed = time.perf_counter() - start
times.append(elapsed)
print(f" Run {i+1}: {elapsed:.2f}s - Response: {str(response)[:50]}")
avg = sum(times) / len(times)
print(f" Average: {avg:.2f}s")
return avg
def save_results(results: dict, model: str, iterations: int, prompt: str):
"""Save results to markdown file."""
import os
from datetime import datetime
filepath = os.path.join(os.path.dirname(__file__), 'EXECUTION_BENCHMARK_RESULTS.md')
versions = get_package_versions()
# Sort by time
sorted_results = sorted(results.items(), key=lambda x: x[1])
fastest = sorted_results[0][1]
with open(filepath, 'w') as f:
f.write('# PraisonAI Agents - Real Execution Benchmark\n\n')
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
f.write(f'**Model:** {model}\n')
f.write(f'**Iterations:** {iterations}\n')
f.write(f'**Prompt:** "{prompt}"\n\n')
f.write('## Results\n\n')
f.write('| Framework | Method | Avg Time | Relative |\n')
f.write('|-----------|--------|----------|----------|\n')
methods = {
'PraisonAI': 'agent.start()',
'PraisonAI (LiteLLM)': 'agent.start()',
'Agno': 'agent.run()',
'CrewAI': 'crew.kickoff()',
}
for name, avg in sorted_results:
ratio = avg / fastest
method = methods.get(name, 'unknown')
if ratio == 1.0:
f.write(f'| **{name}** | `{method}` | **{avg:.2f}s** | **1.00x (fastest)** |\n')
else:
f.write(f'| {name} | `{method}` | {avg:.2f}s | {ratio:.2f}x |\n')
f.write('\n## Package Versions\n\n')
f.write('| Package | Version |\n')
f.write('|---------|--------|\n')
for pkg, ver in versions.items():
f.write(f'| {pkg} | {ver} |\n')
f.write('\n## How to Reproduce\n\n')
f.write('```bash\n')
f.write('export OPENAI_API_KEY=your_key\n')
f.write('cd praisonai-agents\n')
f.write('python benchmarks/execution_benchmark.py\n')
f.write('```\n')
print(f'\nResults saved to: {filepath}')
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('--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}")')
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)
model = args.model
iterations = args.iterations
prompt = args.prompt
print('=' * 60)
print('PraisonAI Agents - Real Execution Benchmark')
print('=' * 60)
print(f'\nModel: {model}')
print(f'Iterations: {iterations}')
print(f'Prompt: "{prompt}"')
versions = get_package_versions()
print('\nPackage versions:')
for pkg, ver in versions.items():
print(f' {pkg}: {ver}')
results = {}
# Run benchmarks
try:
results['PraisonAI'] = benchmark_praisonai(model, iterations, prompt)
except Exception as e:
print(f"PraisonAI error: {e}")
try:
results['PraisonAI (LiteLLM)'] = benchmark_praisonai_litellm(model, iterations, prompt)
except Exception as e:
print(f"PraisonAI (LiteLLM) error: {e}")
try:
results['Agno'] = benchmark_agno(model, iterations, prompt)
except Exception as e:
print(f"Agno error: {e}")
try:
results['CrewAI'] = benchmark_crewai(model, iterations, prompt)
except Exception as e:
print(f"CrewAI error: {e}")
# Summary
print('\n' + '=' * 60)
print('SUMMARY')
print('=' * 60)
if results:
sorted_results = sorted(results.items(), key=lambda x: x[1])
fastest = sorted_results[0][1]
print(f"\n{'Framework':<25} {'Avg Time':<12} {'Relative':<10}")
print('-' * 47)
for name, avg in sorted_results:
ratio = avg / fastest
print(f'{name:<25} {avg:<12.2f}s {ratio:.2f}x')
print('\n' + '=' * 60)
# Save results (unless --no-save)
if results and not args.no_save:
save_results(results, model, iterations, prompt)
elif args.no_save:
print('\nResults not saved (--no-save flag used)')
@@ -259,6 +259,110 @@ def instantiate_crewai_agent():
)
def get_package_versions():
"""Get version numbers for all benchmarked packages."""
from importlib.metadata import version as get_version
packages = {
'PraisonAI': 'praisonaiagents',
'Agno': 'agno',
'PydanticAI': 'pydantic-ai',
'OpenAI Agents SDK': 'openai-agents',
'LangGraph': 'langgraph',
'CrewAI': 'crewai'
}
versions = {}
for display_name, pkg_name in packages.items():
try:
versions[display_name] = get_version(pkg_name)
except Exception:
versions[display_name] = 'not installed'
return versions
def save_benchmark_results(results: dict, baseline):
"""Save benchmark results to BENCHMARK_RESULTS.md and update README.md"""
import os
import re
from datetime import datetime
benchmarks_dir = os.path.dirname(__file__)
filepath = os.path.join(benchmarks_dir, 'BENCHMARK_RESULTS.md')
# Get package versions
versions = get_package_versions()
display_names = {
'agno': 'Agno',
'praisonai': 'PraisonAI',
'praisonai_litellm': 'PraisonAI (LiteLLM)',
'pydantic': 'PydanticAI',
'openai_agents': 'OpenAI Agents SDK',
'langgraph': 'LangGraph',
'crewai': 'CrewAI'
}
# Build table rows
table_rows = []
sorted_results = sorted(results.items(), key=lambda x: x[1].avg_run_time)
for name, result in sorted_results:
display_name = display_names.get(name, name)
avg_us = result.avg_run_time * 1e6
ratio = result.avg_run_time / baseline.avg_run_time if baseline.avg_run_time > 0 else 0
if name == 'praisonai':
table_rows.append(f'| **{display_name}** | **{avg_us:.2f}** | **1.00x (fastest)** |')
else:
avg_str = f'{avg_us:,.2f}' if avg_us >= 1000 else f'{avg_us:.2f}'
ratio_str = f'{ratio:,.0f}x' if ratio >= 100 else f'{ratio:.2f}x'
table_rows.append(f'| {display_name} | {avg_str} | {ratio_str} |')
# Save to BENCHMARK_RESULTS.md
with open(filepath, 'w') as f:
f.write('# PraisonAI Agents - Benchmark Results\n\n')
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
f.write('**Methodology:** 10 warmup runs + 1000 measured iterations\n\n')
f.write('## Agent Instantiation Time\n\n')
f.write('| Framework | Avg Time (μs) | Relative |\n')
f.write('|-----------|---------------|----------|\n')
f.write('\n'.join(table_rows) + '\n')
f.write('\n## Package Versions\n\n')
f.write('| Package | Version |\n')
f.write('|---------|--------|\n')
for pkg, ver in versions.items():
f.write(f'| {pkg} | {ver} |\n')
f.write('\n## How to Reproduce\n\n')
f.write('```bash\n')
f.write('cd praisonai-agents\n')
f.write('python benchmarks/performance_benchmark.py\n')
f.write('```\n')
print(f'\nResults saved to: {filepath}')
# Update README.md
readme_path = os.path.join(benchmarks_dir, '..', '..', '..', 'README.md')
if os.path.exists(readme_path):
with open(readme_path, 'r') as f:
content = f.read()
new_table = '''| Framework | Avg Time (μs) | Relative |
|-----------|---------------|----------|
''' + '\n'.join(table_rows)
pattern = r'(\| Framework \| Avg Time \(μs\) \| Relative \|\n\|[-|]+\|\n)(\|[^\n]+\|\n)+'
if re.search(pattern, content):
content = re.sub(pattern, new_table + '\n', content)
with open(readme_path, 'w') as f:
f.write(content)
print(f'README.md updated: {readme_path}')
if __name__ == "__main__":
print("="*70)
print("PraisonAI Agents - Comprehensive Performance Benchmark")
@@ -414,3 +518,6 @@ if __name__ == "__main__":
print(f"{display_name:<25} {result.avg_run_time*1e6:<15.2f} {result.avg_memory_usage*1024:<18.2f} {time_str:<15}")
print("\n" + "="*70)
# Save results to files
save_benchmark_results(results, baseline)
@@ -0,0 +1,481 @@
#!/usr/bin/env python3
"""
PraisonAI Agents - Real-World Execution Benchmark
Benchmarks actual agent execution with LLM calls across frameworks.
Tests single agent, 2-agent, and 3-agent workflows.
Usage:
python benchmarks/real_benchmark.py
"""
import time
import statistics
import os
from typing import Literal, List
from dataclasses import dataclass, field
from importlib.metadata import version as get_version
# Ensure API key is set
if not os.environ.get('OPENAI_API_KEY'):
print("ERROR: OPENAI_API_KEY environment variable not set")
exit(1)
@dataclass
class ExecutionResult:
"""Execution benchmark results"""
times: List[float] = field(default_factory=list)
@property
def avg_time(self) -> float:
return statistics.mean(self.times) if self.times else 0
@property
def min_time(self) -> float:
return min(self.times) if self.times else 0
@property
def max_time(self) -> float:
return max(self.times) if self.times else 0
@property
def std_dev(self) -> float:
return statistics.stdev(self.times) if len(self.times) > 1 else 0
def get_package_versions():
"""Get version numbers for benchmarked packages."""
packages = {
'PraisonAI': 'praisonaiagents',
'Agno': 'agno',
}
versions = {}
for display_name, pkg_name in packages.items():
try:
versions[display_name] = get_version(pkg_name)
except Exception:
versions[display_name] = 'not installed'
return versions
# ============================================================================
# Tool Definitions
# ============================================================================
def get_weather(city: Literal["nyc", "sf"]) -> str:
"""Get weather information for a city."""
if city == "nyc":
return "New York: Cloudy, 45°F, 60% humidity"
return "San Francisco: Sunny, 72°F, 40% humidity"
def get_time(timezone: Literal["est", "pst"]) -> str:
"""Get current time in a timezone."""
if timezone == "est":
return "Eastern Time: 2:30 PM"
return "Pacific Time: 11:30 AM"
def calculate(expression: str) -> str:
"""Calculate a math expression."""
try:
result = eval(expression)
return f"Result: {result}"
except Exception:
return "Error: Invalid expression"
# ============================================================================
# PraisonAI Benchmarks
# ============================================================================
def benchmark_praisonai_single(iterations: int = 3) -> ExecutionResult:
"""Benchmark single PraisonAI agent execution."""
from praisonaiagents import Agent
result = ExecutionResult()
for i in range(iterations):
print(f" PraisonAI single agent iteration {i+1}/{iterations}...")
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant. Be very brief.",
llm="gpt-4o-mini",
tools=[get_weather],
verbose=False
)
start = time.perf_counter()
response = agent.start("What's the weather in NYC? Reply in one sentence.")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
print(f" Time: {elapsed:.2f}s")
return result
def benchmark_praisonai_two_agents(iterations: int = 3) -> ExecutionResult:
"""Benchmark 2 PraisonAI agents working together."""
from praisonaiagents import Agent, PraisonAIAgents
result = ExecutionResult()
for i in range(iterations):
print(f" PraisonAI 2-agent iteration {i+1}/{iterations}...")
researcher = Agent(
name="Researcher",
role="Research Assistant",
goal="Gather weather information",
instructions="Get weather data and pass to reporter. Be brief.",
llm="gpt-4o-mini",
tools=[get_weather],
verbose=False
)
reporter = Agent(
name="Reporter",
role="News Reporter",
goal="Create weather report",
instructions="Create a one-sentence weather summary.",
llm="gpt-4o-mini",
verbose=False
)
agents = PraisonAIAgents(
agents=[researcher, reporter],
process="sequential",
verbose=False
)
start = time.perf_counter()
response = agents.start("Get NYC weather and create a brief report.")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
print(f" Time: {elapsed:.2f}s")
return result
def benchmark_praisonai_three_agents(iterations: int = 3) -> ExecutionResult:
"""Benchmark 3 PraisonAI agents working together."""
from praisonaiagents import Agent, PraisonAIAgents
result = ExecutionResult()
for i in range(iterations):
print(f" PraisonAI 3-agent iteration {i+1}/{iterations}...")
weather_agent = Agent(
name="WeatherAgent",
role="Weather Specialist",
goal="Get weather data",
instructions="Fetch weather information. Be brief.",
llm="gpt-4o-mini",
tools=[get_weather],
verbose=False
)
time_agent = Agent(
name="TimeAgent",
role="Time Specialist",
goal="Get time information",
instructions="Fetch timezone information. Be brief.",
llm="gpt-4o-mini",
tools=[get_time],
verbose=False
)
summarizer = Agent(
name="Summarizer",
role="Summary Writer",
goal="Combine information",
instructions="Create a one-sentence summary combining weather and time.",
llm="gpt-4o-mini",
verbose=False
)
agents = PraisonAIAgents(
agents=[weather_agent, time_agent, summarizer],
process="sequential",
verbose=False
)
start = time.perf_counter()
response = agents.start("Get NYC weather and EST time, then summarize.")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
print(f" Time: {elapsed:.2f}s")
return result
# ============================================================================
# Agno Benchmarks
# ============================================================================
def benchmark_agno_single(iterations: int = 3) -> ExecutionResult:
"""Benchmark single Agno agent execution."""
from agno.agent import Agent
from agno.models.openai import OpenAIChat
result = ExecutionResult()
for i in range(iterations):
print(f" Agno single agent iteration {i+1}/{iterations}...")
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
instructions=["You are a helpful assistant. Be very brief."],
tools=[get_weather],
)
start = time.perf_counter()
response = agent.run("What's the weather in NYC? Reply in one sentence.")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
print(f" Time: {elapsed:.2f}s")
return result
def benchmark_agno_two_agents(iterations: int = 3) -> ExecutionResult:
"""Benchmark 2 Agno agents working together (using Team)."""
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.team import Team
result = ExecutionResult()
for i in range(iterations):
print(f" Agno 2-agent iteration {i+1}/{iterations}...")
researcher = Agent(
name="Researcher",
model=OpenAIChat(id="gpt-4o-mini"),
instructions=["Get weather data. Be brief."],
tools=[get_weather],
)
reporter = Agent(
name="Reporter",
model=OpenAIChat(id="gpt-4o-mini"),
instructions=["Create a one-sentence weather summary."],
)
team = Team(
members=[researcher, reporter],
model=OpenAIChat(id="gpt-4o-mini"),
)
start = time.perf_counter()
response = team.run("Get NYC weather and create a brief report.")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
print(f" Time: {elapsed:.2f}s")
return result
def benchmark_agno_three_agents(iterations: int = 3) -> ExecutionResult:
"""Benchmark 3 Agno agents working together."""
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.team import Team
result = ExecutionResult()
for i in range(iterations):
print(f" Agno 3-agent iteration {i+1}/{iterations}...")
weather_agent = Agent(
name="WeatherAgent",
model=OpenAIChat(id="gpt-4o-mini"),
instructions=["Fetch weather information. Be brief."],
tools=[get_weather],
)
time_agent = Agent(
name="TimeAgent",
model=OpenAIChat(id="gpt-4o-mini"),
instructions=["Fetch timezone information. Be brief."],
tools=[get_time],
)
summarizer = Agent(
name="Summarizer",
model=OpenAIChat(id="gpt-4o-mini"),
instructions=["Create a one-sentence summary combining weather and time."],
)
team = Team(
members=[weather_agent, time_agent, summarizer],
model=OpenAIChat(id="gpt-4o-mini"),
)
start = time.perf_counter()
response = team.run("Get NYC weather and EST time, then summarize.")
elapsed = time.perf_counter() - start
result.times.append(elapsed)
print(f" Time: {elapsed:.2f}s")
return result
# ============================================================================
# Save Results
# ============================================================================
def save_results(results: dict, versions: dict):
"""Save benchmark results to file."""
import os
from datetime import datetime
filepath = os.path.join(os.path.dirname(__file__), 'REAL_BENCHMARK_RESULTS.md')
with open(filepath, 'w') as f:
f.write('# PraisonAI Agents - Real-World Execution Benchmark\n\n')
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
f.write('**Model:** gpt-4o-mini\n')
f.write('**Iterations:** 3 per test\n\n')
f.write('## Results Summary\n\n')
f.write('| Test | PraisonAI (avg) | Agno (avg) | Difference |\n')
f.write('|------|-----------------|------------|------------|\n')
tests = ['single', 'two_agents', 'three_agents']
test_names = ['Single Agent', '2 Agents', '3 Agents']
for test, name in zip(tests, test_names):
praison_key = f'praisonai_{test}'
agno_key = f'agno_{test}'
if praison_key in results and agno_key in results:
praison_avg = results[praison_key].avg_time
agno_avg = results[agno_key].avg_time
if praison_avg < agno_avg:
diff = f'PraisonAI {agno_avg/praison_avg:.2f}x faster'
else:
diff = f'Agno {praison_avg/agno_avg:.2f}x faster'
f.write(f'| {name} | {praison_avg:.2f}s | {agno_avg:.2f}s | {diff} |\n')
f.write('\n## Detailed Results\n\n')
for key, result in results.items():
f.write(f'### {key}\n')
f.write(f'- Average: {result.avg_time:.2f}s\n')
f.write(f'- Min: {result.min_time:.2f}s\n')
f.write(f'- Max: {result.max_time:.2f}s\n')
f.write(f'- Std Dev: {result.std_dev:.2f}s\n')
f.write(f'- Times: {[f"{t:.2f}s" for t in result.times]}\n\n')
f.write('## Package Versions\n\n')
f.write('| Package | Version |\n')
f.write('|---------|--------|\n')
for pkg, ver in versions.items():
f.write(f'| {pkg} | {ver} |\n')
f.write('\n## How to Reproduce\n\n')
f.write('```bash\n')
f.write('export OPENAI_API_KEY=your_key\n')
f.write('cd praisonai-agents\n')
f.write('python benchmarks/real_benchmark.py\n')
f.write('```\n')
print(f'\nResults saved to: {filepath}')
# ============================================================================
# Main
# ============================================================================
if __name__ == "__main__":
print("="*70)
print("PraisonAI Agents - Real-World Execution Benchmark")
print("="*70)
print("\nThis benchmark measures actual agent execution time including:")
print(" - LLM API calls")
print(" - Tool execution")
print(" - Multi-agent coordination")
print("\nModel: gpt-4o-mini")
print("Iterations: 3 per test")
versions = get_package_versions()
print(f"\nPackage versions:")
for pkg, ver in versions.items():
print(f" {pkg}: {ver}")
results = {}
# Single Agent Tests
print("\n" + "="*70)
print("SINGLE AGENT BENCHMARK")
print("="*70)
print("\nTesting PraisonAI single agent...")
results['praisonai_single'] = benchmark_praisonai_single()
print("\nTesting Agno single agent...")
results['agno_single'] = benchmark_agno_single()
# 2-Agent Tests
print("\n" + "="*70)
print("2-AGENT BENCHMARK")
print("="*70)
print("\nTesting PraisonAI 2 agents...")
results['praisonai_two_agents'] = benchmark_praisonai_two_agents()
print("\nTesting Agno 2 agents (Team)...")
results['agno_two_agents'] = benchmark_agno_two_agents()
# 3-Agent Tests
print("\n" + "="*70)
print("3-AGENT BENCHMARK")
print("="*70)
print("\nTesting PraisonAI 3 agents...")
results['praisonai_three_agents'] = benchmark_praisonai_three_agents()
print("\nTesting Agno 3 agents (Team)...")
results['agno_three_agents'] = benchmark_agno_three_agents()
# Summary
print("\n" + "="*70)
print("RESULTS SUMMARY")
print("="*70)
print(f"\n{'Test':<20} {'PraisonAI (avg)':<18} {'Agno (avg)':<15} {'Difference':<20}")
print("-" * 73)
tests = [
('Single Agent', 'praisonai_single', 'agno_single'),
('2 Agents', 'praisonai_two_agents', 'agno_two_agents'),
('3 Agents', 'praisonai_three_agents', 'agno_three_agents'),
]
for name, praison_key, agno_key in tests:
praison_avg = results[praison_key].avg_time
agno_avg = results[agno_key].avg_time
if praison_avg < agno_avg:
diff = f'PraisonAI {agno_avg/praison_avg:.2f}x faster'
else:
diff = f'Agno {praison_avg/agno_avg:.2f}x faster'
print(f"{name:<20} {praison_avg:<18.2f}s {agno_avg:<15.2f}s {diff:<20}")
print("\n" + "="*70)
# Save results
save_results(results, versions)
@@ -0,0 +1,194 @@
❯ python benchmarks/performance_benchmark.py
/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/_warning_patch.py:47: UserWarning: Mixing V1 models and V2 models (or constructs, like `TypeAdapter`) is not supported. Please upgrade `Settings` to V2.
_original_warn(message, category, stacklevel, source)
[13:04:15] INFO [13:04:15] config.py:50
config.py:50 INFO
Using config path:
/Users/praison/.con
fig/crewai/settings
.json
======================================================================
PraisonAI Agents - Comprehensive Performance Benchmark
======================================================================
This benchmark measures:
1. Agent instantiation time (how fast an agent object is created)
2. Memory footprint (how much memory an agent uses)
Methodology:
- 10 warmup runs (not measured)
- 1000 measured iterations
- tracemalloc for memory measurement
- Baseline subtraction for accuracy
Frameworks available:
- PraisonAI: ✅
- Agno: ✅
- PydanticAI: ✅
- OpenAI Agents SDK: ✅
- LangGraph: ✅
- CrewAI: ✅
======================================================================
Testing PraisonAI Agents
======================================================================
============================================================
Benchmarking: PraisonAI
============================================================
Warmup runs: 10
Measured iterations: 1000
Measuring runtime...
Measuring memory...
Results for PraisonAI:
Average Time: 0.000003 seconds (3.20 μs)
Min Time: 0.000003 seconds
Max Time: 0.000025 seconds
Std Dev: 0.000001 seconds
Median Time: 0.000003 seconds
Avg Memory: 0.003678 MiB (3.77 KiB)
======================================================================
Testing PraisonAI Agents (LiteLLM)
======================================================================
============================================================
Benchmarking: PraisonAI (LiteLLM)
============================================================
Warmup runs: 10
Measured iterations: 1000
Measuring runtime...
Measuring memory...
Results for PraisonAI (LiteLLM):
Average Time: 0.000007 seconds (7.31 μs)
Min Time: 0.000007 seconds
Max Time: 0.000060 seconds
Std Dev: 0.000002 seconds
Median Time: 0.000007 seconds
Avg Memory: 0.006419 MiB (6.57 KiB)
======================================================================
Testing Agno
======================================================================
============================================================
Benchmarking: Agno
============================================================
Warmup runs: 10
Measured iterations: 1000
Measuring runtime...
Measuring memory...
Results for Agno:
Average Time: 0.000005 seconds (5.30 μs)
Min Time: 0.000005 seconds
Max Time: 0.000039 seconds
Std Dev: 0.000002 seconds
Median Time: 0.000005 seconds
Avg Memory: 0.006599 MiB (6.76 KiB)
======================================================================
Testing PydanticAI
======================================================================
============================================================
Benchmarking: PydanticAI Agent Instantiation
============================================================
Warmup runs: 10
Measured iterations: 1000
Measuring runtime...
Measuring memory...
Results for PydanticAI Agent Instantiation:
Average Time: 0.000247 seconds (247.34 μs)
Min Time: 0.000218 seconds
Max Time: 0.000552 seconds
Std Dev: 0.000031 seconds
Median Time: 0.000237 seconds
Avg Memory: 0.028486 MiB (29.17 KiB)
======================================================================
Testing OpenAI Agents SDK
======================================================================
============================================================
Benchmarking: OpenAI Agents SDK Instantiation
============================================================
Warmup runs: 10
Measured iterations: 1000
Measuring runtime...
Measuring memory...
Results for OpenAI Agents SDK Instantiation:
Average Time: 0.000282 seconds (282.25 μs)
Min Time: 0.000247 seconds
Max Time: 0.001110 seconds
Std Dev: 0.000055 seconds
Median Time: 0.000263 seconds
Avg Memory: 0.029032 MiB (29.73 KiB)
======================================================================
Testing LangGraph
======================================================================
============================================================
Benchmarking: LangGraph Agent Instantiation
============================================================
Warmup runs: 10
Measured iterations: 1000
Measuring runtime...
Measuring memory...
Results for LangGraph Agent Instantiation:
Average Time: 0.003200 seconds (3200.15 μs)
Min Time: 0.002808 seconds
Max Time: 0.116363 seconds
Std Dev: 0.003628 seconds
Median Time: 0.002972 seconds
Avg Memory: 0.207509 MiB (212.49 KiB)
======================================================================
Testing CrewAI
======================================================================
============================================================
Benchmarking: CrewAI Agent Instantiation
============================================================
Warmup runs: 10
Measured iterations: 1000
Measuring runtime...
Measuring memory...
Results for CrewAI Agent Instantiation:
Average Time: 0.016772 seconds (16771.69 μs)
Min Time: 0.014404 seconds
Max Time: 0.091841 seconds
Std Dev: 0.003445 seconds
Median Time: 0.015816 seconds
Avg Memory: 0.065050 MiB (66.61 KiB)
======================================================================
RESULTS SUMMARY
======================================================================
Framework Avg Time (μs) Avg Memory (KiB) Relative
-------------------------------------------------------------------------
PraisonAI 3.20 3.77 1.00x
Agno 5.30 6.76 1.66x
PraisonAI (LiteLLM) 7.31 6.57 2.28x
PydanticAI 247.34 29.17 77.25x
OpenAI Agents SDK 282.25 29.73 88.15x
LangGraph 3200.15 212.49 999.46x
CrewAI 16771.69 66.61 5238.06x
======================================================================
❯
@@ -33,8 +33,7 @@ def measure_instantiation(create_fn, iterations=ITERATIONS):
def run_benchmark():
"""Run the benchmark across all available frameworks."""
tools = [sample_tool]
"""Run the benchmark across all available frameworks (without tools)."""
results = {}
print('=' * 60)
@@ -43,15 +42,16 @@ def run_benchmark():
print(f'\nIterations: {ITERATIONS}')
print('Metric: Agent instantiation time (microseconds)\n')
# PraisonAI
# PraisonAI (without tools)
print("Testing PraisonAI...")
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', verbose=False)
)
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', verbose=False)
)
# Other frameworks for comparison
@@ -61,7 +61,7 @@ def run_benchmark():
from agno.agent import Agent as AgnoAgent
from agno.models.openai import OpenAIChat
results['Agno'] = measure_instantiation(
lambda: AgnoAgent(model=OpenAIChat(id='gpt-4o-mini'), tools=tools)
lambda: AgnoAgent(model=OpenAIChat(id='gpt-4o-mini'))
)
except ImportError:
pass
@@ -82,6 +82,43 @@ def run_benchmark():
except ImportError:
pass
try:
from langchain_core.tools import tool as langchain_tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
@langchain_tool
def get_weather_lg(city: Literal['nyc', 'sf']):
"""Get weather info."""
return 'sunny' if city == 'sf' else 'cloudy'
results['LangGraph'] = measure_instantiation(
lambda: create_react_agent(model=ChatOpenAI(model='gpt-4o-mini'), tools=[get_weather_lg])
)
except ImportError:
pass
try:
from crewai.agent import Agent as CrewAgent
from crewai.tools import tool as crewai_tool
@crewai_tool("Weather Tool")
def get_weather_crew(city: Literal['nyc', 'sf']):
"""Get weather info."""
return 'sunny' if city == 'sf' else 'cloudy'
results['CrewAI'] = measure_instantiation(
lambda: CrewAgent(
role='Weather Agent',
goal='Provide weather info',
backstory='A weather expert',
tools=[get_weather_crew],
verbose=False
)
)
except ImportError:
pass
# Print results
print('\n' + '=' * 60)
print('RESULTS')
@@ -99,5 +136,108 @@ def run_benchmark():
return results
def get_package_versions():
"""Get version numbers for benchmarked packages."""
from importlib.metadata import version as get_version
packages = {
'PraisonAI': 'praisonaiagents',
'Agno': 'agno',
'PydanticAI': 'pydantic-ai',
'OpenAI Agents SDK': 'openai-agents',
'LangGraph': 'langgraph',
'CrewAI': 'crewai'
}
versions = {}
for display_name, pkg_name in packages.items():
try:
versions[display_name] = get_version(pkg_name)
except Exception:
versions[display_name] = 'not installed'
return versions
def save_results(results: dict, filename: str = 'BENCHMARK_RESULTS.md'):
"""Save benchmark results to a markdown file and update README."""
import os
import re
from datetime import datetime
filepath = os.path.join(os.path.dirname(__file__), filename)
versions = get_package_versions()
# Find the fastest result (baseline)
sorted_results = sorted(results.items(), key=lambda x: x[1])
fastest_time = sorted_results[0][1] if sorted_results else 1
# Build the table rows
table_rows = []
for name, avg in sorted_results:
ratio = avg / fastest_time
if ratio == 1.0:
table_rows.append(f'| **{name}** | **{avg:.2f}** | **1.00x (fastest)** |')
else:
avg_str = f'{avg:,.2f}' if avg >= 1000 else f'{avg:.2f}'
ratio_str = f'{ratio:,.0f}x' if ratio >= 100 else f'{ratio:.2f}x'
table_rows.append(f'| {name} | {avg_str} | {ratio_str} |')
# Save to BENCHMARK_RESULTS.md
with open(filepath, 'w') as f:
f.write('# PraisonAI Agents - Benchmark Results\n\n')
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
f.write(f'**Iterations:** {ITERATIONS}\n')
f.write('**Test:** Agent instantiation (without tools)\n\n')
f.write('## Results\n\n')
f.write('| Framework | Avg Time (μs) | Relative |\n')
f.write('|-----------|---------------|----------|\n')
f.write('\n'.join(table_rows) + '\n')
f.write('\n## Package Versions\n\n')
f.write('| Package | Version |\n')
f.write('|---------|--------|\n')
for pkg, ver in versions.items():
f.write(f'| {pkg} | {ver} |\n')
f.write('\n## How to Reproduce\n\n')
f.write('```bash\n')
f.write('cd praisonai-agents\n')
f.write('python benchmarks/simple_benchmark.py\n')
f.write('```\n')
print(f'\nResults saved to: {filepath}')
# Also update the main README.md
readme_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'README.md')
if os.path.exists(readme_path):
update_readme(readme_path, table_rows)
return filepath
def update_readme(readme_path: str, table_rows: list):
"""Update the performance section in README.md with latest results."""
import re
with open(readme_path, 'r') as f:
content = f.read()
# Build the new table
new_table = '''| Framework | Avg Time (μs) | Relative |
|-----------|---------------|----------|
''' + '\n'.join(table_rows)
# Pattern to match the performance table
pattern = r'(\| Framework \| Avg Time \(μs\) \| Relative \|\n\|[-|]+\|\n)(\|[^\n]+\|\n)+'
if re.search(pattern, content):
content = re.sub(pattern, new_table + '\n', content)
with open(readme_path, 'w') as f:
f.write(content)
print(f'README.md updated: {readme_path}')
if __name__ == '__main__':
run_benchmark()
results = run_benchmark()
save_results(results)
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
PraisonAI Agents - Tools Benchmark
Compares agent instantiation times WITH TOOLS across frameworks.
Usage:
python benchmarks/tools_benchmark.py
"""
import time
from typing import Literal
from importlib.metadata import version as get_version
ITERATIONS = 100
def sample_tool(city: Literal['nyc', 'sf']):
"""Sample tool for benchmark testing."""
if city == 'nyc':
return 'cloudy'
return 'sunny'
def measure_instantiation(create_fn, iterations=ITERATIONS):
"""Measure average instantiation time in microseconds."""
times = []
# Warmup
for _ in range(10):
create_fn()
# Measure
for _ in range(iterations):
start = time.perf_counter()
create_fn()
elapsed = time.perf_counter() - start
times.append(elapsed * 1_000_000) # Convert to microseconds
return sum(times) / len(times)
def run_benchmark():
"""Run the benchmark across all available frameworks WITH TOOLS."""
tools = [sample_tool]
results = {}
print('=' * 60)
print('PraisonAI Agents - Tools Benchmark')
print('=' * 60)
print(f'\nIterations: {ITERATIONS}')
print('Metric: Agent instantiation time WITH TOOLS (microseconds)\n')
# PraisonAI with tools
print("Testing PraisonAI...")
from praisonaiagents import Agent as PraisonAgent
results['PraisonAI'] = measure_instantiation(
lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', tools=tools, verbose=False)
)
results['PraisonAI (LiteLLM)'] = measure_instantiation(
lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', tools=tools, verbose=False)
)
# Other frameworks with tools
print("Testing other frameworks...")
try:
from agno.agent import Agent as AgnoAgent
from agno.models.openai import OpenAIChat
results['Agno'] = measure_instantiation(
lambda: AgnoAgent(model=OpenAIChat(id='gpt-4o-mini'), tools=tools)
)
except ImportError:
pass
try:
from agents import Agent as OpenAIAgent, function_tool
results['OpenAI Agents SDK'] = measure_instantiation(
lambda: OpenAIAgent(name='Test', model='gpt-4o-mini', tools=[function_tool(sample_tool)])
)
except ImportError:
pass
try:
from langchain_core.tools import tool as langchain_tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
@langchain_tool
def get_weather_lg(city: Literal['nyc', 'sf']):
"""Get weather info."""
return 'sunny' if city == 'sf' else 'cloudy'
results['LangGraph'] = measure_instantiation(
lambda: create_react_agent(model=ChatOpenAI(model='gpt-4o-mini'), tools=[get_weather_lg])
)
except ImportError:
pass
try:
from crewai.agent import Agent as CrewAgent
from crewai.tools import tool as crewai_tool
@crewai_tool("Weather Tool")
def get_weather_crew(city: Literal['nyc', 'sf']):
"""Get weather info."""
return 'sunny' if city == 'sf' else 'cloudy'
results['CrewAI'] = measure_instantiation(
lambda: CrewAgent(
role='Weather Agent',
goal='Provide weather info',
backstory='A weather expert',
tools=[get_weather_crew],
verbose=False
)
)
except ImportError:
pass
# Print results
print('\n' + '=' * 60)
print('RESULTS (WITH TOOLS)')
print('=' * 60)
sorted_results = sorted(results.items(), key=lambda x: x[1])
baseline = sorted_results[0][1] if sorted_results else 1
print(f"\n{'Framework':<25} {'Avg Time (μs)':<15} {'Relative':<10}")
print('-' * 50)
for name, avg in sorted_results:
ratio = avg / baseline
print(f'{name:<25} {avg:<15.2f} {ratio:.2f}x')
print('\n' + '=' * 60)
return results
def get_package_versions():
"""Get version numbers for benchmarked packages."""
packages = {
'PraisonAI': 'praisonaiagents',
'Agno': 'agno',
'OpenAI Agents SDK': 'openai-agents',
'LangGraph': 'langgraph',
'CrewAI': 'crewai'
}
versions = {}
for display_name, pkg_name in packages.items():
try:
versions[display_name] = get_version(pkg_name)
except Exception:
versions[display_name] = 'not installed'
return versions
def save_results(results: dict, filename: str = 'TOOLS_BENCHMARK_RESULTS.md'):
"""Save benchmark results to a markdown file."""
import os
from datetime import datetime
filepath = os.path.join(os.path.dirname(__file__), filename)
versions = get_package_versions()
# Find the fastest result (baseline)
sorted_results = sorted(results.items(), key=lambda x: x[1])
fastest_time = sorted_results[0][1] if sorted_results else 1
# Build the table rows
table_rows = []
for name, avg in sorted_results:
ratio = avg / fastest_time
if ratio == 1.0:
table_rows.append(f'| **{name}** | **{avg:.2f}** | **1.00x (fastest)** |')
else:
avg_str = f'{avg:,.2f}' if avg >= 1000 else f'{avg:.2f}'
ratio_str = f'{ratio:,.0f}x' if ratio >= 100 else f'{ratio:.2f}x'
table_rows.append(f'| {name} | {avg_str} | {ratio_str} |')
# Save to file
with open(filepath, 'w') as f:
f.write('# PraisonAI Agents - Tools Benchmark Results\n\n')
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
f.write(f'**Iterations:** {ITERATIONS}\n')
f.write('**Test:** Agent instantiation WITH TOOLS\n\n')
f.write('## Results\n\n')
f.write('| Framework | Avg Time (μs) | Relative |\n')
f.write('|-----------|---------------|----------|\n')
f.write('\n'.join(table_rows) + '\n')
f.write('\n## Package Versions\n\n')
f.write('| Package | Version |\n')
f.write('|---------|--------|\n')
for pkg, ver in versions.items():
f.write(f'| {pkg} | {ver} |\n')
f.write('\n## How to Reproduce\n\n')
f.write('```bash\n')
f.write('cd praisonai-agents\n')
f.write('python benchmarks/tools_benchmark.py\n')
f.write('```\n')
print(f'\nResults saved to: {filepath}')
return filepath
if __name__ == '__main__':
results = run_benchmark()
save_results(results)
@@ -0,0 +1,90 @@
"""
Unified Web Search Example
This example demonstrates the search_web tool that automatically tries
multiple search providers with fallback support.
Search Provider Priority:
1. Tavily (requires TAVILY_API_KEY + tavily-python)
2. Exa (requires EXA_API_KEY + exa_py)
3. You.com (requires YDC_API_KEY + youdotcom)
4. DuckDuckGo (requires duckduckgo_search package, no API key)
5. SearxNG (requires requests + running SearxNG instance)
Installation:
pip install praisonaiagents duckduckgo_search
Usage:
python web_search_example.py
"""
from praisonaiagents import Agent
from praisonaiagents.tools import search_web, get_available_providers
def main():
print("=" * 60)
print("Unified Web Search - Provider Status")
print("=" * 60)
# Check which providers are available
providers = get_available_providers()
for p in providers:
status = "✓ Available" if p["available"] else f"✗ {p['reason']}"
print(f" {p['name']:12} {status}")
print("\n" + "=" * 60)
print("Example 1: Basic Search")
print("=" * 60)
# Simple search - automatically uses best available provider
results = search_web("Python programming best practices", max_results=3)
if results and "error" not in results[0]:
print(f"\nFound {len(results)} results using {results[0].get('provider', 'unknown')}:\n")
for r in results:
print(f" Title: {r.get('title', 'N/A')[:60]}...")
print(f" URL: {r.get('url', 'N/A')}")
print()
else:
print(f"\nSearch failed: {results}")
print("=" * 60)
print("Example 2: Specify Providers")
print("=" * 60)
# Only try specific providers
results = search_web(
"machine learning tutorials",
max_results=3,
providers=["duckduckgo", "tavily"] # Try these in order
)
if results and "error" not in results[0]:
print(f"\nFound {len(results)} results using {results[0].get('provider', 'unknown')}:\n")
for r in results:
print(f" Title: {r.get('title', 'N/A')[:60]}...")
print(f" URL: {r.get('url', 'N/A')}")
print()
else:
print(f"\nSearch failed: {results}")
print("=" * 60)
print("Example 3: With PraisonAI Agent")
print("=" * 60)
# Use search_web as an agent tool
agent = Agent(
name="SearchAgent",
role="Web Researcher",
goal="Find and summarize information from the web",
instructions="Search the web and provide concise summaries of findings.",
tools=[search_web]
)
result = agent.start("What are the top 3 AI trends in 2025?")
print(f"\nAgent Response:\n{result}")
if __name__ == "__main__":
main()
@@ -3,7 +3,7 @@ Praison AI Agents - A package for hierarchical AI agent task execution
"""
# Apply warning patch BEFORE any imports to intercept warnings at the source
from . import _warning_patch
from . import _warning_patch # noqa: F401
# Import centralized logging configuration FIRST
from . import _logging
@@ -78,6 +78,19 @@ def __getattr__(name):
elif name == "LineRange":
from praisonaiagents.context.fast import LineRange
return LineRange
# Agent Skills support (lazy loaded for zero performance impact)
elif name == "SkillManager":
from praisonaiagents.skills import SkillManager
return SkillManager
elif name == "SkillProperties":
from praisonaiagents.skills import SkillProperties
return SkillProperties
elif name == "SkillMetadata":
from praisonaiagents.skills import SkillMetadata
return SkillMetadata
elif name == "SkillLoader":
from praisonaiagents.skills import SkillLoader
return SkillLoader
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# Planning mode support
@@ -289,6 +302,23 @@ __all__ = [
'ExpandResult'
]
# Add workflow exports
__all__.extend([
'Workflow',
'WorkflowStep',
'WorkflowContext',
'StepResult',
'Route',
'Parallel',
'Loop',
'Repeat',
'route',
'parallel',
'loop',
'repeat',
'Pipeline'
])
# Add MCP to __all__ if available
if _mcp_available:
__all__.append('MCP')
@@ -305,3 +335,29 @@ __all__.extend([
'LineRange'
])
# Add Agent Skills exports (lazy loaded)
__all__.extend([
'SkillManager',
'SkillProperties',
'SkillMetadata',
'SkillLoader'
])
# AG-UI support (optional, lazy loaded)
try:
from praisonaiagents.ui.agui import AGUI
__all__.append('AGUI')
_agui_available = True
except ImportError:
_agui_available = False
AGUI = None
# A2A support (optional, lazy loaded)
try:
from praisonaiagents.ui.a2a import A2A
__all__.append('A2A')
_a2a_available = True
except ImportError:
_a2a_available = False
A2A = None
@@ -55,10 +55,10 @@ def _get_all_noisy_loggers() -> List[str]:
def _configure_loggers():
"""Configure all loggers based on LOGLEVEL environment variable."""
loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
loglevel = os.environ.get('LOGLEVEL', 'WARNING').upper()
# When DEBUG is set, allow some HTTP logging for API endpoints
if loglevel == 'DEBUG':
# When DEBUG or INFO is set, allow some HTTP logging for API endpoints
if loglevel in ('DEBUG', 'INFO'):
allowed_debug_loggers = {"httpx", "httpx._client", "openai._client"}
for logger_name in _get_all_noisy_loggers():
@@ -109,10 +109,10 @@ def configure_root_logger():
"""Configure the root logger with RichHandler."""
from rich.logging import RichHandler
loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
loglevel = os.environ.get('LOGLEVEL', 'WARNING').upper()
logging.basicConfig(
level=getattr(logging, loglevel, logging.INFO),
level=getattr(logging, loglevel, logging.WARNING),
format="%(asctime)s %(filename)s:%(lineno)d %(levelname)s %(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)],
@@ -8,9 +8,10 @@ import functools
import sys
# Apply aggressive warning filters first
warnings.filterwarnings("ignore", message=".*There is no current event loop.*", category=DeprecationWarning)
warnings.filterwarnings("ignore", message=".*Pydantic serializer warnings.*")
warnings.filterwarnings("ignore", message=".*PydanticSerializationUnexpectedValue.*")
warnings.filterwarnings("ignore", message=".*Expected 9 fields but got.*")
warnings.filterwarnings("ignore", message=".*Expected \\d+ fields but got.*")
warnings.filterwarnings("ignore", message=".*Expected `StreamingChoices` but got.*")
warnings.filterwarnings("ignore", message=".*serialized value may not be as expected.*")
warnings.filterwarnings("ignore", message=".*Use 'content=<...>' to upload raw bytes/text content.*")
@@ -27,9 +28,12 @@ SUPPRESSED_PATTERNS = [
"The `dict` method is deprecated; use `model_dump` instead",
"Pydantic serializer warnings",
"PydanticSerializationUnexpectedValue",
"Expected 9 fields but got 5 for type `Message`",
"Expected", # Catches all "Expected N fields but got M" patterns
"Expected `StreamingChoices` but got `Choices`",
"serialized value may not be as expected"
"serialized value may not be as expected",
"Mixing V1 models and V2 models",
"Please upgrade `Settings` to V2",
"There is no current event loop"
]
@functools.wraps(_original_warn)
@@ -41,7 +45,7 @@ def _patched_warn(message, category=None, stacklevel=1, source=None):
if pattern in msg_str:
return
if category == UserWarning and "pydantic" in msg_str.lower():
if category is UserWarning and "pydantic" in msg_str.lower():
return
_original_warn(message, category, stacklevel, source)
@@ -55,7 +59,7 @@ def _patched_warn_explicit(message, category, filename, lineno, module=None, reg
if pattern in msg_str:
return
if category == UserWarning and "pydantic" in msg_str.lower():
if category is UserWarning and "pydantic" in msg_str.lower():
return
if module and "pydantic" in str(module):
@@ -250,7 +250,9 @@ class Agent:
fast_context_parallelism: int = 8,
fast_context_timeout: float = 30.0,
history_in_context: Optional[int] = None,
auto_save: Optional[str] = None
auto_save: Optional[str] = None,
skills: Optional[List[str]] = None,
skills_dirs: Optional[List[str]] = None
):
"""Initialize an Agent instance.
@@ -607,6 +609,12 @@ Your Goal: {self.goal}
self.fast_context_timeout = fast_context_timeout
self._fast_context_instance = None # Lazy loaded
# Agent Skills configuration (lazy loaded for zero performance impact)
self._skills = skills
self._skills_dirs = skills_dirs
self._skill_manager = None # Lazy loaded
self._skills_initialized = False
@property
def console(self):
"""Lazily initialize Rich Console only when needed."""
@@ -626,6 +634,35 @@ Your Goal: {self.goal}
def fast_context_path(self, value):
self._fast_context_path = value
@property
def skill_manager(self):
"""Lazily initialize SkillManager only when skills are accessed."""
if self._skill_manager is None and (self._skills or self._skills_dirs):
from ..skills import SkillManager
self._skill_manager = SkillManager()
# Add explicit skill paths
if self._skills:
for skill_path in self._skills:
self._skill_manager.add_skill(skill_path)
# Discover skills from directories
if self._skills_dirs:
self._skill_manager.discover(self._skills_dirs, include_defaults=False)
self._skills_initialized = True
return self._skill_manager
def get_skills_prompt(self) -> str:
"""Get the XML prompt for available skills.
Returns:
XML string with <available_skills> block, or empty string if no skills
"""
if self.skill_manager is None:
return ""
return self.skill_manager.to_prompt()
@property
def _openai_client(self):
"""Lazily initialize OpenAI client only when needed."""
@@ -1538,13 +1575,13 @@ Your Goal: {self.goal}"""
logging.debug(f"{self.name} executing tool {function_name} with arguments: {arguments}")
# Check if approval is required for this tool
from ..approval import is_approval_required, console_approval_callback, get_risk_level, mark_approved, ApprovalDecision
from ..approval import is_approval_required, console_approval_callback, get_risk_level, mark_approved, ApprovalDecision, get_approval_callback
if is_approval_required(function_name):
risk_level = get_risk_level(function_name)
logging.info(f"Tool {function_name} requires approval (risk level: {risk_level})")
logging.debug(f"Tool {function_name} requires approval (risk level: {risk_level})")
# Use global approval callback or default console callback
callback = approval_callback or console_approval_callback
callback = get_approval_callback() or console_approval_callback
try:
decision = callback(function_name, arguments, risk_level)
@@ -1370,6 +1370,61 @@ Context:
agent_names = ", ".join([agent.name for agent in self.agents])
print(f"📊 Available agents for this endpoint ({len(self.agents)}): {agent_names}")
# Create per-agent endpoints for individual agent access
# This allows n8n and other tools to call specific agents
agents_dict = {agent.name.lower().replace(' ', '_'): agent for agent in self.agents}
# Add GET endpoint to list available agents
@_agents_shared_apps[port].get(f"{path}/list")
async def list_agents():
return {
"agents": [
{"name": agent.name, "id": agent.name.lower().replace(' ', '_')}
for agent in self.agents
]
}
# Add per-agent POST endpoints
for agent_id, agent_instance in agents_dict.items():
agent_path = f"{path}/{agent_id}"
# Create a closure to capture the agent instance
def create_agent_handler(agent):
async def handle_single_agent(request: Request):
try:
request_data = await request.json()
query = request_data.get("query", "")
if not query:
raise HTTPException(status_code=400, detail="Missing 'query' field")
except:
raise HTTPException(status_code=400, detail="Invalid JSON body")
try:
if asyncio.iscoroutinefunction(agent.chat):
response = await agent.achat(query)
else:
loop = asyncio.get_running_loop()
response = await loop.run_in_executor(None, lambda q=query: agent.chat(q))
return {
"agent": agent.name,
"query": query,
"response": response
}
except Exception as e:
logging.error(f"Error with agent {agent.name}: {str(e)}", exc_info=True)
return JSONResponse(
status_code=500,
content={"error": f"Agent error: {str(e)}"}
)
return handle_single_agent
# Register the endpoint
_agents_shared_apps[port].post(agent_path)(create_agent_handler(agent_instance))
_agents_registered_endpoints[port][agent_path] = f"{endpoint_id}_{agent_id}"
print(f"🔗 Per-agent endpoints: {', '.join([f'{path}/{aid}' for aid in agents_dict.keys()])}")
# Start the server if it's not already running for this port
if not _agents_server_started.get(port, False):
# Mark the server as started first to prevent duplicate starts
@@ -43,6 +43,15 @@ def set_approval_callback(callback_fn: Callable):
global approval_callback
approval_callback = callback_fn
def get_approval_callback() -> Optional[Callable]:
"""Get the current approval callback function.
Returns the custom callback if set, otherwise None.
This should be used instead of directly accessing approval_callback
to ensure the latest callback is always used.
"""
return approval_callback
def mark_approved(tool_name: str):
"""Mark a tool as approved in the current context."""
approved = _approved_context.get(set())
@@ -90,7 +99,7 @@ def require_approval(risk_level: RiskLevel = "high"):
except Exception as e:
# Fallback to sync approval if async fails
logging.warning(f"Async approval failed, using sync fallback: {e}")
callback = approval_callback or console_approval_callback
callback = get_approval_callback() or console_approval_callback
decision = callback(tool_name, kwargs, risk_level)
if not decision.approved:
@@ -198,7 +207,7 @@ async def request_approval(function_name: str, arguments: Dict[str, Any]) -> App
risk_level = TOOL_RISK_LEVELS.get(function_name, "medium")
# Use custom callback if set, otherwise use console callback
callback = approval_callback or console_approval_callback
callback = get_approval_callback() or console_approval_callback
try:
# Handle async callbacks
@@ -0,0 +1,68 @@
"""
PraisonAI Agents Evaluation Framework.
Provides comprehensive evaluation capabilities for AI agents with zero performance
impact when not in use through lazy loading.
Evaluator Types:
- AccuracyEvaluator: Compare output against expected output using LLM-as-judge
- PerformanceEvaluator: Measure runtime and memory usage
- ReliabilityEvaluator: Verify expected tool calls are made
- CriteriaEvaluator: Evaluate against custom criteria
Example:
>>> from praisonaiagents.eval import AccuracyEvaluator
>>> evaluator = AccuracyEvaluator(
... agent=my_agent,
... input_text="What is 2+2?",
... expected_output="4"
... )
>>> result = evaluator.run(print_summary=True)
"""
__all__ = [
"BaseEvaluator",
"AccuracyEvaluator",
"PerformanceEvaluator",
"ReliabilityEvaluator",
"CriteriaEvaluator",
"AccuracyResult",
"PerformanceResult",
"ReliabilityResult",
"CriteriaResult",
"EvaluationScore",
"PerformanceMetrics",
"ToolCallResult",
"CriteriaScore",
]
_LAZY_IMPORTS = {
"BaseEvaluator": ("base", "BaseEvaluator"),
"AccuracyEvaluator": ("accuracy", "AccuracyEvaluator"),
"PerformanceEvaluator": ("performance", "PerformanceEvaluator"),
"ReliabilityEvaluator": ("reliability", "ReliabilityEvaluator"),
"CriteriaEvaluator": ("criteria", "CriteriaEvaluator"),
"AccuracyResult": ("results", "AccuracyResult"),
"PerformanceResult": ("results", "PerformanceResult"),
"ReliabilityResult": ("results", "ReliabilityResult"),
"CriteriaResult": ("results", "CriteriaResult"),
"EvaluationScore": ("results", "EvaluationScore"),
"PerformanceMetrics": ("results", "PerformanceMetrics"),
"ToolCallResult": ("results", "ToolCallResult"),
"CriteriaScore": ("results", "CriteriaScore"),
}
def __getattr__(name: str):
"""Lazy import mechanism for zero-cost imports when not used."""
if name in _LAZY_IMPORTS:
module_name, attr_name = _LAZY_IMPORTS[name]
import importlib
module = importlib.import_module(f".{module_name}", __name__)
return getattr(module, attr_name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__():
"""Return list of available attributes for tab completion."""
return __all__
@@ -0,0 +1,239 @@
"""
Accuracy evaluator for PraisonAI Agents.
Evaluates agent output accuracy by comparing against expected output using LLM-as-judge.
"""
import os
import logging
from typing import Any, Callable, List, Optional, Union, TYPE_CHECKING
from .base import BaseEvaluator
from .results import AccuracyResult, EvaluationScore
if TYPE_CHECKING:
from ..agent.agent import Agent
from ..agents.agents import PraisonAIAgents
logger = logging.getLogger(__name__)
class AccuracyEvaluator(BaseEvaluator):
"""
Evaluates the accuracy of agent outputs against expected outputs.
Uses an LLM as a judge to score outputs on a scale of 1-10.
"""
def __init__(
self,
agent: Optional[Union["Agent", "PraisonAIAgents"]] = None,
func: Optional[Callable[..., str]] = None,
input_text: str = "",
expected_output: str = "",
num_iterations: int = 1,
model: Optional[str] = None,
name: Optional[str] = None,
save_results_path: Optional[str] = None,
verbose: bool = False,
):
"""
Initialize the accuracy evaluator.
Args:
agent: Agent or PraisonAIAgents instance to evaluate
func: Alternative callable that returns output string
input_text: Input to provide to the agent/function
expected_output: Expected output to compare against
num_iterations: Number of evaluation iterations
model: LLM model to use for judging (defaults to gpt-4o-mini)
name: Name for this evaluation
save_results_path: Path to save results
verbose: Enable verbose output
"""
super().__init__(name=name, save_results_path=save_results_path, verbose=verbose)
self.agent = agent
self.func = func
self.input_text = input_text
self.expected_output = expected_output
self.num_iterations = num_iterations
self.model = model or os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini")
if agent is None and func is None:
raise ValueError("Either 'agent' or 'func' must be provided")
def _get_output(self) -> str:
"""Get output from agent or function."""
if self.func:
return str(self.func(self.input_text))
if hasattr(self.agent, 'chat'):
return str(self.agent.chat(self.input_text))
elif hasattr(self.agent, 'start'):
result = self.agent.start(self.input_text)
if hasattr(result, 'raw'):
return str(result.raw)
return str(result)
else:
raise ValueError("Agent must have 'chat' or 'start' method")
def _judge_output(self, output: str) -> EvaluationScore:
"""
Use LLM to judge the output against expected output.
Args:
output: The actual output to evaluate
Returns:
EvaluationScore with score and reasoning
"""
try:
from openai import OpenAI
except ImportError:
raise ImportError("OpenAI package required for accuracy evaluation. Install with: pip install openai")
client = OpenAI()
prompt = f"""You are an expert evaluator. Compare the actual output against the expected output and provide a score from 1-10.
Input: {self.input_text}
Expected Output: {self.expected_output}
Actual Output: {output}
Scoring Guidelines:
- 10: Perfect match in meaning and completeness
- 8-9: Very close, minor differences that don't affect correctness
- 6-7: Mostly correct but missing some details or has minor errors
- 4-5: Partially correct but significant issues
- 2-3: Mostly incorrect but shows some understanding
- 1: Completely wrong or irrelevant
Respond in this exact format:
SCORE: [number 1-10]
REASONING: [brief explanation]"""
response = client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
max_tokens=200
)
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()
return EvaluationScore(
score=score,
reasoning=reasoning,
input_text=self.input_text,
output_text=output,
expected_output=self.expected_output
)
def run(self, print_summary: bool = False) -> AccuracyResult:
"""
Execute the accuracy evaluation.
Args:
print_summary: Whether to print summary after evaluation
Returns:
AccuracyResult with all evaluation scores
"""
self.before_run()
result = AccuracyResult(
eval_id=self.eval_id,
name=self.name
)
for i in range(self.num_iterations):
if self.verbose:
logger.info(f"Running iteration {i + 1}/{self.num_iterations}")
try:
output = self._get_output()
score = self._judge_output(output)
result.evaluations.append(score)
if self.verbose:
logger.info(f" Score: {score.score}/10 - {score.reasoning}")
except Exception as e:
logger.error(f"Error in iteration {i + 1}: {e}")
result.evaluations.append(EvaluationScore(
score=0.0,
reasoning=f"Error: {str(e)}",
input_text=self.input_text,
output_text="",
expected_output=self.expected_output
))
self.after_run(result)
if print_summary:
result.print_summary()
return result
async def run_async(self, print_summary: bool = False) -> AccuracyResult:
"""
Execute the accuracy evaluation asynchronously.
Args:
print_summary: Whether to print summary after evaluation
Returns:
AccuracyResult with all evaluation scores
"""
await self.async_before_run()
result = self.run(print_summary=False)
await self.async_after_run(result)
if print_summary:
result.print_summary()
return result
def evaluate_output(self, output: str, print_summary: bool = False) -> AccuracyResult:
"""
Evaluate a pre-generated output without running the agent.
Args:
output: The output to evaluate
print_summary: Whether to print summary after evaluation
Returns:
AccuracyResult with evaluation score
"""
self.before_run()
result = AccuracyResult(
eval_id=self.eval_id,
name=self.name
)
score = self._judge_output(output)
result.evaluations.append(score)
self.after_run(result)
if print_summary:
result.print_summary()
return result
@@ -0,0 +1,143 @@
"""
Base evaluator class for PraisonAI Agents evaluation framework.
This module provides the abstract base class that all evaluators inherit from.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Union, TYPE_CHECKING
import uuid
import logging
if TYPE_CHECKING:
from .results import AccuracyResult, PerformanceResult, ReliabilityResult, CriteriaResult
logger = logging.getLogger(__name__)
class BaseEvaluator(ABC):
"""
Abstract base class for all evaluators in PraisonAI Agents.
Provides common functionality for evaluation lifecycle management,
result storage, and telemetry integration.
"""
def __init__(
self,
name: Optional[str] = None,
save_results_path: Optional[str] = None,
verbose: bool = False,
):
"""
Initialize the base evaluator.
Args:
name: Optional name for this evaluation run
save_results_path: Optional file path to save results (supports {name}, {eval_id} placeholders)
verbose: Enable verbose output during evaluation
"""
self.eval_id = str(uuid.uuid4())[:8]
self.name = name or f"eval_{self.eval_id}"
self.save_results_path = save_results_path
self.verbose = verbose
self._result = None
def before_run(self) -> None:
"""
Hook called before evaluation runs.
Override in subclasses for custom pre-evaluation logic.
"""
if self.verbose:
logger.info(f"Starting evaluation: {self.name} (ID: {self.eval_id})")
def after_run(self, result: Any) -> None:
"""
Hook called after evaluation completes.
Override in subclasses for custom post-evaluation logic.
Args:
result: The evaluation result
"""
if self.verbose:
logger.info(f"Completed evaluation: {self.name}")
if self.save_results_path:
self._save_result(result)
async def async_before_run(self) -> None:
"""
Async hook called before evaluation runs.
Override in subclasses for custom async pre-evaluation logic.
"""
self.before_run()
async def async_after_run(self, result: Any) -> None:
"""
Async hook called after evaluation completes.
Override in subclasses for custom async post-evaluation logic.
Args:
result: The evaluation result
"""
self.after_run(result)
@abstractmethod
def run(self, **kwargs) -> Any:
"""
Execute the evaluation synchronously.
Returns:
Evaluation result (type depends on evaluator)
"""
pass
async def run_async(self, **kwargs) -> Any:
"""
Execute the evaluation asynchronously.
Default implementation calls sync run().
Override for true async evaluation.
Returns:
Evaluation result (type depends on evaluator)
"""
return self.run(**kwargs)
def _save_result(self, result: Any) -> None:
"""
Save evaluation result to file.
Args:
result: The evaluation result to save
"""
if not self.save_results_path:
return
try:
import json
from pathlib import Path
file_path = self.save_results_path.format(
name=self.name,
eval_id=self.eval_id
)
path = Path(file_path)
path.parent.mkdir(parents=True, exist_ok=True)
if hasattr(result, 'to_dict'):
data = result.to_dict()
elif hasattr(result, '__dict__'):
data = result.__dict__
else:
data = {"result": str(result)}
path.write_text(json.dumps(data, indent=2, default=str))
if self.verbose:
logger.info(f"Saved results to: {file_path}")
except Exception as e:
logger.warning(f"Failed to save results: {e}")
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, eval_id={self.eval_id!r})"
@@ -0,0 +1,289 @@
"""
Criteria evaluator for PraisonAI Agents.
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 .base import BaseEvaluator
from .results import CriteriaResult, CriteriaScore
if TYPE_CHECKING:
from ..agent.agent import Agent
from ..agents.agents import PraisonAIAgents
logger = logging.getLogger(__name__)
class CriteriaEvaluator(BaseEvaluator):
"""
Evaluates agent outputs against custom criteria.
Uses an LLM as a judge to score outputs based on user-defined criteria.
Supports both numeric (1-10) and binary (pass/fail) scoring.
"""
def __init__(
self,
criteria: str,
agent: Optional[Union["Agent", "PraisonAIAgents"]] = None,
func: Optional[Callable[..., str]] = None,
input_text: str = "",
scoring_type: Literal["numeric", "binary"] = "numeric",
threshold: float = 7.0,
num_iterations: int = 1,
model: Optional[str] = None,
on_fail: Optional[Callable[[CriteriaScore], None]] = None,
name: Optional[str] = None,
save_results_path: Optional[str] = None,
verbose: bool = False,
):
"""
Initialize the criteria evaluator.
Args:
criteria: The criteria to evaluate against (e.g., "Response is helpful and accurate")
agent: Agent or PraisonAIAgents instance to evaluate
func: Alternative callable that returns output string
input_text: Input to provide to the agent/function
scoring_type: "numeric" (1-10 scale) or "binary" (pass/fail)
threshold: Score threshold for passing (numeric mode only)
num_iterations: Number of evaluation iterations
model: LLM model to use for judging (defaults to gpt-4o-mini)
on_fail: Callback function called when evaluation fails
name: Name for this evaluation
save_results_path: Path to save results
verbose: Enable verbose output
"""
super().__init__(name=name, save_results_path=save_results_path, verbose=verbose)
self.criteria = criteria
self.agent = agent
self.func = func
self.input_text = input_text
self.scoring_type = scoring_type
self.threshold = threshold
self.num_iterations = num_iterations
self.model = model or os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini")
self.on_fail = on_fail
def _get_output(self) -> str:
"""Get output from agent or function."""
if self.func:
return str(self.func(self.input_text))
if self.agent is None:
raise ValueError("Either 'agent' or 'func' must be provided")
if hasattr(self.agent, 'chat'):
return str(self.agent.chat(self.input_text))
elif hasattr(self.agent, 'start'):
result = self.agent.start(self.input_text)
if hasattr(result, 'raw'):
return str(result.raw)
return str(result)
else:
raise ValueError("Agent must have 'chat' or 'start' method")
def _judge_output(self, output: str) -> CriteriaScore:
"""
Use LLM to judge the output against criteria.
Args:
output: The output to evaluate
Returns:
CriteriaScore with score, passed status, and reasoning
"""
try:
from openai import OpenAI
except ImportError:
raise ImportError("OpenAI package required for criteria evaluation. Install with: pip install openai")
client = OpenAI()
if self.scoring_type == "binary":
prompt = f"""You are an expert evaluator. Evaluate the following output against the given criteria.
Criteria: {self.criteria}
Output to evaluate:
{output}
Determine if the output PASSES or FAILS the criteria.
Respond in this exact format:
RESULT: [PASS or FAIL]
REASONING: [brief explanation]"""
else:
prompt = f"""You are an expert evaluator. Evaluate the following output against the given criteria.
Criteria: {self.criteria}
Output to evaluate:
{output}
Score the output from 1-10 based on how well it meets the criteria.
- 10: Perfectly meets all criteria
- 8-9: Meets criteria very well with minor issues
- 6-7: Meets most criteria but has some gaps
- 4-5: Partially meets criteria
- 2-3: Barely meets criteria
- 1: Does not meet criteria at all
Respond in this exact format:
SCORE: [number 1-10]
REASONING: [brief explanation]"""
response = client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
max_tokens=200
)
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":
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()
return CriteriaScore(
score=score,
passed=passed,
reasoning=reasoning,
output_text=output,
criteria=self.criteria
)
def run(self, print_summary: bool = False) -> CriteriaResult:
"""
Execute the criteria evaluation.
Args:
print_summary: Whether to print summary after evaluation
Returns:
CriteriaResult with all evaluation scores
"""
self.before_run()
result = CriteriaResult(
criteria=self.criteria,
scoring_type=self.scoring_type,
threshold=self.threshold,
eval_id=self.eval_id,
name=self.name
)
for i in range(self.num_iterations):
if self.verbose:
logger.info(f"Running iteration {i + 1}/{self.num_iterations}")
try:
output = self._get_output()
score = self._judge_output(output)
result.evaluations.append(score)
if self.verbose:
status = "PASS" if score.passed else "FAIL"
logger.info(f" {status} (Score: {score.score}) - {score.reasoning}")
if not score.passed and self.on_fail:
self.on_fail(score)
except Exception as e:
logger.error(f"Error in iteration {i + 1}: {e}")
error_score = CriteriaScore(
score=0.0,
passed=False,
reasoning=f"Error: {str(e)}",
output_text="",
criteria=self.criteria
)
result.evaluations.append(error_score)
if self.on_fail:
self.on_fail(error_score)
self.after_run(result)
if print_summary:
result.print_summary()
return result
async def run_async(self, print_summary: bool = False) -> CriteriaResult:
"""
Execute the criteria evaluation asynchronously.
Args:
print_summary: Whether to print summary after evaluation
Returns:
CriteriaResult with all evaluation scores
"""
await self.async_before_run()
result = self.run(print_summary=False)
await self.async_after_run(result)
if print_summary:
result.print_summary()
return result
def evaluate_output(self, output: str, print_summary: bool = False) -> CriteriaResult:
"""
Evaluate a pre-generated output without running the agent.
Args:
output: The output to evaluate
print_summary: Whether to print summary after evaluation
Returns:
CriteriaResult with evaluation score
"""
self.before_run()
result = CriteriaResult(
criteria=self.criteria,
scoring_type=self.scoring_type,
threshold=self.threshold,
eval_id=self.eval_id,
name=self.name
)
score = self._judge_output(output)
result.evaluations.append(score)
if not score.passed and self.on_fail:
self.on_fail(score)
self.after_run(result)
if print_summary:
result.print_summary()
return result
@@ -0,0 +1,184 @@
"""
Performance evaluator for PraisonAI Agents.
Measures runtime and memory usage of agent executions or functions.
"""
import time
import logging
from typing import Callable, Optional, TYPE_CHECKING
from .base import BaseEvaluator
from .results import PerformanceResult, PerformanceMetrics
if TYPE_CHECKING:
from ..agent.agent import Agent
from ..agents.agents import PraisonAIAgents
logger = logging.getLogger(__name__)
class PerformanceEvaluator(BaseEvaluator):
"""
Evaluates the performance of agent executions or functions.
Measures runtime and memory usage across multiple iterations.
"""
def __init__(
self,
func: Optional[Callable[[], None]] = None,
agent: Optional["Agent"] = None,
input_text: str = "",
num_iterations: int = 10,
warmup_runs: int = 2,
track_memory: bool = True,
name: Optional[str] = None,
save_results_path: Optional[str] = None,
verbose: bool = False,
):
"""
Initialize the performance evaluator.
Args:
func: Callable to benchmark (takes no arguments)
agent: Agent to benchmark (will call chat with input_text)
input_text: Input text for agent benchmarking
num_iterations: Number of benchmark iterations
warmup_runs: Number of warmup runs before measurement
track_memory: Whether to track memory usage
name: Name for this evaluation
save_results_path: Path to save results
verbose: Enable verbose output
"""
super().__init__(name=name, save_results_path=save_results_path, verbose=verbose)
self.func = func
self.agent = agent
self.input_text = input_text
self.num_iterations = num_iterations
self.warmup_runs = warmup_runs
self.track_memory = track_memory
if func is None and agent is None:
raise ValueError("Either 'func' or 'agent' must be provided")
def _get_memory_usage(self) -> float:
"""Get current memory usage in MB."""
if not self.track_memory:
return 0.0
try:
import tracemalloc
current, _ = tracemalloc.get_traced_memory()
return current / (1024 * 1024)
except Exception:
return 0.0
def _execute(self) -> None:
"""Execute the function or agent."""
if self.func:
self.func()
elif self.agent:
if hasattr(self.agent, 'chat'):
self.agent.chat(self.input_text)
elif hasattr(self.agent, 'start'):
self.agent.start(self.input_text)
else:
raise ValueError("Agent must have 'chat' or 'start' method")
def run(self, print_summary: bool = False) -> PerformanceResult:
"""
Execute the performance evaluation.
Args:
print_summary: Whether to print summary after evaluation
Returns:
PerformanceResult with all performance metrics
"""
self.before_run()
result = PerformanceResult(
warmup_runs=self.warmup_runs,
eval_id=self.eval_id,
name=self.name
)
if self.track_memory:
try:
import tracemalloc
tracemalloc.start()
except Exception as e:
logger.warning(f"Could not start memory tracking: {e}")
self.track_memory = False
if self.verbose:
logger.info(f"Running {self.warmup_runs} warmup iterations...")
for i in range(self.warmup_runs):
try:
self._execute()
except Exception as e:
logger.warning(f"Warmup iteration {i + 1} failed: {e}")
if self.verbose:
logger.info(f"Running {self.num_iterations} benchmark iterations...")
for i in range(self.num_iterations):
memory_before = self._get_memory_usage()
start_time = time.perf_counter()
try:
self._execute()
success = True
except Exception as e:
logger.error(f"Iteration {i + 1} failed: {e}")
success = False
end_time = time.perf_counter()
memory_after = self._get_memory_usage()
if success:
metrics = PerformanceMetrics(
run_time=end_time - start_time,
memory_usage=max(0, memory_after - memory_before),
iteration=i + 1
)
result.metrics.append(metrics)
if self.verbose:
logger.info(f" Iteration {i + 1}: {metrics.run_time:.4f}s, {metrics.memory_usage:.2f}MB")
if self.track_memory:
try:
import tracemalloc
tracemalloc.stop()
except Exception:
pass
self.after_run(result)
if print_summary:
result.print_summary()
return result
async def run_async(self, print_summary: bool = False) -> PerformanceResult:
"""
Execute the performance evaluation asynchronously.
Args:
print_summary: Whether to print summary after evaluation
Returns:
PerformanceResult with all performance metrics
"""
await self.async_before_run()
result = self.run(print_summary=False)
await self.async_after_run(result)
if print_summary:
result.print_summary()
return result
@@ -0,0 +1,225 @@
"""
Reliability evaluator for PraisonAI Agents.
Evaluates agent reliability by verifying expected tool calls are made.
"""
import logging
from typing import Dict, List, Optional, Set, Any, TYPE_CHECKING
from .base import BaseEvaluator
from .results import ReliabilityResult, ToolCallResult
if TYPE_CHECKING:
from ..agent.agent import Agent
from ..agents.agents import PraisonAIAgents
logger = logging.getLogger(__name__)
class ReliabilityEvaluator(BaseEvaluator):
"""
Evaluates the reliability of agent tool usage.
Verifies that expected tools are called during agent execution.
"""
def __init__(
self,
agent: Optional["Agent"] = None,
input_text: str = "",
expected_tools: Optional[List[str]] = None,
forbidden_tools: Optional[List[str]] = None,
name: Optional[str] = None,
save_results_path: Optional[str] = None,
verbose: bool = False,
):
"""
Initialize the reliability evaluator.
Args:
agent: Agent to evaluate
input_text: Input to provide to the agent
expected_tools: List of tool names that should be called
forbidden_tools: List of tool names that should NOT be called
name: Name for this evaluation
save_results_path: Path to save results
verbose: Enable verbose output
"""
super().__init__(name=name, save_results_path=save_results_path, verbose=verbose)
self.agent = agent
self.input_text = input_text
self.expected_tools = set(expected_tools or [])
self.forbidden_tools = set(forbidden_tools or [])
if agent is None:
raise ValueError("'agent' must be provided")
def _extract_tool_calls(self, response: Any) -> Set[str]:
"""
Extract tool names from agent response.
Args:
response: Agent response object
Returns:
Set of tool names that were called
"""
tool_calls = set()
if hasattr(response, 'tool_calls') and response.tool_calls:
for tc in response.tool_calls:
if hasattr(tc, 'name'):
tool_calls.add(tc.name)
elif hasattr(tc, 'function') and hasattr(tc.function, 'name'):
tool_calls.add(tc.function.name)
if hasattr(self.agent, 'chat_history'):
for msg in self.agent.chat_history:
if isinstance(msg, dict):
if msg.get('role') == 'assistant' and 'tool_calls' in msg:
for tc in msg.get('tool_calls', []):
if isinstance(tc, dict) and 'function' in tc:
tool_calls.add(tc['function'].get('name', ''))
elif msg.get('role') == 'tool':
tool_name = msg.get('name', '')
if tool_name:
tool_calls.add(tool_name)
return tool_calls
def run(self, print_summary: bool = False) -> ReliabilityResult:
"""
Execute the reliability evaluation.
Args:
print_summary: Whether to print summary after evaluation
Returns:
ReliabilityResult with tool call verification results
"""
self.before_run()
result = ReliabilityResult(
eval_id=self.eval_id,
name=self.name
)
try:
if hasattr(self.agent, 'chat'):
response = self.agent.chat(self.input_text)
elif hasattr(self.agent, 'start'):
response = self.agent.start(self.input_text)
else:
raise ValueError("Agent must have 'chat' or 'start' method")
actual_tools = self._extract_tool_calls(response)
if self.verbose:
logger.info(f"Expected tools: {self.expected_tools}")
logger.info(f"Actual tools called: {actual_tools}")
for tool in self.expected_tools:
was_called = tool in actual_tools
result.tool_results.append(ToolCallResult(
tool_name=tool,
expected=True,
actual=was_called,
passed=was_called
))
for tool in self.forbidden_tools:
was_called = tool in actual_tools
result.tool_results.append(ToolCallResult(
tool_name=tool,
expected=False,
actual=was_called,
passed=not was_called
))
except Exception as e:
logger.error(f"Error during reliability evaluation: {e}")
for tool in self.expected_tools:
result.tool_results.append(ToolCallResult(
tool_name=tool,
expected=True,
actual=False,
passed=False,
metadata={"error": str(e)}
))
self.after_run(result)
if print_summary:
result.print_summary()
return result
async def run_async(self, print_summary: bool = False) -> ReliabilityResult:
"""
Execute the reliability evaluation asynchronously.
Args:
print_summary: Whether to print summary after evaluation
Returns:
ReliabilityResult with tool call verification results
"""
await self.async_before_run()
result = self.run(print_summary=False)
await self.async_after_run(result)
if print_summary:
result.print_summary()
return result
def evaluate_tool_calls(
self,
actual_tools: List[str],
print_summary: bool = False
) -> ReliabilityResult:
"""
Evaluate a pre-recorded list of tool calls without running the agent.
Args:
actual_tools: List of tool names that were called
print_summary: Whether to print summary after evaluation
Returns:
ReliabilityResult with tool call verification results
"""
self.before_run()
result = ReliabilityResult(
eval_id=self.eval_id,
name=self.name
)
actual_set = set(actual_tools)
for tool in self.expected_tools:
was_called = tool in actual_set
result.tool_results.append(ToolCallResult(
tool_name=tool,
expected=True,
actual=was_called,
passed=was_called
))
for tool in self.forbidden_tools:
was_called = tool in actual_set
result.tool_results.append(ToolCallResult(
tool_name=tool,
expected=False,
actual=was_called,
passed=not was_called
))
self.after_run(result)
if print_summary:
result.print_summary()
return result
@@ -0,0 +1,463 @@
"""
Result dataclasses for PraisonAI Agents evaluation framework.
This module provides structured result types for different evaluation methods.
"""
from dataclasses import dataclass, field, asdict
from typing import List, Optional, Dict, Any
from datetime import datetime
import statistics
import json
@dataclass
class EvaluationScore:
"""Individual evaluation score with reasoning."""
score: float
reasoning: str
input_text: str
output_text: str
expected_output: Optional[str] = None
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class AccuracyResult:
"""Result from accuracy evaluation comparing output to expected output."""
evaluations: List[EvaluationScore] = field(default_factory=list)
eval_id: str = ""
name: str = ""
@property
def scores(self) -> List[float]:
"""Get list of all scores."""
return [e.score for e in self.evaluations]
@property
def avg_score(self) -> float:
"""Calculate average score."""
if not self.scores:
return 0.0
return statistics.mean(self.scores)
@property
def min_score(self) -> float:
"""Get minimum score."""
if not self.scores:
return 0.0
return min(self.scores)
@property
def max_score(self) -> float:
"""Get maximum score."""
if not self.scores:
return 0.0
return max(self.scores)
@property
def std_dev(self) -> float:
"""Calculate standard deviation of scores."""
if len(self.scores) < 2:
return 0.0
return statistics.stdev(self.scores)
@property
def passed(self) -> bool:
"""Check if evaluation passed (avg score >= 7)."""
return self.avg_score >= 7.0
def to_dict(self) -> Dict[str, Any]:
"""Convert result to dictionary."""
return {
"eval_id": self.eval_id,
"name": self.name,
"evaluations": [asdict(e) for e in self.evaluations],
"avg_score": self.avg_score,
"min_score": self.min_score,
"max_score": self.max_score,
"std_dev": self.std_dev,
"passed": self.passed,
"num_evaluations": len(self.evaluations)
}
def to_json(self) -> str:
"""Convert result to JSON string."""
return json.dumps(self.to_dict(), indent=2)
def print_summary(self) -> None:
"""Print a summary of the evaluation results."""
try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel # noqa: F401
console = Console()
table = Table(title="Accuracy Evaluation Summary")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Evaluations", str(len(self.evaluations)))
table.add_row("Average Score", f"{self.avg_score:.2f}")
table.add_row("Min Score", f"{self.min_score:.2f}")
table.add_row("Max Score", f"{self.max_score:.2f}")
table.add_row("Std Dev", f"{self.std_dev:.2f}")
table.add_row("Status", "✅ PASSED" if self.passed else "❌ FAILED")
console.print(table)
except ImportError:
print("Accuracy Evaluation Summary")
print(f" Evaluations: {len(self.evaluations)}")
print(f" Average Score: {self.avg_score:.2f}")
print(f" Min Score: {self.min_score:.2f}")
print(f" Max Score: {self.max_score:.2f}")
print(f" Std Dev: {self.std_dev:.2f}")
print(f" Status: {'PASSED' if self.passed else 'FAILED'}")
@dataclass
class PerformanceMetrics:
"""Individual performance measurement."""
run_time: float
memory_usage: float
iteration: int
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class PerformanceResult:
"""Result from performance evaluation measuring runtime and memory."""
metrics: List[PerformanceMetrics] = field(default_factory=list)
warmup_runs: int = 0
eval_id: str = ""
name: str = ""
@property
def run_times(self) -> List[float]:
"""Get list of all run times."""
return [m.run_time for m in self.metrics]
@property
def memory_usages(self) -> List[float]:
"""Get list of all memory usages."""
return [m.memory_usage for m in self.metrics]
@property
def avg_run_time(self) -> float:
"""Calculate average run time."""
if not self.run_times:
return 0.0
return statistics.mean(self.run_times)
@property
def min_run_time(self) -> float:
"""Get minimum run time."""
if not self.run_times:
return 0.0
return min(self.run_times)
@property
def max_run_time(self) -> float:
"""Get maximum run time."""
if not self.run_times:
return 0.0
return max(self.run_times)
@property
def std_dev_run_time(self) -> float:
"""Calculate standard deviation of run times."""
if len(self.run_times) < 2:
return 0.0
return statistics.stdev(self.run_times)
@property
def median_run_time(self) -> float:
"""Calculate median run time."""
if not self.run_times:
return 0.0
return statistics.median(self.run_times)
@property
def p95_run_time(self) -> float:
"""Calculate 95th percentile run time."""
if not self.run_times:
return 0.0
sorted_times = sorted(self.run_times)
idx = int(len(sorted_times) * 0.95)
return sorted_times[min(idx, len(sorted_times) - 1)]
@property
def avg_memory(self) -> float:
"""Calculate average memory usage."""
if not self.memory_usages:
return 0.0
return statistics.mean(self.memory_usages)
@property
def max_memory(self) -> float:
"""Get maximum memory usage."""
if not self.memory_usages:
return 0.0
return max(self.memory_usages)
def to_dict(self) -> Dict[str, Any]:
"""Convert result to dictionary."""
return {
"eval_id": self.eval_id,
"name": self.name,
"metrics": [asdict(m) for m in self.metrics],
"warmup_runs": self.warmup_runs,
"avg_run_time": self.avg_run_time,
"min_run_time": self.min_run_time,
"max_run_time": self.max_run_time,
"std_dev_run_time": self.std_dev_run_time,
"median_run_time": self.median_run_time,
"p95_run_time": self.p95_run_time,
"avg_memory": self.avg_memory,
"max_memory": self.max_memory,
"num_iterations": len(self.metrics)
}
def to_json(self) -> str:
"""Convert result to JSON string."""
return json.dumps(self.to_dict(), indent=2)
def print_summary(self) -> None:
"""Print a summary of the performance results."""
try:
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Performance Evaluation Summary")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Iterations", str(len(self.metrics)))
table.add_row("Warmup Runs", str(self.warmup_runs))
table.add_row("Avg Run Time", f"{self.avg_run_time:.4f}s")
table.add_row("Min Run Time", f"{self.min_run_time:.4f}s")
table.add_row("Max Run Time", f"{self.max_run_time:.4f}s")
table.add_row("Std Dev", f"{self.std_dev_run_time:.4f}s")
table.add_row("Median", f"{self.median_run_time:.4f}s")
table.add_row("P95", f"{self.p95_run_time:.4f}s")
table.add_row("Avg Memory", f"{self.avg_memory:.2f} MB")
table.add_row("Max Memory", f"{self.max_memory:.2f} MB")
console.print(table)
except ImportError:
print("Performance Evaluation Summary")
print(f" Iterations: {len(self.metrics)}")
print(f" Warmup Runs: {self.warmup_runs}")
print(f" Avg Run Time: {self.avg_run_time:.4f}s")
print(f" Min Run Time: {self.min_run_time:.4f}s")
print(f" Max Run Time: {self.max_run_time:.4f}s")
print(f" Std Dev: {self.std_dev_run_time:.4f}s")
print(f" Median: {self.median_run_time:.4f}s")
print(f" P95: {self.p95_run_time:.4f}s")
print(f" Avg Memory: {self.avg_memory:.2f} MB")
print(f" Max Memory: {self.max_memory:.2f} MB")
@dataclass
class ToolCallResult:
"""Result of a single tool call verification."""
tool_name: str
expected: bool
actual: bool
passed: bool
arguments: Optional[Dict[str, Any]] = None
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class ReliabilityResult:
"""Result from reliability evaluation checking tool calls."""
tool_results: List[ToolCallResult] = field(default_factory=list)
eval_id: str = ""
name: str = ""
@property
def passed_calls(self) -> List[ToolCallResult]:
"""Get list of passed tool calls."""
return [t for t in self.tool_results if t.passed]
@property
def failed_calls(self) -> List[ToolCallResult]:
"""Get list of failed tool calls."""
return [t for t in self.tool_results if not t.passed]
@property
def pass_rate(self) -> float:
"""Calculate pass rate."""
if not self.tool_results:
return 0.0
return len(self.passed_calls) / len(self.tool_results)
@property
def status(self) -> str:
"""Get overall status."""
return "PASSED" if len(self.failed_calls) == 0 else "FAILED"
@property
def passed(self) -> bool:
"""Check if all tool calls passed."""
return len(self.failed_calls) == 0
def to_dict(self) -> Dict[str, Any]:
"""Convert result to dictionary."""
return {
"eval_id": self.eval_id,
"name": self.name,
"tool_results": [asdict(t) for t in self.tool_results],
"passed_count": len(self.passed_calls),
"failed_count": len(self.failed_calls),
"pass_rate": self.pass_rate,
"status": self.status
}
def to_json(self) -> str:
"""Convert result to JSON string."""
return json.dumps(self.to_dict(), indent=2)
def print_summary(self) -> None:
"""Print a summary of the reliability results."""
try:
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Reliability Evaluation Summary")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Total Tool Calls", str(len(self.tool_results)))
table.add_row("Passed", str(len(self.passed_calls)))
table.add_row("Failed", str(len(self.failed_calls)))
table.add_row("Pass Rate", f"{self.pass_rate:.1%}")
table.add_row("Status", "✅ PASSED" if self.passed else "❌ FAILED")
console.print(table)
if self.failed_calls:
fail_table = Table(title="Failed Tool Calls")
fail_table.add_column("Tool", style="red")
fail_table.add_column("Expected", style="yellow")
fail_table.add_column("Actual", style="yellow")
for tc in self.failed_calls:
fail_table.add_row(tc.tool_name, str(tc.expected), str(tc.actual))
console.print(fail_table)
except ImportError:
print("Reliability Evaluation Summary")
print(f" Total Tool Calls: {len(self.tool_results)}")
print(f" Passed: {len(self.passed_calls)}")
print(f" Failed: {len(self.failed_calls)}")
print(f" Pass Rate: {self.pass_rate:.1%}")
print(f" Status: {self.status}")
@dataclass
class CriteriaScore:
"""Individual criteria evaluation score."""
score: float
passed: bool
reasoning: str
output_text: str
criteria: str
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class CriteriaResult:
"""Result from criteria-based evaluation (agent-as-judge)."""
evaluations: List[CriteriaScore] = field(default_factory=list)
criteria: str = ""
scoring_type: str = "numeric" # "numeric" or "binary"
threshold: float = 7.0
eval_id: str = ""
name: str = ""
@property
def scores(self) -> List[float]:
"""Get list of all scores."""
return [e.score for e in self.evaluations]
@property
def avg_score(self) -> float:
"""Calculate average score."""
if not self.scores:
return 0.0
return statistics.mean(self.scores)
@property
def pass_rate(self) -> float:
"""Calculate pass rate."""
if not self.evaluations:
return 0.0
passed = sum(1 for e in self.evaluations if e.passed)
return passed / len(self.evaluations)
@property
def passed(self) -> bool:
"""Check if evaluation passed overall."""
if self.scoring_type == "binary":
return self.pass_rate >= 0.5
return self.avg_score >= self.threshold
def to_dict(self) -> Dict[str, Any]:
"""Convert result to dictionary."""
return {
"eval_id": self.eval_id,
"name": self.name,
"criteria": self.criteria,
"scoring_type": self.scoring_type,
"threshold": self.threshold,
"evaluations": [asdict(e) for e in self.evaluations],
"avg_score": self.avg_score,
"pass_rate": self.pass_rate,
"passed": self.passed,
"num_evaluations": len(self.evaluations)
}
def to_json(self) -> str:
"""Convert result to JSON string."""
return json.dumps(self.to_dict(), indent=2)
def print_summary(self) -> None:
"""Print a summary of the criteria evaluation results."""
try:
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Criteria Evaluation Summary")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Criteria", self.criteria[:50] + "..." if len(self.criteria) > 50 else self.criteria)
table.add_row("Scoring Type", self.scoring_type)
table.add_row("Threshold", str(self.threshold))
table.add_row("Evaluations", str(len(self.evaluations)))
table.add_row("Average Score", f"{self.avg_score:.2f}")
table.add_row("Pass Rate", f"{self.pass_rate:.1%}")
table.add_row("Status", "✅ PASSED" if self.passed else "❌ FAILED")
console.print(table)
except ImportError:
print("Criteria Evaluation Summary")
print(f" Criteria: {self.criteria[:50]}...")
print(f" Scoring Type: {self.scoring_type}")
print(f" Threshold: {self.threshold}")
print(f" Evaluations: {len(self.evaluations)}")
print(f" Average Score: {self.avg_score:.2f}")
print(f" Pass Rate: {self.pass_rate:.1%}")
print(f" Status: {'PASSED' if self.passed else 'FAILED'}")
@@ -0,0 +1,154 @@
"""
Utility functions for PraisonAI Agents evaluation framework.
"""
import json
import logging
from pathlib import Path
from typing import Any, Dict, Optional, Union
logger = logging.getLogger(__name__)
def save_result_to_file(
result: Any,
file_path: str,
eval_id: Optional[str] = None,
name: Optional[str] = None
) -> bool:
"""
Save evaluation result to a file.
Args:
result: The evaluation result to save
file_path: Path to save the result (supports {name}, {eval_id} placeholders)
eval_id: Optional eval ID for placeholder substitution
name: Optional name for placeholder substitution
Returns:
True if save was successful, False otherwise
"""
try:
formatted_path = file_path.format(name=name or "", eval_id=eval_id or "")
path = Path(formatted_path)
path.parent.mkdir(parents=True, exist_ok=True)
if hasattr(result, 'to_dict'):
data = result.to_dict()
elif hasattr(result, '__dict__'):
data = result.__dict__
else:
data = {"result": str(result)}
path.write_text(json.dumps(data, indent=2, default=str))
logger.debug(f"Saved result to: {formatted_path}")
return True
except Exception as e:
logger.warning(f"Failed to save result: {e}")
return False
def load_result_from_file(file_path: str) -> Optional[Dict[str, Any]]:
"""
Load evaluation result from a file.
Args:
file_path: Path to the result file
Returns:
Dictionary containing the result data, or None if load failed
"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"Result file not found: {file_path}")
return None
data = json.loads(path.read_text())
return data
except Exception as e:
logger.warning(f"Failed to load result: {e}")
return None
def format_score(score: float, max_score: float = 10.0) -> str:
"""
Format a score for display.
Args:
score: The score value
max_score: Maximum possible score
Returns:
Formatted score string
"""
return f"{score:.1f}/{max_score:.0f}"
def format_percentage(value: float) -> str:
"""
Format a value as a percentage.
Args:
value: Value between 0 and 1
Returns:
Formatted percentage string
"""
return f"{value * 100:.1f}%"
def format_duration(seconds: float) -> str:
"""
Format a duration in seconds for display.
Args:
seconds: Duration in seconds
Returns:
Formatted duration string
"""
if seconds < 0.001:
return f"{seconds * 1000000:.0f}μs"
elif seconds < 1:
return f"{seconds * 1000:.2f}ms"
elif seconds < 60:
return f"{seconds:.2f}s"
else:
minutes = int(seconds // 60)
secs = seconds % 60
return f"{minutes}m {secs:.1f}s"
def format_memory(mb: float) -> str:
"""
Format memory usage for display.
Args:
mb: Memory in megabytes
Returns:
Formatted memory string
"""
if mb < 1:
return f"{mb * 1024:.1f} KB"
elif mb < 1024:
return f"{mb:.2f} MB"
else:
return f"{mb / 1024:.2f} GB"
def calculate_pass_rate(passed: int, total: int) -> float:
"""
Calculate pass rate.
Args:
passed: Number of passed evaluations
total: Total number of evaluations
Returns:
Pass rate as a float between 0 and 1
"""
if total == 0:
return 0.0
return passed / total
@@ -1054,7 +1054,7 @@ class LLM:
**kwargs
) -> str:
"""Enhanced get_response with all OpenAI-like features"""
logging.info(f"Getting response from {self.model}")
logging.debug(f"Getting response from {self.model}")
# Log all self values when in debug mode
self._log_llm_config(
'LLM instance',
@@ -2472,7 +2472,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
"""Async version of get_response with identical functionality."""
try:
import litellm
logging.info(f"Getting async response from {self.model}")
logging.debug(f"Getting async response from {self.model}")
# Log all self values when in debug mode
self._log_llm_config(
'get_response_async',
@@ -19,6 +19,15 @@ from rich.console import Console
from rich.live import Live
import inspect
# Import display_tool_call for callback support (lazy import to avoid circular imports)
_display_tool_call = None
def _get_display_tool_call():
global _display_tool_call
if _display_tool_call is None:
from ..main import display_tool_call
_display_tool_call = display_tool_call
return _display_tool_call
# Constants
LOCAL_SERVER_API_KEY_PLACEHOLDER = "not-needed"
@@ -978,16 +987,19 @@ class OpenAIClient:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Always trigger callback for tool call tracking (even when verbose=False)
display_tool_call_fn = _get_display_tool_call()
display_tool_call_fn(f"Calling function: {function_name}", console=console if verbose else None)
if verbose and console:
console.print(f"[bold]Calling function:[/bold] {function_name}")
console.print(f"[dim]Arguments:[/dim] {arguments}")
# Execute the tool
tool_result = execute_tool_fn(function_name, arguments)
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
if verbose and console:
console.print(f"[dim]Result:[/dim] {results_str}")
# Trigger callback with result
display_tool_call_fn(f"Function {function_name} returned: {results_str[:200]}{'...' if len(results_str) > 200 else ''}", console=console if verbose else None)
messages.append({
"role": "tool",
@@ -1150,8 +1162,11 @@ class OpenAIClient:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Always trigger callback for tool call tracking (even when verbose=False)
display_tool_call_fn = _get_display_tool_call()
display_tool_call_fn(f"Calling function: {function_name}", console=console if verbose else None)
if verbose and console:
console.print(f"[bold]Calling function:[/bold] {function_name}")
console.print(f"[dim]Arguments:[/dim] {arguments}")
# Execute the tool (async)
@@ -1167,8 +1182,8 @@ class OpenAIClient:
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
if verbose and console:
console.print(f"[dim]Result:[/dim] {results_str}")
# Trigger callback with result
display_tool_call_fn(f"Function {function_name} returned: {results_str[:200]}{'...' if len(results_str) > 200 else ''}", console=console if verbose else None)
messages.append({
"role": "tool",
@@ -1299,6 +1314,10 @@ class OpenAIClient:
yield f"\n[Error parsing arguments for {function_name if 'function_name' in locals() else 'unknown function'}: {str(e)}]"
continue
# Always trigger callback for tool call tracking (even when verbose=False)
display_tool_call_fn = _get_display_tool_call()
display_tool_call_fn(f"Calling function: {function_name}", console=None)
if verbose:
yield f"\n[Calling function: {function_name}]"
@@ -1311,6 +1330,9 @@ class OpenAIClient:
if verbose:
yield f"\n[Function error: {str(e)}]"
# Trigger callback with result
display_tool_call_fn(f"Function {function_name} returned: {results_str[:200]}{'...' if len(results_str) > 200 else ''}", console=None)
if verbose:
yield f"\n[Function result: {results_str}]"
+4 -4
View File
@@ -231,15 +231,15 @@ def display_tool_call(message: str, console=None):
if not message or not message.strip():
logging.debug("Empty message in display_tool_call, returning early")
return
if console is None:
console = Console()
message = _clean_display_content(str(message))
logging.debug(f"Cleaned message in display_tool_call: {repr(message)}")
# Execute synchronous callbacks
# Execute synchronous callbacks (always, even when console is None)
execute_sync_callback('tool_call', message=message)
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
# Only print panel if console is provided (verbose mode)
if console is not None:
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
def display_error(message: str, console=None):
if not message or not message.strip():
@@ -0,0 +1,76 @@
"""Agent Skills module for PraisonAI Agents.
This module provides support for the open Agent Skills standard (agentskills.io),
enabling agents to load and use modular capabilities through SKILL.md files.
Zero Performance Impact:
- All imports are lazy loaded via __getattr__
- Skills only loaded when explicitly enabled
- No auto-discovery at import time
Usage:
from praisonaiagents.skills import SkillManager, SkillProperties
manager = SkillManager()
manager.discover(["./skills"])
prompt_xml = manager.to_prompt()
"""
__all__ = [
# Models
"SkillProperties",
"SkillMetadata",
"ParseError",
"ValidationError",
# Parser
"parse_frontmatter",
"find_skill_md",
"read_properties",
# Validator
"validate",
"validate_metadata",
# Prompt
"to_prompt",
"generate_skills_xml",
"format_skill_for_prompt",
# Discovery
"discover_skills",
"get_default_skill_dirs",
# Loader
"SkillLoader",
# Manager
"SkillManager",
]
def __getattr__(name: str):
"""Lazy load module components to avoid import overhead."""
if name in ("SkillProperties", "SkillMetadata", "ParseError", "ValidationError"):
from .models import SkillProperties, SkillMetadata, ParseError, ValidationError
return locals()[name]
if name in ("parse_frontmatter", "find_skill_md", "read_properties"):
from .parser import parse_frontmatter, find_skill_md, read_properties
return locals()[name]
if name in ("validate", "validate_metadata", "_validate_name", "_validate_description", "_validate_compatibility"):
from .validator import validate, validate_metadata, _validate_name, _validate_description, _validate_compatibility
return locals()[name]
if name in ("to_prompt", "generate_skills_xml", "format_skill_for_prompt"):
from .prompt import to_prompt, generate_skills_xml, format_skill_for_prompt
return locals()[name]
if name in ("discover_skills", "get_default_skill_dirs"):
from .discovery import discover_skills, get_default_skill_dirs
return locals()[name]
if name == "SkillLoader":
from .loader import SkillLoader
return SkillLoader
if name == "SkillManager":
from .manager import SkillManager
return SkillManager
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,129 @@
"""Skill discovery and directory scanning."""
from pathlib import Path
from typing import List, Optional
from .parser import find_skill_md, read_properties
from .models import SkillProperties
def get_default_skill_dirs() -> List[Path]:
"""Get default skill directory locations.
Returns directories in precedence order (high to low):
1. Project: ./.praison/skills/ or ./.claude/skills/
2. User: ~/.praison/skills/
3. System: /etc/praison/skills/ (admin-managed)
Returns:
List of existing skill directories
"""
dirs = []
cwd = Path.cwd()
# Project-level directories
project_dirs = [
cwd / ".praison" / "skills",
cwd / ".claude" / "skills",
]
for d in project_dirs:
if d.exists() and d.is_dir():
dirs.append(d)
# User-level directory
user_dir = Path.home() / ".praison" / "skills"
if user_dir.exists() and user_dir.is_dir():
dirs.append(user_dir)
# System-level directory (Unix-like systems)
system_dir = Path("/etc/praison/skills")
if system_dir.exists() and system_dir.is_dir():
dirs.append(system_dir)
return dirs
def discover_skills(
skill_dirs: Optional[List[str]] = None,
include_defaults: bool = True,
) -> List[SkillProperties]:
"""Discover all valid skills in the given directories.
Args:
skill_dirs: List of directory paths to scan for skills.
Each directory should contain skill subdirectories.
include_defaults: Whether to include default skill directories
Returns:
List of SkillProperties for all valid skills found
"""
all_dirs = []
# Add explicit directories
if skill_dirs:
for d in skill_dirs:
path = Path(d).expanduser().resolve()
if path.exists() and path.is_dir():
all_dirs.append(path)
# Add default directories
if include_defaults:
all_dirs.extend(get_default_skill_dirs())
# Remove duplicates while preserving order
seen = set()
unique_dirs = []
for d in all_dirs:
if d not in seen:
seen.add(d)
unique_dirs.append(d)
skills = []
for parent_dir in unique_dirs:
# Each subdirectory in parent_dir might be a skill
try:
for item in parent_dir.iterdir():
if not item.is_dir():
continue
# Check if this directory contains a SKILL.md
skill_md = find_skill_md(item)
if skill_md is None:
continue
try:
props = read_properties(item)
skills.append(props)
except Exception:
# Skip invalid skills
continue
except PermissionError:
# Skip directories we can't read
continue
return skills
def discover_skill(skill_path: str) -> Optional[SkillProperties]:
"""Discover a single skill from a directory path.
Args:
skill_path: Path to a skill directory
Returns:
SkillProperties if valid, None otherwise
"""
path = Path(skill_path).expanduser().resolve()
if not path.exists() or not path.is_dir():
return None
skill_md = find_skill_md(path)
if skill_md is None:
return None
try:
return read_properties(path)
except Exception:
return None
@@ -0,0 +1,236 @@
"""Progressive skill loading for Agent Skills."""
from pathlib import Path
from typing import Optional
from dataclasses import dataclass, field
from .parser import find_skill_md, read_properties, parse_frontmatter
from .models import SkillProperties, SkillMetadata
@dataclass
class LoadedSkill:
"""A fully or partially loaded skill.
Supports progressive disclosure:
- Level 1: metadata only (name, description, location)
- Level 2: full instructions (SKILL.md body)
- Level 3: resources (scripts, references, assets)
"""
properties: SkillProperties
instructions: Optional[str] = None
resources_loaded: bool = False
_scripts: dict = field(default_factory=dict)
_references: dict = field(default_factory=dict)
_assets: dict = field(default_factory=dict)
@property
def metadata(self) -> SkillMetadata:
"""Get lightweight metadata for system prompt."""
return SkillMetadata.from_properties(self.properties)
@property
def is_activated(self) -> bool:
"""Check if skill has been activated (instructions loaded)."""
return self.instructions is not None
def get_scripts(self) -> dict:
"""Get loaded scripts."""
return self._scripts
def get_references(self) -> dict:
"""Get loaded references."""
return self._references
def get_assets(self) -> dict:
"""Get loaded assets."""
return self._assets
class SkillLoader:
"""Loader for Agent Skills with progressive disclosure support.
Usage:
loader = SkillLoader()
# Level 1: Load metadata only
skill = loader.load_metadata("/path/to/skill")
# Level 2: Activate skill (load instructions)
loader.activate(skill)
# Level 3: Load specific resources
loader.load_scripts(skill)
loader.load_references(skill)
"""
def load_metadata(self, skill_path: str) -> Optional[LoadedSkill]:
"""Load skill metadata only (Level 1).
This is the minimal load for system prompt injection.
Only reads the YAML frontmatter, not the full SKILL.md body.
Args:
skill_path: Path to skill directory
Returns:
LoadedSkill with metadata only, or None if invalid
"""
path = Path(skill_path).expanduser().resolve()
if not path.exists() or not path.is_dir():
return None
try:
props = read_properties(path)
return LoadedSkill(properties=props)
except Exception:
return None
def activate(self, skill: LoadedSkill) -> bool:
"""Activate a skill by loading its full instructions (Level 2).
Args:
skill: LoadedSkill instance to activate
Returns:
True if activation succeeded
"""
if skill.is_activated:
return True
if skill.properties.path is None:
return False
skill_md = find_skill_md(skill.properties.path)
if skill_md is None:
return False
try:
content = skill_md.read_text()
_, body = parse_frontmatter(content)
skill.instructions = body
return True
except Exception:
return False
def load_scripts(self, skill: LoadedSkill) -> dict:
"""Load scripts from skill's scripts/ directory (Level 3).
Args:
skill: LoadedSkill instance
Returns:
Dict mapping script names to their contents
"""
if skill.properties.path is None:
return {}
scripts_dir = skill.properties.path / "scripts"
if not scripts_dir.exists() or not scripts_dir.is_dir():
return {}
scripts = {}
try:
for script_file in scripts_dir.iterdir():
if script_file.is_file():
try:
scripts[script_file.name] = script_file.read_text()
except Exception:
continue
except PermissionError:
pass
skill._scripts = scripts
return scripts
def load_references(self, skill: LoadedSkill) -> dict:
"""Load references from skill's references/ directory (Level 3).
Args:
skill: LoadedSkill instance
Returns:
Dict mapping reference names to their contents
"""
if skill.properties.path is None:
return {}
refs_dir = skill.properties.path / "references"
if not refs_dir.exists() or not refs_dir.is_dir():
return {}
refs = {}
try:
for ref_file in refs_dir.iterdir():
if ref_file.is_file():
try:
refs[ref_file.name] = ref_file.read_text()
except Exception:
continue
except PermissionError:
pass
skill._references = refs
return refs
def load_assets(self, skill: LoadedSkill) -> dict:
"""Load asset paths from skill's assets/ directory (Level 3).
Note: For binary assets, only paths are returned, not contents.
Args:
skill: LoadedSkill instance
Returns:
Dict mapping asset names to their paths
"""
if skill.properties.path is None:
return {}
assets_dir = skill.properties.path / "assets"
if not assets_dir.exists() or not assets_dir.is_dir():
return {}
assets = {}
try:
for asset_file in assets_dir.iterdir():
if asset_file.is_file():
assets[asset_file.name] = str(asset_file)
except PermissionError:
pass
skill._assets = assets
skill.resources_loaded = True
return assets
def load_all_resources(self, skill: LoadedSkill) -> None:
"""Load all resources for a skill (Level 3 complete).
Args:
skill: LoadedSkill instance
"""
self.load_scripts(skill)
self.load_references(skill)
self.load_assets(skill)
skill.resources_loaded = True
@classmethod
def load(cls, skill_path: str, activate: bool = False) -> Optional[LoadedSkill]:
"""Convenience method to load a skill.
Args:
skill_path: Path to skill directory
activate: Whether to also load instructions
Returns:
LoadedSkill or None if invalid
"""
loader = cls()
skill = loader.load_metadata(skill_path)
if skill and activate:
loader.activate(skill)
return skill
@@ -0,0 +1,189 @@
"""SkillManager for Agent Skills integration."""
from typing import List, Optional, Dict
from .models import SkillMetadata
from .discovery import discover_skills
from .loader import SkillLoader, LoadedSkill
from .prompt import generate_skills_xml
class SkillManager:
"""Manager for discovering, loading, and using Agent Skills.
This is the main entry point for Agent Skills integration.
Supports progressive disclosure and zero performance impact
when skills are not used.
Usage:
manager = SkillManager()
# Discover skills from directories
manager.discover(["./skills", "~/.praison/skills"])
# Get XML for system prompt
prompt_xml = manager.to_prompt()
# Activate a specific skill
skill = manager.get_skill("pdf-processing")
manager.activate(skill)
"""
def __init__(self):
"""Initialize the SkillManager."""
self._skills: Dict[str, LoadedSkill] = {}
self._loader = SkillLoader()
self._discovered = False
@property
def skills(self) -> List[LoadedSkill]:
"""Get all loaded skills."""
return list(self._skills.values())
@property
def skill_names(self) -> List[str]:
"""Get names of all loaded skills."""
return list(self._skills.keys())
def discover(
self,
skill_dirs: Optional[List[str]] = None,
include_defaults: bool = True,
) -> int:
"""Discover skills from directories.
Args:
skill_dirs: List of directory paths to scan
include_defaults: Whether to include default skill directories
Returns:
Number of skills discovered
"""
props_list = discover_skills(skill_dirs, include_defaults)
for props in props_list:
if props.name not in self._skills:
self._skills[props.name] = LoadedSkill(properties=props)
self._discovered = True
return len(props_list)
def add_skill(self, skill_path: str) -> Optional[LoadedSkill]:
"""Add a single skill from a directory path.
Args:
skill_path: Path to skill directory
Returns:
LoadedSkill if successful, None otherwise
"""
skill = self._loader.load_metadata(skill_path)
if skill:
self._skills[skill.properties.name] = skill
return skill
def get_skill(self, name: str) -> Optional[LoadedSkill]:
"""Get a skill by name.
Args:
name: Skill name
Returns:
LoadedSkill or None if not found
"""
return self._skills.get(name)
def activate(self, skill: LoadedSkill) -> bool:
"""Activate a skill (load its instructions).
Args:
skill: LoadedSkill to activate
Returns:
True if activation succeeded
"""
return self._loader.activate(skill)
def activate_by_name(self, name: str) -> bool:
"""Activate a skill by name.
Args:
name: Skill name
Returns:
True if activation succeeded
"""
skill = self.get_skill(name)
if skill is None:
return False
return self.activate(skill)
def get_available_skills(self) -> List[SkillMetadata]:
"""Get metadata for all available skills.
This is used for system prompt injection.
Returns:
List of SkillMetadata instances
"""
return [skill.metadata for skill in self._skills.values()]
def to_prompt(self) -> str:
"""Generate XML prompt for available skills.
Returns:
XML string with <available_skills> block
"""
metadata_list = self.get_available_skills()
return generate_skills_xml(metadata_list)
def get_instructions(self, name: str) -> Optional[str]:
"""Get instructions for a skill, activating if needed.
Args:
name: Skill name
Returns:
Skill instructions or None if not found
"""
skill = self.get_skill(name)
if skill is None:
return None
if not skill.is_activated:
self.activate(skill)
return skill.instructions
def load_resources(self, name: str) -> bool:
"""Load all resources for a skill.
Args:
name: Skill name
Returns:
True if resources loaded successfully
"""
skill = self.get_skill(name)
if skill is None:
return False
self._loader.load_all_resources(skill)
return True
def clear(self) -> None:
"""Clear all loaded skills."""
self._skills.clear()
self._discovered = False
def __len__(self) -> int:
"""Get number of loaded skills."""
return len(self._skills)
def __contains__(self, name: str) -> bool:
"""Check if a skill is loaded."""
return name in self._skills
def __iter__(self):
"""Iterate over loaded skills."""
return iter(self._skills.values())
@@ -0,0 +1,95 @@
"""Data models for Agent Skills."""
from dataclasses import dataclass, field
from typing import Optional
from pathlib import Path
class ParseError(Exception):
"""Raised when SKILL.md parsing fails."""
pass
class ValidationError(Exception):
"""Raised when skill validation fails."""
pass
@dataclass
class SkillProperties:
"""Properties parsed from a skill's SKILL.md frontmatter.
Attributes:
name: Skill name in kebab-case (required)
description: What the skill does and when the model should use it (required)
license: License for the skill (optional)
compatibility: Compatibility information for the skill (optional)
allowed_tools: Tool patterns the skill requires (optional, experimental)
metadata: Key-value pairs for client-specific properties (defaults to
empty dict; omitted from to_dict() output when empty)
path: Path to the skill directory (optional, added for PraisonAI)
"""
name: str
description: str
license: Optional[str] = None
compatibility: Optional[str] = None
allowed_tools: Optional[str] = None
metadata: dict = field(default_factory=dict)
path: Optional[Path] = None
def to_dict(self) -> dict:
"""Convert to dictionary, excluding None values and empty metadata."""
result = {"name": self.name, "description": self.description}
if self.license is not None:
result["license"] = self.license
if self.compatibility is not None:
result["compatibility"] = self.compatibility
if self.allowed_tools is not None:
result["allowed-tools"] = self.allowed_tools
if self.metadata:
result["metadata"] = self.metadata
return result
@dataclass
class SkillMetadata:
"""Lightweight skill metadata for system prompt injection.
This is the minimal information needed to include a skill in the
system prompt's <available_skills> block (~50-100 tokens per skill).
Attributes:
name: Skill name
description: What the skill does and when to use it
location: Absolute path to the SKILL.md file
"""
name: str
description: str
location: str
@classmethod
def from_properties(cls, props: SkillProperties) -> "SkillMetadata":
"""Create SkillMetadata from SkillProperties.
Args:
props: SkillProperties instance with path set
Returns:
SkillMetadata with location pointing to SKILL.md
"""
if props.path:
# Look for SKILL.md or skill.md
skill_md = props.path / "SKILL.md"
if not skill_md.exists():
skill_md = props.path / "skill.md"
location = str(skill_md)
else:
location = ""
return cls(
name=props.name,
description=props.description,
location=location
)
@@ -0,0 +1,165 @@
"""YAML frontmatter parsing for SKILL.md files."""
from pathlib import Path
from typing import Optional, Tuple
import re
from .models import ParseError, ValidationError, SkillProperties
def find_skill_md(skill_dir: Path) -> Optional[Path]:
"""Find the SKILL.md file in a skill directory.
Prefers SKILL.md (uppercase) but accepts skill.md (lowercase).
Args:
skill_dir: Path to the skill directory
Returns:
Path to the SKILL.md file, or None if not found
"""
for name in ("SKILL.md", "skill.md"):
path = skill_dir / name
if path.exists():
return path
return None
def parse_frontmatter(content: str) -> Tuple[dict, str]:
"""Parse YAML frontmatter from SKILL.md content.
Args:
content: Raw content of SKILL.md file
Returns:
Tuple of (metadata dict, markdown body)
Raises:
ParseError: If frontmatter is missing or invalid
"""
if not content.startswith("---"):
raise ParseError("SKILL.md must start with YAML frontmatter (---)")
parts = content.split("---", 2)
if len(parts) < 3:
raise ParseError("SKILL.md frontmatter not properly closed with ---")
frontmatter_str = parts[1]
body = parts[2].strip()
try:
metadata = _parse_yaml(frontmatter_str)
except Exception as e:
raise ParseError(f"Invalid YAML in frontmatter: {e}")
if not isinstance(metadata, dict):
raise ParseError("SKILL.md frontmatter must be a YAML mapping")
# Convert metadata values to strings where needed
if "metadata" in metadata and isinstance(metadata["metadata"], dict):
metadata["metadata"] = {str(k): str(v) for k, v in metadata["metadata"].items()}
return metadata, body
def _parse_yaml(yaml_str: str) -> dict:
"""Parse YAML string to dictionary.
Uses a simple parser to avoid strictyaml dependency.
Falls back to yaml if available.
Args:
yaml_str: YAML string to parse
Returns:
Parsed dictionary
"""
# Try to use PyYAML if available
try:
import yaml
return yaml.safe_load(yaml_str) or {}
except ImportError:
pass
# Simple fallback parser for basic YAML
result = {}
current_key = None
current_dict = None
for line in yaml_str.strip().split("\n"):
if not line.strip():
continue
# Check for nested dict
if line.startswith(" ") and current_dict is not None:
match = re.match(r"^\s+(\w+):\s*(.*)$", line)
if match:
key, value = match.groups()
current_dict[key] = value.strip().strip('"').strip("'")
continue
# Top-level key
match = re.match(r"^(\S+):\s*(.*)$", line)
if match:
key, value = match.groups()
value = value.strip()
if not value:
# Could be start of nested dict
result[key] = {}
current_key = key
current_dict = result[key]
else:
result[key] = value.strip('"').strip("'")
current_dict = None
return result
def read_properties(skill_dir: Path) -> SkillProperties:
"""Read skill properties from SKILL.md frontmatter.
This function parses the frontmatter and returns properties.
It does NOT perform full validation. Use validate() for that.
Args:
skill_dir: Path to the skill directory
Returns:
SkillProperties with parsed metadata
Raises:
ParseError: If SKILL.md is missing or has invalid YAML
ValidationError: If required fields (name, description) are missing
"""
skill_dir = Path(skill_dir)
skill_md = find_skill_md(skill_dir)
if skill_md is None:
raise ParseError(f"SKILL.md not found in {skill_dir}")
content = skill_md.read_text()
metadata, _ = parse_frontmatter(content)
if "name" not in metadata:
raise ValidationError("Missing required field in frontmatter: name")
if "description" not in metadata:
raise ValidationError("Missing required field in frontmatter: description")
name = metadata["name"]
description = metadata["description"]
if not isinstance(name, str) or not name.strip():
raise ValidationError("Field 'name' must be a non-empty string")
if not isinstance(description, str) or not description.strip():
raise ValidationError("Field 'description' must be a non-empty string")
return SkillProperties(
name=name.strip(),
description=description.strip(),
license=metadata.get("license"),
compatibility=metadata.get("compatibility"),
allowed_tools=metadata.get("allowed-tools"),
metadata=metadata.get("metadata") or {},
path=skill_dir,
)
@@ -0,0 +1,74 @@
"""XML prompt generation for Agent Skills."""
from pathlib import Path
from typing import List
import html
from .models import SkillMetadata
from .parser import read_properties
def format_skill_for_prompt(skill: SkillMetadata) -> str:
"""Format a single skill as XML for system prompt.
Args:
skill: SkillMetadata instance
Returns:
XML string for the skill
"""
name = html.escape(skill.name)
description = html.escape(skill.description)
location = html.escape(skill.location)
return f""" <skill>
<name>{name}</name>
<description>{description}</description>
<location>{location}</location>
</skill>"""
def generate_skills_xml(skills: List[SkillMetadata]) -> str:
"""Generate XML block for available skills.
Args:
skills: List of SkillMetadata instances
Returns:
XML string with <available_skills> block
"""
if not skills:
return "<available_skills>\n</available_skills>"
skill_entries = "\n".join(format_skill_for_prompt(s) for s in skills)
return f"<available_skills>\n{skill_entries}\n</available_skills>"
def to_prompt(skill_dirs: List[Path]) -> str:
"""Generate prompt XML from skill directories.
This is the main entry point for generating the <available_skills>
block to inject into agent system prompts.
Args:
skill_dirs: List of paths to skill directories
Returns:
XML string with <available_skills> block
"""
skills = []
for skill_dir in skill_dirs:
skill_dir = Path(skill_dir)
if not skill_dir.exists() or not skill_dir.is_dir():
continue
try:
props = read_properties(skill_dir)
meta = SkillMetadata.from_properties(props)
skills.append(meta)
except Exception:
# Skip invalid skills silently
continue
return generate_skills_xml(skills)
@@ -0,0 +1,176 @@
"""Skill validation logic."""
import unicodedata
from pathlib import Path
from typing import Optional, List
from .models import ParseError
from .parser import find_skill_md, parse_frontmatter
MAX_SKILL_NAME_LENGTH = 64
MAX_DESCRIPTION_LENGTH = 1024
MAX_COMPATIBILITY_LENGTH = 500
ALLOWED_FIELDS = {
"name",
"description",
"license",
"allowed-tools",
"metadata",
"compatibility",
}
def _validate_name(name: str, skill_dir: Optional[Path]) -> List[str]:
"""Validate skill name format and directory match.
Skill names support i18n characters (Unicode letters) plus hyphens.
Names must be lowercase and cannot start/end with hyphens.
"""
errors = []
if not name or not isinstance(name, str) or not name.strip():
errors.append("Field 'name' must be a non-empty string")
return errors
name = unicodedata.normalize("NFKC", name.strip())
if len(name) > MAX_SKILL_NAME_LENGTH:
errors.append(
f"Skill name '{name}' exceeds {MAX_SKILL_NAME_LENGTH} character limit "
f"({len(name)} chars)"
)
if name != name.lower():
errors.append(f"Skill name '{name}' must be lowercase")
if name.startswith("-") or name.endswith("-"):
errors.append("Skill name cannot start or end with a hyphen")
if "--" in name:
errors.append("Skill name cannot contain consecutive hyphens")
if not all(c.isalnum() or c == "-" for c in name):
errors.append(
f"Skill name '{name}' contains invalid characters. "
"Only letters, digits, and hyphens are allowed."
)
if skill_dir:
dir_name = unicodedata.normalize("NFKC", skill_dir.name)
if dir_name != name:
errors.append(
f"Directory name '{skill_dir.name}' must match skill name '{name}'"
)
return errors
def _validate_description(description: str) -> List[str]:
"""Validate description format."""
errors = []
if not description or not isinstance(description, str) or not description.strip():
errors.append("Field 'description' must be a non-empty string")
return errors
if len(description) > MAX_DESCRIPTION_LENGTH:
errors.append(
f"Description exceeds {MAX_DESCRIPTION_LENGTH} character limit "
f"({len(description)} chars)"
)
return errors
def _validate_compatibility(compatibility: str) -> List[str]:
"""Validate compatibility format."""
errors = []
if not isinstance(compatibility, str):
errors.append("Field 'compatibility' must be a string")
return errors
if len(compatibility) > MAX_COMPATIBILITY_LENGTH:
errors.append(
f"Compatibility exceeds {MAX_COMPATIBILITY_LENGTH} character limit "
f"({len(compatibility)} chars)"
)
return errors
def _validate_metadata_fields(metadata: dict) -> List[str]:
"""Validate that only allowed fields are present."""
errors = []
extra_fields = set(metadata.keys()) - ALLOWED_FIELDS
if extra_fields:
errors.append(
f"Unexpected fields in frontmatter: {', '.join(sorted(extra_fields))}. "
f"Only {sorted(ALLOWED_FIELDS)} are allowed."
)
return errors
def validate_metadata(metadata: dict, skill_dir: Optional[Path] = None) -> List[str]:
"""Validate parsed skill metadata.
This is the core validation function that works on already-parsed metadata,
avoiding duplicate file I/O when called from the parser.
Args:
metadata: Parsed YAML frontmatter dictionary
skill_dir: Optional path to skill directory (for name-directory match check)
Returns:
List of validation error messages. Empty list means valid.
"""
errors = []
errors.extend(_validate_metadata_fields(metadata))
if "name" not in metadata:
errors.append("Missing required field in frontmatter: name")
else:
errors.extend(_validate_name(metadata["name"], skill_dir))
if "description" not in metadata:
errors.append("Missing required field in frontmatter: description")
else:
errors.extend(_validate_description(metadata["description"]))
if "compatibility" in metadata:
errors.extend(_validate_compatibility(metadata["compatibility"]))
return errors
def validate(skill_dir: Path) -> List[str]:
"""Validate a skill directory.
Args:
skill_dir: Path to the skill directory
Returns:
List of validation error messages. Empty list means valid.
"""
skill_dir = Path(skill_dir)
if not skill_dir.exists():
return [f"Path does not exist: {skill_dir}"]
if not skill_dir.is_dir():
return [f"Not a directory: {skill_dir}"]
skill_md = find_skill_md(skill_dir)
if skill_md is None:
return ["Missing required file: SKILL.md"]
try:
content = skill_md.read_text()
metadata, _ = parse_frontmatter(content)
except ParseError as e:
return [str(e)]
return validate_metadata(metadata, skill_dir)
@@ -218,6 +218,10 @@ TOOL_MAPPINGS = {
'crawl4ai_extract_sync': ('.crawl4ai_tools', None),
'Crawl4AITools': ('.crawl4ai_tools', 'Crawl4AITools'),
'crawl4ai_tools': ('.crawl4ai_tools', None),
# Unified Web Search (auto-fallback across providers)
'search_web': ('.web_search', None),
'get_available_providers': ('.web_search', None),
}
_instances = {} # Cache for class instances
@@ -254,7 +258,8 @@ def __getattr__(name: str) -> Any:
'exa', 'exa_search', 'exa_search_contents', 'exa_find_similar', 'exa_answer',
'exa_search_async', 'exa_search_contents_async', 'exa_answer_async',
'crawl4ai', 'crawl4ai_many', 'crawl4ai_extract', 'crawl4ai_llm_extract',
'crawl4ai_sync', 'crawl4ai_extract_sync'
'crawl4ai_sync', 'crawl4ai_extract_sync',
'search_web', 'get_available_providers'
]:
return getattr(module, name)
if name in ['file_tools', 'pandas_tools', 'wikipedia_tools',
@@ -14,6 +14,7 @@ Usage:
context, cleaned_prompt = parser.process("@file:main.py explain this code")
"""
import os
import re
import logging
from pathlib import Path
@@ -43,10 +44,14 @@ class MentionsParser:
"url": re.compile(r'@url:(https?://[^\s]+)'),
}
# Default max file chars: 500K (~125K tokens) - fits GPT-4o (128K), Claude 3.5 (200K)
DEFAULT_MAX_FILE_CHARS = 500000
def __init__(
self,
workspace_path: Optional[str] = None,
verbose: int = 0
verbose: int = 0,
max_file_chars: Optional[int] = None
):
"""
Initialize MentionsParser.
@@ -54,10 +59,24 @@ class MentionsParser:
Args:
workspace_path: Path to workspace/project root
verbose: Verbosity level
max_file_chars: Maximum characters to include from files.
Default: 500000 (500K chars, ~125K tokens).
Set to 0 for no limit.
Can be overridden via PRAISON_MAX_FILE_CHARS env var.
"""
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
self.verbose = verbose
# Configure max file chars: constructor > env var > default
if max_file_chars is not None:
self.max_file_chars = max_file_chars
else:
env_val = os.environ.get('PRAISON_MAX_FILE_CHARS')
if env_val is not None:
self.max_file_chars = int(env_val)
else:
self.max_file_chars = self.DEFAULT_MAX_FILE_CHARS
# Lazy-loaded managers
self._docs_manager = None
self._rules_manager = None
@@ -158,9 +177,15 @@ class MentionsParser:
content = full_path.read_text(encoding="utf-8")
# Limit content size
if len(content) > 50000:
content = content[:50000] + "\n... (truncated)"
# Limit content size (0 means no limit)
if self.max_file_chars > 0 and len(content) > self.max_file_chars:
original_size = len(content)
content = content[:self.max_file_chars] + "\n... (truncated)"
self._log(
f"File {file_path} truncated from {original_size:,} to {self.max_file_chars:,} chars. "
f"Set PRAISON_MAX_FILE_CHARS=0 for no limit.",
logging.WARNING
)
# Detect language for code fence
ext = full_path.suffix.lower()
@@ -253,9 +278,9 @@ class MentionsParser:
with urllib.request.urlopen(req, timeout=10) as response:
content = response.read().decode('utf-8', errors='ignore')
# Limit content size
if len(content) > 50000:
content = content[:50000] + "\n... (truncated)"
# Limit content size (0 means no limit)
if self.max_file_chars > 0 and len(content) > self.max_file_chars:
content = content[:self.max_file_chars] + "\n... (truncated)"
# Try to extract text from HTML
if '<html' in content.lower():
@@ -0,0 +1,329 @@
"""Unified web search tool with automatic fallback across multiple providers.
This module provides a single `search_web` function that automatically tries
multiple search providers in order, falling back to the next if one fails.
Search Provider Priority:
1. Tavily (requires TAVILY_API_KEY + tavily-python)
2. Exa (requires EXA_API_KEY + exa_py)
3. You.com (requires YDC_API_KEY + youdotcom)
4. DuckDuckGo (requires duckduckgo_search package, no API key)
5. SearxNG (requires requests + running SearxNG instance)
Usage:
from praisonaiagents.tools import search_web
# Simple search - automatically uses best available provider
results = search_web("AI news 2024")
# With max results
results = search_web("Python tutorials", max_results=10)
"""
from typing import List, Dict, Any, Optional
import logging
import os
from importlib import util
logger = logging.getLogger(__name__)
def _check_tavily() -> tuple[bool, Optional[str]]:
"""Check if Tavily is available."""
if util.find_spec("tavily") is None:
return False, "tavily-python package not installed"
if not os.environ.get("TAVILY_API_KEY"):
return False, "TAVILY_API_KEY not set"
return True, None
def _check_exa() -> tuple[bool, Optional[str]]:
"""Check if Exa is available."""
if util.find_spec("exa_py") is None:
return False, "exa_py package not installed"
if not os.environ.get("EXA_API_KEY"):
return False, "EXA_API_KEY not set"
return True, None
def _check_youdotcom() -> tuple[bool, Optional[str]]:
"""Check if You.com is available."""
if util.find_spec("youdotcom") is None:
return False, "youdotcom package not installed"
if not os.environ.get("YDC_API_KEY"):
return False, "YDC_API_KEY not set"
return True, None
def _check_duckduckgo() -> tuple[bool, Optional[str]]:
"""Check if DuckDuckGo is available."""
if util.find_spec("duckduckgo_search") is None:
return False, "duckduckgo_search package not installed"
return True, None
def _check_searxng() -> tuple[bool, Optional[str]]:
"""Check if SearxNG is available."""
if util.find_spec("requests") is None:
return False, "requests package not installed"
return True, None
def _search_tavily(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
"""Search using Tavily."""
from tavily import TavilyClient
client = TavilyClient(api_key=os.environ.get("TAVILY_API_KEY"))
response = client.search(query=query, max_results=max_results)
results = []
for r in response.get("results", []):
results.append({
"title": r.get("title", ""),
"url": r.get("url", ""),
"snippet": r.get("content", ""),
"provider": "tavily"
})
return results
def _search_exa(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
"""Search using Exa."""
from exa_py import Exa
client = Exa(os.environ.get("EXA_API_KEY"))
response = client.search(query=query, num_results=max_results)
results = []
for r in response.results:
results.append({
"title": getattr(r, "title", "") or "",
"url": r.url,
"snippet": getattr(r, "text", "") or "",
"provider": "exa"
})
return results
def _search_youdotcom(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
"""Search using You.com."""
from youdotcom import You
client = You(api_key_auth=os.environ.get("YDC_API_KEY"))
response = client.search.unified(query=query, count=max_results)
results = []
# Handle response object
if hasattr(response, 'results'):
web_results = getattr(response.results, 'web', []) or []
for r in web_results[:max_results]:
results.append({
"title": getattr(r, "title", "") or "",
"url": getattr(r, "url", "") or "",
"snippet": getattr(r, "description", "") or "",
"provider": "youdotcom"
})
elif isinstance(response, dict):
web_results = response.get("results", {}).get("web", [])
for r in web_results[:max_results]:
results.append({
"title": r.get("title", ""),
"url": r.get("url", ""),
"snippet": r.get("description", ""),
"provider": "youdotcom"
})
return results
def _search_duckduckgo(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
"""Search using DuckDuckGo."""
from duckduckgo_search import DDGS
results = []
ddgs = DDGS()
for result in ddgs.text(keywords=query, max_results=max_results):
results.append({
"title": result.get("title", ""),
"url": result.get("href", ""),
"snippet": result.get("body", ""),
"provider": "duckduckgo"
})
return results
def _search_searxng(query: str, max_results: int = 5, searxng_url: Optional[str] = None) -> List[Dict[str, Any]]:
"""Search using SearxNG."""
import requests
url = searxng_url or os.environ.get("SEARXNG_URL", "http://localhost:32768/search")
params = {
'q': query,
'format': 'json',
'engines': 'google,bing,duckduckgo',
'safesearch': '1'
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
raw_results = response.json().get('results', [])
results = []
for result in raw_results[:max_results]:
results.append({
"title": result.get("title", ""),
"url": result.get("url", ""),
"snippet": result.get("content", ""),
"provider": "searxng"
})
return results
# Provider configuration: (name, check_func, search_func)
SEARCH_PROVIDERS = [
("tavily", _check_tavily, _search_tavily),
("exa", _check_exa, _search_exa),
("youdotcom", _check_youdotcom, _search_youdotcom),
("duckduckgo", _check_duckduckgo, _search_duckduckgo),
("searxng", _check_searxng, _search_searxng),
]
def search_web(
query: str,
max_results: int = 5,
providers: Optional[List[str]] = None,
searxng_url: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""Search the web using multiple providers with automatic fallback.
Tries each search provider in order until one succeeds. For each provider,
it first checks if the API key is set (if required) and if the package is
installed, then attempts the search. If the search fails, it moves to the
next provider.
Args:
query: The search query string
max_results: Maximum number of results to return (default: 5)
providers: Optional list of provider names to try, in order.
If not specified, uses default order:
["tavily", "exa", "youdotcom", "duckduckgo", "searxng"]
searxng_url: Optional custom SearxNG instance URL
Returns:
List of search results, each containing:
- title: Result title
- url: Result URL
- snippet: Result description/snippet
- provider: Name of the provider that returned the result
If all providers fail, returns a list with a single error dict.
Example:
# Use default provider order
results = search_web("AI news 2024")
# Specify providers to try
results = search_web("Python tutorials", providers=["duckduckgo", "tavily"])
# With custom SearxNG URL
results = search_web("tech news", searxng_url="http://my-searxng:8080/search")
"""
errors = []
# Determine which providers to try
if providers:
# Filter to only valid provider names
provider_order = []
for name in providers:
for pname, check_fn, search_fn in SEARCH_PROVIDERS:
if pname == name.lower():
provider_order.append((pname, check_fn, search_fn))
break
else:
provider_order = SEARCH_PROVIDERS
for provider_name, check_func, search_func in provider_order:
# Step 1: Check if provider is available (API key + package)
is_available, check_error = check_func()
if not is_available:
logger.debug(f"Skipping {provider_name}: {check_error}")
errors.append(f"{provider_name}: {check_error}")
continue
# Step 2: Try to search
try:
logger.debug(f"Trying {provider_name} for query: {query}")
if provider_name == "searxng":
results = search_func(query, max_results, searxng_url)
else:
results = search_func(query, max_results)
if results:
logger.info(f"search_web: Successfully used {provider_name}")
return results
else:
errors.append(f"{provider_name}: No results returned")
except Exception as e:
error_msg = f"{provider_name}: {str(e)}"
logger.debug(f"Search failed with {provider_name}: {e}")
errors.append(error_msg)
continue
# All providers failed
error_summary = "; ".join(errors) if errors else "No search providers available"
logger.error(f"search_web: All providers failed - {error_summary}")
return [{"error": f"All search providers failed: {error_summary}"}]
def get_available_providers() -> List[Dict[str, Any]]:
"""Get list of available search providers and their status.
Returns:
List of dicts with provider info:
- name: Provider name
- available: Whether the provider is ready to use
- reason: Reason if not available
Example:
providers = get_available_providers()
for p in providers:
print(f"{p['name']}: {'✓' if p['available'] else '✗'} {p.get('reason', '')}")
"""
result = []
for name, check_func, _ in SEARCH_PROVIDERS:
is_available, error = check_func()
result.append({
"name": name,
"available": is_available,
"reason": error if not is_available else None
})
return result
if __name__ == "__main__":
# Example usage and provider status check
print("=" * 60)
print("Web Search Tool - Provider Status")
print("=" * 60)
providers = get_available_providers()
for p in providers:
status = "✓ Available" if p["available"] else f"✗ {p['reason']}"
print(f" {p['name']:12} {status}")
print("\n" + "=" * 60)
print("Testing search_web...")
print("=" * 60)
results = search_web("Python programming", max_results=3)
if results and "error" not in results[0]:
print(f"\nFound {len(results)} results using {results[0].get('provider', 'unknown')}:")
for r in results:
print(f"\n Title: {r.get('title', 'N/A')[:50]}...")
print(f" URL: {r.get('url', 'N/A')}")
else:
print(f"\nSearch failed: {results}")
@@ -0,0 +1,13 @@
"""
UI Integrations for PraisonAI Agents
This module provides UI protocol integrations for exposing PraisonAI Agents
via various frontend protocols.
Available integrations:
- agui: AG-UI (Agent-User Interface) protocol for CopilotKit and compatible frontends
"""
from praisonaiagents.ui.agui import AGUI
__all__ = ["AGUI"]
@@ -0,0 +1,74 @@
"""
A2A Protocol Integration for PraisonAI Agents
This module provides A2A (Agent2Agent) protocol support,
enabling PraisonAI Agents to be exposed as A2A Servers
for agent-to-agent communication.
Usage:
from praisonaiagents import Agent
from praisonaiagents.ui.a2a import A2A
from fastapi import FastAPI
agent = Agent(name="Assistant", role="Helper", goal="Help users")
a2a = A2A(agent=agent)
app = FastAPI()
app.include_router(a2a.get_router())
# Agent Card available at: GET /.well-known/agent.json
# A2A endpoint at: POST /a2a (JSON-RPC)
"""
from praisonaiagents.ui.a2a.a2a import A2A
from praisonaiagents.ui.a2a.types import (
# Enums
TaskState,
Role,
# Part types
TextPart,
FilePart,
DataPart,
# Core types
Message,
TaskStatus,
Task,
Artifact,
# Agent Card types
AgentSkill,
AgentCapabilities,
AgentCard,
# Request/Response types
MessageConfig,
SendMessageRequest,
# Streaming events
TaskStatusUpdateEvent,
TaskArtifactUpdateEvent,
)
__all__ = [
# Main class
"A2A",
# Enums
"TaskState",
"Role",
# Part types
"TextPart",
"FilePart",
"DataPart",
# Core types
"Message",
"TaskStatus",
"Task",
"Artifact",
# Agent Card types
"AgentSkill",
"AgentCapabilities",
"AgentCard",
# Request/Response types
"MessageConfig",
"SendMessageRequest",
# Streaming events
"TaskStatusUpdateEvent",
"TaskArtifactUpdateEvent",
]
@@ -0,0 +1,154 @@
"""
A2A - Main A2A Interface Class
Exposes PraisonAI Agents via the A2A (Agent2Agent) protocol.
"""
import logging
from typing import List, Optional, TYPE_CHECKING
from praisonaiagents.ui.a2a.types import AgentCard
from praisonaiagents.ui.a2a.agent_card import generate_agent_card
from praisonaiagents.ui.a2a.task_store import TaskStore
if TYPE_CHECKING:
from praisonaiagents import Agent, PraisonAIAgents
from fastapi import APIRouter
logger = logging.getLogger(__name__)
class A2A:
"""
A2A Interface for PraisonAI Agents.
Exposes a PraisonAI Agent or PraisonAIAgents workflow via the A2A protocol,
enabling agent-to-agent communication with other A2A-compatible systems.
Usage:
from praisonaiagents import Agent
from praisonaiagents.ui.a2a import A2A
from fastapi import FastAPI
agent = Agent(name="Assistant", role="Helper", goal="Help users")
a2a = A2A(agent=agent, url="http://localhost:8000/a2a")
app = FastAPI()
app.include_router(a2a.get_router())
# Agent Card at: GET /.well-known/agent.json
# A2A endpoint at: POST /a2a
Args:
agent: Single PraisonAI Agent instance
agents: PraisonAIAgents instance for multi-agent workflows
name: Name for the A2A endpoint (defaults to agent name)
description: Description of the agent
url: URL where the A2A endpoint is hosted
version: Version string for the agent
prefix: URL prefix for the router
tags: OpenAPI tags for the router
"""
def __init__(
self,
agent: Optional["Agent"] = None,
agents: Optional["PraisonAIAgents"] = None,
name: Optional[str] = None,
description: Optional[str] = None,
url: str = "http://localhost:8000/a2a",
version: str = "1.0.0",
prefix: str = "",
tags: Optional[List[str]] = None,
):
if agent is None and agents is None:
raise ValueError("A2A requires an agent or agents instance")
self.agent = agent
self.agents = agents
self.url = url
self.version = version
self.prefix = prefix
self.tags = tags or ["A2A"]
# Set name from agent if not provided
if name:
self.name = name
elif agent and hasattr(agent, 'name'):
self.name = agent.name
elif agents and hasattr(agents, 'name'):
self.name = agents.name
else:
self.name = "PraisonAI Agent"
# Set description
if description:
self.description = description
elif agent and hasattr(agent, 'role'):
self.description = agent.role
else:
self.description = "PraisonAI Agent via A2A"
# Initialize task store
self.task_store = TaskStore()
# Router cache
self._router: Optional["APIRouter"] = None
self._agent_card: Optional[AgentCard] = None
def get_agent_card(self) -> AgentCard:
"""
Get the Agent Card for this A2A instance.
Returns:
AgentCard object for A2A discovery
"""
if self._agent_card is None:
self._agent_card = generate_agent_card(
agent=self.agent,
url=self.url,
version=self.version,
streaming=True,
)
return self._agent_card
def get_router(self) -> "APIRouter":
"""
Get the FastAPI router for this A2A instance.
Returns:
FastAPI APIRouter with A2A endpoints
"""
if self._router is None:
self._router = self._create_router()
return self._router
def _create_router(self) -> "APIRouter":
"""Create FastAPI router with A2A endpoints."""
from fastapi import APIRouter
from fastapi.responses import JSONResponse
router = APIRouter(prefix=self.prefix, tags=self.tags)
# Agent Card endpoint (well-known URI)
@router.get("/.well-known/agent.json")
async def get_agent_card():
"""Return the Agent Card for discovery."""
card = self.get_agent_card()
return JSONResponse(content=card.model_dump(by_alias=True, exclude_none=True))
# Status endpoint
@router.get("/status")
async def get_status():
"""Return server status."""
return {
"status": "ok",
"name": self.name,
"version": self.version,
}
# TODO: Add JSON-RPC endpoint for message/send
# TODO: Add streaming endpoint for message/stream
# TODO: Add task management endpoints
return router
@@ -0,0 +1,123 @@
"""
A2A Agent Card Generation
Generate Agent Cards from PraisonAI Agents for A2A discovery.
"""
import inspect
from typing import TYPE_CHECKING, List, Optional
from praisonaiagents.ui.a2a.types import (
AgentCard,
AgentCapabilities,
AgentSkill,
)
if TYPE_CHECKING:
from praisonaiagents import Agent
def extract_skills_from_tools(tools: Optional[List]) -> List[AgentSkill]:
"""
Extract AgentSkill objects from a list of tools.
Args:
tools: List of tool functions or objects
Returns:
List of AgentSkill objects
"""
if not tools:
return []
skills = []
for tool in tools:
# Skip non-callable items
if not callable(tool):
continue
# Get function name
name = getattr(tool, '__name__', None)
if not name:
continue
# Get description from docstring
docstring = inspect.getdoc(tool)
description = docstring.split('\n')[0] if docstring else f"Tool: {name}"
# Create skill
skill = AgentSkill(
id=name,
name=name,
description=description,
tags=["tool"],
)
skills.append(skill)
return skills
def generate_agent_card(
agent: "Agent",
url: str,
version: str = "1.0.0",
streaming: bool = False,
push_notifications: bool = False,
) -> AgentCard:
"""
Generate an A2A Agent Card from a PraisonAI Agent.
Args:
agent: PraisonAI Agent instance
url: URL where the A2A endpoint is hosted
version: Version string for the agent
streaming: Whether streaming is supported
push_notifications: Whether push notifications are supported
Returns:
AgentCard object for A2A discovery
"""
# Get agent name
name = getattr(agent, 'name', None) or "PraisonAI Agent"
# Build description from role, goal, or instructions
description_parts = []
role = getattr(agent, 'role', None)
if role:
description_parts.append(role)
goal = getattr(agent, 'goal', None)
if goal:
description_parts.append(goal)
instructions = getattr(agent, 'instructions', None)
if instructions and not description_parts:
# Use first line of instructions if no role/goal
description_parts.append(instructions.split('\n')[0][:200])
description = ". ".join(description_parts) if description_parts else "PraisonAI Agent"
# Extract skills from tools
tools = getattr(agent, 'tools', None)
skills = extract_skills_from_tools(tools)
# Build capabilities
capabilities = AgentCapabilities(
streaming=streaming,
push_notifications=push_notifications,
)
# Create Agent Card
card = AgentCard(
name=name,
url=url,
version=version,
description=description,
capabilities=capabilities,
skills=skills if skills else None,
provider={"name": "PraisonAI"},
)
return card
@@ -0,0 +1,131 @@
"""
A2A Message Conversion
Convert between A2A protocol messages and PraisonAI internal format.
"""
import uuid
from typing import Any, Dict, List, Optional
from praisonaiagents.ui.a2a.types import (
Message,
Role,
TextPart,
Artifact,
)
def a2a_to_praisonai_messages(messages: List[Message]) -> List[Dict[str, Any]]:
"""
Convert A2A messages to PraisonAI/OpenAI chat format.
Args:
messages: List of A2A Message objects
Returns:
List of dicts in OpenAI chat format {"role": ..., "content": ...}
"""
result = []
for msg in messages:
# Map A2A role to OpenAI role
role = "user" if msg.role == Role.USER else "assistant"
# Combine all text parts
content_parts = []
for part in msg.parts:
if isinstance(part, TextPart):
content_parts.append(part.text)
elif hasattr(part, 'text'):
content_parts.append(part.text)
content = " ".join(content_parts)
result.append({
"role": role,
"content": content,
})
return result
def praisonai_to_a2a_message(
response: str,
message_id: Optional[str] = None,
context_id: Optional[str] = None,
task_id: Optional[str] = None,
) -> Message:
"""
Convert PraisonAI response to A2A Message.
Args:
response: Text response from agent
message_id: Optional message ID (auto-generated if not provided)
context_id: Optional context ID
task_id: Optional task ID
Returns:
A2A Message object
"""
msg_id = message_id or f"msg-{uuid.uuid4().hex[:12]}"
return Message(
message_id=msg_id,
role=Role.AGENT,
parts=[TextPart(text=response)],
context_id=context_id,
task_id=task_id,
)
def create_artifact(
content: str,
artifact_id: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
) -> Artifact:
"""
Create an A2A Artifact from response content.
Args:
content: Text content for the artifact
artifact_id: Optional artifact ID (auto-generated if not provided)
name: Optional artifact name
description: Optional artifact description
Returns:
A2A Artifact object
"""
art_id = artifact_id or f"art-{uuid.uuid4().hex[:12]}"
return Artifact(
artifact_id=art_id,
name=name,
description=description,
parts=[TextPart(text=content)],
)
def extract_user_input(messages: List[Message]) -> str:
"""
Extract the last user input from a list of messages.
Args:
messages: List of A2A Message objects
Returns:
Text content of the last user message, or empty string if none
"""
# Find the last user message
for msg in reversed(messages):
if msg.role == Role.USER:
# Combine text parts
content_parts = []
for part in msg.parts:
if isinstance(part, TextPart):
content_parts.append(part.text)
elif hasattr(part, 'text'):
content_parts.append(part.text)
return " ".join(content_parts)
return ""
@@ -0,0 +1,189 @@
"""
A2A Streaming Events
SSE streaming support for A2A task updates.
"""
import json
from typing import AsyncIterator, Optional
from praisonaiagents.ui.a2a.types import (
Task,
TaskState,
TaskStatus,
TaskStatusUpdateEvent,
TaskArtifactUpdateEvent,
Artifact,
TextPart,
)
class A2AEventEncoder:
"""Encode A2A events for SSE streaming."""
@staticmethod
def encode_event(event_type: str, data: dict) -> str:
"""
Encode an event for SSE streaming.
Args:
event_type: Type of event (e.g., "task.status", "task.artifact")
data: Event data as dict
Returns:
SSE-formatted string
"""
json_data = json.dumps(data, default=str)
return f"event: {event_type}\ndata: {json_data}\n\n"
@staticmethod
def encode_task_status(event: TaskStatusUpdateEvent) -> str:
"""Encode TaskStatusUpdateEvent for SSE."""
data = event.model_dump(by_alias=True, exclude_none=True)
return A2AEventEncoder.encode_event("task.status", data)
@staticmethod
def encode_task_artifact(event: TaskArtifactUpdateEvent) -> str:
"""Encode TaskArtifactUpdateEvent for SSE."""
data = event.model_dump(by_alias=True, exclude_none=True)
return A2AEventEncoder.encode_event("task.artifact", data)
@staticmethod
def encode_task(task: Task) -> str:
"""Encode Task object for SSE."""
data = task.model_dump(by_alias=True, exclude_none=True)
return A2AEventEncoder.encode_event("task", data)
@staticmethod
def encode_done() -> str:
"""Encode stream completion event."""
return "event: done\ndata: {}\n\n"
def create_status_event(
task_id: str,
state: TaskState,
context_id: Optional[str] = None,
final: bool = False,
) -> TaskStatusUpdateEvent:
"""
Create a TaskStatusUpdateEvent.
Args:
task_id: Task ID
state: New task state
context_id: Optional context ID
final: Whether this is the final event
Returns:
TaskStatusUpdateEvent
"""
return TaskStatusUpdateEvent(
task_id=task_id,
context_id=context_id,
status=TaskStatus(state=state),
final=final,
)
def create_artifact_event(
task_id: str,
content: str,
artifact_id: Optional[str] = None,
context_id: Optional[str] = None,
append: bool = False,
last_chunk: bool = False,
) -> TaskArtifactUpdateEvent:
"""
Create a TaskArtifactUpdateEvent.
Args:
task_id: Task ID
content: Artifact content
artifact_id: Optional artifact ID
context_id: Optional context ID
append: Whether to append to existing artifact
last_chunk: Whether this is the last chunk
Returns:
TaskArtifactUpdateEvent
"""
import uuid
art_id = artifact_id or f"art-{uuid.uuid4().hex[:12]}"
artifact = Artifact(
artifact_id=art_id,
parts=[TextPart(text=content)],
)
return TaskArtifactUpdateEvent(
task_id=task_id,
context_id=context_id,
artifact=artifact,
append=append,
last_chunk=last_chunk,
)
async def stream_agent_response(
agent,
user_input: str,
task_id: str,
context_id: Optional[str] = None,
) -> AsyncIterator[str]:
"""
Stream agent response as A2A events.
Args:
agent: PraisonAI Agent instance
user_input: User input text
task_id: Task ID
context_id: Optional context ID
Yields:
SSE-formatted event strings
"""
encoder = A2AEventEncoder()
# Emit working status
working_event = create_status_event(
task_id=task_id,
state=TaskState.WORKING,
context_id=context_id,
)
yield encoder.encode_task_status(working_event)
try:
# Get response from agent
response = agent.chat(user_input)
# Emit artifact with response
artifact_event = create_artifact_event(
task_id=task_id,
content=str(response),
context_id=context_id,
last_chunk=True,
)
yield encoder.encode_task_artifact(artifact_event)
# Emit completed status
completed_event = create_status_event(
task_id=task_id,
state=TaskState.COMPLETED,
context_id=context_id,
final=True,
)
yield encoder.encode_task_status(completed_event)
except Exception:
# Emit failed status
failed_event = create_status_event(
task_id=task_id,
state=TaskState.FAILED,
context_id=context_id,
final=True,
)
yield encoder.encode_task_status(failed_event)
# Emit done
yield encoder.encode_done()
@@ -0,0 +1,209 @@
"""
A2A Task Store
In-memory storage for A2A Task lifecycle management.
"""
import uuid
from datetime import datetime, timezone
from typing import Dict, List, Optional
from praisonaiagents.ui.a2a.types import (
Task,
TaskState,
TaskStatus,
Message,
Artifact,
)
class TaskStore:
"""
In-memory store for A2A Tasks.
Manages task lifecycle including creation, status updates,
artifact storage, and message history.
"""
def __init__(self):
"""Initialize empty task store."""
self._tasks: Dict[str, Task] = {}
def create_task(self, message: Message, context_id: Optional[str] = None) -> Task:
"""
Create a new task from an incoming message.
Args:
message: The initial message that triggers the task
context_id: Optional context ID (uses message's context_id if not provided)
Returns:
Newly created Task in SUBMITTED state
"""
task_id = f"task-{uuid.uuid4().hex[:12]}"
ctx_id = context_id or message.context_id
# Create initial status
status = TaskStatus(
state=TaskState.SUBMITTED,
timestamp=datetime.now(timezone.utc).isoformat(),
)
# Create task with initial message in history
task = Task(
id=task_id,
context_id=ctx_id,
status=status,
history=[message],
artifacts=None,
)
self._tasks[task_id] = task
return task
def get_task(self, task_id: str) -> Optional[Task]:
"""
Retrieve a task by its ID.
Args:
task_id: The task ID to look up
Returns:
Task if found, None otherwise
"""
return self._tasks.get(task_id)
def update_status(
self,
task_id: str,
state: TaskState,
message: Optional[Message] = None,
) -> Optional[Task]:
"""
Update the status of a task.
Args:
task_id: The task ID to update
state: New task state
message: Optional status message
Returns:
Updated Task if found, None otherwise
"""
task = self._tasks.get(task_id)
if not task:
return None
# Create new status
new_status = TaskStatus(
state=state,
message=message,
timestamp=datetime.now(timezone.utc).isoformat(),
)
# Update task (create new instance for immutability)
updated = Task(
id=task.id,
context_id=task.context_id,
status=new_status,
artifacts=task.artifacts,
history=task.history,
metadata=task.metadata,
)
self._tasks[task_id] = updated
return updated
def add_artifact(self, task_id: str, artifact: Artifact) -> Optional[Task]:
"""
Add an artifact to a task.
Args:
task_id: The task ID to update
artifact: Artifact to add
Returns:
Updated Task if found, None otherwise
"""
task = self._tasks.get(task_id)
if not task:
return None
# Get existing artifacts or create new list
artifacts = list(task.artifacts) if task.artifacts else []
artifacts.append(artifact)
# Update task
updated = Task(
id=task.id,
context_id=task.context_id,
status=task.status,
artifacts=artifacts,
history=task.history,
metadata=task.metadata,
)
self._tasks[task_id] = updated
return updated
def add_to_history(self, task_id: str, message: Message) -> Optional[Task]:
"""
Add a message to task history.
Args:
task_id: The task ID to update
message: Message to add to history
Returns:
Updated Task if found, None otherwise
"""
task = self._tasks.get(task_id)
if not task:
return None
# Get existing history or create new list
history = list(task.history) if task.history else []
history.append(message)
# Update task
updated = Task(
id=task.id,
context_id=task.context_id,
status=task.status,
artifacts=task.artifacts,
history=history,
metadata=task.metadata,
)
self._tasks[task_id] = updated
return updated
def list_tasks(self, context_id: Optional[str] = None) -> List[Task]:
"""
List all tasks, optionally filtered by context.
Args:
context_id: Optional context ID to filter by
Returns:
List of tasks
"""
if context_id:
return [t for t in self._tasks.values() if t.context_id == context_id]
return list(self._tasks.values())
def cancel_task(self, task_id: str) -> Optional[Task]:
"""
Cancel a task.
Args:
task_id: The task ID to cancel
Returns:
Updated Task if found, None otherwise
"""
return self.update_status(task_id, TaskState.CANCELLED)
def clear(self):
"""Clear all tasks from the store."""
self._tasks.clear()
@@ -0,0 +1,266 @@
"""
A2A Protocol Types
Pydantic models for the Agent2Agent (A2A) protocol.
Based on the A2A Protocol Specification v1.0.
"""
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, ConfigDict, Field
# =============================================================================
# Enums
# =============================================================================
class TaskState(str, Enum):
"""
Defines the possible lifecycle states of a Task.
Based on A2A Protocol Specification Section 4.1.3.
"""
SUBMITTED = "submitted"
WORKING = "working"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
INPUT_REQUIRED = "input_required"
AUTH_REQUIRED = "auth_required"
class Role(str, Enum):
"""
Defines the sender of a message in A2A protocol communication.
Based on A2A Protocol Specification Section 4.1.5.
"""
USER = "user"
AGENT = "agent"
# =============================================================================
# Part Types
# =============================================================================
class TextPart(BaseModel):
"""
A text content part.
Based on A2A Protocol Specification Section 4.1.6.
"""
text: str
metadata: Optional[Dict[str, Any]] = None
class FilePart(BaseModel):
"""
A file content part.
Can contain either a URI reference or inline bytes.
Based on A2A Protocol Specification Section 4.1.7.
"""
file_uri: Optional[str] = Field(default=None, alias="fileWithUri")
file_bytes: Optional[bytes] = Field(default=None, alias="fileWithBytes")
media_type: Optional[str] = Field(default=None, alias="mediaType")
name: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
model_config = ConfigDict(populate_by_name=True)
class DataPart(BaseModel):
"""
A structured data part (JSON blob).
Based on A2A Protocol Specification Section 4.1.8.
"""
data: Dict[str, Any]
metadata: Optional[Dict[str, Any]] = None
# Union type for all Part types
Part = Union[TextPart, FilePart, DataPart]
# =============================================================================
# Core Types
# =============================================================================
class Message(BaseModel):
"""
A communication turn between client and server.
Based on A2A Protocol Specification Section 4.1.4.
"""
model_config = ConfigDict(populate_by_name=True)
message_id: str = Field(alias="messageId")
role: Role
parts: List[Part]
context_id: Optional[str] = Field(default=None, alias="contextId")
task_id: Optional[str] = Field(default=None, alias="taskId")
metadata: Optional[Dict[str, Any]] = None
extensions: Optional[Dict[str, str]] = None
reference_task_ids: Optional[List[str]] = Field(default=None, alias="referenceTaskIds")
class TaskStatus(BaseModel):
"""
Container for the status of a task.
Based on A2A Protocol Specification Section 4.1.2.
"""
state: TaskState
message: Optional[Message] = None
timestamp: Optional[str] = None
class Artifact(BaseModel):
"""
An output generated by the agent as a result of a task.
Based on A2A Protocol Specification Section 4.1.9.
"""
model_config = ConfigDict(populate_by_name=True)
artifact_id: str = Field(alias="artifactId")
name: Optional[str] = None
description: Optional[str] = None
parts: List[Part]
metadata: Optional[Dict[str, Any]] = None
extensions: Optional[Dict[str, str]] = None
class Task(BaseModel):
"""
The core unit of action for A2A.
Based on A2A Protocol Specification Section 4.1.1.
"""
model_config = ConfigDict(populate_by_name=True)
id: str
context_id: Optional[str] = Field(default=None, alias="contextId")
status: TaskStatus
artifacts: Optional[List[Artifact]] = None
history: Optional[List[Message]] = None
metadata: Optional[Dict[str, Any]] = None
# =============================================================================
# Agent Card Types
# =============================================================================
class AgentSkill(BaseModel):
"""
Describes a skill/capability of an agent.
Based on A2A Protocol Specification Section 4.4.
"""
model_config = ConfigDict(populate_by_name=True)
id: str
name: str
description: str
tags: Optional[List[str]] = None
examples: Optional[List[str]] = None
input_modes: Optional[List[str]] = Field(default=None, alias="inputModes")
output_modes: Optional[List[str]] = Field(default=None, alias="outputModes")
class AgentCapabilities(BaseModel):
"""
Describes the capabilities of an agent.
Based on A2A Protocol Specification Section 4.4.
"""
model_config = ConfigDict(populate_by_name=True)
streaming: bool = False
push_notifications: bool = Field(default=False, alias="pushNotifications")
state_transition_history: bool = Field(default=False, alias="stateTransitionHistory")
class AgentCard(BaseModel):
"""
JSON metadata document describing an agent.
Published by an A2A Server for discovery.
Based on A2A Protocol Specification Section 8.
"""
model_config = ConfigDict(populate_by_name=True)
name: str
url: str
version: str
description: Optional[str] = None
capabilities: AgentCapabilities
skills: Optional[List[AgentSkill]] = None
default_input_modes: Optional[List[str]] = Field(default=None, alias="defaultInputModes")
default_output_modes: Optional[List[str]] = Field(default=None, alias="defaultOutputModes")
provider: Optional[Dict[str, str]] = None
documentation_url: Optional[str] = Field(default=None, alias="documentationUrl")
# =============================================================================
# Request/Response Types
# =============================================================================
class MessageConfig(BaseModel):
"""
Configuration for message sending.
Based on A2A Protocol Specification Section 3.2.
"""
model_config = ConfigDict(populate_by_name=True)
accept_content_types: Optional[List[str]] = Field(default=None, alias="acceptContentTypes")
blocking: Optional[bool] = None
history_length: Optional[int] = Field(default=None, alias="historyLength")
class SendMessageRequest(BaseModel):
"""
Request to send a message to an agent.
Based on A2A Protocol Specification Section 3.1.1.
"""
message: Message
configuration: Optional[MessageConfig] = None
metadata: Optional[Dict[str, Any]] = None
# =============================================================================
# Streaming Event Types
# =============================================================================
class TaskStatusUpdateEvent(BaseModel):
"""
Event sent by agent to notify client of task status change.
Based on A2A Protocol Specification Section 4.2.1.
"""
model_config = ConfigDict(populate_by_name=True)
task_id: str = Field(alias="taskId")
context_id: Optional[str] = Field(default=None, alias="contextId")
status: TaskStatus
final: bool = False
metadata: Optional[Dict[str, Any]] = None
class TaskArtifactUpdateEvent(BaseModel):
"""
Event sent when an artifact is generated.
Based on A2A Protocol Specification Section 4.2.2.
"""
model_config = ConfigDict(populate_by_name=True)
task_id: str = Field(alias="taskId")
context_id: Optional[str] = Field(default=None, alias="contextId")
artifact: Artifact
append: bool = False
last_chunk: bool = Field(default=False, alias="lastChunk")
metadata: Optional[Dict[str, Any]] = None
@@ -0,0 +1,126 @@
"""
A2UI (Agent-to-User Interface) Protocol Integration for PraisonAI Agents
This module provides A2UI protocol support, enabling PraisonAI Agents
to generate rich, interactive UIs declaratively via JSON.
A2UI is Google's open standard that allows agents to "speak UI" by sending
declarative JSON describing UI components, which clients then render natively.
Usage:
from praisonaiagents.ui.a2ui import (
# Message types
CreateSurfaceMessage,
UpdateComponentsMessage,
UpdateDataModelMessage,
DeleteSurfaceMessage,
# Component types
TextComponent,
ImageComponent,
ButtonComponent,
CardComponent,
RowComponent,
ColumnComponent,
# Extension helpers
create_a2ui_part,
is_a2ui_part,
)
"""
from praisonaiagents.ui.a2ui.types import (
# Data binding
PathBinding,
resolve_string_or_path,
# Action types
ActionContext,
Action,
# Children types
ChildrenTemplate,
# Component types
TextComponent,
ImageComponent,
IconComponent,
ButtonComponent,
CardComponent,
RowComponent,
ColumnComponent,
ListComponent,
TextFieldComponent,
CheckBoxComponent,
SliderComponent,
DateTimeInputComponent,
# Message types
CreateSurfaceMessage,
UpdateComponentsMessage,
UpdateDataModelMessage,
DeleteSurfaceMessage,
# Data part
A2UIDataPart,
)
from praisonaiagents.ui.a2ui.extension import (
A2UI_MIME_TYPE,
A2UI_EXTENSION_URI,
STANDARD_CATALOG_ID,
create_a2ui_part,
is_a2ui_part,
get_a2ui_agent_extension,
AgentExtension,
)
from praisonaiagents.ui.a2ui.surface import Surface
from praisonaiagents.ui.a2ui.agent import A2UIAgent
from praisonaiagents.ui.a2ui.templates import (
ChatTemplate,
ListTemplate,
FormTemplate,
DashboardTemplate,
)
__all__ = [
# Data binding
"PathBinding",
"resolve_string_or_path",
# Action types
"ActionContext",
"Action",
# Children types
"ChildrenTemplate",
# Component types
"TextComponent",
"ImageComponent",
"IconComponent",
"ButtonComponent",
"CardComponent",
"RowComponent",
"ColumnComponent",
"ListComponent",
"TextFieldComponent",
"CheckBoxComponent",
"SliderComponent",
"DateTimeInputComponent",
# Message types
"CreateSurfaceMessage",
"UpdateComponentsMessage",
"UpdateDataModelMessage",
"DeleteSurfaceMessage",
# Data part
"A2UIDataPart",
# Extension
"A2UI_MIME_TYPE",
"A2UI_EXTENSION_URI",
"STANDARD_CATALOG_ID",
"create_a2ui_part",
"is_a2ui_part",
"get_a2ui_agent_extension",
"AgentExtension",
# Surface builder
"Surface",
# Agent wrapper
"A2UIAgent",
# Templates
"ChatTemplate",
"ListTemplate",
"FormTemplate",
"DashboardTemplate",
]
@@ -0,0 +1,358 @@
"""
A2UI Agent Integration
Provides A2UIAgent wrapper for generating A2UI responses from PraisonAI agents.
"""
import uuid
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from praisonaiagents.ui.a2ui.surface import Surface
from praisonaiagents.ui.a2ui.types import (
PathBinding,
A2UIDataPart,
)
from praisonaiagents.ui.a2ui.extension import (
create_a2ui_part,
STANDARD_CATALOG_ID,
)
if TYPE_CHECKING:
from praisonaiagents import Agent
class A2UIAgent:
"""
A2UI wrapper for PraisonAI agents.
Provides methods to generate A2UI responses from agent interactions.
Example:
>>> from praisonaiagents import Agent
>>> from praisonaiagents.ui.a2ui import A2UIAgent
>>>
>>> agent = Agent(name="Assistant", role="Helper", goal="Help users")
>>> a2ui_agent = A2UIAgent(agent=agent)
>>>
>>> # Render a simple text response
>>> messages = a2ui_agent.render_text("Hello, World!")
>>>
>>> # Render a list of items
>>> messages = a2ui_agent.render_list(
... title="Results",
... items=[{"name": "Item 1"}, {"name": "Item 2"}]
... )
"""
def __init__(
self,
agent: "Agent",
surface_id: Optional[str] = None,
catalog_id: str = STANDARD_CATALOG_ID,
):
"""
Initialize A2UIAgent wrapper.
Args:
agent: The PraisonAI Agent to wrap
surface_id: Unique ID for the UI surface (auto-generated if not provided)
catalog_id: The component catalog to use
"""
self.agent = agent
self.surface_id = surface_id or f"surface-{uuid.uuid4().hex[:8]}"
self.catalog_id = catalog_id
self._surface: Optional[Surface] = None
def get_surface(self) -> Surface:
"""
Get the underlying Surface object.
Creates a new Surface if one doesn't exist.
Returns:
The Surface object
"""
if self._surface is None:
self._surface = Surface(
surface_id=self.surface_id,
catalog_id=self.catalog_id,
)
return self._surface
def _reset_surface(self) -> Surface:
"""Reset and return a new Surface."""
self._surface = Surface(
surface_id=self.surface_id,
catalog_id=self.catalog_id,
)
return self._surface
# =========================================================================
# Rendering methods
# =========================================================================
def render_text(
self,
text: str,
title: Optional[str] = None,
usage_hint: str = "body",
) -> List[Dict[str, Any]]:
"""
Render a simple text response as A2UI.
Args:
text: The text content to display
title: Optional title heading
usage_hint: Text style hint (h1, h2, body, caption)
Returns:
List of A2UI message dictionaries
"""
surface = self._reset_surface()
children = []
if title:
surface.text(id="title", text=title, usage_hint="h1")
children.append("title")
surface.text(id="content", text=text, usage_hint=usage_hint)
children.append("content")
# Add root column
surface.column(id="root", children=children)
return surface.to_messages()
def render_list(
self,
title: str,
items: List[Dict[str, Any]],
item_title_key: str = "title",
item_description_key: str = "description",
item_image_key: Optional[str] = None,
item_action: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
Render a list of items as A2UI cards.
Args:
title: List title
items: List of item dictionaries
item_title_key: Key for item title in each dict
item_description_key: Key for item description
item_image_key: Optional key for item image URL
item_action: Optional action name for item buttons
Returns:
List of A2UI message dictionaries
"""
surface = self._reset_surface()
# Title
surface.text(id="list-title", text=title, usage_hint="h1")
# Create card for each item
card_ids = []
for i, item in enumerate(items):
card_id = f"card-{i}"
content_id = f"card-content-{i}"
title_id = f"item-title-{i}"
desc_id = f"item-desc-{i}"
# Item title
surface.text(
id=title_id,
text=item.get(item_title_key, f"Item {i+1}"),
usage_hint="h3"
)
# Item description
surface.text(
id=desc_id,
text=item.get(item_description_key, ""),
usage_hint="body"
)
content_children = [title_id, desc_id]
# Optional action button
if item_action:
btn_id = f"btn-{i}"
btn_text_id = f"btn-text-{i}"
surface.text(id=btn_text_id, text="Select")
surface.button(
id=btn_id,
child=btn_text_id,
action_name=item_action,
action_context=[{"key": "index", "value": i}],
primary=True
)
content_children.append(btn_id)
# Card content column
surface.column(id=content_id, children=content_children)
# Card
surface.card(id=card_id, child=content_id)
card_ids.append(card_id)
# List column
surface.column(id="item-list", children=card_ids)
# Root column
surface.column(id="root", children=["list-title", "item-list"])
# Set data
surface.set_data("items", items)
return surface.to_messages()
def render_form(
self,
title: str,
fields: List[Dict[str, str]],
submit_action: str = "submit",
submit_label: str = "Submit",
) -> List[Dict[str, Any]]:
"""
Render a form as A2UI.
Args:
title: Form title
fields: List of field definitions with id, label, type
submit_action: Action name for submit button
submit_label: Label for submit button
Returns:
List of A2UI message dictionaries
"""
surface = self._reset_surface()
# Title
surface.text(id="form-title", text=title, usage_hint="h1")
# Fields
field_ids = []
form_data = {}
for field in fields:
field_id = field.get("id", f"field-{len(field_ids)}")
label = field.get("label", field_id)
field_type = field.get("type", "text")
surface.text_field(
id=field_id,
label=label,
text=PathBinding(path=f"/{field_id}"),
field_type=field_type,
)
field_ids.append(field_id)
form_data[field_id] = field.get("default", "")
# Submit button
surface.text(id="submit-text", text=submit_label)
# Build action context from fields
action_context = [
{"key": f["id"], "value": {"path": f"/{f['id']}"}}
for f in fields
]
surface.button(
id="submit-btn",
child="submit-text",
action_name=submit_action,
action_context=action_context,
primary=True,
)
field_ids.append("submit-btn")
# Form column
surface.column(id="form-fields", children=field_ids)
# Root column
surface.column(id="root", children=["form-title", "form-fields"])
# Set initial form data
for key, value in form_data.items():
surface.set_data(key, value)
return surface.to_messages()
# =========================================================================
# Agent interaction methods
# =========================================================================
async def chat_async(self, message: str) -> str:
"""
Send a message to the agent and get a response.
Args:
message: The message to send
Returns:
The agent's response text
"""
# Use the agent's chat method
if hasattr(self.agent, 'chat'):
response = self.agent.chat(message)
return response
elif hasattr(self.agent, 'run'):
response = self.agent.run(message)
return response
else:
raise NotImplementedError("Agent does not have chat or run method")
def chat(self, message: str) -> str:
"""
Send a message to the agent and get a response (sync).
Args:
message: The message to send
Returns:
The agent's response text
"""
if hasattr(self.agent, 'chat'):
return self.agent.chat(message)
elif hasattr(self.agent, 'run'):
return self.agent.run(message)
else:
raise NotImplementedError("Agent does not have chat or run method")
# =========================================================================
# Export methods
# =========================================================================
def to_messages(self) -> List[Dict[str, Any]]:
"""
Get the current surface as A2UI messages.
Returns:
List of A2UI message dictionaries
"""
return self.get_surface().to_messages()
def to_json(self, indent: Optional[int] = None) -> str:
"""
Get the current surface as JSON string.
Args:
indent: JSON indentation level
Returns:
JSON string of A2UI messages
"""
return self.get_surface().to_json(indent=indent)
def to_a2a_part(self) -> A2UIDataPart:
"""
Get the current surface as an A2A DataPart.
Returns:
A2UIDataPart with A2UI MIME type
"""
messages = self.to_messages()
# For A2A, we send messages as a list
return create_a2ui_part({"messages": messages})
@@ -0,0 +1,115 @@
"""
A2UI Extension for A2A Protocol
Provides helpers for integrating A2UI with the A2A (Agent-to-Agent) protocol.
"""
from dataclasses import dataclass
from typing import Any, Dict, Optional
from praisonaiagents.ui.a2ui.types import A2UIDataPart
# =============================================================================
# Constants
# =============================================================================
A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.9"
A2UI_MIME_TYPE = "application/json+a2ui"
STANDARD_CATALOG_ID = "https://raw.githubusercontent.com/google/A2UI/refs/heads/main/specification/0.9/json/standard_catalog_definition.json"
# =============================================================================
# Agent Extension Type
# =============================================================================
@dataclass
class AgentExtension:
"""
A2A Agent Extension configuration.
Describes an extension that an agent supports.
"""
uri: str
description: str = ""
params: Optional[Dict[str, Any]] = None
# =============================================================================
# Helper Functions
# =============================================================================
def create_a2ui_part(a2ui_data: Dict[str, Any]) -> A2UIDataPart:
"""
Create an A2UI data part for A2A integration.
Wraps A2UI data in an A2UIDataPart with the appropriate MIME type.
Args:
a2ui_data: The A2UI data dictionary (e.g., createSurface, updateComponents)
Returns:
An A2UIDataPart with the A2UI MIME type in metadata
Example:
>>> part = create_a2ui_part({"createSurface": {"surfaceId": "main", "catalogId": "..."}})
>>> part.metadata["mimeType"]
'application/json+a2ui'
"""
return A2UIDataPart(
data=a2ui_data,
metadata={"mimeType": A2UI_MIME_TYPE}
)
def is_a2ui_part(part: A2UIDataPart) -> bool:
"""
Check if a data part contains A2UI data.
Args:
part: The data part to check
Returns:
True if the part contains A2UI data (has A2UI MIME type), False otherwise
Example:
>>> part = create_a2ui_part({"createSurface": {...}})
>>> is_a2ui_part(part)
True
"""
if not isinstance(part, A2UIDataPart):
return False
if part.metadata is None:
return False
return part.metadata.get("mimeType") == A2UI_MIME_TYPE
def get_a2ui_agent_extension(
accepts_inline_custom_catalog: bool = False,
) -> AgentExtension:
"""
Create the A2UI AgentExtension configuration.
Returns an AgentExtension that can be added to an A2A Agent Card
to indicate A2UI support.
Args:
accepts_inline_custom_catalog: Whether the agent accepts inline custom catalogs
Returns:
The configured A2UI AgentExtension
Example:
>>> ext = get_a2ui_agent_extension()
>>> ext.uri
'https://a2ui.org/a2a-extension/a2ui/v0.9'
"""
params: Optional[Dict[str, Any]] = None
if accepts_inline_custom_catalog:
params = {"acceptsInlineCustomCatalog": True}
return AgentExtension(
uri=A2UI_EXTENSION_URI,
description="Provides agent-driven UI using the A2UI JSON format.",
params=params
)
@@ -0,0 +1,364 @@
"""
A2UI Surface Builder
Provides a fluent API for building A2UI surfaces with components and data.
"""
import json
from typing import Any, Dict, List, Optional
from praisonaiagents.ui.a2ui.extension import STANDARD_CATALOG_ID
from praisonaiagents.ui.a2ui.types import (
Component,
TextComponent,
ImageComponent,
ButtonComponent,
CardComponent,
RowComponent,
ColumnComponent,
ListComponent,
TextFieldComponent,
Action,
ActionContext,
PathBinding,
CreateSurfaceMessage,
UpdateComponentsMessage,
UpdateDataModelMessage,
StringOrPath,
ChildrenType,
)
class Surface:
"""
A2UI Surface builder for creating rich agent-generated UIs.
Provides a fluent API for adding components and data, then
generating the A2UI JSON messages.
Example:
>>> surface = Surface(surface_id="main")
>>> surface.text(id="title", text="Hello World", usage_hint="h1")
>>> surface.button(id="submit", child="submit-text", action_name="submit")
>>> surface.set_data("title", "Hello World")
>>> messages = surface.to_messages()
"""
def __init__(
self,
surface_id: str,
catalog_id: str = STANDARD_CATALOG_ID,
):
"""
Initialize a new Surface.
Args:
surface_id: Unique identifier for this surface
catalog_id: The component catalog to use (defaults to standard catalog)
"""
self.surface_id = surface_id
self.catalog_id = catalog_id
self.components: List[Component] = []
self.data: Dict[str, Any] = {}
def add(self, component: Component) -> "Surface":
"""
Add a component to the surface.
Args:
component: The component to add
Returns:
self for method chaining
"""
self.components.append(component)
return self
def set_data(self, key: str, value: Any) -> "Surface":
"""
Set a value in the data model.
Args:
key: The key in the data model
value: The value to set
Returns:
self for method chaining
"""
self.data[key] = value
return self
# =========================================================================
# Helper methods for common components
# =========================================================================
def text(
self,
id: str,
text: StringOrPath,
usage_hint: Optional[str] = None,
weight: Optional[float] = None,
) -> "Surface":
"""
Add a Text component.
Args:
id: Component ID
text: Text content or path binding
usage_hint: Style hint (h1, h2, h3, body, caption)
weight: Flex weight
Returns:
self for method chaining
"""
return self.add(TextComponent(
id=id,
text=text,
usage_hint=usage_hint,
weight=weight,
))
def image(
self,
id: str,
url: StringOrPath,
fit: Optional[str] = None,
usage_hint: Optional[str] = None,
weight: Optional[float] = None,
) -> "Surface":
"""
Add an Image component.
Args:
id: Component ID
url: Image URL or path binding
fit: Image fit (contain, cover, fill, none, scale-down)
usage_hint: Size hint (icon, avatar, smallFeature, etc.)
weight: Flex weight
Returns:
self for method chaining
"""
return self.add(ImageComponent(
id=id,
url=url,
fit=fit,
usage_hint=usage_hint,
weight=weight,
))
def button(
self,
id: str,
child: str,
action_name: Optional[str] = None,
action_context: Optional[List[Dict[str, Any]]] = None,
primary: bool = False,
weight: Optional[float] = None,
) -> "Surface":
"""
Add a Button component.
Args:
id: Component ID
child: Child component ID (usually a Text component)
action_name: Name of the action to trigger
action_context: List of {key, value} dicts for action context
primary: Whether this is a primary button
weight: Flex weight
Returns:
self for method chaining
"""
action = None
if action_name:
context = []
if action_context:
for ctx in action_context:
value = ctx.get("value")
if isinstance(value, dict) and "path" in value:
value = PathBinding(path=value["path"])
context.append(ActionContext(key=ctx["key"], value=value))
action = Action(name=action_name, context=context)
return self.add(ButtonComponent(
id=id,
child=child,
action=action,
primary=primary,
weight=weight,
))
def card(
self,
id: str,
child: str,
weight: Optional[float] = None,
) -> "Surface":
"""
Add a Card component.
Args:
id: Component ID
child: Child component ID
weight: Flex weight
Returns:
self for method chaining
"""
return self.add(CardComponent(
id=id,
child=child,
weight=weight,
))
def row(
self,
id: str,
children: ChildrenType,
weight: Optional[float] = None,
) -> "Surface":
"""
Add a Row (horizontal layout) component.
Args:
id: Component ID
children: List of child IDs or ChildrenTemplate
weight: Flex weight
Returns:
self for method chaining
"""
return self.add(RowComponent(
id=id,
children=children,
weight=weight,
))
def column(
self,
id: str,
children: ChildrenType,
weight: Optional[float] = None,
) -> "Surface":
"""
Add a Column (vertical layout) component.
Args:
id: Component ID
children: List of child IDs or ChildrenTemplate
weight: Flex weight
Returns:
self for method chaining
"""
return self.add(ColumnComponent(
id=id,
children=children,
weight=weight,
))
def list(
self,
id: str,
children: ChildrenType,
direction: str = "vertical",
weight: Optional[float] = None,
) -> "Surface":
"""
Add a List component.
Args:
id: Component ID
children: List of child IDs or ChildrenTemplate for dynamic lists
direction: "vertical" or "horizontal"
weight: Flex weight
Returns:
self for method chaining
"""
return self.add(ListComponent(
id=id,
children=children,
direction=direction,
weight=weight,
))
def text_field(
self,
id: str,
label: StringOrPath,
text: StringOrPath,
field_type: str = "text",
weight: Optional[float] = None,
) -> "Surface":
"""
Add a TextField input component.
Args:
id: Component ID
label: Field label
text: Text value or path binding
field_type: Input type (text, number, email, etc.)
weight: Flex weight
Returns:
self for method chaining
"""
return self.add(TextFieldComponent(
id=id,
label=label,
text=text,
field_type=field_type,
weight=weight,
))
# =========================================================================
# Message generation
# =========================================================================
def to_messages(self) -> List[Dict[str, Any]]:
"""
Generate A2UI messages for this surface.
Returns:
List of A2UI message dictionaries
"""
messages: List[Dict[str, Any]] = []
# 1. CreateSurface message
create_msg = CreateSurfaceMessage(
surface_id=self.surface_id,
catalog_id=self.catalog_id,
)
messages.append(create_msg.to_dict())
# 2. UpdateComponents message (if we have components)
if self.components:
update_msg = UpdateComponentsMessage(
surface_id=self.surface_id,
components=self.components,
)
messages.append(update_msg.to_dict())
# 3. UpdateDataModel message (if we have data)
if self.data:
data_msg = UpdateDataModelMessage(
surface_id=self.surface_id,
value=self.data,
)
messages.append(data_msg.to_dict())
return messages
def to_json(self, indent: Optional[int] = None) -> str:
"""
Generate A2UI messages as a JSON string.
Args:
indent: JSON indentation level (None for compact)
Returns:
JSON string of A2UI messages
"""
return json.dumps(self.to_messages(), indent=indent)
@@ -0,0 +1,17 @@
"""
A2UI Templates
Pre-built UI templates for common use cases.
"""
from praisonaiagents.ui.a2ui.templates.chat import ChatTemplate
from praisonaiagents.ui.a2ui.templates.list import ListTemplate
from praisonaiagents.ui.a2ui.templates.form import FormTemplate
from praisonaiagents.ui.a2ui.templates.dashboard import DashboardTemplate
__all__ = [
"ChatTemplate",
"ListTemplate",
"FormTemplate",
"DashboardTemplate",
]
@@ -0,0 +1,122 @@
"""
Chat Template for A2UI
Provides a conversational chat UI template.
"""
from typing import Any, Dict, List, Optional
from praisonaiagents.ui.a2ui.surface import Surface
from praisonaiagents.ui.a2ui.extension import STANDARD_CATALOG_ID
class ChatTemplate:
"""
Chat UI template for conversational interfaces.
Creates a chat-style UI with user and agent messages.
Example:
>>> template = ChatTemplate(surface_id="chat")
>>> template.add_user_message("Hello!")
>>> template.add_agent_message("Hi there! How can I help?")
>>> messages = template.to_messages()
"""
def __init__(
self,
surface_id: str = "chat",
catalog_id: str = STANDARD_CATALOG_ID,
title: Optional[str] = None,
):
"""
Initialize ChatTemplate.
Args:
surface_id: Unique ID for the surface
catalog_id: Component catalog to use
title: Optional title for the chat
"""
self.surface_id = surface_id
self.catalog_id = catalog_id
self.title = title
self.messages: List[Dict[str, Any]] = []
def add_user_message(self, content: str) -> "ChatTemplate":
"""
Add a user message.
Args:
content: Message content
Returns:
self for chaining
"""
self.messages.append({
"role": "user",
"content": content,
})
return self
def add_agent_message(self, content: str) -> "ChatTemplate":
"""
Add an agent message.
Args:
content: Message content
Returns:
self for chaining
"""
self.messages.append({
"role": "agent",
"content": content,
})
return self
def to_messages(self) -> List[Dict[str, Any]]:
"""
Generate A2UI messages for the chat.
Returns:
List of A2UI message dictionaries
"""
surface = Surface(
surface_id=self.surface_id,
catalog_id=self.catalog_id,
)
children = []
# Title if provided
if self.title:
surface.text(id="chat-title", text=self.title, usage_hint="h1")
children.append("chat-title")
# Message components
msg_ids = []
for i, msg in enumerate(self.messages):
msg_id = f"msg-{i}"
role = msg["role"]
content = msg["content"]
# Style based on role
if role == "user":
surface.text(id=msg_id, text=f"You: {content}", usage_hint="body")
else:
surface.text(id=msg_id, text=f"Agent: {content}", usage_hint="body")
msg_ids.append(msg_id)
# Messages column
if msg_ids:
surface.column(id="messages", children=msg_ids)
children.append("messages")
# Root column
surface.column(id="root", children=children)
# Set data
surface.set_data("messages", self.messages)
return surface.to_messages()
@@ -0,0 +1,128 @@
"""
Dashboard Template for A2UI
Provides a dashboard UI template with multiple panels.
"""
from typing import Any, Dict, List, Optional
from praisonaiagents.ui.a2ui.surface import Surface
from praisonaiagents.ui.a2ui.extension import STANDARD_CATALOG_ID
class DashboardTemplate:
"""
Dashboard UI template with multiple panels.
Creates a dashboard layout with cards for each panel.
Example:
>>> template = DashboardTemplate(surface_id="dashboard", title="My Dashboard")
>>> template.add_panel(id="stats", title="Statistics", content="100 items")
>>> template.add_panel(id="chart", title="Chart", content="[Chart here]")
>>> messages = template.to_messages()
"""
def __init__(
self,
surface_id: str = "dashboard",
title: str = "Dashboard",
catalog_id: str = STANDARD_CATALOG_ID,
columns: int = 2,
):
"""
Initialize DashboardTemplate.
Args:
surface_id: Unique ID for the surface
title: Dashboard title
catalog_id: Component catalog to use
columns: Number of columns in the grid
"""
self.surface_id = surface_id
self.title = title
self.catalog_id = catalog_id
self.columns = columns
self.panels: List[Dict[str, Any]] = []
def add_panel(
self,
id: str,
title: str,
content: str,
weight: Optional[float] = None,
) -> "DashboardTemplate":
"""
Add a panel to the dashboard.
Args:
id: Panel ID
title: Panel title
content: Panel content text
weight: Optional flex weight
Returns:
self for chaining
"""
self.panels.append({
"id": id,
"title": title,
"content": content,
"weight": weight,
})
return self
def to_messages(self) -> List[Dict[str, Any]]:
"""
Generate A2UI messages for the dashboard.
Returns:
List of A2UI message dictionaries
"""
surface = Surface(
surface_id=self.surface_id,
catalog_id=self.catalog_id,
)
# Title
surface.text(id="dashboard-title", text=self.title, usage_hint="h1")
# Create cards for each panel
card_ids = []
for panel in self.panels:
panel_id = panel["id"]
content_id = f"{panel_id}-content"
title_id = f"{panel_id}-title"
text_id = f"{panel_id}-text"
# Panel title
surface.text(id=title_id, text=panel["title"], usage_hint="h3")
# Panel content
surface.text(id=text_id, text=panel["content"], usage_hint="body")
# Panel content column
surface.column(id=content_id, children=[title_id, text_id])
# Panel card
surface.card(id=panel_id, child=content_id, weight=panel.get("weight"))
card_ids.append(panel_id)
# Arrange panels in rows based on columns setting
row_ids = []
for i in range(0, len(card_ids), self.columns):
row_cards = card_ids[i:i + self.columns]
row_id = f"row-{i // self.columns}"
surface.row(id=row_id, children=row_cards)
row_ids.append(row_id)
# Panels container
surface.column(id="panels", children=row_ids)
# Root column
surface.column(id="root", children=["dashboard-title", "panels"])
# Set data
surface.set_data("panels", self.panels)
return surface.to_messages()
@@ -0,0 +1,205 @@
"""
Form Template for A2UI
Provides a form UI template for data collection.
"""
from typing import Any, Dict, List
from praisonaiagents.ui.a2ui.surface import Surface
from praisonaiagents.ui.a2ui.types import PathBinding
from praisonaiagents.ui.a2ui.extension import STANDARD_CATALOG_ID
class FormTemplate:
"""
Form UI template for data collection.
Creates a form with various input fields and a submit button.
Example:
>>> template = FormTemplate(surface_id="form", title="Contact")
>>> template.add_text_field(id="name", label="Name")
>>> template.add_email_field(id="email", label="Email")
>>> template.set_submit_action("submit_form", "Send")
>>> messages = template.to_messages()
"""
def __init__(
self,
surface_id: str = "form",
title: str = "Form",
catalog_id: str = STANDARD_CATALOG_ID,
):
"""
Initialize FormTemplate.
Args:
surface_id: Unique ID for the surface
title: Form title
catalog_id: Component catalog to use
"""
self.surface_id = surface_id
self.title = title
self.catalog_id = catalog_id
self.fields: List[Dict[str, Any]] = []
self.submit_action = "submit"
self.submit_label = "Submit"
def add_text_field(
self,
id: str,
label: str,
default: str = "",
required: bool = False,
) -> "FormTemplate":
"""
Add a text input field.
Args:
id: Field ID
label: Field label
default: Default value
required: Whether field is required
Returns:
self for chaining
"""
self.fields.append({
"id": id,
"label": label,
"type": "text",
"default": default,
"required": required,
})
return self
def add_number_field(
self,
id: str,
label: str,
default: float = 0,
required: bool = False,
) -> "FormTemplate":
"""
Add a number input field.
Args:
id: Field ID
label: Field label
default: Default value
required: Whether field is required
Returns:
self for chaining
"""
self.fields.append({
"id": id,
"label": label,
"type": "number",
"default": default,
"required": required,
})
return self
def add_email_field(
self,
id: str,
label: str,
default: str = "",
required: bool = False,
) -> "FormTemplate":
"""
Add an email input field.
Args:
id: Field ID
label: Field label
default: Default value
required: Whether field is required
Returns:
self for chaining
"""
self.fields.append({
"id": id,
"label": label,
"type": "email",
"default": default,
"required": required,
})
return self
def set_submit_action(
self,
action: str,
label: str = "Submit",
) -> "FormTemplate":
"""
Set the submit button action.
Args:
action: Action name
label: Button label
Returns:
self for chaining
"""
self.submit_action = action
self.submit_label = label
return self
def to_messages(self) -> List[Dict[str, Any]]:
"""
Generate A2UI messages for the form.
Returns:
List of A2UI message dictionaries
"""
surface = Surface(
surface_id=self.surface_id,
catalog_id=self.catalog_id,
)
# Title
surface.text(id="form-title", text=self.title, usage_hint="h1")
# Fields
field_ids = []
for field in self.fields:
field_id = field["id"]
surface.text_field(
id=field_id,
label=field["label"],
text=PathBinding(path=f"/{field_id}"),
field_type=field["type"],
)
field_ids.append(field_id)
# Set default value
surface.set_data(field_id, field.get("default", ""))
# Submit button
surface.text(id="submit-text", text=self.submit_label)
action_context = [
{"key": f["id"], "value": {"path": f"/{f['id']}"}}
for f in self.fields
]
surface.button(
id="submit-btn",
child="submit-text",
action_name=self.submit_action,
action_context=action_context,
primary=True,
)
field_ids.append("submit-btn")
# Form fields column
surface.column(id="fields", children=field_ids)
# Root column
surface.column(id="root", children=["form-title", "fields"])
return surface.to_messages()
@@ -0,0 +1,147 @@
"""
List Template for A2UI
Provides a list UI template for displaying items.
"""
from typing import Any, Dict, List, Optional
from praisonaiagents.ui.a2ui.surface import Surface
from praisonaiagents.ui.a2ui.extension import STANDARD_CATALOG_ID
class ListTemplate:
"""
List UI template for displaying items.
Creates a list of cards with title, description, and optional image.
Example:
>>> template = ListTemplate(surface_id="list", title="Results")
>>> template.add_item(title="Item 1", description="First item")
>>> template.add_item(title="Item 2", description="Second item")
>>> messages = template.to_messages()
"""
def __init__(
self,
surface_id: str = "list",
title: str = "Items",
catalog_id: str = STANDARD_CATALOG_ID,
):
"""
Initialize ListTemplate.
Args:
surface_id: Unique ID for the surface
title: List title
catalog_id: Component catalog to use
"""
self.surface_id = surface_id
self.title = title
self.catalog_id = catalog_id
self.items: List[Dict[str, Any]] = []
def add_item(
self,
title: str,
description: str = "",
image_url: Optional[str] = None,
action: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> "ListTemplate":
"""
Add an item to the list.
Args:
title: Item title
description: Item description
image_url: Optional image URL
action: Optional action name for item button
metadata: Optional additional metadata
Returns:
self for chaining
"""
item = {
"title": title,
"description": description,
}
if image_url:
item["image_url"] = image_url
if action:
item["action"] = action
if metadata:
item.update(metadata)
self.items.append(item)
return self
def to_messages(self) -> List[Dict[str, Any]]:
"""
Generate A2UI messages for the list.
Returns:
List of A2UI message dictionaries
"""
surface = Surface(
surface_id=self.surface_id,
catalog_id=self.catalog_id,
)
# Title
surface.text(id="list-title", text=self.title, usage_hint="h1")
# Create cards for each item
card_ids = []
for i, item in enumerate(self.items):
card_id = f"card-{i}"
content_id = f"content-{i}"
title_id = f"title-{i}"
desc_id = f"desc-{i}"
# Item title
surface.text(id=title_id, text=item["title"], usage_hint="h3")
# Item description
surface.text(id=desc_id, text=item.get("description", ""), usage_hint="body")
content_children = [title_id, desc_id]
# Optional image
if item.get("image_url"):
img_id = f"img-{i}"
surface.image(id=img_id, url=item["image_url"], usage_hint="smallFeature")
content_children.insert(0, img_id)
# Optional action button
if item.get("action"):
btn_id = f"btn-{i}"
btn_text_id = f"btn-text-{i}"
surface.text(id=btn_text_id, text="Select")
surface.button(
id=btn_id,
child=btn_text_id,
action_name=item["action"],
action_context=[{"key": "index", "value": i}],
primary=True,
)
content_children.append(btn_id)
# Card content
surface.column(id=content_id, children=content_children)
# Card
surface.card(id=card_id, child=content_id)
card_ids.append(card_id)
# Items column
surface.column(id="items", children=card_ids)
# Root column
surface.column(id="root", children=["list-title", "items"])
# Set data
surface.set_data("items", self.items)
return surface.to_messages()
@@ -0,0 +1,523 @@
"""
A2UI Protocol Types
Pydantic-style classes for the A2UI (Agent-to-User Interface) protocol.
Based on the A2UI Specification v0.9.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
from enum import Enum
# =============================================================================
# Data Binding Types
# =============================================================================
@dataclass
class PathBinding:
"""
A reference to a value in the data model.
Used for data binding in A2UI components.
"""
path: str
def to_dict(self) -> Dict[str, str]:
return {"path": self.path}
# Type alias for values that can be literal or path-bound
StringOrPath = Union[str, PathBinding]
NumberOrPath = Union[int, float, PathBinding]
BooleanOrPath = Union[bool, PathBinding]
def resolve_string_or_path(value: StringOrPath) -> Union[str, Dict[str, str]]:
"""Resolve a StringOrPath to its serialized form."""
if isinstance(value, PathBinding):
return value.to_dict()
return value
def resolve_number_or_path(value: NumberOrPath) -> Union[int, float, Dict[str, str]]:
"""Resolve a NumberOrPath to its serialized form."""
if isinstance(value, PathBinding):
return value.to_dict()
return value
def resolve_boolean_or_path(value: BooleanOrPath) -> Union[bool, Dict[str, str]]:
"""Resolve a BooleanOrPath to its serialized form."""
if isinstance(value, PathBinding):
return value.to_dict()
return value
# =============================================================================
# Action Types
# =============================================================================
@dataclass
class ActionContext:
"""
A key-value pair for action context.
Used to pass data from UI to action handlers.
"""
key: str
value: Union[str, int, float, bool, PathBinding]
def to_dict(self) -> Dict[str, Any]:
if isinstance(self.value, PathBinding):
return {"key": self.key, "value": self.value.to_dict()}
return {"key": self.key, "value": self.value}
@dataclass
class Action:
"""
An action triggered by a UI component (e.g., button click).
"""
name: str
context: List[ActionContext] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"name": self.name,
"context": [c.to_dict() for c in self.context]
}
# =============================================================================
# Children Types
# =============================================================================
@dataclass
class ChildrenTemplate:
"""
A template for generating dynamic children from a data model list.
"""
component_id: str
path: str
def to_dict(self) -> Dict[str, str]:
return {
"componentId": self.component_id,
"path": self.path
}
# Type alias for children - either explicit list or template
ChildrenType = Union[List[str], ChildrenTemplate]
def resolve_children(children: ChildrenType) -> Union[List[str], Dict[str, str]]:
"""Resolve children to serialized form."""
if isinstance(children, ChildrenTemplate):
return children.to_dict()
return children
# =============================================================================
# Component Types
# =============================================================================
class UsageHint(str, Enum):
"""Text usage hints for styling."""
H1 = "h1"
H2 = "h2"
H3 = "h3"
H4 = "h4"
H5 = "h5"
CAPTION = "caption"
BODY = "body"
class ImageFit(str, Enum):
"""Image fit options."""
CONTAIN = "contain"
COVER = "cover"
FILL = "fill"
NONE = "none"
SCALE_DOWN = "scale-down"
class ImageUsageHint(str, Enum):
"""Image usage hints."""
ICON = "icon"
AVATAR = "avatar"
SMALL_FEATURE = "smallFeature"
MEDIUM_FEATURE = "mediumFeature"
LARGE_FEATURE = "largeFeature"
HEADER = "header"
@dataclass
class TextComponent:
"""Text display component."""
id: str
text: StringOrPath
usage_hint: Optional[str] = None
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "Text",
"text": resolve_string_or_path(self.text)
}
if self.usage_hint:
result["usageHint"] = self.usage_hint
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class ImageComponent:
"""Image display component."""
id: str
url: StringOrPath
fit: Optional[str] = None
usage_hint: Optional[str] = None
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "Image",
"url": resolve_string_or_path(self.url)
}
if self.fit:
result["fit"] = self.fit
if self.usage_hint:
result["usageHint"] = self.usage_hint
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class IconComponent:
"""Icon display component."""
id: str
name: StringOrPath
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "Icon",
"name": resolve_string_or_path(self.name)
}
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class ButtonComponent:
"""Button component with action."""
id: str
child: str
action: Optional[Action] = None
primary: bool = False
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "Button",
"child": self.child
}
if self.primary:
result["primary"] = True
if self.action:
result["action"] = self.action.to_dict()
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class CardComponent:
"""Card container component."""
id: str
child: str
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "Card",
"child": self.child
}
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class RowComponent:
"""Horizontal layout component."""
id: str
children: ChildrenType
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "Row",
"children": resolve_children(self.children)
}
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class ColumnComponent:
"""Vertical layout component."""
id: str
children: ChildrenType
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "Column",
"children": resolve_children(self.children)
}
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class ListComponent:
"""List component for repeating items."""
id: str
children: ChildrenType
direction: str = "vertical"
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "List",
"direction": self.direction,
"children": resolve_children(self.children)
}
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class TextFieldComponent:
"""Text input field component."""
id: str
label: StringOrPath
text: StringOrPath
field_type: str = "text"
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "TextField",
"label": resolve_string_or_path(self.label),
"text": resolve_string_or_path(self.text),
"type": self.field_type
}
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class CheckBoxComponent:
"""Checkbox input component."""
id: str
label: StringOrPath
checked: BooleanOrPath = False
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "CheckBox",
"label": resolve_string_or_path(self.label),
"checked": resolve_boolean_or_path(self.checked)
}
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class SliderComponent:
"""Slider input component."""
id: str
min_value: NumberOrPath
max_value: NumberOrPath
value: NumberOrPath
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "Slider",
"min": resolve_number_or_path(self.min_value),
"max": resolve_number_or_path(self.max_value),
"value": resolve_number_or_path(self.value)
}
if self.weight is not None:
result["weight"] = self.weight
return result
@dataclass
class DateTimeInputComponent:
"""Date/time input component."""
id: str
label: StringOrPath
value: StringOrPath
enable_date: bool = True
enable_time: bool = False
weight: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"id": self.id,
"component": "DateTimeInput",
"label": resolve_string_or_path(self.label),
"value": resolve_string_or_path(self.value),
"enableDate": self.enable_date,
"enableTime": self.enable_time
}
if self.weight is not None:
result["weight"] = self.weight
return result
# Type alias for any component
Component = Union[
TextComponent,
ImageComponent,
IconComponent,
ButtonComponent,
CardComponent,
RowComponent,
ColumnComponent,
ListComponent,
TextFieldComponent,
CheckBoxComponent,
SliderComponent,
DateTimeInputComponent,
]
# =============================================================================
# Message Types
# =============================================================================
@dataclass
class CreateSurfaceMessage:
"""
Message to create a new UI surface.
Signals the client to create a new surface and begin rendering it.
"""
surface_id: str
catalog_id: str
def to_dict(self) -> Dict[str, Any]:
return {
"createSurface": {
"surfaceId": self.surface_id,
"catalogId": self.catalog_id
}
}
@dataclass
class UpdateComponentsMessage:
"""
Message to update surface components.
Updates a surface with a new set of components.
"""
surface_id: str
components: List[Component]
def to_dict(self) -> Dict[str, Any]:
return {
"updateComponents": {
"surfaceId": self.surface_id,
"components": [c.to_dict() for c in self.components]
}
}
@dataclass
class UpdateDataModelMessage:
"""
Message to update the data model.
Updates the data model for an existing surface.
"""
surface_id: str
value: Any
path: str = "/"
op: str = "replace"
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"updateDataModel": {
"surfaceId": self.surface_id,
"path": self.path,
"op": self.op,
"value": self.value
}
}
return result
@dataclass
class DeleteSurfaceMessage:
"""
Message to delete a surface.
Signals the client to delete the surface.
"""
surface_id: str
def to_dict(self) -> Dict[str, Any]:
return {
"deleteSurface": {
"surfaceId": self.surface_id
}
}
# =============================================================================
# Data Part Type
# =============================================================================
@dataclass
class A2UIDataPart:
"""
A2UI data part for A2A integration.
Wraps A2UI data with the appropriate MIME type.
"""
data: Dict[str, Any]
metadata: Optional[Dict[str, Any]] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {"data": self.data}
if self.metadata:
result["metadata"] = self.metadata
return result
Loaded 100 of 259 files, more files were not shown because too many files have changed in this diff. Show more