Compare commits

...
6 Commits
Author SHA1 Message Date
Mervin Praison 08b21dabe3 Release v2.5.5 2025-12-26 08:16:54 +00:00
Mervin Praison 8c70d840df docs: add knowledge export/import CLI commands to README
- Add knowledge export/import row to features table with CLI link
- Document `praisonai knowledge export <file.json>` command
- Document `praisonai knowledge import <file.json>` command
2025-12-26 06:53:20 +00:00
Mervin Praison d8e944c1d3 Release v2.5.4 2025-12-26 06:49:33 +00:00
Mervin Praison 79b2b88f72 Release v2.5.3 2025-12-26 05:54:09 +00:00
Mervin Praison f5dfbd5670 fix: resolve Core Tests failures in knowledge/reranker integration
- Fix #1: MemoryConfig Pydantic validation - add _prepare_mem0_config() to strip
  PraisonAI-specific reranker fields (enabled, default_rerank) before passing to
  mem0, which only accepts provider and config fields

- Fix #2: LLM Reranker 'Chat' object not callable - check if client.chat is
  callable, not just if it exists. OpenAI client has chat as namespace object.
  Also handle max_completion_tokens vs max_tokens for different model versions.

- Fix #3: Chroma vector store metadata validation - ensure non-empty metadata
  dicts by adding placeholder key when metadata is empty or None

- Fix test file issues: add user_id parameter (required by mem0 API), handle
  dict return type from knowledge.search, use gpt-4o-mini for reranker test
  (gpt-5-nano returns empty responses with max_completion_tokens=10)
2025-12-26 05:52:46 +00:00
Mervin Praison aa6aa5933d fix: update test files to use gpt-5-nano for cheaper CI runs
- Update test_workflow_patterns.py to use gpt-5-nano instead of gpt-4o/gpt-4o-mini
- Also set OPENAI_MODEL_NAME=gpt-5-nano as CI secret
2025-12-26 05:26:40 +00:00
72 changed files with 9741 additions and 655 deletions

No files matched your search

+493 -162
View File
@@ -35,46 +35,108 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
---
> **Quick Paths:**
> - 🚀 **New here?** → [Quick Start](#-quick-start)
> - 📦 **Installing?** → [Installation](#-installation)
> - 💻 **Python SDK?** → [Python Examples](#-using-python-code)
> - 🎯 **CLI user?** → [CLI Reference](#-cli--no-code-interface)
> - 🔧 **Need config?** → [Configuration](#-configuration--integration)
> - 🤝 **Contributing?** → [Development](#-development)
---
## 📑 Table of Contents
### Getting Started
- [⚡ Performance](#-performance)
- [🚀 Quick Start](#-quick-start)
- [📦 Installation](#-installation)
- [Python SDK](#python-sdk)
- [JavaScript SDK](#javascript-sdk)
- [💻 Usage](#-usage)
- [Python Code Examples](#python-code-examples)
- [CLI / No-Code Interface](#cli--no-code-interface)
- [JavaScript Code Examples](#javascript-code-examples)
- [Environment Variables](#environment-variables)
### Core Concepts
- [✨ Key Features](#-key-features)
- [🌐 Supported Providers](#-supported-providers)
### Python SDK
- [📘 Python Examples](#-using-python-code)
- [1. Single Agent](#1-single-agent)
- [2. Multi Agents](#2-multi-agents)
- [3. Planning Mode](#3-agent-with-planning-mode)
- [4. Deep Research](#4-deep-research-agent)
- [5. Query Rewriter](#5-query-rewriter-agent)
- [6. Agent Memory](#6-agent-memory-zero-dependencies)
- [7. Rules & Instructions](#7-rules--instructions)
- [8. Auto-Generated Memories](#8-auto-generated-memories)
- [9. Agentic Workflows](#9-agentic-workflows)
- [10. Hooks](#10-hooks)
- [11. Shadow Git Checkpoints](#11-shadow-git-checkpoints)
- [12. Background Tasks](#12-background-tasks)
- [13. Policy Engine](#13-policy-engine)
- [14. Thinking Budgets](#14-thinking-budgets)
- [15. Output Styles](#15-output-styles)
- [16. Context Compaction](#16-context-compaction)
- [17. Field Names Reference](#17-field-names-reference-a-i-g-s)
- [18. Extended agents.yaml](#18-extended-agentsyaml-with-workflow-patterns)
- [19. MCP Protocol](#19-mcp-model-context-protocol)
- [20. A2A Protocol](#20-a2a-agent2agent-protocol)
- [🛠️ Custom Tools](#️-custom-tools)
### JavaScript SDK
- [💻 JavaScript Examples](#-using-javascript-code)
### CLI Reference
- [🎯 CLI Overview](#-cli--no-code-interface)
- [Auto Mode](#auto-mode)
- [Interactive Mode](#interactive-mode-cli)
- [Deep Research CLI](#deep-research-cli)
- [Planning Mode CLI](#planning-mode-cli)
- [Memory CLI](#memory-cli)
- [Workflow CLI](#workflow-cli)
- [Knowledge CLI](#knowledge-cli)
- [Session CLI](#session-cli)
- [Tools CLI](#tools-cli)
- [MCP Config CLI](#mcp-config-cli)
- [External Agents CLI](#external-agents-cli)
- [CLI Features Summary](#cli-features)
### Configuration
- [🔧 Configuration & Integration](#-configuration--integration)
- [Ollama Integration](#ollama-integration)
- [Groq Integration](#groq-integration)
- [100+ Models Support](#100-models-support)
- [📋 Workflows](#-workflows)
- [Workflow Patterns](#workflow-patterns)
- [YAML Configuration](#yaml-configuration)
- [Agents Playbook](#agents-playbook)
- [🧠 Memory & Context](#-memory--context)
- [📋 Agents Playbook](#-agents-playbook)
### Advanced Features
- [🔬 Advanced Features](#-advanced-features)
- [Research & Intelligence](#research--intelligence)
- [Memory & Caching](#memory--caching)
- [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)
### Architecture
- [📊 Process Types & Patterns](#-process-types--patterns)
- [Sequential Process](#sequential-process)
- [Hierarchical Process](#hierarchical-process)
- [Workflow Process](#workflow-process)
- [Agentic Patterns](#agentic-patterns)
### Data & Persistence
- [💾 Persistence (Databases)](#-persistence-databases)
- [📚 Knowledge & Retrieval (RAG)](#-knowledge--retrieval-rag)
- [🔧 Tools Table](#-tools-table)
### Learning & Community
- [🎓 Video Tutorials](#-video-tutorials)
- [⭐ Star History](#-star-history)
- [👥 Contributing](#-contributing)
- [🔧 Development](#-development)
- [❓ FAQ & Troubleshooting](#-faq--troubleshooting)
---
@@ -143,6 +205,30 @@ pip install praisonai
npm install praisonai
```
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `OPENAI_API_KEY` | Yes* | OpenAI API key |
| `ANTHROPIC_API_KEY` | No | Anthropic Claude API key |
| `GOOGLE_API_KEY` | No | Google Gemini API key |
| `GROQ_API_KEY` | No | Groq API key |
| `OPENAI_BASE_URL` | No | Custom API endpoint (for Ollama, Groq, etc.) |
> *At least one LLM provider API key is required.
```bash
# Set your API key
export OPENAI_API_KEY=your_key_here
# For Ollama (local models)
export OPENAI_BASE_URL=http://localhost:11434/v1
# For Groq
export OPENAI_API_KEY=your_groq_key
export OPENAI_BASE_URL=https://api.groq.com/openai/v1
```
---
## 💻 Usage
@@ -169,147 +255,263 @@ npm install praisonai
## ✨ Key Features
<details open>
<summary><strong>🤖 Core Agents</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| **🤖 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) |
| ↳ Agentic Routing (`route()`) | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/routing) |
| ↳ Parallel Execution (`parallel()`) | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| ↳ Loop over List/CSV (`loop()`) | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/repetitive) |
| ↳ Evaluator-Optimizer (`repeat()`) | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| ↳ Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ 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) |
| **💻 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) |
| ↳ Data Readers (PDF, DOCX, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-readers-api) |
| ↳ Vector Store Selection | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-vector-store-api) |
| ↳ Retrieval Strategies | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-retrieval-api) |
| ↳ Rerankers | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-reranker-api) |
| ↳ Index Types (Vector/Keyword/Hybrid) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-index-api) |
| ↳ Query Engines (Sub-Question, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-query-engine-api) |
| **🔬 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) |
| ↳ Middleware System | [Example](examples/middleware/basic_middleware.py) | [📖](https://docs.praison.ai/features/middleware) |
| ↳ Configurable Model | [Example](examples/middleware/configurable_model.py) | [📖](https://docs.praison.ai/features/configurable-model) |
| ↳ Rate Limiter | [Example](examples/middleware/rate_limiter.py) | [📖](https://docs.praison.ai/features/rate-limiter) |
| ↳ Injected Tool State | [Example](examples/middleware/injected_state.py) | [📖](https://docs.praison.ai/features/injected-state) |
| ↳ Shadow Git Checkpoints | [Example](#10-shadow-git-checkpoints) | [📖](https://docs.praison.ai/features/checkpoints) |
| ↳ Background Tasks | [Example](examples/background/basic_background.py) | [📖](https://docs.praison.ai/features/background-tasks) |
| ↳ Policy Engine | [Example](examples/policy/basic_policy.py) | [📖](https://docs.praison.ai/features/policy-engine) |
| ↳ Thinking Budgets | [Example](examples/thinking/basic_thinking.py) | [📖](https://docs.praison.ai/features/thinking-budgets) |
| ↳ Output Styles | [Example](examples/output/basic_output.py) | [📖](https://docs.praison.ai/features/output-styles) |
| ↳ Context Compaction | [Example](examples/compaction/basic_compaction.py) | [📖](https://docs.praison.ai/features/context-compaction) |
| **📊 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) |
| ↳ Profile/Benchmark | [Example](#profile-benchmark) | [📖](https://docs.praison.ai/docs/cli/profile) |
| ↳ Auto Mode | [Example](#auto-mode) | [📖](https://docs.praison.ai/docs/cli/auto) |
| ↳ Init | [Example](#init) | [📖](https://docs.praison.ai/docs/cli/init) |
| ↳ File Input | [Example](#file-input) | [📖](https://docs.praison.ai/docs/cli/file-input) |
| ↳ Final Agent | [Example](#final-agent) | [📖](https://docs.praison.ai/docs/cli/final-agent) |
| ↳ Max Tokens | [Example](#max-tokens) | [📖](https://docs.praison.ai/docs/cli/max-tokens) |
| **🧪 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) |
| 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) |
</details>
<details>
<summary><strong>🔄 Workflows</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
| Agentic Routing (`route()`) | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/routing) |
| Parallel Execution (`parallel()`) | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| Loop over List/CSV (`loop()`) | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/repetitive) |
| Evaluator-Optimizer (`repeat()`) | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| 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) |
</details>
<details>
<summary><strong>💻 Code & Development</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🧠 Memory & Knowledge</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
| Data Readers (PDF, DOCX, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-readers-api) |
| Vector Store Selection | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-vector-store-api) |
| Retrieval Strategies | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-retrieval-api) |
| Rerankers | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-reranker-api) |
| Index Types (Vector/Keyword/Hybrid) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-index-api) |
| Query Engines (Sub-Question, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-query-engine-api) |
</details>
<details>
<summary><strong>🔬 Research & Intelligence</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>📋 Planning & Execution</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>👥 Specialized Agents</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🎨 Media & Multimodal</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🔌 Protocols & Integration</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🛡️ Safety & Control</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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](#7-rules--instructions) | [📖](https://docs.praison.ai/features/rules) |
</details>
<details>
<summary><strong>⚙️ Advanced Features</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🛠️ Tools & Configuration</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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](#10-hooks) | [📖](https://docs.praison.ai/features/hooks) |
| Middleware System | [Example](examples/middleware/basic_middleware.py) | [📖](https://docs.praison.ai/features/middleware) |
| Configurable Model | [Example](examples/middleware/configurable_model.py) | [📖](https://docs.praison.ai/features/configurable-model) |
| Rate Limiter | [Example](examples/middleware/rate_limiter.py) | [📖](https://docs.praison.ai/features/rate-limiter) |
| Injected Tool State | [Example](examples/middleware/injected_state.py) | [📖](https://docs.praison.ai/features/injected-state) |
| Shadow Git Checkpoints | [Example](#11-shadow-git-checkpoints) | [📖](https://docs.praison.ai/features/checkpoints) |
| Background Tasks | [Example](examples/background/basic_background.py) | [📖](https://docs.praison.ai/features/background-tasks) |
| Policy Engine | [Example](examples/policy/basic_policy.py) | [📖](https://docs.praison.ai/features/policy-engine) |
| Thinking Budgets | [Example](examples/thinking/basic_thinking.py) | [📖](https://docs.praison.ai/features/thinking-budgets) |
| Output Styles | [Example](examples/output/basic_output.py) | [📖](https://docs.praison.ai/features/output-styles) |
| Context Compaction | [Example](examples/compaction/basic_compaction.py) | [📖](https://docs.praison.ai/features/context-compaction) |
</details>
<details>
<summary><strong>📊 Monitoring & Management</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🖥️ CLI Features</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
| Profile/Benchmark | [Example](#profile-benchmark) | [📖](https://docs.praison.ai/docs/cli/profile) |
| Auto Mode | [Example](#auto-mode) | [📖](https://docs.praison.ai/docs/cli/auto) |
| Init | [Example](#init) | [📖](https://docs.praison.ai/docs/cli/init) |
| File Input | [Example](#file-input) | [📖](https://docs.praison.ai/docs/cli/file-input) |
| Final Agent | [Example](#final-agent) | [📖](https://docs.praison.ai/docs/cli/final-agent) |
| Max Tokens | [Example](#max-tokens) | [📖](https://docs.praison.ai/docs/cli/max-tokens) |
</details>
<details>
<summary><strong>🧪 Evaluation</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🎯 Agent Skills</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>⏰ 24/7 Scheduling</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/docs/cli/scheduler) |
</details>
---
@@ -543,7 +745,7 @@ memory.handle_command("/memory save my_session")
| `memory="sqlite"` | Built-in | SQLite with indexing |
| `memory="chromadb"` | chromadb | Vector/semantic search |
### 6. Rules & Instructions
### 7. Rules & Instructions
PraisonAI auto-discovers instruction files from your project root and git root:
@@ -592,7 +794,7 @@ activation: always # always, glob, manual, ai_decision
- Follow PEP 8
```
### 7. Auto-Generated Memories
### 8. Auto-Generated Memories
```python
from praisonaiagents.memory import FileMemory, AutoMemory
@@ -607,7 +809,7 @@ memories = auto.process_interaction(
# Extracts: name="John", preference="Python for backend"
```
### 8. Agentic Workflows
### 9. Agentic Workflows
Create powerful multi-agent workflows with the `Workflow` class:
@@ -877,7 +1079,7 @@ callbacks:
on_workflow_complete: log_complete
```
### 9. Hooks
### 10. Hooks
Intercept and modify agent behavior at various lifecycle points:
@@ -916,7 +1118,7 @@ praisonai hooks validate hooks.json # Validate configuration
```
### 10. Shadow Git Checkpoints
### 11. Shadow Git Checkpoints
File-level undo/restore using shadow git:
@@ -953,7 +1155,7 @@ praisonai checkpoint restore abc123 # Restore to checkpoint
---
### 11. Background Tasks
### 12. Background Tasks
Run agent tasks asynchronously without blocking:
@@ -991,7 +1193,7 @@ praisonai background clear # Clear completed tasks
---
### 12. Policy Engine
### 13. Policy Engine
Control what agents can and cannot do with policy-based execution:
@@ -1032,7 +1234,7 @@ praisonai policy init # Create template
---
### 13. Thinking Budgets
### 14. Thinking Budgets
Configure token budgets for extended thinking:
@@ -1065,7 +1267,7 @@ praisonai thinking stats # Show usage statistics
---
### 14. Output Styles
### 15. Output Styles
Configure how agents format their responses:
@@ -1095,7 +1297,7 @@ praisonai output set concise # Set output style
---
### 15. Context Compaction
### 16. Context Compaction
Automatically manage context window size:
@@ -1130,7 +1332,7 @@ praisonai compaction stats # Show statistics
---
### 16. Field Names Reference (A-I-G-S)
### 17. Field Names Reference (A-I-G-S)
PraisonAI accepts both old (agents.yaml) and new (workflow.yaml) field names. Use the **canonical names** for new projects:
@@ -1164,7 +1366,7 @@ steps: # Define steps (not 'tasks')
> **Note:** The parser accepts both old and new names. Run `praisonai workflow validate <file.yaml>` to see suggestions for canonical names.
### 12. Extended agents.yaml with Workflow Patterns
### 18. Extended agents.yaml with Workflow Patterns
**Feature Parity:** Both `agents.yaml` and `workflow.yaml` now support the same features:
- All workflow patterns (route, parallel, loop, repeat)
@@ -1240,7 +1442,7 @@ Run with the same simple command:
praisonai agents.yaml
```
### 12. MCP (Model Context Protocol)
### 19. MCP (Model Context Protocol)
PraisonAI supports MCP Protocol Revision 2025-11-25 with multiple transports.
@@ -1316,7 +1518,7 @@ server.run() # stdio for Claude Desktop
| Security | Origin validation, DNS rebinding prevention |
| WebSocket | Auto-reconnect with exponential backoff |
### 13. A2A (Agent2Agent Protocol)
### 20. 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.
@@ -1364,6 +1566,22 @@ app.include_router(a2a.get_router())
PraisonAI provides a powerful CLI for no-code automation and quick prototyping.
### CLI Quick Reference
| Category | Commands |
|----------|----------|
| **Execution** | `praisonai`, `--auto`, `--interactive`, `--chat` |
| **Research** | `research`, `--query-rewrite`, `--deep-research` |
| **Planning** | `--planning`, `--planning-tools`, `--planning-reasoning` |
| **Workflows** | `workflow run`, `workflow list`, `workflow auto` |
| **Memory** | `memory show`, `memory add`, `memory search`, `memory clear` |
| **Knowledge** | `knowledge add`, `knowledge query`, `knowledge list` |
| **Sessions** | `session list`, `session resume`, `session delete` |
| **Tools** | `tools list`, `tools info`, `tools search` |
| **MCP** | `mcp list`, `mcp create`, `mcp enable` |
| **Development** | `commit`, `docs`, `checkpoint`, `hooks` |
| **Scheduling** | `schedule start`, `schedule list`, `schedule stop` |
### Auto Mode
```bash
pip install praisonai
@@ -3023,6 +3241,119 @@ cd src/praisonai && uv publish
---
## ❓ FAQ & Troubleshooting
<details>
<summary><strong>ModuleNotFoundError: No module named 'praisonaiagents'</strong></summary>
Install the package:
```bash
pip install praisonaiagents
```
</details>
<details>
<summary><strong>API key not found / Authentication error</strong></summary>
Ensure your API key is set:
```bash
export OPENAI_API_KEY=your_key_here
```
For other providers, see [Environment Variables](#environment-variables).
</details>
<details>
<summary><strong>How do I use a local model (Ollama)?</strong></summary>
```bash
# Start Ollama server first
ollama serve
# Set environment variable
export OPENAI_BASE_URL=http://localhost:11434/v1
```
See [Ollama Integration](#ollama-integration) for more details.
</details>
<details>
<summary><strong>How do I persist conversations to a database?</strong></summary>
Use the `db` parameter:
```python
from praisonaiagents import Agent, db
agent = Agent(
name="Assistant",
db=db(database_url="postgresql://localhost/mydb"),
session_id="my-session"
)
```
See [Persistence (Databases)](#-persistence-databases) for supported databases.
</details>
<details>
<summary><strong>How do I enable agent memory?</strong></summary>
```python
from praisonaiagents import Agent
agent = Agent(
name="Assistant",
memory=True, # Enables file-based memory (no extra deps!)
user_id="user123"
)
```
See [Agent Memory](#6-agent-memory-zero-dependencies) for more options.
</details>
<details>
<summary><strong>How do I run multiple agents together?</strong></summary>
```python
from praisonaiagents import Agent, PraisonAIAgents
agent1 = Agent(instructions="Research topics")
agent2 = Agent(instructions="Summarize findings")
agents = PraisonAIAgents(agents=[agent1, agent2])
agents.start()
```
See [Multi Agents](#2-multi-agents) for more examples.
</details>
<details>
<summary><strong>How do I use MCP tools?</strong></summary>
```python
from praisonaiagents import Agent, MCP
agent = Agent(
tools=MCP("npx @modelcontextprotocol/server-memory")
)
```
See [MCP Protocol](#19-mcp-model-context-protocol) for all transport options.
</details>
### Getting Help
- 📚 [Full Documentation](https://docs.praison.ai)
- 🐛 [Report Issues](https://github.com/MervinPraison/PraisonAI/issues)
- 💬 [Discussions](https://github.com/MervinPraison/PraisonAI/discussions)
---
<div align="center">
<p><strong>Made with ❤️ by the PraisonAI Team</strong></p>
<p>
+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.5.2" \
"praisonai>=2.5.5" \
"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.5.2" \
"praisonai>=2.5.5" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.2" \
"praisonai>=2.5.5" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -89,11 +89,9 @@ class AccuracyEvaluator(BaseEvaluator):
EvaluationScore with score and reasoning
"""
try:
from openai import OpenAI
import litellm
except ImportError:
raise ImportError("OpenAI package required for accuracy evaluation. Install with: pip install openai")
client = OpenAI()
raise ImportError("litellm package required for accuracy evaluation. Install with: pip install litellm")
prompt = f"""You are an expert evaluator. Compare the actual output against the expected output and provide a score from 1-10.
@@ -115,7 +113,8 @@ Respond in this exact format:
SCORE: [number 1-10]
REASONING: [brief explanation]"""
response = client.chat.completions.create(
# Use litellm for unified multi-provider support
response = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
@@ -99,11 +99,9 @@ class CriteriaEvaluator(BaseEvaluator):
CriteriaScore with score, passed status, and reasoning
"""
try:
from openai import OpenAI
import litellm
except ImportError:
raise ImportError("OpenAI package required for criteria evaluation. Install with: pip install openai")
client = OpenAI()
raise ImportError("litellm package required for criteria evaluation. Install with: pip install litellm")
if self.scoring_type == "binary":
prompt = f"""You are an expert evaluator. Evaluate the following output against the given criteria.
@@ -138,7 +136,8 @@ Respond in this exact format:
SCORE: [number 1-10]
REASONING: [brief explanation]"""
response = client.chat.completions.create(
# Use litellm for unified multi-provider support
response = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
@@ -91,19 +91,20 @@ class MongoDBMemory:
raise Exception(f"Failed to initialize MongoDB: {e}")
def _init_embedding_model(self):
"""Initialize embedding model from config."""
"""Initialize embedding model from config using litellm for unified provider support."""
try:
# Set up embedding model based on config
embedder_config = self.config.get("embedder", {})
if embedder_config.get("provider") == "openai":
import openai
self.embedding_model = openai.OpenAI()
self.embedding_model_name = embedder_config.get("config", {}).get("model", "text-embedding-3-small")
else:
# Default to OpenAI
import openai
self.embedding_model = openai.OpenAI()
self.embedding_model_name = "text-embedding-3-small"
provider = embedder_config.get("provider", "openai")
model_name = embedder_config.get("config", {}).get("model", "text-embedding-3-small")
# Store model name for later use
self.embedding_model_name = model_name
# Use litellm for embeddings - it handles all providers uniformly
# We'll use litellm.embedding() in _get_embeddings() instead of storing a client
self.embedding_provider = provider
self.embedding_model = None # Will use litellm.embedding() directly
except Exception as e:
raise Exception(f"Failed to initialize embedding model: {e}")
@@ -172,13 +173,16 @@ class MongoDBMemory:
logging.warning(f"Could not create vector search index: {e}")
def _get_embedding(self, text):
"""Get embedding for text."""
"""Get embedding for text using litellm for unified provider support."""
try:
response = self.embedding_model.embeddings.create(
input=text,
model=self.embedding_model_name
import litellm
# Use litellm.embedding() for unified multi-provider support
response = litellm.embedding(
model=self.embedding_model_name,
input=text
)
return response.data[0].embedding
return response.data[0]["embedding"]
except Exception as e:
logging.error(f"Error getting embedding: {e}")
return None
@@ -500,6 +504,33 @@ class Knowledge:
base_config["graph_store"] = self._config["graph_store"]
return base_config
def _prepare_mem0_config(self, config):
"""Prepare config for mem0 by removing PraisonAI-specific fields.
mem0's RerankerConfig only accepts 'provider' and 'config' fields.
PraisonAI adds 'enabled' and 'default_rerank' for internal use.
"""
mem0_config = config.copy()
# Strip PraisonAI-specific reranker fields that mem0 doesn't accept
if "reranker" in mem0_config:
reranker = mem0_config["reranker"]
if isinstance(reranker, dict):
# Keep only mem0-compatible fields: provider, config
mem0_reranker = {}
if "provider" in reranker:
mem0_reranker["provider"] = reranker["provider"]
if "config" in reranker:
mem0_reranker["config"] = reranker["config"]
# If no valid mem0 fields, remove reranker entirely
if mem0_reranker:
mem0_config["reranker"] = mem0_reranker
else:
del mem0_config["reranker"]
return mem0_config
@cached_property
def memory(self):
# Check if MongoDB provider is specified
@@ -511,19 +542,22 @@ class Knowledge:
# Fall back to default memory
pass
# Prepare config for mem0 (strip PraisonAI-specific fields)
mem0_config = self._prepare_mem0_config(self.config)
# Default Mem0 memory
try:
return CustomMemory.from_config(self.config)
return CustomMemory.from_config(mem0_config)
except (NotImplementedError, ValueError) as e:
if "list_collections" in str(e) or "Extra fields not allowed" in str(e):
# Keep only allowed fields
vector_store_config = {
"collection_name": self.config["vector_store"]["config"]["collection_name"],
"path": self.config["vector_store"]["config"]["path"]
"collection_name": mem0_config["vector_store"]["config"]["collection_name"],
"path": mem0_config["vector_store"]["config"]["path"]
}
self.config["vector_store"]["config"] = vector_store_config
mem0_config["vector_store"]["config"] = vector_store_config
from mem0 import Memory
return Memory.from_config(self.config)
return Memory.from_config(mem0_config)
raise
@cached_property
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.3.2"
version = "0.3.4"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
@@ -1189,16 +1189,16 @@ class TestWorkflowConfiguration:
"""Test that workflow planning setting is stored."""
def step1(ctx): return StepResult(output="Done")
workflow = Workflow(steps=[step1], planning=True, planning_llm="gpt-4o")
workflow = Workflow(steps=[step1], planning=True, planning_llm="gpt-5-nano")
assert workflow.planning == True
assert workflow.planning_llm == "gpt-4o"
assert workflow.planning_llm == "gpt-5-nano"
def test_workflow_default_llm(self):
"""Test default LLM setting."""
def step1(ctx): return StepResult(output="Done")
workflow = Workflow(steps=[step1], default_llm="gpt-4o-mini")
assert workflow.default_llm == "gpt-4o-mini"
workflow = Workflow(steps=[step1], default_llm="gpt-5-nano")
assert workflow.default_llm == "gpt-5-nano"
def test_workflow_default_agent_config(self):
"""Test default agent config."""
@@ -1393,10 +1393,10 @@ class TestPlanningAndReasoning:
workflow = Workflow(
steps=[lambda ctx: StepResult(output="test")],
planning=True,
planning_llm="gpt-4o"
planning_llm="gpt-5-nano"
)
assert workflow.planning == True
assert workflow.planning_llm == "gpt-4o"
assert workflow.planning_llm == "gpt-5-nano"
def test_workflow_reasoning_parameter_exists(self):
"""Test that Workflow accepts reasoning parameter."""
@@ -91,14 +91,11 @@ class TestAccuracyEvaluator:
def test_judge_output(self):
"""Test LLM judging of output."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 8\nREASONING: Good match"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "4"
@@ -115,14 +112,11 @@ class TestAccuracyEvaluator:
def test_run_returns_accuracy_result(self):
"""Test that run returns AccuracyResult."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 9\nREASONING: Excellent"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "4"
@@ -140,14 +134,11 @@ class TestAccuracyEvaluator:
def test_evaluate_output(self):
"""Test evaluate_output method for pre-generated outputs."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 10\nREASONING: Perfect"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "unused"
@@ -325,14 +316,11 @@ class TestCriteriaEvaluator:
def test_judge_output_numeric(self):
"""Test numeric scoring."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 8\nREASONING: Good"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "test"
@@ -350,14 +338,11 @@ class TestCriteriaEvaluator:
def test_judge_output_binary(self):
"""Test binary scoring."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "RESULT: PASS\nREASONING: Meets criteria"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "test"
@@ -374,14 +359,11 @@ class TestCriteriaEvaluator:
def test_on_fail_callback(self):
"""Test on_fail callback is called."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 3\nREASONING: Poor"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
on_fail_called = []
def on_fail(score):
@@ -403,14 +385,11 @@ class TestCriteriaEvaluator:
def test_evaluate_output_criteria(self):
"""Test evaluate_output method."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 9\nREASONING: Great"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "unused"
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.3.2"
version = "0.3.4"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -0,0 +1,206 @@
{
"version": "1.0.0",
"lastUpdated": "2024-12-26",
"implementationStatus": "Phase 3 Complete - Full Documentation",
"summary": {
"pythonCoreFeatures": 85,
"pythonWrapperFeatures": 45,
"typescriptFeatures": 48,
"gapCount": 25,
"priorityP0": 12,
"priorityP1": 15,
"priorityP2": 20,
"priorityP3": 20
},
"pythonCoreSDK": {
"path": "/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents",
"exports": [
"Agent", "ImageAgent", "ContextAgent", "DeepResearchAgent", "QueryRewriterAgent", "PromptExpanderAgent",
"PraisonAIAgents", "Agents", "Task", "Tools", "BaseTool", "ToolResult", "tool", "FunctionTool",
"ToolRegistry", "get_registry", "register_tool", "get_tool", "AutoAgents", "Session", "Memory",
"Knowledge", "Chunking", "db", "obs", "MCP", "Workflow", "Pipeline", "WorkflowStep", "WorkflowContext",
"StepResult", "Route", "Parallel", "Loop", "Repeat", "GuardrailResult", "LLMGuardrail",
"Handoff", "handoff", "FlowDisplay", "FastContext", "SkillManager", "SkillProperties",
"get_telemetry", "enable_telemetry", "MinimalTelemetry", "TelemetryCollector", "AGUI", "A2A"
],
"modules": {
"agent": ["Agent", "ImageAgent", "ContextAgent", "DeepResearchAgent", "QueryRewriterAgent", "PromptExpanderAgent", "RouterAgent", "Handoff"],
"agents": ["PraisonAIAgents", "AutoAgents"],
"task": ["Task"],
"tools": ["Tools", "BaseTool", "ToolResult", "tool", "FunctionTool", "ToolRegistry", "40+ built-in tools"],
"llm": ["LLM", "OpenAIClient", "ModelRouter", "RateLimiter", "ModelCapabilities"],
"memory": ["Memory", "AutoMemory", "FileMemory", "RulesManager", "DocsManager", "Hooks"],
"knowledge": ["Knowledge", "Chunking", "VectorStore", "QueryEngine", "Readers", "Rerankers"],
"db": ["DbAdapter", "AsyncDbAdapter", "DbMessage", "DbToolCall", "DbRun", "DbSpan", "DbTrace"],
"obs": ["ObservabilityAdapter", "Span", "Trace", "16 provider integrations"],
"mcp": ["MCP", "MCPServer", "MCPSession", "MCPTransport", "MCPWebSocket", "MCPSSE", "MCPHTTPStream"],
"workflows": ["Workflow", "Pipeline", "WorkflowStep", "WorkflowContext", "StepResult", "Route", "Parallel", "Loop", "Repeat", "YAMLWorkflowParser"],
"guardrails": ["GuardrailResult", "LLMGuardrail"],
"eval": ["AccuracyEval", "PerformanceEval", "ReliabilityEval", "CriteriaEval"],
"skills": ["SkillManager", "SkillProperties", "SkillLoader", "SkillDiscovery", "SkillValidator"],
"telemetry": ["MinimalTelemetry", "TelemetryCollector", "PerformanceMonitor", "TokenCollector"],
"context": ["FastContext", "FastContextResult"],
"session": ["Session"],
"ui": ["AGUI", "A2A"]
}
},
"pythonWrapper": {
"path": "/Users/praison/praisonai-package/src/praisonai/praisonai",
"exports": ["PraisonAI"],
"cliFeatures": [
"slash_commands", "autonomy_mode", "cost_tracker", "repo_map", "interactive_tui",
"git_integration", "sandbox_executor", "agent_scheduler", "agents", "at_mentions",
"auto_memory", "background", "checkpoints", "compare", "eval", "external_agents",
"fast_context", "flow_display", "guardrail", "handoff", "hooks", "image", "jobs",
"knowledge", "mcp", "message_queue", "metrics", "n8n", "observability", "persistence",
"profile", "profiling", "router", "session", "skills", "telemetry", "todo", "tools", "workflow"
]
},
"typescriptSDK": {
"path": "/Users/praison/praisonai-package/src/praisonai-ts/src",
"exports": ["Agent", "PraisonAIAgents", "Task", "Tool", "BaseTool", "Memory", "BaseMemoryStore", "Process", "BaseProcess", "OpenAIService", "Logger"],
"modules": {
"agent": ["Agent (simple)", "Agent (types/proxy)", "PraisonAIAgents"],
"task": ["Task"],
"tools": ["Tool interface", "BaseTool", "arxivTools", "mcpSse"],
"llm": ["OpenAIService (OpenAI only)"],
"memory": ["Memory interface", "BaseMemoryStore (in-memory only)"],
"process": ["Process interface", "BaseProcess"],
"utils": ["Logger"]
}
},
"gapMatrix": {
"P0_CoreParity": [
{"feature": "Multi-provider LLM support (LiteLLM equivalent)", "python": true, "typescript": false, "priority": "P0", "effort": "high"},
{"feature": "Streaming responses", "python": true, "typescript": "partial", "priority": "P0", "effort": "medium"},
{"feature": "Structured outputs (JSON schema)", "python": true, "typescript": false, "priority": "P0", "effort": "medium"},
{"feature": "Tool/function calling with validation", "python": true, "typescript": "partial", "priority": "P0", "effort": "medium"},
{"feature": "Session management (session_id, run_id)", "python": true, "typescript": false, "priority": "P0", "effort": "medium"},
{"feature": "Multi-agent orchestration patterns", "python": true, "typescript": "partial", "priority": "P0", "effort": "high"},
{"feature": "Agent handoff", "python": true, "typescript": false, "priority": "P0", "effort": "medium"},
{"feature": "Tool decorator (@tool)", "python": true, "typescript": false, "priority": "P0", "effort": "low"},
{"feature": "Tool registry", "python": true, "typescript": false, "priority": "P0", "effort": "medium"},
{"feature": "Async agent execution", "python": true, "typescript": "partial", "priority": "P0", "effort": "low"},
{"feature": "Error handling and retries", "python": true, "typescript": "partial", "priority": "P0", "effort": "medium"},
{"feature": "Verbose/debug logging", "python": true, "typescript": "partial", "priority": "P0", "effort": "low"}
],
"P1_Persistence": [
{"feature": "Database adapter protocol", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
{"feature": "SQLite adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "PostgreSQL adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "Redis adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "Message persistence", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "Run/trace persistence", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "Memory persistence (vector)", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
{"feature": "Knowledge base (RAG)", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
{"feature": "Chunking strategies", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "Vector store integration", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
{"feature": "Semantic search/recall", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
{"feature": "Working memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "File-based memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "Auto-memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
{"feature": "Session export/import", "python": true, "typescript": false, "priority": "P1", "effort": "medium"}
],
"P2_CLI": [
{"feature": "CLI entrypoint", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Slash commands", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Interactive TUI", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
{"feature": "Cost tracking", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Repo map", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
{"feature": "Git integration", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Sandbox executor", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Autonomy modes", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Agent scheduler", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
{"feature": "Background jobs", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Checkpoints", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Flow display", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Profiling", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Metrics", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "External agent integrations", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
{"feature": "N8N integration", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
{"feature": "MCP CLI support", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Skills CLI", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Eval CLI", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
{"feature": "Workflow CLI", "python": true, "typescript": false, "priority": "P2", "effort": "medium"}
],
"P3_Advanced": [
{"feature": "Workflows/Pipelines", "python": true, "typescript": false, "priority": "P3", "effort": "high"},
{"feature": "Route pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Parallel pattern", "python": true, "typescript": "partial", "priority": "P3", "effort": "low"},
{"feature": "Loop pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Repeat pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "YAML workflow parser", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Guardrails", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "LLM Guardrail", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Evaluation framework", "python": true, "typescript": false, "priority": "P3", "effort": "high"},
{"feature": "Accuracy eval", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Performance eval", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Reliability eval", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Criteria eval", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Observability protocol", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Telemetry", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Performance monitoring", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Token tracking", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Skills system", "python": true, "typescript": false, "priority": "P3", "effort": "high"},
{"feature": "Fast context", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
{"feature": "Image agent", "python": true, "typescript": false, "priority": "P3", "effort": "medium"}
]
},
"mastraCapabilities": {
"implemented": [
"Agent core", "Tool calling", "Streaming (partial)", "Multi-agent (partial)"
],
"missing": [
"Memory with semantic recall", "Working memory", "Thread management", "Resource scoping",
"Storage abstraction", "Vector store integration", "Embeddings", "Evaluation scorers",
"Workflow engine", "Step execution", "Branching logic", "Event-driven workflows",
"MCP integration", "Voice/TTS", "Observability hooks", "Request context",
"Tool validation", "Tool suspend/resume", "Provider options per tool"
]
},
"implementationPlan": {
"phase1_P0": {
"description": "Core agent parity - streaming, structured outputs, sessions, tool system",
"tasks": [
"Implement AI SDK provider abstraction (OpenAI, Anthropic, Google)",
"Add streaming response support with proper typing",
"Implement structured output with JSON schema/Zod",
"Add session management (session_id, run_id, trace_id)",
"Implement tool decorator and registry",
"Add agent handoff support",
"Improve error handling and retries"
]
},
"phase2_P1": {
"description": "Persistence layer - db adapters, memory, knowledge",
"tasks": [
"Implement DbAdapter protocol",
"Add SQLite adapter",
"Add message/run/trace persistence",
"Implement Memory with persistence",
"Add Knowledge base (RAG) support",
"Implement vector store integration"
]
},
"phase3_P2": {
"description": "CLI parity",
"tasks": [
"Create CLI entrypoint",
"Implement slash commands",
"Add cost tracking",
"Implement interactive mode"
]
},
"phase4_P3": {
"description": "Advanced features",
"tasks": [
"Implement Workflow engine",
"Add workflow patterns (Route, Parallel, Loop, Repeat)",
"Implement Guardrails",
"Add Evaluation framework",
"Implement Observability hooks",
"Add Skills system"
]
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ const config: Config.InitialOptions = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.main.test.ts', '**/*.spec.ts'],
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
coverageDirectory: 'coverage',
collectCoverageFrom: [
+4 -2
View File
@@ -1,9 +1,12 @@
{
"name": "praisonai",
"version": "1.0.19",
"version": "1.1.0",
"description": "PraisonAI TypeScript AI Agents Framework - Node.js, npm, and Javascript AI Agents Framework",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"praisonai-ts": "./dist/cli/index.js"
},
"scripts": {
"build": "tsc",
"test": "jest",
@@ -62,7 +65,6 @@
"fast-xml-parser": "^4.5.1",
"node-fetch": "^2.6.9",
"openai": "^4.81.0",
"praisonai": "^1.0.19",
"@modelcontextprotocol/sdk": "^1.12.1"
},
"optionalDependencies": {
+159
View File
@@ -0,0 +1,159 @@
/**
* Context Agent - Agent with enhanced context management
*/
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
import { Session } from '../session';
import { KnowledgeBase, type SearchResult } from '../knowledge/rag';
export interface ContextAgentConfig {
name?: string;
instructions: string;
llm?: string;
knowledgeBase?: KnowledgeBase;
contextWindow?: number;
maxContextTokens?: number;
verbose?: boolean;
}
export interface ContextMessage {
role: 'system' | 'user' | 'assistant';
content: string;
timestamp: number;
metadata?: Record<string, any>;
}
/**
* Context Agent - Agent with RAG and context management
*/
export class ContextAgent {
readonly name: string;
readonly instructions: string;
private provider: LLMProvider;
private knowledgeBase?: KnowledgeBase;
private contextWindow: number;
private maxContextTokens: number;
private messages: ContextMessage[] = [];
private verbose: boolean;
constructor(config: ContextAgentConfig) {
this.name = config.name || `ContextAgent_${Math.random().toString(36).substr(2, 9)}`;
this.instructions = config.instructions;
this.provider = createProvider(config.llm || 'gpt-4o-mini');
this.knowledgeBase = config.knowledgeBase;
this.contextWindow = config.contextWindow ?? 10;
this.maxContextTokens = config.maxContextTokens ?? 4000;
this.verbose = config.verbose ?? false;
}
/**
* Chat with context awareness
*/
async chat(prompt: string): Promise<{ text: string; context?: SearchResult[] }> {
// Add user message
this.messages.push({
role: 'user',
content: prompt,
timestamp: Date.now()
});
// Build context from knowledge base
let ragContext: SearchResult[] = [];
let contextString = '';
if (this.knowledgeBase) {
ragContext = await this.knowledgeBase.search(prompt);
if (ragContext.length > 0) {
contextString = this.knowledgeBase.buildContext(ragContext);
if (this.verbose) {
console.log(`[ContextAgent] Found ${ragContext.length} relevant documents`);
}
}
}
// Build messages array with context
const systemPrompt = contextString
? `${this.instructions}\n\nRelevant context:\n${contextString}`
: this.instructions;
const messages: Message[] = [
{ role: 'system', content: systemPrompt },
...this.getRecentMessages()
];
// Generate response
const result = await this.provider.generateText({ messages });
// Add assistant message
this.messages.push({
role: 'assistant',
content: result.text,
timestamp: Date.now()
});
return {
text: result.text,
context: ragContext.length > 0 ? ragContext : undefined
};
}
/**
* Get recent messages within context window
*/
private getRecentMessages(): Message[] {
const recent = this.messages.slice(-this.contextWindow);
return recent.map(m => ({
role: m.role,
content: m.content
}));
}
/**
* Add document to knowledge base
*/
async addDocument(id: string, content: string, metadata?: Record<string, any>): Promise<void> {
if (!this.knowledgeBase) {
throw new Error('No knowledge base configured');
}
await this.knowledgeBase.add({ id, content, metadata });
}
/**
* Search knowledge base
*/
async searchKnowledge(query: string, limit?: number): Promise<SearchResult[]> {
if (!this.knowledgeBase) {
return [];
}
return this.knowledgeBase.search(query, limit);
}
/**
* Clear conversation history
*/
clearHistory(): void {
this.messages = [];
}
/**
* Get conversation history
*/
getHistory(): ContextMessage[] {
return [...this.messages];
}
/**
* Set knowledge base
*/
setKnowledgeBase(kb: KnowledgeBase): void {
this.knowledgeBase = kb;
}
}
/**
* Create a context agent
*/
export function createContextAgent(config: ContextAgentConfig): ContextAgent {
return new ContextAgent(config);
}
+337
View File
@@ -0,0 +1,337 @@
/**
* Enhanced Agent - Agent with session management, provider abstraction, and tool support
*/
import { createProvider, type LLMProvider, type Message, type ToolCall, type GenerateTextResult } from '../llm/providers';
import { Session, Run, Trace, getSessionManager } from '../session';
import { FunctionTool, ToolRegistry, tool } from '../tools/decorator';
import { Logger } from '../utils/logger';
export interface EnhancedAgentConfig {
name?: string;
instructions: string;
llm?: string;
tools?: Array<FunctionTool | Function | { name: string; description?: string; parameters?: any; execute: Function }>;
session?: Session;
sessionId?: string;
verbose?: boolean;
stream?: boolean;
maxToolCalls?: number;
temperature?: number;
maxTokens?: number;
outputSchema?: any;
}
export interface ChatOptions {
stream?: boolean;
temperature?: number;
maxTokens?: number;
outputSchema?: any;
onToken?: (token: string) => void;
}
export interface ChatResult {
text: string;
structured?: any;
toolCalls?: ToolCall[];
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
runId: string;
sessionId: string;
}
/**
* Enhanced Agent with full feature support
*/
export class EnhancedAgent {
readonly name: string;
readonly instructions: string;
readonly sessionId: string;
private provider: LLMProvider;
private session: Session;
private toolRegistry: ToolRegistry;
private verbose: boolean;
private stream: boolean;
private maxToolCalls: number;
private temperature: number;
private maxTokens?: number;
private outputSchema?: any;
constructor(config: EnhancedAgentConfig) {
this.name = config.name || `Agent_${Math.random().toString(36).substr(2, 9)}`;
this.instructions = config.instructions;
this.verbose = config.verbose ?? true;
this.stream = config.stream ?? true;
this.maxToolCalls = config.maxToolCalls ?? 10;
this.temperature = config.temperature ?? 0.7;
this.maxTokens = config.maxTokens;
this.outputSchema = config.outputSchema;
// Initialize provider
this.provider = createProvider(config.llm || 'gpt-4o-mini');
// Initialize session
if (config.session) {
this.session = config.session;
} else if (config.sessionId) {
this.session = getSessionManager().getOrCreate(config.sessionId);
} else {
this.session = getSessionManager().create();
}
this.sessionId = this.session.id;
// Initialize tool registry
this.toolRegistry = new ToolRegistry();
if (config.tools) {
this.registerTools(config.tools);
}
Logger.setVerbose(this.verbose);
}
private registerTools(tools: EnhancedAgentConfig['tools']): void {
if (!tools) return;
for (const t of tools) {
if (t instanceof FunctionTool) {
this.toolRegistry.register(t);
} else if (typeof t === 'function') {
// Convert function to tool
const funcName = t.name || `function_${Math.random().toString(36).substr(2, 9)}`;
this.toolRegistry.register(tool({
name: funcName,
description: `Function ${funcName}`,
execute: t as any,
}));
} else if (typeof t === 'object' && 'execute' in t) {
// Tool config object
this.toolRegistry.register(tool({
name: t.name,
description: t.description,
parameters: t.parameters,
execute: t.execute as any,
}));
}
}
}
/**
* Add a tool to the agent
*/
addTool(t: FunctionTool | { name: string; description?: string; parameters?: any; execute: Function }): this {
if (t instanceof FunctionTool) {
this.toolRegistry.register(t);
} else {
this.toolRegistry.register(tool({
name: t.name,
description: t.description,
parameters: t.parameters,
execute: t.execute as any,
}));
}
return this;
}
/**
* Get all registered tools
*/
getTools(): FunctionTool[] {
return this.toolRegistry.list();
}
/**
* Chat with the agent
*/
async chat(prompt: string, options: ChatOptions = {}): Promise<ChatResult> {
const run = this.session.createRun().start();
const trace = run.createTrace({ name: 'chat' }).start();
try {
// Add user message to session
this.session.addMessage({ role: 'user', content: prompt });
// Build messages array
const messages: Message[] = [
{ role: 'system', content: this.instructions },
...this.session.getMessagesForLLM() as Message[],
];
// Get tool definitions
const tools = this.toolRegistry.list().length > 0
? this.toolRegistry.getDefinitions()
: undefined;
let result: GenerateTextResult;
let fullText = '';
let allToolCalls: ToolCall[] = [];
let iterations = 0;
// Tool calling loop
while (iterations < this.maxToolCalls) {
iterations++;
if (options.stream && !tools && options.onToken) {
// Streaming without tools
const stream = await this.provider.streamText({
messages,
temperature: options.temperature ?? this.temperature,
maxTokens: options.maxTokens ?? this.maxTokens,
onToken: options.onToken,
});
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
for await (const chunk of stream) {
if (chunk.text) fullText += chunk.text;
if (chunk.usage) usage = { ...usage, ...chunk.usage };
}
result = {
text: fullText,
usage,
finishReason: 'stop',
};
break;
} else if (options.outputSchema) {
// Structured output
const objResult = await this.provider.generateObject({
messages,
schema: options.outputSchema ?? this.outputSchema,
temperature: options.temperature ?? this.temperature,
maxTokens: options.maxTokens ?? this.maxTokens,
});
result = {
text: JSON.stringify(objResult.object),
usage: objResult.usage,
finishReason: 'stop',
};
fullText = result.text;
break;
} else {
// Regular generation with potential tool calls
result = await this.provider.generateText({
messages,
temperature: options.temperature ?? this.temperature,
maxTokens: options.maxTokens ?? this.maxTokens,
tools,
});
if (result.toolCalls && result.toolCalls.length > 0) {
allToolCalls.push(...result.toolCalls);
// Add assistant message with tool calls
messages.push({
role: 'assistant',
content: result.text || null,
tool_calls: result.toolCalls,
});
// Execute tool calls
for (const tc of result.toolCalls) {
const toolTrace = trace.createChild({ name: `tool:${tc.function.name}` }).start();
try {
const toolFn = this.toolRegistry.get(tc.function.name);
if (!toolFn) {
throw new Error(`Tool '${tc.function.name}' not found`);
}
const args = JSON.parse(tc.function.arguments);
const toolResult = await toolFn.execute(args, {
agentName: this.name,
sessionId: this.sessionId,
runId: run.id,
});
const resultStr = typeof toolResult === 'string'
? toolResult
: JSON.stringify(toolResult);
messages.push({
role: 'tool',
content: resultStr,
tool_call_id: tc.id,
});
toolTrace.complete({ result: resultStr });
} catch (error: any) {
messages.push({
role: 'tool',
content: `Error: ${error.message}`,
tool_call_id: tc.id,
});
toolTrace.fail(error);
}
}
// Continue loop to get final response
continue;
}
// No tool calls, we have our final response
fullText = result.text;
break;
}
}
// Add assistant message to session
this.session.addMessage({
role: 'assistant',
content: fullText,
tool_calls: allToolCalls.length > 0 ? allToolCalls : undefined,
});
trace.complete({ responseLength: fullText.length });
run.complete();
// Parse structured output if schema was provided
let structured: any;
if (options.outputSchema) {
try {
structured = JSON.parse(fullText);
} catch {
// Not valid JSON
}
}
return {
text: fullText,
structured,
toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined,
usage: result!.usage,
runId: run.id,
sessionId: this.sessionId,
};
} catch (error: any) {
trace.fail(error);
run.fail(error);
throw error;
}
}
/**
* Start method for compatibility
*/
async start(prompt: string, options?: ChatOptions): Promise<string> {
const result = await this.chat(prompt, options);
return result.text;
}
/**
* Get the session
*/
getSession(): Session {
return this.session;
}
/**
* Clear conversation history
*/
clearHistory(): void {
this.session.clearMessages();
}
}
+162
View File
@@ -0,0 +1,162 @@
/**
* Agent Handoff - Transfer conversations between agents
*/
import type { EnhancedAgent } from './enhanced';
export interface HandoffConfig {
agent: EnhancedAgent;
name?: string;
description?: string;
condition?: (context: HandoffContext) => boolean;
transformContext?: (messages: any[]) => any[];
}
export interface HandoffContext {
messages: any[];
lastMessage: string;
topic?: string;
metadata?: Record<string, any>;
}
export interface HandoffResult {
handedOffTo: string;
response: string;
context: HandoffContext;
}
/**
* Handoff class - Represents a handoff target
*/
export class Handoff {
readonly targetAgent: EnhancedAgent;
readonly name: string;
readonly description: string;
readonly condition?: (context: HandoffContext) => boolean;
readonly transformContext?: (messages: any[]) => any[];
constructor(config: HandoffConfig) {
this.targetAgent = config.agent;
this.name = config.name || `handoff_to_${config.agent.name}`;
this.description = config.description || `Transfer conversation to ${config.agent.name}`;
this.condition = config.condition;
this.transformContext = config.transformContext;
}
/**
* Check if handoff should be triggered
*/
shouldTrigger(context: HandoffContext): boolean {
if (this.condition) {
return this.condition(context);
}
return true;
}
/**
* Execute the handoff
*/
async execute(context: HandoffContext): Promise<HandoffResult> {
let messages = context.messages;
if (this.transformContext) {
messages = this.transformContext(messages);
}
// Transfer context to target agent
const response = await this.targetAgent.chat(context.lastMessage);
return {
handedOffTo: this.targetAgent.name,
response: response.text,
context: {
...context,
messages,
},
};
}
/**
* Get tool definition for LLM
*/
getToolDefinition(): { name: string; description: string; parameters: any } {
return {
name: this.name,
description: this.description,
parameters: {
type: 'object',
properties: {
reason: {
type: 'string',
description: 'Reason for the handoff',
},
},
required: [],
},
};
}
}
/**
* Create a handoff configuration
*/
export function handoff(config: HandoffConfig): Handoff {
return new Handoff(config);
}
/**
* Handoff filters - Common condition functions
*/
export const handoffFilters = {
/**
* Trigger handoff based on topic keywords
*/
topic: (keywords: string | string[]) => {
const keywordList = Array.isArray(keywords) ? keywords : [keywords];
return (context: HandoffContext): boolean => {
const lastMessage = context.lastMessage.toLowerCase();
return keywordList.some(kw => lastMessage.includes(kw.toLowerCase()));
};
},
/**
* Trigger handoff based on metadata
*/
metadata: (key: string, value: any) => {
return (context: HandoffContext): boolean => {
return context.metadata?.[key] === value;
};
},
/**
* Always trigger
*/
always: () => {
return (): boolean => true;
},
/**
* Never trigger (manual only)
*/
never: () => {
return (): boolean => false;
},
/**
* Combine multiple conditions with AND
*/
and: (...conditions: Array<(context: HandoffContext) => boolean>) => {
return (context: HandoffContext): boolean => {
return conditions.every(cond => cond(context));
};
},
/**
* Combine multiple conditions with OR
*/
or: (...conditions: Array<(context: HandoffContext) => boolean>) => {
return (context: HandoffContext): boolean => {
return conditions.some(cond => cond(context));
};
},
};
+146
View File
@@ -0,0 +1,146 @@
/**
* Router Agent - Route requests to specialized agents
*/
import type { EnhancedAgent } from './enhanced';
export interface RouteConfig {
agent: EnhancedAgent;
condition: (input: string, context?: RouteContext) => boolean | Promise<boolean>;
priority?: number;
}
export interface RouteContext {
history?: string[];
metadata?: Record<string, any>;
}
export interface RouterConfig {
name?: string;
routes: RouteConfig[];
defaultAgent?: EnhancedAgent;
verbose?: boolean;
}
/**
* Router Agent - Routes requests to the most appropriate agent
*/
export class RouterAgent {
readonly name: string;
private routes: RouteConfig[];
private defaultAgent?: EnhancedAgent;
private verbose: boolean;
constructor(config: RouterConfig) {
this.name = config.name || 'Router';
this.routes = config.routes.sort((a, b) => (b.priority || 0) - (a.priority || 0));
this.defaultAgent = config.defaultAgent;
this.verbose = config.verbose ?? false;
}
/**
* Route a request to the appropriate agent
*/
async route(input: string, context?: RouteContext): Promise<{ agent: EnhancedAgent; response: string } | null> {
for (const route of this.routes) {
const matches = await route.condition(input, context);
if (matches) {
if (this.verbose) {
console.log(`[Router] Routing to: ${route.agent.name}`);
}
const response = await route.agent.chat(input);
return { agent: route.agent, response: response.text };
}
}
if (this.defaultAgent) {
if (this.verbose) {
console.log(`[Router] Using default agent: ${this.defaultAgent.name}`);
}
const response = await this.defaultAgent.chat(input);
return { agent: this.defaultAgent, response: response.text };
}
return null;
}
/**
* Add a route
*/
addRoute(config: RouteConfig): this {
this.routes.push(config);
this.routes.sort((a, b) => (b.priority || 0) - (a.priority || 0));
return this;
}
/**
* Get all routes
*/
getRoutes(): RouteConfig[] {
return [...this.routes];
}
}
/**
* Route condition helpers
*/
export const routeConditions = {
/**
* Match by keywords
*/
keywords: (keywords: string | string[]) => {
const keywordList = Array.isArray(keywords) ? keywords : [keywords];
return (input: string): boolean => {
const lower = input.toLowerCase();
return keywordList.some(kw => lower.includes(kw.toLowerCase()));
};
},
/**
* Match by regex
*/
pattern: (regex: RegExp) => {
return (input: string): boolean => regex.test(input);
},
/**
* Match by metadata
*/
metadata: (key: string, value: any) => {
return (_input: string, context?: RouteContext): boolean => {
return context?.metadata?.[key] === value;
};
},
/**
* Always match (for default routes)
*/
always: () => {
return (): boolean => true;
},
/**
* Combine conditions with AND
*/
and: (...conditions: Array<(input: string, context?: RouteContext) => boolean>) => {
return (input: string, context?: RouteContext): boolean => {
return conditions.every(c => c(input, context));
};
},
/**
* Combine conditions with OR
*/
or: (...conditions: Array<(input: string, context?: RouteContext) => boolean>) => {
return (input: string, context?: RouteContext): boolean => {
return conditions.some(c => c(input, context));
};
}
};
/**
* Create a router agent
*/
export function createRouter(config: RouterConfig): RouterAgent {
return new RouterAgent(config);
}
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* PraisonAI TypeScript CLI
*/
import { createProvider, isProviderAvailable, getAvailableProviders } from '../llm/providers';
import { Session } from '../session';
import { tool, ToolRegistry } from '../tools/decorator';
interface CLIOptions {
model?: string;
stream?: boolean;
verbose?: boolean;
sessionId?: string;
}
async function chat(prompt: string, options: CLIOptions = {}): Promise<string> {
const model = options.model || process.env.PRAISONAI_MODEL || 'openai/gpt-4o-mini';
const provider = createProvider(model);
const session = new Session({ id: options.sessionId });
session.addMessage({ role: 'user', content: prompt });
if (options.stream) {
let fullText = '';
const stream = await provider.streamText({
messages: [{ role: 'user', content: prompt }],
onToken: (token) => {
process.stdout.write(token);
fullText += token;
}
});
for await (const chunk of stream) {
// Stream is consumed by onToken
}
console.log(); // New line after streaming
return fullText;
} else {
const result = await provider.generateText({
messages: [{ role: 'user', content: prompt }]
});
console.log(result.text);
return result.text;
}
}
async function listProviders(): Promise<void> {
console.log('Available Providers:');
const available = getAvailableProviders();
const providers = ['openai', 'anthropic', 'google'];
for (const p of providers) {
const status = available.includes(p) ? '✅' : '❌';
console.log(` ${status} ${p}`);
}
}
async function version(): Promise<void> {
const pkg = require('../../package.json');
console.log(`praisonai v${pkg.version}`);
}
async function help(): Promise<void> {
console.log(`
PraisonAI TypeScript CLI
Usage:
praisonai-ts chat <prompt> Chat with an AI agent
praisonai-ts providers List available providers
praisonai-ts version Show version
praisonai-ts help Show this help
Options:
--model, -m <model> Model to use (e.g., openai/gpt-4o-mini)
--stream, -s Enable streaming output
--verbose, -v Verbose output
--session <id> Session ID for conversation continuity
Environment Variables:
OPENAI_API_KEY OpenAI API key
ANTHROPIC_API_KEY Anthropic API key
GOOGLE_API_KEY Google API key
PRAISONAI_MODEL Default model
Examples:
praisonai-ts chat "Hello, how are you?"
praisonai-ts chat "Write a poem" --stream
praisonai-ts chat "Explain AI" -m anthropic/claude-sonnet-4-20250514
praisonai-ts providers
`);
}
function parseArgs(args: string[]): { command: string; prompt: string; options: CLIOptions } {
const options: CLIOptions = {};
let command = 'help';
let prompt = '';
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg === '--model' || arg === '-m') {
options.model = args[++i];
} else if (arg === '--stream' || arg === '-s') {
options.stream = true;
} else if (arg === '--verbose' || arg === '-v') {
options.verbose = true;
} else if (arg === '--session') {
options.sessionId = args[++i];
} else if (!arg.startsWith('-')) {
if (!command || command === 'help') {
command = arg;
} else {
prompt = arg;
}
}
i++;
}
return { command, prompt, options };
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
const { command, prompt, options } = parseArgs(args);
try {
switch (command) {
case 'chat':
if (!prompt) {
console.error('Error: Please provide a prompt');
process.exit(1);
}
await chat(prompt, options);
break;
case 'providers':
await listProviders();
break;
case 'version':
await version();
break;
case 'help':
default:
await help();
break;
}
} catch (error: any) {
console.error('Error:', error.message);
if (options.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
// Export for programmatic use
export { chat, listProviders, version, help, parseArgs };
// Run CLI if executed directly
if (require.main === module) {
main();
}
+207
View File
@@ -0,0 +1,207 @@
/**
* Evaluation Framework - Accuracy, Performance, and Reliability evaluation
*/
export interface EvalResult {
passed: boolean;
score: number;
message?: string;
details?: Record<string, any>;
duration: number;
}
export interface AccuracyEvalConfig {
input: string;
expectedOutput: string;
actualOutput: string;
threshold?: number;
}
export interface PerformanceEvalConfig {
func: () => Promise<any>;
iterations?: number;
warmupRuns?: number;
}
export interface PerformanceResult extends EvalResult {
avgTime: number;
minTime: number;
maxTime: number;
p95Time: number;
times: number[];
}
export interface ReliabilityEvalConfig {
expectedToolCalls: string[];
actualToolCalls: string[];
}
/**
* Accuracy Evaluation - Compare actual output to expected
*/
export async function accuracyEval(config: AccuracyEvalConfig): Promise<EvalResult> {
const start = Date.now();
const threshold = config.threshold ?? 0.8;
const similarity = calculateSimilarity(config.expectedOutput, config.actualOutput);
const passed = similarity >= threshold;
return {
passed,
score: similarity,
message: passed ? 'Output matches expected' : 'Output does not match expected',
details: {
expected: config.expectedOutput,
actual: config.actualOutput,
threshold
},
duration: Date.now() - start
};
}
/**
* Performance Evaluation - Measure execution time
*/
export async function performanceEval(config: PerformanceEvalConfig): Promise<PerformanceResult> {
const iterations = config.iterations ?? 10;
const warmupRuns = config.warmupRuns ?? 2;
const times: number[] = [];
// Warmup runs
for (let i = 0; i < warmupRuns; i++) {
await config.func();
}
// Actual runs
const start = Date.now();
for (let i = 0; i < iterations; i++) {
const runStart = Date.now();
await config.func();
times.push(Date.now() - runStart);
}
const sortedTimes = [...times].sort((a, b) => a - b);
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = sortedTimes[0];
const maxTime = sortedTimes[sortedTimes.length - 1];
const p95Index = Math.floor(sortedTimes.length * 0.95);
const p95Time = sortedTimes[p95Index] || maxTime;
return {
passed: true,
score: 1,
avgTime,
minTime,
maxTime,
p95Time,
times,
duration: Date.now() - start,
details: {
iterations,
warmupRuns
}
};
}
/**
* Reliability Evaluation - Check tool call accuracy
*/
export async function reliabilityEval(config: ReliabilityEvalConfig): Promise<EvalResult> {
const start = Date.now();
const expected = new Set(config.expectedToolCalls);
const actual = new Set(config.actualToolCalls);
const matched = config.expectedToolCalls.filter(t => actual.has(t));
const missing = config.expectedToolCalls.filter(t => !actual.has(t));
const extra = config.actualToolCalls.filter(t => !expected.has(t));
const score = expected.size > 0 ? matched.length / expected.size : 1;
const passed = missing.length === 0;
return {
passed,
score,
message: passed ? 'All expected tool calls made' : `Missing tool calls: ${missing.join(', ')}`,
details: {
matched,
missing,
extra,
expected: config.expectedToolCalls,
actual: config.actualToolCalls
},
duration: Date.now() - start
};
}
/**
* Calculate text similarity (simple Jaccard similarity)
*/
function calculateSimilarity(a: string, b: string): number {
const wordsA = new Set(a.toLowerCase().split(/\s+/));
const wordsB = new Set(b.toLowerCase().split(/\s+/));
const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
const union = new Set([...wordsA, ...wordsB]);
return union.size > 0 ? intersection.size / union.size : 0;
}
/**
* Eval Suite - Run multiple evaluations
*/
export class EvalSuite {
private results: Map<string, EvalResult> = new Map();
async runAccuracy(name: string, config: AccuracyEvalConfig): Promise<EvalResult> {
const result = await accuracyEval(config);
this.results.set(name, result);
return result;
}
async runPerformance(name: string, config: PerformanceEvalConfig): Promise<PerformanceResult> {
const result = await performanceEval(config);
this.results.set(name, result);
return result;
}
async runReliability(name: string, config: ReliabilityEvalConfig): Promise<EvalResult> {
const result = await reliabilityEval(config);
this.results.set(name, result);
return result;
}
getResults(): Map<string, EvalResult> {
return new Map(this.results);
}
getSummary(): { total: number; passed: number; failed: number; avgScore: number } {
const results = Array.from(this.results.values());
const passed = results.filter(r => r.passed).length;
const avgScore = results.length > 0
? results.reduce((a, b) => a + b.score, 0) / results.length
: 0;
return {
total: results.length,
passed,
failed: results.length - passed,
avgScore
};
}
printSummary(): void {
const summary = this.getSummary();
console.log('\n=== Evaluation Summary ===');
console.log(`Total: ${summary.total}`);
console.log(`Passed: ${summary.passed}`);
console.log(`Failed: ${summary.failed}`);
console.log(`Avg Score: ${(summary.avgScore * 100).toFixed(1)}%`);
console.log('\nResults:');
for (const [name, result] of this.results) {
const status = result.passed ? '✅' : '❌';
console.log(` ${status} ${name}: ${(result.score * 100).toFixed(1)}%`);
}
}
}
+244
View File
@@ -0,0 +1,244 @@
/**
* Guardrails - Input/output validation and safety checks
*/
export type GuardrailStatus = 'passed' | 'failed' | 'warning';
export interface GuardrailResult {
status: GuardrailStatus;
message?: string;
details?: Record<string, any>;
modifiedContent?: string;
}
export type GuardrailFunction = (
content: string,
context?: GuardrailContext
) => Promise<GuardrailResult> | GuardrailResult;
export interface GuardrailContext {
role: 'input' | 'output';
agentName?: string;
sessionId?: string;
metadata?: Record<string, any>;
}
export interface GuardrailConfig {
name: string;
description?: string;
check: GuardrailFunction;
onFail?: 'block' | 'warn' | 'modify';
}
/**
* Guardrail class
*/
export class Guardrail {
readonly name: string;
readonly description: string;
readonly check: GuardrailFunction;
readonly onFail: 'block' | 'warn' | 'modify';
constructor(config: GuardrailConfig) {
this.name = config.name;
this.description = config.description || `Guardrail: ${config.name}`;
this.check = config.check;
this.onFail = config.onFail || 'block';
}
async run(content: string, context?: GuardrailContext): Promise<GuardrailResult> {
try {
return await this.check(content, context);
} catch (error: any) {
return {
status: 'failed',
message: `Guardrail error: ${error.message}`,
details: { error: error.message },
};
}
}
}
/**
* Create a guardrail
*/
export function guardrail(config: GuardrailConfig): Guardrail {
return new Guardrail(config);
}
/**
* Guardrail Manager - Run multiple guardrails
*/
export class GuardrailManager {
private guardrails: Guardrail[] = [];
add(g: Guardrail): this {
this.guardrails.push(g);
return this;
}
async runAll(
content: string,
context?: GuardrailContext
): Promise<{ passed: boolean; results: Array<{ name: string; result: GuardrailResult }> }> {
const results: Array<{ name: string; result: GuardrailResult }> = [];
let passed = true;
for (const g of this.guardrails) {
const result = await g.run(content, context);
results.push({ name: g.name, result });
if (result.status === 'failed') {
passed = false;
if (g.onFail === 'block') {
break;
}
}
}
return { passed, results };
}
get count(): number {
return this.guardrails.length;
}
}
/**
* Built-in guardrails
*/
export const builtinGuardrails = {
/**
* Check for maximum length
*/
maxLength: (maxChars: number): Guardrail => {
return guardrail({
name: 'max_length',
description: `Ensure content is under ${maxChars} characters`,
check: (content) => {
if (content.length > maxChars) {
return {
status: 'failed',
message: `Content exceeds maximum length of ${maxChars} characters`,
details: { length: content.length, max: maxChars },
};
}
return { status: 'passed' };
},
});
},
/**
* Check for minimum length
*/
minLength: (minChars: number): Guardrail => {
return guardrail({
name: 'min_length',
description: `Ensure content is at least ${minChars} characters`,
check: (content) => {
if (content.length < minChars) {
return {
status: 'failed',
message: `Content is below minimum length of ${minChars} characters`,
details: { length: content.length, min: minChars },
};
}
return { status: 'passed' };
},
});
},
/**
* Check for blocked words
*/
blockedWords: (words: string[]): Guardrail => {
return guardrail({
name: 'blocked_words',
description: 'Check for blocked words',
check: (content) => {
const lowerContent = content.toLowerCase();
const found = words.filter(w => lowerContent.includes(w.toLowerCase()));
if (found.length > 0) {
return {
status: 'failed',
message: `Content contains blocked words`,
details: { blockedWords: found },
};
}
return { status: 'passed' };
},
});
},
/**
* Check for required words
*/
requiredWords: (words: string[]): Guardrail => {
return guardrail({
name: 'required_words',
description: 'Check for required words',
check: (content) => {
const lowerContent = content.toLowerCase();
const missing = words.filter(w => !lowerContent.includes(w.toLowerCase()));
if (missing.length > 0) {
return {
status: 'failed',
message: `Content missing required words`,
details: { missingWords: missing },
};
}
return { status: 'passed' };
},
});
},
/**
* Regex pattern check
*/
pattern: (regex: RegExp, mustMatch: boolean = true): Guardrail => {
return guardrail({
name: 'pattern',
description: `Check content against pattern: ${regex}`,
check: (content) => {
const matches = regex.test(content);
if (mustMatch && !matches) {
return {
status: 'failed',
message: `Content does not match required pattern`,
details: { pattern: regex.toString() },
};
}
if (!mustMatch && matches) {
return {
status: 'failed',
message: `Content matches forbidden pattern`,
details: { pattern: regex.toString() },
};
}
return { status: 'passed' };
},
});
},
/**
* JSON validity check
*/
validJson: (): Guardrail => {
return guardrail({
name: 'valid_json',
description: 'Ensure content is valid JSON',
check: (content) => {
try {
JSON.parse(content);
return { status: 'passed' };
} catch (e: any) {
return {
status: 'failed',
message: 'Content is not valid JSON',
details: { error: e.message },
};
}
},
});
},
};
+71 -1
View File
@@ -4,4 +4,74 @@ export * from './knowledge';
export * from './llm';
export * from './memory';
export * from './process';
export * from './tools';
// Export tools (excluding conflicting types)
export {
Tool, BaseTool,
FunctionTool, tool, ToolRegistry, getRegistry, registerTool, getTool,
type ToolConfig, type ToolContext, type ToolParameters
} from './tools';
export * from './tools/arxivTools';
export * from './tools/mcpSse';
// Export session management
export * from './session';
// Export database adapters
export * from './db';
// Export workflows
export * from './workflows';
// Export guardrails
export * from './guardrails';
// Export handoff
export { Handoff, handoff, handoffFilters, type HandoffConfig, type HandoffContext, type HandoffResult } from './agent/handoff';
// Export router agent
export { RouterAgent, createRouter, routeConditions, type RouterConfig, type RouteConfig, type RouteContext } from './agent/router';
// Export context agent
export { ContextAgent, createContextAgent, type ContextAgentConfig, type ContextMessage } from './agent/context';
// Export knowledge base (RAG)
export { KnowledgeBase, createKnowledgeBase, type Document, type SearchResult, type EmbeddingProvider, type KnowledgeBaseConfig } from './knowledge/rag';
// Export evaluation framework
export { accuracyEval, performanceEval, reliabilityEval, EvalSuite, type EvalResult, type PerformanceResult, type AccuracyEvalConfig, type PerformanceEvalConfig, type ReliabilityEvalConfig } from './eval';
// Export observability
export { MemoryObservabilityAdapter, ConsoleObservabilityAdapter, setObservabilityAdapter, getObservabilityAdapter, type ObservabilityAdapter, type TraceContext, type SpanContext, type SpanData, type TraceData } from './observability';
// Export skills
export { SkillManager, createSkillManager, parseSkillFile, type Skill, type SkillMetadata, type SkillDiscoveryOptions } from './skills';
// Export CLI
export { chat, listProviders, version, help } from './cli';
// Export providers with explicit names to avoid conflicts
export {
createProvider,
getDefaultProvider,
parseModelString,
isProviderAvailable,
getAvailableProviders,
OpenAIProvider,
AnthropicProvider,
GoogleProvider,
BaseProvider,
type LLMProvider,
type ProviderConfig,
type ProviderFactory,
type GenerateTextOptions,
type GenerateTextResult,
type StreamTextOptions,
type StreamChunk,
type GenerateObjectOptions,
type GenerateObjectResult,
type TokenUsage,
type Message as ProviderMessage,
type ToolCall,
type ToolDefinition as ProviderToolDefinition,
} from './llm/providers';
+193
View File
@@ -0,0 +1,193 @@
/**
* Knowledge Base (RAG) - Retrieval Augmented Generation
*/
export interface Document {
id: string;
content: string;
metadata?: Record<string, any>;
embedding?: number[];
}
export interface SearchResult {
document: Document;
score: number;
}
export interface EmbeddingProvider {
embed(text: string): Promise<number[]>;
embedBatch(texts: string[]): Promise<number[][]>;
}
export interface KnowledgeBaseConfig {
embeddingProvider?: EmbeddingProvider;
similarityThreshold?: number;
maxResults?: number;
}
/**
* Simple in-memory vector store for RAG
*/
export class KnowledgeBase {
private documents: Map<string, Document> = new Map();
private embeddingProvider?: EmbeddingProvider;
private similarityThreshold: number;
private maxResults: number;
constructor(config: KnowledgeBaseConfig = {}) {
this.embeddingProvider = config.embeddingProvider;
this.similarityThreshold = config.similarityThreshold ?? 0.7;
this.maxResults = config.maxResults ?? 5;
}
/**
* Add a document to the knowledge base
*/
async add(doc: Omit<Document, 'embedding'>): Promise<Document> {
const document: Document = { ...doc };
if (this.embeddingProvider) {
document.embedding = await this.embeddingProvider.embed(doc.content);
}
this.documents.set(doc.id, document);
return document;
}
/**
* Add multiple documents
*/
async addBatch(docs: Array<Omit<Document, 'embedding'>>): Promise<Document[]> {
if (this.embeddingProvider && docs.length > 0) {
const embeddings = await this.embeddingProvider.embedBatch(docs.map(d => d.content));
return Promise.all(docs.map(async (doc, i) => {
const document: Document = { ...doc, embedding: embeddings[i] };
this.documents.set(doc.id, document);
return document;
}));
}
return Promise.all(docs.map(doc => this.add(doc)));
}
/**
* Get a document by ID
*/
get(id: string): Document | undefined {
return this.documents.get(id);
}
/**
* Delete a document
*/
delete(id: string): boolean {
return this.documents.delete(id);
}
/**
* Search for similar documents
*/
async search(query: string, limit?: number): Promise<SearchResult[]> {
const maxResults = limit ?? this.maxResults;
if (!this.embeddingProvider) {
// Fallback to simple text matching
return this.textSearch(query, maxResults);
}
const queryEmbedding = await this.embeddingProvider.embed(query);
const results: SearchResult[] = [];
for (const doc of this.documents.values()) {
if (doc.embedding) {
const score = this.cosineSimilarity(queryEmbedding, doc.embedding);
if (score >= this.similarityThreshold) {
results.push({ document: doc, score });
}
}
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, maxResults);
}
/**
* Simple text-based search fallback
*/
private textSearch(query: string, limit: number): SearchResult[] {
const queryLower = query.toLowerCase();
const results: SearchResult[] = [];
for (const doc of this.documents.values()) {
const contentLower = doc.content.toLowerCase();
if (contentLower.includes(queryLower)) {
const score = queryLower.length / contentLower.length;
results.push({ document: doc, score: Math.min(score * 10, 1) });
}
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
/**
* Calculate cosine similarity between two vectors
*/
private cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) return 0;
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator === 0 ? 0 : dotProduct / denominator;
}
/**
* Get all documents
*/
list(): Document[] {
return Array.from(this.documents.values());
}
/**
* Clear all documents
*/
clear(): void {
this.documents.clear();
}
/**
* Get document count
*/
get size(): number {
return this.documents.size;
}
/**
* Build context from search results for RAG
*/
buildContext(results: SearchResult[]): string {
if (results.length === 0) return '';
return results
.map((r, i) => `[${i + 1}] ${r.document.content}`)
.join('\n\n');
}
}
/**
* Create a knowledge base
*/
export function createKnowledgeBase(config?: KnowledgeBaseConfig): KnowledgeBase {
return new KnowledgeBase(config);
}
@@ -0,0 +1,334 @@
/**
* Anthropic Provider - Implementation for Anthropic Claude API
*/
import { BaseProvider } from './base';
import type {
ProviderConfig,
GenerateTextOptions,
GenerateTextResult,
StreamTextOptions,
StreamChunk,
GenerateObjectOptions,
GenerateObjectResult,
Message,
ToolDefinition,
ToolCall,
} from './types';
interface AnthropicMessage {
role: 'user' | 'assistant';
content: string | Array<{ type: string; text?: string; tool_use_id?: string; content?: string }>;
}
interface AnthropicTool {
name: string;
description: string;
input_schema: Record<string, any>;
}
export class AnthropicProvider extends BaseProvider {
readonly providerId = 'anthropic';
private apiKey: string;
private baseUrl: string;
constructor(modelId: string, config: ProviderConfig = {}) {
super(modelId, config);
this.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY || '';
this.baseUrl = config.baseUrl || 'https://api.anthropic.com';
if (!this.apiKey) {
throw new Error('ANTHROPIC_API_KEY is required');
}
}
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
return this.withRetry(async () => {
const { systemPrompt, messages } = this.extractSystemPrompt(options.messages);
const response = await fetch(`${this.baseUrl}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: this.modelId,
max_tokens: options.maxTokens || 4096,
system: systemPrompt,
messages: this.formatMessages(messages),
temperature: options.temperature ?? 0.7,
tools: options.tools ? this.formatTools(options.tools) : undefined,
stop_sequences: options.stop,
top_p: options.topP,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw Object.assign(new Error(error.error?.message || `Anthropic API error: ${response.status}`), {
status: response.status,
});
}
const data = await response.json();
let text = '';
const toolCalls: ToolCall[] = [];
for (const block of data.content || []) {
if (block.type === 'text') {
text += block.text;
} else if (block.type === 'tool_use') {
toolCalls.push({
id: block.id,
type: 'function',
function: {
name: block.name,
arguments: JSON.stringify(block.input),
},
});
}
}
return {
text,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: {
promptTokens: data.usage?.input_tokens || 0,
completionTokens: data.usage?.output_tokens || 0,
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
},
finishReason: this.mapStopReason(data.stop_reason),
raw: data,
};
});
}
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
const self = this;
const { systemPrompt, messages } = this.extractSystemPrompt(options.messages);
return {
async *[Symbol.asyncIterator]() {
const response = await fetch(`${self.baseUrl}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': self.apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: self.modelId,
max_tokens: options.maxTokens || 4096,
system: systemPrompt,
messages: self.formatMessages(messages),
temperature: options.temperature ?? 0.7,
tools: options.tools ? self.formatTools(options.tools) : undefined,
stream: true,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error?.message || `Anthropic API error: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
const toolCalls: ToolCall[] = [];
let currentToolCall: Partial<ToolCall> | null = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const event = JSON.parse(data);
if (event.type === 'content_block_delta') {
if (event.delta?.type === 'text_delta') {
const text = event.delta.text;
if (options.onToken) options.onToken(text);
yield { text };
} else if (event.delta?.type === 'input_json_delta') {
if (currentToolCall) {
currentToolCall.function!.arguments += event.delta.partial_json;
}
}
} else if (event.type === 'content_block_start') {
if (event.content_block?.type === 'tool_use') {
currentToolCall = {
id: event.content_block.id,
type: 'function',
function: {
name: event.content_block.name,
arguments: '',
},
};
}
} else if (event.type === 'content_block_stop') {
if (currentToolCall) {
toolCalls.push(currentToolCall as ToolCall);
currentToolCall = null;
}
} else if (event.type === 'message_delta') {
yield {
finishReason: self.mapStopReason(event.delta?.stop_reason),
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: event.usage ? {
promptTokens: 0,
completionTokens: event.usage.output_tokens,
totalTokens: event.usage.output_tokens,
} : undefined,
};
}
} catch (e) {
// Skip malformed JSON
}
}
}
},
};
}
async generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
const { systemPrompt, messages } = this.extractSystemPrompt(options.messages);
// Add JSON instruction to system prompt
const jsonSystemPrompt = `${systemPrompt}\n\nYou must respond with valid JSON matching this schema:\n${JSON.stringify(options.schema, null, 2)}`;
return this.withRetry(async () => {
const response = await fetch(`${this.baseUrl}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: this.modelId,
max_tokens: options.maxTokens || 4096,
system: jsonSystemPrompt,
messages: this.formatMessages(messages),
temperature: options.temperature ?? 0.7,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error?.message || `Anthropic API error: ${response.status}`);
}
const data = await response.json();
let text = '';
for (const block of data.content || []) {
if (block.type === 'text') {
text += block.text;
}
}
// Extract JSON from response
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error(`No JSON found in response: ${text}`);
}
let parsed: T;
try {
parsed = JSON.parse(jsonMatch[0]);
} catch (e) {
throw new Error(`Failed to parse JSON: ${jsonMatch[0]}`);
}
return {
object: parsed,
usage: {
promptTokens: data.usage?.input_tokens || 0,
completionTokens: data.usage?.output_tokens || 0,
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
},
raw: data,
};
});
}
private extractSystemPrompt(messages: Message[]): { systemPrompt: string; messages: Message[] } {
const systemMessages = messages.filter(m => m.role === 'system');
const otherMessages = messages.filter(m => m.role !== 'system');
const systemPrompt = systemMessages.map(m => m.content).join('\n');
return { systemPrompt, messages: otherMessages };
}
protected formatMessages(messages: Message[]): AnthropicMessage[] {
const result: AnthropicMessage[] = [];
for (const msg of messages) {
if (msg.role === 'system') continue; // Handled separately
if (msg.role === 'tool') {
// Tool results need to be part of user message in Anthropic
result.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: msg.tool_call_id || '',
content: msg.content || '',
}],
});
} else if (msg.role === 'assistant' && msg.tool_calls) {
const content: any[] = [];
if (msg.content) {
content.push({ type: 'text', text: msg.content });
}
for (const tc of msg.tool_calls) {
content.push({
type: 'tool_use',
id: tc.id,
name: tc.function.name,
input: JSON.parse(tc.function.arguments),
});
}
result.push({ role: 'assistant', content });
} else {
result.push({
role: msg.role as 'user' | 'assistant',
content: msg.content || '',
});
}
}
return result;
}
protected formatTools(tools: ToolDefinition[]): AnthropicTool[] {
return tools.map(tool => ({
name: tool.name,
description: tool.description || `Function ${tool.name}`,
input_schema: tool.parameters || { type: 'object', properties: {} },
}));
}
private mapStopReason(reason: string | null): GenerateTextResult['finishReason'] {
switch (reason) {
case 'end_turn': return 'stop';
case 'max_tokens': return 'length';
case 'tool_use': return 'tool_calls';
default: return 'stop';
}
}
}
@@ -0,0 +1,77 @@
/**
* Base Provider - Abstract base class for LLM providers
*/
import type {
LLMProvider,
ProviderConfig,
GenerateTextOptions,
GenerateTextResult,
StreamTextOptions,
StreamChunk,
GenerateObjectOptions,
GenerateObjectResult,
Message,
ToolDefinition,
} from './types';
export abstract class BaseProvider implements LLMProvider {
abstract readonly providerId: string;
readonly modelId: string;
protected config: ProviderConfig;
constructor(modelId: string, config: ProviderConfig = {}) {
this.modelId = modelId;
this.config = {
maxRetries: 3,
timeout: 60000,
...config,
};
}
abstract generateText(options: GenerateTextOptions): Promise<GenerateTextResult>;
abstract streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>>;
abstract generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>>;
/**
* Convert tool definitions to provider-specific format
*/
protected abstract formatTools(tools: ToolDefinition[]): any[];
/**
* Convert messages to provider-specific format
*/
protected abstract formatMessages(messages: Message[]): any[];
/**
* Retry logic with exponential backoff
*/
protected async withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = this.config.maxRetries || 3
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
// Check if error is retryable (rate limit, server error)
const isRetryable =
error.status === 429 ||
error.status >= 500 ||
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT';
if (!isRetryable || attempt === maxRetries) {
throw error;
}
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
}
@@ -0,0 +1,322 @@
/**
* Google Provider - Implementation for Google Gemini API
*/
import { BaseProvider } from './base';
import type {
ProviderConfig,
GenerateTextOptions,
GenerateTextResult,
StreamTextOptions,
StreamChunk,
GenerateObjectOptions,
GenerateObjectResult,
Message,
ToolDefinition,
ToolCall,
} from './types';
interface GeminiContent {
role: 'user' | 'model';
parts: Array<{ text?: string; functionCall?: any; functionResponse?: any }>;
}
export class GoogleProvider extends BaseProvider {
readonly providerId = 'google';
private apiKey: string;
private baseUrl: string;
constructor(modelId: string, config: ProviderConfig = {}) {
super(modelId, config);
this.apiKey = config.apiKey || process.env.GOOGLE_API_KEY || '';
this.baseUrl = config.baseUrl || 'https://generativelanguage.googleapis.com/v1beta';
if (!this.apiKey) {
throw new Error('GOOGLE_API_KEY is required');
}
}
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
return this.withRetry(async () => {
const { systemInstruction, contents } = this.formatRequest(options.messages);
const response = await fetch(
`${this.baseUrl}/models/${this.modelId}:generateContent?key=${this.apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
contents,
generationConfig: {
temperature: options.temperature ?? 0.7,
maxOutputTokens: options.maxTokens,
topP: options.topP,
stopSequences: options.stop,
},
tools: options.tools ? this.formatTools(options.tools) : undefined,
}),
}
);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw Object.assign(new Error(error.error?.message || `Google API error: ${response.status}`), {
status: response.status,
});
}
const data = await response.json();
const candidate = data.candidates?.[0];
let text = '';
const toolCalls: ToolCall[] = [];
for (const part of candidate?.content?.parts || []) {
if (part.text) {
text += part.text;
} else if (part.functionCall) {
toolCalls.push({
id: `call_${Math.random().toString(36).substr(2, 9)}`,
type: 'function',
function: {
name: part.functionCall.name,
arguments: JSON.stringify(part.functionCall.args || {}),
},
});
}
}
return {
text,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: {
promptTokens: data.usageMetadata?.promptTokenCount || 0,
completionTokens: data.usageMetadata?.candidatesTokenCount || 0,
totalTokens: data.usageMetadata?.totalTokenCount || 0,
},
finishReason: this.mapFinishReason(candidate?.finishReason),
raw: data,
};
});
}
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
const self = this;
const { systemInstruction, contents } = this.formatRequest(options.messages);
return {
async *[Symbol.asyncIterator]() {
const response = await fetch(
`${self.baseUrl}/models/${self.modelId}:streamGenerateContent?key=${self.apiKey}&alt=sse`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
contents,
generationConfig: {
temperature: options.temperature ?? 0.7,
maxOutputTokens: options.maxTokens,
},
tools: options.tools ? self.formatTools(options.tools) : undefined,
}),
}
);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error?.message || `Google API error: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
const toolCalls: ToolCall[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const event = JSON.parse(data);
const candidate = event.candidates?.[0];
for (const part of candidate?.content?.parts || []) {
if (part.text) {
if (options.onToken) options.onToken(part.text);
yield { text: part.text };
} else if (part.functionCall) {
toolCalls.push({
id: `call_${Math.random().toString(36).substr(2, 9)}`,
type: 'function',
function: {
name: part.functionCall.name,
arguments: JSON.stringify(part.functionCall.args || {}),
},
});
}
}
if (candidate?.finishReason) {
yield {
finishReason: self.mapFinishReason(candidate.finishReason),
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: event.usageMetadata ? {
promptTokens: event.usageMetadata.promptTokenCount || 0,
completionTokens: event.usageMetadata.candidatesTokenCount || 0,
totalTokens: event.usageMetadata.totalTokenCount || 0,
} : undefined,
};
}
} catch (e) {
// Skip malformed JSON
}
}
}
},
};
}
async generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
const { systemInstruction, contents } = this.formatRequest(options.messages);
return this.withRetry(async () => {
const response = await fetch(
`${this.baseUrl}/models/${this.modelId}:generateContent?key=${this.apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: systemInstruction
? { parts: [{ text: `${systemInstruction}\n\nRespond with valid JSON matching this schema:\n${JSON.stringify(options.schema, null, 2)}` }] }
: { parts: [{ text: `Respond with valid JSON matching this schema:\n${JSON.stringify(options.schema, null, 2)}` }] },
contents,
generationConfig: {
temperature: options.temperature ?? 0.7,
maxOutputTokens: options.maxTokens,
responseMimeType: 'application/json',
},
}),
}
);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error?.message || `Google API error: ${response.status}`);
}
const data = await response.json();
const candidate = data.candidates?.[0];
let text = '';
for (const part of candidate?.content?.parts || []) {
if (part.text) text += part.text;
}
let parsed: T;
try {
parsed = JSON.parse(text);
} catch (e) {
// Try to extract JSON from text
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error(`No JSON found: ${text}`);
parsed = JSON.parse(jsonMatch[0]);
}
return {
object: parsed,
usage: {
promptTokens: data.usageMetadata?.promptTokenCount || 0,
completionTokens: data.usageMetadata?.candidatesTokenCount || 0,
totalTokens: data.usageMetadata?.totalTokenCount || 0,
},
raw: data,
};
});
}
private formatRequest(messages: Message[]): { systemInstruction: string | null; contents: GeminiContent[] } {
const systemMessages = messages.filter(m => m.role === 'system');
const otherMessages = messages.filter(m => m.role !== 'system');
const systemInstruction = systemMessages.length > 0
? systemMessages.map(m => m.content).join('\n')
: null;
return {
systemInstruction,
contents: this.formatMessages(otherMessages),
};
}
protected formatMessages(messages: Message[]): GeminiContent[] {
const result: GeminiContent[] = [];
for (const msg of messages) {
if (msg.role === 'system') continue;
const role = msg.role === 'assistant' ? 'model' : 'user';
if (msg.role === 'tool') {
result.push({
role: 'user',
parts: [{
functionResponse: {
name: msg.name || 'function',
response: { result: msg.content },
},
}],
});
} else if (msg.role === 'assistant' && msg.tool_calls) {
const parts: any[] = [];
if (msg.content) parts.push({ text: msg.content });
for (const tc of msg.tool_calls) {
parts.push({
functionCall: {
name: tc.function.name,
args: JSON.parse(tc.function.arguments),
},
});
}
result.push({ role: 'model', parts });
} else {
result.push({
role,
parts: [{ text: msg.content || '' }],
});
}
}
return result;
}
protected formatTools(tools: ToolDefinition[]): any[] {
return [{
functionDeclarations: tools.map(tool => ({
name: tool.name,
description: tool.description || `Function ${tool.name}`,
parameters: tool.parameters || { type: 'object', properties: {} },
})),
}];
}
private mapFinishReason(reason: string | null): GenerateTextResult['finishReason'] {
switch (reason) {
case 'STOP': return 'stop';
case 'MAX_TOKENS': return 'length';
case 'SAFETY': return 'content_filter';
default: return 'stop';
}
}
}
+104
View File
@@ -0,0 +1,104 @@
/**
* LLM Providers - Factory and exports for multi-provider support
*/
export * from './types';
export * from './base';
export { OpenAIProvider } from './openai';
export { AnthropicProvider } from './anthropic';
export { GoogleProvider } from './google';
import type { LLMProvider, ProviderConfig } from './types';
import { OpenAIProvider } from './openai';
import { AnthropicProvider } from './anthropic';
import { GoogleProvider } from './google';
/**
* Provider registry for dynamic provider loading
*/
const PROVIDER_MAP: Record<string, new (modelId: string, config?: ProviderConfig) => LLMProvider> = {
openai: OpenAIProvider,
anthropic: AnthropicProvider,
google: GoogleProvider,
gemini: GoogleProvider, // Alias
};
/**
* Parse model string into provider and model ID
* Supports formats:
* - "provider/model" (e.g., "openai/gpt-4o")
* - "model" (defaults to OpenAI, e.g., "gpt-4o-mini")
*/
export function parseModelString(model: string): { providerId: string; modelId: string } {
if (model.includes('/')) {
const [providerId, ...rest] = model.split('/');
return { providerId: providerId.toLowerCase(), modelId: rest.join('/') };
}
// Default to OpenAI for common model prefixes
if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3')) {
return { providerId: 'openai', modelId: model };
}
if (model.startsWith('claude-')) {
return { providerId: 'anthropic', modelId: model };
}
if (model.startsWith('gemini-')) {
return { providerId: 'google', modelId: model };
}
// Default to OpenAI
return { providerId: 'openai', modelId: model };
}
/**
* Create a provider instance from a model string
*
* @example
* ```typescript
* const provider = createProvider('openai/gpt-4o');
* const provider = createProvider('anthropic/claude-3-5-sonnet-latest');
* const provider = createProvider('google/gemini-2.0-flash');
* const provider = createProvider('gpt-4o-mini'); // Defaults to OpenAI
* ```
*/
export function createProvider(model: string, config?: ProviderConfig): LLMProvider {
const { providerId, modelId } = parseModelString(model);
const ProviderClass = PROVIDER_MAP[providerId];
if (!ProviderClass) {
throw new Error(`Unknown provider: ${providerId}. Available providers: ${Object.keys(PROVIDER_MAP).join(', ')}`);
}
return new ProviderClass(modelId, config);
}
/**
* Get the default provider (OpenAI with gpt-4o-mini)
*/
export function getDefaultProvider(config?: ProviderConfig): LLMProvider {
return createProvider('openai/gpt-4o-mini', config);
}
/**
* Check if a provider is available (has required API key)
*/
export function isProviderAvailable(providerId: string): boolean {
switch (providerId.toLowerCase()) {
case 'openai':
return !!process.env.OPENAI_API_KEY;
case 'anthropic':
return !!process.env.ANTHROPIC_API_KEY;
case 'google':
case 'gemini':
return !!process.env.GOOGLE_API_KEY;
default:
return false;
}
}
/**
* Get list of available providers
*/
export function getAvailableProviders(): string[] {
return Object.keys(PROVIDER_MAP).filter(isProviderAvailable);
}
@@ -0,0 +1,227 @@
/**
* OpenAI Provider - Implementation for OpenAI API
*/
import OpenAI from 'openai';
import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';
import { BaseProvider } from './base';
import type {
ProviderConfig,
GenerateTextOptions,
GenerateTextResult,
StreamTextOptions,
StreamChunk,
GenerateObjectOptions,
GenerateObjectResult,
Message,
ToolDefinition,
ToolCall,
} from './types';
export class OpenAIProvider extends BaseProvider {
readonly providerId = 'openai';
private client: OpenAI;
constructor(modelId: string, config: ProviderConfig = {}) {
super(modelId, config);
this.client = new OpenAI({
apiKey: config.apiKey || process.env.OPENAI_API_KEY,
baseURL: config.baseUrl,
timeout: config.timeout || 60000,
maxRetries: 0, // We handle retries ourselves
});
}
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
return this.withRetry(async () => {
const response = await this.client.chat.completions.create({
model: this.modelId,
messages: this.formatMessages(options.messages),
temperature: options.temperature ?? 0.7,
max_tokens: options.maxTokens,
tools: options.tools ? this.formatTools(options.tools) : undefined,
tool_choice: options.toolChoice as any,
stop: options.stop,
top_p: options.topP,
frequency_penalty: options.frequencyPenalty,
presence_penalty: options.presencePenalty,
});
const choice = response.choices[0];
const message = choice.message;
return {
text: message.content || '',
toolCalls: message.tool_calls?.map(tc => ({
id: tc.id,
type: 'function' as const,
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
})),
usage: {
promptTokens: response.usage?.prompt_tokens || 0,
completionTokens: response.usage?.completion_tokens || 0,
totalTokens: response.usage?.total_tokens || 0,
},
finishReason: this.mapFinishReason(choice.finish_reason),
raw: response,
};
});
}
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
const self = this;
return {
async *[Symbol.asyncIterator]() {
const stream = await self.client.chat.completions.create({
model: self.modelId,
messages: self.formatMessages(options.messages),
temperature: options.temperature ?? 0.7,
max_tokens: options.maxTokens,
tools: options.tools ? self.formatTools(options.tools) : undefined,
tool_choice: options.toolChoice as any,
stop: options.stop,
stream: true,
});
let toolCalls: ToolCall[] = [];
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (delta?.content) {
if (options.onToken) {
options.onToken(delta.content);
}
yield { text: delta.content };
}
if (delta?.tool_calls) {
for (const tc of delta.tool_calls) {
if (tc.index !== undefined) {
if (!toolCalls[tc.index]) {
toolCalls[tc.index] = {
id: tc.id || '',
type: 'function',
function: { name: '', arguments: '' },
};
}
if (tc.id) toolCalls[tc.index].id = tc.id;
if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
}
}
}
if (chunk.choices[0]?.finish_reason) {
yield {
finishReason: chunk.choices[0].finish_reason,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: chunk.usage ? {
promptTokens: chunk.usage.prompt_tokens,
completionTokens: chunk.usage.completion_tokens,
totalTokens: chunk.usage.total_tokens,
} : undefined,
};
}
}
},
};
}
async generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
return this.withRetry(async () => {
const response = await this.client.chat.completions.create({
model: this.modelId,
messages: this.formatMessages(options.messages),
temperature: options.temperature ?? 0.7,
max_tokens: options.maxTokens,
response_format: {
type: 'json_schema',
json_schema: {
name: 'response',
schema: this.normalizeSchema(options.schema),
strict: true,
},
},
});
const choice = response.choices[0];
const content = choice.message.content || '{}';
let parsed: T;
try {
parsed = JSON.parse(content);
} catch (e) {
throw new Error(`Failed to parse JSON response: ${content}`);
}
return {
object: parsed,
usage: {
promptTokens: response.usage?.prompt_tokens || 0,
completionTokens: response.usage?.completion_tokens || 0,
totalTokens: response.usage?.total_tokens || 0,
},
raw: response,
};
});
}
protected formatMessages(messages: Message[]): ChatCompletionMessageParam[] {
return messages.map(msg => {
if (msg.role === 'tool') {
return {
role: 'tool' as const,
content: msg.content || '',
tool_call_id: msg.tool_call_id || '',
};
}
if (msg.role === 'assistant' && msg.tool_calls) {
return {
role: 'assistant' as const,
content: msg.content,
tool_calls: msg.tool_calls,
};
}
return {
role: msg.role as 'system' | 'user' | 'assistant',
content: msg.content || '',
};
});
}
protected formatTools(tools: ToolDefinition[]): ChatCompletionTool[] {
return tools.map(tool => ({
type: 'function' as const,
function: {
name: tool.name,
description: tool.description || `Function ${tool.name}`,
parameters: tool.parameters || { type: 'object', properties: {} },
},
}));
}
private mapFinishReason(reason: string | null): GenerateTextResult['finishReason'] {
switch (reason) {
case 'stop': return 'stop';
case 'length': return 'length';
case 'tool_calls': return 'tool_calls';
case 'content_filter': return 'content_filter';
default: return 'stop';
}
}
private normalizeSchema(schema: any): any {
// If it's a Zod schema, convert to JSON schema
if (schema && typeof schema.parse === 'function' && typeof schema._def === 'object') {
// This is a Zod schema - we need zod-to-json-schema
// For now, throw an error suggesting to use JSON schema directly
throw new Error('Zod schemas require zod-to-json-schema. Please use JSON schema directly or install zod-to-json-schema.');
}
return schema;
}
}
+103
View File
@@ -0,0 +1,103 @@
/**
* LLM Provider Types - Core type definitions for the provider abstraction layer
*/
export interface Message {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string | null;
name?: string;
tool_call_id?: string;
tool_calls?: ToolCall[];
}
export interface ToolCall {
id: string;
type: 'function';
function: {
name: string;
arguments: string;
};
}
export interface ToolDefinition {
name: string;
description?: string;
parameters?: Record<string, any>;
}
export interface TokenUsage {
promptTokens: number;
completionTokens: number;
totalTokens: number;
}
export interface GenerateTextOptions {
messages: Message[];
temperature?: number;
maxTokens?: number;
tools?: ToolDefinition[];
toolChoice?: 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } };
stop?: string[];
topP?: number;
frequencyPenalty?: number;
presencePenalty?: number;
}
export interface GenerateTextResult {
text: string;
toolCalls?: ToolCall[];
usage: TokenUsage;
finishReason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error';
raw?: any;
}
export interface StreamTextOptions extends GenerateTextOptions {
onToken?: (token: string) => void;
onToolCall?: (toolCall: ToolCall) => void;
}
export interface StreamChunk {
text?: string;
toolCalls?: ToolCall[];
usage?: Partial<TokenUsage>;
finishReason?: string;
}
export interface GenerateObjectOptions<T = any> {
messages: Message[];
schema: Record<string, any> | any; // JSON Schema or Zod schema
temperature?: number;
maxTokens?: number;
maxRetries?: number;
}
export interface GenerateObjectResult<T = any> {
object: T;
usage: TokenUsage;
raw?: any;
}
export interface ProviderConfig {
apiKey?: string;
baseUrl?: string;
maxRetries?: number;
timeout?: number;
defaultModel?: string;
}
/**
* Base interface for all LLM providers
*/
export interface LLMProvider {
readonly providerId: string;
readonly modelId: string;
generateText(options: GenerateTextOptions): Promise<GenerateTextResult>;
streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>>;
generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>>;
}
/**
* Provider factory function type
*/
export type ProviderFactory = (modelId: string, config?: ProviderConfig) => LLMProvider;
+238
View File
@@ -0,0 +1,238 @@
/**
* Observability - Tracing, logging, and metrics hooks
*/
export type SpanKind = 'llm' | 'tool' | 'agent' | 'workflow' | 'custom';
export type SpanStatus = 'pending' | 'running' | 'completed' | 'failed';
export interface SpanData {
id: string;
traceId: string;
parentId?: string;
name: string;
kind: SpanKind;
status: SpanStatus;
startTime: number;
endTime?: number;
attributes: Record<string, any>;
events: SpanEvent[];
}
export interface SpanEvent {
name: string;
timestamp: number;
attributes?: Record<string, any>;
}
export interface TraceData {
id: string;
name: string;
startTime: number;
endTime?: number;
status: SpanStatus;
spans: SpanData[];
metadata: Record<string, any>;
}
/**
* Observability Adapter Protocol
*/
export interface ObservabilityAdapter {
startTrace(name: string, metadata?: Record<string, any>): TraceContext;
endTrace(traceId: string, status?: SpanStatus): void;
startSpan(traceId: string, name: string, kind: SpanKind, parentId?: string): SpanContext;
endSpan(spanId: string, status?: SpanStatus, attributes?: Record<string, any>): void;
addEvent(spanId: string, name: string, attributes?: Record<string, any>): void;
flush(): Promise<void>;
}
export interface TraceContext {
traceId: string;
startSpan(name: string, kind: SpanKind): SpanContext;
end(status?: SpanStatus): void;
}
export interface SpanContext {
spanId: string;
traceId: string;
addEvent(name: string, attributes?: Record<string, any>): void;
setAttributes(attributes: Record<string, any>): void;
end(status?: SpanStatus): void;
}
/**
* In-memory observability adapter for development
*/
export class MemoryObservabilityAdapter implements ObservabilityAdapter {
private traces: Map<string, TraceData> = new Map();
private spans: Map<string, SpanData> = new Map();
startTrace(name: string, metadata: Record<string, any> = {}): TraceContext {
const traceId = this.generateId();
const trace: TraceData = {
id: traceId,
name,
startTime: Date.now(),
status: 'running',
spans: [],
metadata
};
this.traces.set(traceId, trace);
const self = this;
return {
traceId,
startSpan(spanName: string, kind: SpanKind): SpanContext {
return self.startSpan(traceId, spanName, kind);
},
end(status: SpanStatus = 'completed'): void {
self.endTrace(traceId, status);
}
};
}
endTrace(traceId: string, status: SpanStatus = 'completed'): void {
const trace = this.traces.get(traceId);
if (trace) {
trace.endTime = Date.now();
trace.status = status;
}
}
startSpan(traceId: string, name: string, kind: SpanKind, parentId?: string): SpanContext {
const spanId = this.generateId();
const span: SpanData = {
id: spanId,
traceId,
parentId,
name,
kind,
status: 'running',
startTime: Date.now(),
attributes: {},
events: []
};
this.spans.set(spanId, span);
const trace = this.traces.get(traceId);
if (trace) {
trace.spans.push(span);
}
const self = this;
return {
spanId,
traceId,
addEvent(eventName: string, attributes?: Record<string, any>): void {
self.addEvent(spanId, eventName, attributes);
},
setAttributes(attributes: Record<string, any>): void {
const s = self.spans.get(spanId);
if (s) {
s.attributes = { ...s.attributes, ...attributes };
}
},
end(status: SpanStatus = 'completed'): void {
self.endSpan(spanId, status);
}
};
}
endSpan(spanId: string, status: SpanStatus = 'completed', attributes?: Record<string, any>): void {
const span = this.spans.get(spanId);
if (span) {
span.endTime = Date.now();
span.status = status;
if (attributes) {
span.attributes = { ...span.attributes, ...attributes };
}
}
}
addEvent(spanId: string, name: string, attributes?: Record<string, any>): void {
const span = this.spans.get(spanId);
if (span) {
span.events.push({
name,
timestamp: Date.now(),
attributes
});
}
}
async flush(): Promise<void> {
// No-op for memory adapter
}
// Utility methods
getTrace(traceId: string): TraceData | undefined {
return this.traces.get(traceId);
}
getSpan(spanId: string): SpanData | undefined {
return this.spans.get(spanId);
}
getAllTraces(): TraceData[] {
return Array.from(this.traces.values());
}
clear(): void {
this.traces.clear();
this.spans.clear();
}
private generateId(): string {
return Math.random().toString(36).substring(2, 15);
}
}
/**
* Console observability adapter for debugging
*/
export class ConsoleObservabilityAdapter implements ObservabilityAdapter {
private memory = new MemoryObservabilityAdapter();
startTrace(name: string, metadata?: Record<string, any>): TraceContext {
console.log(`[TRACE START] ${name}`, metadata || '');
return this.memory.startTrace(name, metadata);
}
endTrace(traceId: string, status?: SpanStatus): void {
console.log(`[TRACE END] ${traceId} - ${status || 'completed'}`);
this.memory.endTrace(traceId, status);
}
startSpan(traceId: string, name: string, kind: SpanKind, parentId?: string): SpanContext {
console.log(` [SPAN START] ${name} (${kind})`);
return this.memory.startSpan(traceId, name, kind, parentId);
}
endSpan(spanId: string, status?: SpanStatus, attributes?: Record<string, any>): void {
console.log(` [SPAN END] ${spanId} - ${status || 'completed'}`, attributes || '');
this.memory.endSpan(spanId, status, attributes);
}
addEvent(spanId: string, name: string, attributes?: Record<string, any>): void {
console.log(` [EVENT] ${name}`, attributes || '');
this.memory.addEvent(spanId, name, attributes);
}
async flush(): Promise<void> {
await this.memory.flush();
}
}
// Global observability instance
let globalAdapter: ObservabilityAdapter | null = null;
export function setObservabilityAdapter(adapter: ObservabilityAdapter): void {
globalAdapter = adapter;
}
export function getObservabilityAdapter(): ObservabilityAdapter {
if (!globalAdapter) {
globalAdapter = new MemoryObservabilityAdapter();
}
return globalAdapter;
}
+321
View File
@@ -0,0 +1,321 @@
/**
* Session Management - Session, Run, and Trace tracking
*/
import { randomUUID } from 'crypto';
export interface SessionConfig {
id?: string;
metadata?: Record<string, any>;
}
export interface RunConfig {
id?: string;
metadata?: Record<string, any>;
}
export interface TraceConfig {
id?: string;
name: string;
attributes?: Record<string, any>;
}
export type RunStatus = 'pending' | 'running' | 'completed' | 'failed';
export type TraceStatus = 'pending' | 'running' | 'completed' | 'failed';
export interface Message {
id: string;
role: 'system' | 'user' | 'assistant' | 'tool';
content: string | null;
name?: string;
tool_call_id?: string;
tool_calls?: any[];
timestamp: number;
runId?: string;
}
/**
* Trace represents a span within a run (e.g., LLM call, tool call)
*/
export class Trace {
readonly id: string;
readonly runId: string;
readonly name: string;
readonly parentId: string | null;
readonly startTime: number;
endTime: number | null = null;
status: TraceStatus = 'pending';
attributes: Record<string, any>;
private children: Trace[] = [];
constructor(runId: string, config: TraceConfig, parentId: string | null = null) {
this.id = config.id || randomUUID();
this.runId = runId;
this.name = config.name;
this.parentId = parentId;
this.startTime = Date.now();
this.attributes = config.attributes || {};
}
start(): this {
this.status = 'running';
return this;
}
complete(attributes?: Record<string, any>): this {
this.status = 'completed';
this.endTime = Date.now();
if (attributes) {
this.attributes = { ...this.attributes, ...attributes };
}
return this;
}
fail(error?: Error): this {
this.status = 'failed';
this.endTime = Date.now();
if (error) {
this.attributes.error = error.message;
this.attributes.errorStack = error.stack;
}
return this;
}
get duration(): number | null {
if (this.endTime === null) return null;
return this.endTime - this.startTime;
}
createChild(config: TraceConfig): Trace {
const child = new Trace(this.runId, config, this.id);
this.children.push(child);
return child;
}
getChildren(): Trace[] {
return [...this.children];
}
toJSON(): Record<string, any> {
return {
id: this.id,
runId: this.runId,
name: this.name,
parentId: this.parentId,
startTime: this.startTime,
endTime: this.endTime,
duration: this.duration,
status: this.status,
attributes: this.attributes,
children: this.children.map(c => c.toJSON()),
};
}
}
/**
* Run represents a single execution within a session (e.g., one chat call)
*/
export class Run {
readonly id: string;
readonly sessionId: string;
readonly startTime: number;
endTime: number | null = null;
status: RunStatus = 'pending';
error: Error | null = null;
metadata: Record<string, any>;
private traces: Trace[] = [];
private messages: Message[] = [];
constructor(sessionId: string, config: RunConfig = {}) {
this.id = config.id || randomUUID();
this.sessionId = sessionId;
this.startTime = Date.now();
this.metadata = config.metadata || {};
}
start(): this {
this.status = 'running';
return this;
}
complete(metadata?: Record<string, any>): this {
this.status = 'completed';
this.endTime = Date.now();
if (metadata) {
this.metadata = { ...this.metadata, ...metadata };
}
return this;
}
fail(error: Error): this {
this.status = 'failed';
this.endTime = Date.now();
this.error = error;
return this;
}
get duration(): number | null {
if (this.endTime === null) return null;
return this.endTime - this.startTime;
}
createTrace(config: TraceConfig): Trace {
const trace = new Trace(this.id, config);
this.traces.push(trace);
return trace;
}
addMessage(message: Omit<Message, 'id' | 'timestamp' | 'runId'>): Message {
const msg: Message = {
...message,
id: randomUUID(),
timestamp: Date.now(),
runId: this.id,
};
this.messages.push(msg);
return msg;
}
getTraces(): Trace[] {
return [...this.traces];
}
getMessages(): Message[] {
return [...this.messages];
}
toJSON(): Record<string, any> {
return {
id: this.id,
sessionId: this.sessionId,
startTime: this.startTime,
endTime: this.endTime,
duration: this.duration,
status: this.status,
error: this.error?.message,
metadata: this.metadata,
traces: this.traces.map(t => t.toJSON()),
messageCount: this.messages.length,
};
}
}
/**
* Session represents a conversation session with message history
*/
export class Session {
readonly id: string;
readonly createdAt: number;
metadata: Record<string, any>;
private _runs: Run[] = [];
private _messages: Message[] = [];
constructor(config: SessionConfig = {}) {
this.id = config.id || randomUUID();
this.createdAt = Date.now();
this.metadata = config.metadata || {};
}
createRun(config: RunConfig = {}): Run {
const run = new Run(this.id, config);
this._runs.push(run);
return run;
}
get runs(): Run[] {
return [...this._runs];
}
get messages(): Message[] {
return [...this._messages];
}
addMessage(message: Omit<Message, 'id' | 'timestamp'>): Message {
const msg: Message = {
...message,
id: randomUUID(),
timestamp: Date.now(),
};
this._messages.push(msg);
return msg;
}
getMessagesForLLM(): Array<{ role: string; content: string | null; tool_call_id?: string; tool_calls?: any[] }> {
return this._messages.map(m => ({
role: m.role,
content: m.content,
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}),
...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
}));
}
clearMessages(): void {
this._messages = [];
}
toJSON(): Record<string, any> {
return {
id: this.id,
createdAt: this.createdAt,
metadata: this.metadata,
runCount: this._runs.length,
messageCount: this._messages.length,
runs: this._runs.map(r => r.toJSON()),
};
}
}
/**
* SessionManager for managing multiple sessions
*/
export class SessionManager {
private sessions: Map<string, Session> = new Map();
create(config: SessionConfig = {}): Session {
const session = new Session(config);
this.sessions.set(session.id, session);
return session;
}
get(id: string): Session | undefined {
return this.sessions.get(id);
}
getOrCreate(id: string, config: Omit<SessionConfig, 'id'> = {}): Session {
let session = this.sessions.get(id);
if (!session) {
session = new Session({ ...config, id });
this.sessions.set(id, session);
}
return session;
}
list(): Session[] {
return Array.from(this.sessions.values());
}
delete(id: string): boolean {
return this.sessions.delete(id);
}
clear(): void {
this.sessions.clear();
}
toJSON(): Record<string, any> {
return {
sessionCount: this.sessions.size,
sessions: this.list().map(s => s.toJSON()),
};
}
}
// Default session manager instance
let defaultManager: SessionManager | null = null;
export function getSessionManager(): SessionManager {
if (!defaultManager) {
defaultManager = new SessionManager();
}
return defaultManager;
}
+246
View File
@@ -0,0 +1,246 @@
/**
* Skills System - Agent Skills standard implementation
*/
import * as fs from 'fs';
import * as path from 'path';
export interface SkillMetadata {
name: string;
description: string;
license?: string;
compatibility?: string;
allowedTools?: string[];
metadata?: Record<string, string>;
}
export interface Skill {
metadata: SkillMetadata;
instructions: string;
path?: string;
}
export interface SkillDiscoveryOptions {
paths?: string[];
includeSystem?: boolean;
includeUser?: boolean;
includeProject?: boolean;
}
/**
* Parse SKILL.md file
*/
export function parseSkillFile(content: string): Skill {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) {
throw new Error('Invalid SKILL.md format: missing frontmatter');
}
const [, frontmatter, instructions] = frontmatterMatch;
const metadata = parseYamlFrontmatter(frontmatter);
if (!metadata.name || !metadata.description) {
throw new Error('Invalid SKILL.md: name and description are required');
}
return {
metadata: {
name: metadata.name,
description: metadata.description,
license: metadata.license,
compatibility: metadata.compatibility,
allowedTools: metadata['allowed-tools']?.split(/\s+/),
metadata: metadata.metadata
},
instructions: instructions.trim()
};
}
/**
* Simple YAML frontmatter parser
*/
function parseYamlFrontmatter(yaml: string): Record<string, any> {
const result: Record<string, any> = {};
const lines = yaml.split('\n');
let currentKey = '';
let inMetadata = false;
const metadataObj: Record<string, string> = {};
for (const line of lines) {
if (line.startsWith('metadata:')) {
inMetadata = true;
continue;
}
if (inMetadata && line.startsWith(' ')) {
const match = line.match(/^\s+(\w+):\s*(.*)$/);
if (match) {
metadataObj[match[1]] = match[2].replace(/^["']|["']$/g, '');
}
continue;
} else if (inMetadata && !line.startsWith(' ')) {
inMetadata = false;
result.metadata = metadataObj;
}
const match = line.match(/^(\S+):\s*(.*)$/);
if (match) {
currentKey = match[1];
result[currentKey] = match[2].replace(/^["']|["']$/g, '');
}
}
if (inMetadata) {
result.metadata = metadataObj;
}
return result;
}
/**
* Skill Manager - Load and manage skills
*/
export class SkillManager {
private skills: Map<string, Skill> = new Map();
private discoveryPaths: string[] = [];
constructor(options: SkillDiscoveryOptions = {}) {
this.setupDiscoveryPaths(options);
}
private setupDiscoveryPaths(options: SkillDiscoveryOptions): void {
if (options.paths) {
this.discoveryPaths.push(...options.paths);
}
if (options.includeProject !== false) {
this.discoveryPaths.push('./.praison/skills');
this.discoveryPaths.push('./.claude/skills');
}
if (options.includeUser !== false) {
const home = process.env.HOME || process.env.USERPROFILE || '';
if (home) {
this.discoveryPaths.push(path.join(home, '.praison/skills'));
}
}
if (options.includeSystem) {
this.discoveryPaths.push('/etc/praison/skills');
}
}
/**
* Discover and load skills from configured paths
*/
async discover(): Promise<Skill[]> {
const discovered: Skill[] = [];
for (const basePath of this.discoveryPaths) {
if (!fs.existsSync(basePath)) continue;
const entries = fs.readdirSync(basePath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const skillPath = path.join(basePath, entry.name, 'SKILL.md');
if (fs.existsSync(skillPath)) {
try {
const skill = await this.loadSkill(skillPath);
skill.path = path.join(basePath, entry.name);
discovered.push(skill);
this.skills.set(skill.metadata.name, skill);
} catch (e) {
// Skip invalid skills
}
}
}
}
}
return discovered;
}
/**
* Load a skill from a SKILL.md file
*/
async loadSkill(skillPath: string): Promise<Skill> {
const content = fs.readFileSync(skillPath, 'utf-8');
return parseSkillFile(content);
}
/**
* Register a skill manually
*/
register(skill: Skill): void {
this.skills.set(skill.metadata.name, skill);
}
/**
* Get a skill by name
*/
get(name: string): Skill | undefined {
return this.skills.get(name);
}
/**
* List all loaded skills
*/
list(): Skill[] {
return Array.from(this.skills.values());
}
/**
* Generate XML prompt for skills
*/
generatePrompt(skillNames?: string[]): string {
const skillsToInclude = skillNames
? skillNames.map(n => this.skills.get(n)).filter(Boolean) as Skill[]
: this.list();
if (skillsToInclude.length === 0) return '';
const parts = ['<skills>'];
for (const skill of skillsToInclude) {
parts.push(` <skill name="${skill.metadata.name}">`);
parts.push(` <description>${skill.metadata.description}</description>`);
parts.push(` <instructions>${skill.instructions}</instructions>`);
parts.push(' </skill>');
}
parts.push('</skills>');
return parts.join('\n');
}
/**
* Validate a skill
*/
validate(skill: Skill): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!skill.metadata.name) {
errors.push('Missing required field: name');
} else if (!/^[a-z0-9-]{1,64}$/.test(skill.metadata.name)) {
errors.push('Invalid name: must be 1-64 lowercase chars, numbers, or hyphens');
}
if (!skill.metadata.description) {
errors.push('Missing required field: description');
} else if (skill.metadata.description.length > 1024) {
errors.push('Description too long: max 1024 characters');
}
if (skill.instructions.length > 5000) {
errors.push('Instructions too long: max 5000 characters');
}
return { valid: errors.length === 0, errors };
}
}
/**
* Create a skill manager
*/
export function createSkillManager(options?: SkillDiscoveryOptions): SkillManager {
return new SkillManager(options);
}
+225
View File
@@ -0,0 +1,225 @@
/**
* Tool Decorator and Registry - Type-safe tool creation and management
*/
export interface ToolParameters {
type: 'object';
properties: Record<string, {
type: string;
description?: string;
enum?: string[];
default?: any;
}>;
required?: string[];
}
export interface ToolConfig<TParams = any, TResult = any> {
name: string;
description?: string;
parameters?: ToolParameters | any; // Support Zod schemas
category?: string;
execute: (params: TParams, context?: ToolContext) => Promise<TResult> | TResult;
}
export interface ToolContext {
agentName?: string;
sessionId?: string;
runId?: string;
signal?: AbortSignal;
}
export interface ToolDefinition {
name: string;
description: string;
parameters: ToolParameters;
category?: string;
}
/**
* Tool class - Represents an executable tool
*/
export class FunctionTool<TParams = any, TResult = any> {
readonly name: string;
readonly description: string;
readonly parameters: ToolParameters;
readonly category?: string;
private readonly executeFn: (params: TParams, context?: ToolContext) => Promise<TResult> | TResult;
constructor(config: ToolConfig<TParams, TResult>) {
this.name = config.name;
this.description = config.description || `Function ${config.name}`;
this.parameters = this.normalizeParameters(config.parameters);
this.category = config.category;
this.executeFn = config.execute;
}
private normalizeParameters(params: any): ToolParameters {
if (!params) {
return { type: 'object', properties: {}, required: [] };
}
// Check if it's a Zod schema
if (params && typeof params.parse === 'function' && typeof params._def === 'object') {
return this.zodToJsonSchema(params);
}
// Already a JSON schema
return params;
}
private zodToJsonSchema(zodSchema: any): ToolParameters {
// Basic Zod to JSON Schema conversion
// For full support, use zod-to-json-schema package
const def = zodSchema._def;
if (def.typeName === 'ZodObject') {
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(def.shape())) {
const fieldDef = (value as any)._def;
properties[key] = this.zodFieldToJsonSchema(fieldDef);
// Check if field is required (not optional)
if (fieldDef.typeName !== 'ZodOptional' && fieldDef.typeName !== 'ZodDefault') {
required.push(key);
}
}
return { type: 'object', properties, required };
}
return { type: 'object', properties: {}, required: [] };
}
private zodFieldToJsonSchema(def: any): any {
const typeName = def.typeName;
switch (typeName) {
case 'ZodString':
return { type: 'string', description: def.description };
case 'ZodNumber':
return { type: 'number', description: def.description };
case 'ZodBoolean':
return { type: 'boolean', description: def.description };
case 'ZodArray':
return { type: 'array', items: this.zodFieldToJsonSchema(def.type._def) };
case 'ZodEnum':
return { type: 'string', enum: def.values };
case 'ZodOptional':
return this.zodFieldToJsonSchema(def.innerType._def);
case 'ZodDefault':
const inner = this.zodFieldToJsonSchema(def.innerType._def);
inner.default = def.defaultValue();
return inner;
default:
return { type: 'string' };
}
}
async execute(params: TParams, context?: ToolContext): Promise<TResult> {
// Validate parameters if we have a schema
// For now, just execute - validation can be added later
return this.executeFn(params, context);
}
/**
* Get the tool definition for LLM
*/
getDefinition(): ToolDefinition {
return {
name: this.name,
description: this.description,
parameters: this.parameters,
category: this.category,
};
}
/**
* Get OpenAI-compatible tool format
*/
toOpenAITool(): { type: 'function'; function: { name: string; description: string; parameters: any } } {
return {
type: 'function',
function: {
name: this.name,
description: this.description,
parameters: this.parameters,
},
};
}
}
/**
* Create a tool from a configuration object
*/
export function tool<TParams = any, TResult = any>(
config: ToolConfig<TParams, TResult>
): FunctionTool<TParams, TResult> {
return new FunctionTool(config);
}
/**
* Tool Registry - Manages tool registration and lookup
*/
export class ToolRegistry {
private tools: Map<string, FunctionTool> = new Map();
register(tool: FunctionTool, options?: { overwrite?: boolean }): this {
if (this.tools.has(tool.name) && !options?.overwrite) {
throw new Error(`Tool '${tool.name}' is already registered. Use { overwrite: true } to replace.`);
}
this.tools.set(tool.name, tool);
return this;
}
get(name: string): FunctionTool | undefined {
return this.tools.get(name);
}
has(name: string): boolean {
return this.tools.has(name);
}
list(): FunctionTool[] {
return Array.from(this.tools.values());
}
getByCategory(category: string): FunctionTool[] {
return this.list().filter(t => t.category === category);
}
getDefinitions(): ToolDefinition[] {
return this.list().map(t => t.getDefinition());
}
toOpenAITools(): Array<{ type: 'function'; function: any }> {
return this.list().map(t => t.toOpenAITool());
}
delete(name: string): boolean {
return this.tools.delete(name);
}
clear(): void {
this.tools.clear();
}
}
// Global registry instance
let globalRegistry: ToolRegistry | null = null;
export function getRegistry(): ToolRegistry {
if (!globalRegistry) {
globalRegistry = new ToolRegistry();
}
return globalRegistry;
}
export function registerTool(tool: FunctionTool, options?: { overwrite?: boolean }): void {
getRegistry().register(tool, options);
}
export function getTool(name: string): FunctionTool | undefined {
return getRegistry().get(name);
}
+3
View File
@@ -19,6 +19,9 @@ export class BaseTool implements Tool {
}
}
// Export decorator and registry
export * from './decorator';
// Export all tool modules
export * from './arxivTools';
export * from './mcpSse';
+300
View File
@@ -0,0 +1,300 @@
/**
* Workflows - Pipeline and orchestration patterns
*/
import { randomUUID } from 'crypto';
export type StepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
export interface StepResult<T = any> {
stepId: string;
stepName: string;
status: StepStatus;
output?: T;
error?: Error;
duration: number;
startedAt: number;
completedAt?: number;
}
export interface WorkflowContext {
workflowId: string;
stepResults: Map<string, StepResult>;
metadata: Record<string, any>;
get<T = any>(stepName: string): T | undefined;
set(key: string, value: any): void;
}
export type StepFunction<TInput = any, TOutput = any> = (
input: TInput,
context: WorkflowContext
) => Promise<TOutput> | TOutput;
export interface WorkflowStepConfig<TInput = any, TOutput = any> {
name: string;
execute: StepFunction<TInput, TOutput>;
condition?: (context: WorkflowContext) => boolean;
onError?: 'fail' | 'skip' | 'retry';
maxRetries?: number;
timeout?: number;
}
/**
* Workflow Step
*/
export class WorkflowStep<TInput = any, TOutput = any> {
readonly id: string;
readonly name: string;
readonly execute: StepFunction<TInput, TOutput>;
readonly condition?: (context: WorkflowContext) => boolean;
readonly onError: 'fail' | 'skip' | 'retry';
readonly maxRetries: number;
readonly timeout?: number;
constructor(config: WorkflowStepConfig<TInput, TOutput>) {
this.id = randomUUID();
this.name = config.name;
this.execute = config.execute;
this.condition = config.condition;
this.onError = config.onError || 'fail';
this.maxRetries = config.maxRetries || 0;
this.timeout = config.timeout;
}
async run(input: TInput, context: WorkflowContext): Promise<StepResult<TOutput>> {
const startedAt = Date.now();
// Check condition
if (this.condition && !this.condition(context)) {
return {
stepId: this.id,
stepName: this.name,
status: 'skipped',
duration: 0,
startedAt,
completedAt: Date.now(),
};
}
let lastError: Error | undefined;
let attempts = 0;
while (attempts <= this.maxRetries) {
attempts++;
try {
const output = await this.executeWithTimeout(input, context);
const completedAt = Date.now();
return {
stepId: this.id,
stepName: this.name,
status: 'completed',
output,
duration: completedAt - startedAt,
startedAt,
completedAt,
};
} catch (error: any) {
lastError = error;
if (this.onError === 'skip') {
return {
stepId: this.id,
stepName: this.name,
status: 'skipped',
error: lastError,
duration: Date.now() - startedAt,
startedAt,
completedAt: Date.now(),
};
}
if (this.onError !== 'retry' || attempts > this.maxRetries) {
break;
}
}
}
return {
stepId: this.id,
stepName: this.name,
status: 'failed',
error: lastError,
duration: Date.now() - startedAt,
startedAt,
completedAt: Date.now(),
};
}
private async executeWithTimeout(input: TInput, context: WorkflowContext): Promise<TOutput> {
if (!this.timeout) {
return this.execute(input, context);
}
return Promise.race([
this.execute(input, context),
new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Step ${this.name} timed out after ${this.timeout}ms`)), this.timeout);
}),
]);
}
}
/**
* Create a workflow context
*/
function createContext(workflowId: string): WorkflowContext {
const stepResults = new Map<string, StepResult>();
const metadata: Record<string, any> = {};
return {
workflowId,
stepResults,
metadata,
get<T = any>(stepName: string): T | undefined {
const result = stepResults.get(stepName);
return result?.output as T | undefined;
},
set(key: string, value: any): void {
metadata[key] = value;
},
};
}
/**
* Workflow - Sequential pipeline execution
*/
export class Workflow<TInput = any, TOutput = any> {
readonly id: string;
readonly name: string;
private steps: WorkflowStep[] = [];
constructor(name: string) {
this.id = randomUUID();
this.name = name;
}
/**
* Add a step to the workflow
*/
addStep<TStepInput = any, TStepOutput = any>(
config: WorkflowStepConfig<TStepInput, TStepOutput>
): this {
this.steps.push(new WorkflowStep(config));
return this;
}
/**
* Add a step using a simpler syntax
*/
step<TStepInput = any, TStepOutput = any>(
name: string,
execute: StepFunction<TStepInput, TStepOutput>
): this {
return this.addStep({ name, execute });
}
/**
* Run the workflow
*/
async run(input: TInput): Promise<{ output: TOutput | undefined; results: StepResult[]; context: WorkflowContext }> {
const context = createContext(this.id);
const results: StepResult[] = [];
let currentInput: any = input;
for (const step of this.steps) {
const result = await step.run(currentInput, context);
results.push(result);
context.stepResults.set(step.name, result);
if (result.status === 'failed') {
return { output: undefined, results, context };
}
if (result.status === 'completed') {
currentInput = result.output;
}
}
const lastResult = results[results.length - 1];
return {
output: lastResult?.output as TOutput | undefined,
results,
context,
};
}
/**
* Get step count
*/
get stepCount(): number {
return this.steps.length;
}
}
/**
* Pipeline - Alias for Workflow
*/
export const Pipeline = Workflow;
/**
* Parallel execution helper
*/
export async function parallel<T>(
tasks: Array<() => Promise<T>>
): Promise<T[]> {
return Promise.all(tasks.map(task => task()));
}
/**
* Route helper - Execute based on condition
*/
export async function route<T>(
conditions: Array<{ condition: () => boolean; execute: () => Promise<T> }>,
defaultExecute?: () => Promise<T>
): Promise<T | undefined> {
for (const { condition, execute } of conditions) {
if (condition()) {
return execute();
}
}
return defaultExecute?.();
}
/**
* Loop helper - Repeat until condition
*/
export async function loop<T>(
execute: (iteration: number) => Promise<T>,
shouldContinue: (result: T, iteration: number) => boolean,
maxIterations: number = 100
): Promise<T[]> {
const results: T[] = [];
let iteration = 0;
while (iteration < maxIterations) {
const result = await execute(iteration);
results.push(result);
if (!shouldContinue(result, iteration)) {
break;
}
iteration++;
}
return results;
}
/**
* Repeat helper - Execute N times
*/
export async function repeat<T>(
execute: (iteration: number) => Promise<T>,
times: number
): Promise<T[]> {
const results: T[] = [];
for (let i = 0; i < times; i++) {
results.push(await execute(i));
}
return results;
}
@@ -0,0 +1,398 @@
#!/usr/bin/env npx ts-node
/**
* Comprehensive Feature Verification Script
* Tests all implemented features with real API keys
*/
import {
// Providers
createProvider,
isProviderAvailable,
getAvailableProviders,
// Session
Session,
SessionManager,
getSessionManager,
// Tools
tool,
ToolRegistry,
getRegistry,
// Database
MemoryDbAdapter,
db,
// Workflows
Workflow,
parallel,
route,
loop,
// Guardrails
guardrail,
GuardrailManager,
builtinGuardrails,
// Knowledge Base
KnowledgeBase,
createKnowledgeBase,
// Evaluation
accuracyEval,
performanceEval,
reliabilityEval,
EvalSuite,
// Observability
MemoryObservabilityAdapter,
ConsoleObservabilityAdapter,
// Skills
SkillManager,
parseSkillFile,
// Router
RouterAgent,
createRouter,
routeConditions,
// Handoff
Handoff,
handoff,
handoffFilters,
} from '../../src';
console.log('='.repeat(60));
console.log('PraisonAI TypeScript - Comprehensive Feature Verification');
console.log('='.repeat(60));
let passed = 0;
let failed = 0;
async function test(name: string, fn: () => Promise<void>) {
try {
await fn();
console.log(`✅ ${name}`);
passed++;
} catch (error: any) {
console.log(`❌ ${name}: ${error.message}`);
failed++;
}
}
async function main() {
// 1. Provider Tests
console.log('\n--- 1. LLM Providers ---');
await test('Provider availability check', async () => {
const available = getAvailableProviders();
if (!available.includes('openai')) throw new Error('OpenAI not available');
});
await test('OpenAI generateText', async () => {
const provider = createProvider('openai/gpt-4o-mini');
const result = await provider.generateText({
messages: [{ role: 'user', content: 'Say "test" only' }]
});
if (!result.text) throw new Error('No response');
});
await test('OpenAI streaming', async () => {
const provider = createProvider('openai/gpt-4o-mini');
let text = '';
const stream = await provider.streamText({
messages: [{ role: 'user', content: 'Say "ok"' }],
onToken: (t) => { text += t; }
});
// Consume the stream
for await (const chunk of stream) {
if (chunk.text) text += chunk.text;
}
if (!text) throw new Error('No streamed text');
});
await test('Anthropic generateText', async () => {
const provider = createProvider('anthropic/claude-sonnet-4-20250514');
const result = await provider.generateText({
messages: [{ role: 'user', content: 'Say "test" only' }]
});
if (!result.text) throw new Error('No response');
});
// 2. Session Tests
console.log('\n--- 2. Session Management ---');
await test('Session creation', async () => {
const session = new Session();
if (!session.id) throw new Error('No session ID');
});
await test('Session messages', async () => {
const session = new Session();
session.addMessage({ role: 'user', content: 'Hello' });
session.addMessage({ role: 'assistant', content: 'Hi' });
const messages = session.getMessagesForLLM();
if (messages.length !== 2) throw new Error('Wrong message count');
});
await test('Session runs', async () => {
const session = new Session();
const run = session.createRun().start();
run.complete();
if (run.status !== 'completed') throw new Error('Run not completed');
});
await test('SessionManager', async () => {
const manager = getSessionManager();
const session = manager.create({ id: 'test-session' });
const retrieved = manager.get('test-session');
if (!retrieved) throw new Error('Session not found');
manager.delete('test-session');
});
// 3. Tool Tests
console.log('\n--- 3. Tool System ---');
await test('Tool creation', async () => {
const myTool = tool({
name: 'test_tool',
description: 'A test tool',
execute: async () => 'result'
});
const result = await myTool.execute({});
if (result !== 'result') throw new Error('Wrong result');
});
await test('Tool registry', async () => {
const registry = new ToolRegistry();
registry.register(tool({ name: 'tool1', execute: async () => '1' }));
registry.register(tool({ name: 'tool2', execute: async () => '2' }));
if (registry.list().length !== 2) throw new Error('Wrong tool count');
});
await test('OpenAI tool format', async () => {
const myTool = tool({
name: 'calculator',
description: 'Calculate',
parameters: { type: 'object', properties: { expr: { type: 'string' } } },
execute: async () => '42'
});
const format = myTool.toOpenAITool();
if (format.type !== 'function') throw new Error('Wrong format');
});
// 4. Database Tests
console.log('\n--- 4. Database Adapters ---');
await test('Memory adapter', async () => {
const adapter = new MemoryDbAdapter();
await adapter.connect();
await adapter.createSession({ id: 'test', createdAt: Date.now(), updatedAt: Date.now() });
const session = await adapter.getSession('test');
if (!session) throw new Error('Session not found');
await adapter.disconnect();
});
await test('DB factory', async () => {
const adapter = db({ type: 'memory' });
await adapter.connect();
if (!adapter.isConnected()) throw new Error('Not connected');
await adapter.disconnect();
});
// 5. Workflow Tests
console.log('\n--- 5. Workflows ---');
await test('Basic workflow', async () => {
const workflow = new Workflow<number, number>('test')
.step('add', async (n) => n + 10)
.step('multiply', async (n) => n * 2);
const { output } = await workflow.run(5);
if (output !== 30) throw new Error(`Expected 30, got ${output}`);
});
await test('Parallel execution', async () => {
const results = await parallel([
async () => 1,
async () => 2,
async () => 3
]);
if (results.length !== 3) throw new Error('Wrong result count');
});
await test('Route helper', async () => {
const result = await route([
{ condition: () => false, execute: async () => 'a' },
{ condition: () => true, execute: async () => 'b' }
]);
if (result !== 'b') throw new Error('Wrong route');
});
// 6. Guardrails Tests
console.log('\n--- 6. Guardrails ---');
await test('Basic guardrail', async () => {
const g = guardrail({
name: 'test',
check: (content) => ({ status: content.length > 0 ? 'passed' : 'failed' })
});
const result = await g.run('hello');
if (result.status !== 'passed') throw new Error('Should pass');
});
await test('Built-in guardrails', async () => {
const maxLen = builtinGuardrails.maxLength(10);
const result = await maxLen.run('hello');
if (result.status !== 'passed') throw new Error('Should pass');
});
await test('Guardrail manager', async () => {
const manager = new GuardrailManager();
manager.add(builtinGuardrails.minLength(1));
manager.add(builtinGuardrails.maxLength(100));
const { passed } = await manager.runAll('test');
if (!passed) throw new Error('Should pass');
});
// 7. Knowledge Base Tests
console.log('\n--- 7. Knowledge Base (RAG) ---');
await test('Knowledge base add/search', async () => {
const kb = createKnowledgeBase();
await kb.add({ id: 'doc1', content: 'TypeScript is great' });
await kb.add({ id: 'doc2', content: 'Python is also great' });
const results = await kb.search('TypeScript');
if (results.length === 0) throw new Error('No results');
});
await test('Knowledge base context', async () => {
const kb = createKnowledgeBase();
await kb.add({ id: 'doc1', content: 'Test document' });
const results = await kb.search('test');
const context = kb.buildContext(results);
if (!context.includes('Test document')) throw new Error('Wrong context');
});
// 8. Evaluation Tests
console.log('\n--- 8. Evaluation Framework ---');
await test('Accuracy eval', async () => {
const result = await accuracyEval({
input: 'test',
expectedOutput: 'hello world',
actualOutput: 'hello world'
});
if (!result.passed) throw new Error('Should pass');
});
await test('Performance eval', async () => {
const result = await performanceEval({
func: async () => 'done',
iterations: 3,
warmupRuns: 1
});
if (result.times.length !== 3) throw new Error('Wrong iteration count');
});
await test('Reliability eval', async () => {
const result = await reliabilityEval({
expectedToolCalls: ['a', 'b'],
actualToolCalls: ['a', 'b']
});
if (!result.passed) throw new Error('Should pass');
});
await test('Eval suite', async () => {
const suite = new EvalSuite();
await suite.runAccuracy('test', {
input: 'x',
expectedOutput: 'same',
actualOutput: 'same'
});
const summary = suite.getSummary();
if (summary.total !== 1) throw new Error('Wrong count');
});
// 9. Observability Tests
console.log('\n--- 9. Observability ---');
await test('Memory observability', async () => {
const adapter = new MemoryObservabilityAdapter();
const trace = adapter.startTrace('test');
const span = trace.startSpan('operation', 'custom');
span.end();
trace.end();
const traceData = adapter.getTrace(trace.traceId);
if (!traceData) throw new Error('Trace not found');
});
// 10. Skills Tests
console.log('\n--- 10. Skills System ---');
await test('Parse skill file', async () => {
const content = `---
name: test-skill
description: A test skill
---
# Instructions
Do something`;
const skill = parseSkillFile(content);
if (skill.metadata.name !== 'test-skill') throw new Error('Wrong name');
});
await test('Skill manager', async () => {
const manager = new SkillManager({ paths: [] });
manager.register({
metadata: { name: 'manual-skill', description: 'Manual' },
instructions: 'Do this'
});
const skill = manager.get('manual-skill');
if (!skill) throw new Error('Skill not found');
});
// 11. Router Tests
console.log('\n--- 11. Router Agent ---');
await test('Route conditions', async () => {
const kw = routeConditions.keywords(['math', 'calc']);
if (!kw('I need math help')) throw new Error('Should match');
if (kw('I need code help')) throw new Error('Should not match');
});
await test('Router creation', async () => {
const router = createRouter({
routes: [],
name: 'TestRouter'
});
if (router.name !== 'TestRouter') throw new Error('Wrong name');
});
// 12. Handoff Tests
console.log('\n--- 12. Handoff System ---');
await test('Handoff filters', async () => {
const topicFilter = handoffFilters.topic(['billing', 'payment']);
if (!topicFilter({ messages: [], lastMessage: 'I have a billing question' })) {
throw new Error('Should match');
}
});
await test('Handoff creation', async () => {
const mockAgent = { name: 'SupportAgent', chat: async () => ({ text: 'ok' }) } as any;
const h = handoff({ agent: mockAgent, name: 'support_handoff' });
if (h.name !== 'support_handoff') throw new Error('Wrong name');
});
// Summary
console.log('\n' + '='.repeat(60));
console.log(`RESULTS: ${passed} passed, ${failed} failed`);
console.log('='.repeat(60));
if (failed > 0) {
process.exit(1);
}
}
main().catch(console.error);
@@ -0,0 +1,150 @@
/**
* Direct verification script for provider implementation
* Run with: npx ts-node tests/development/verify-providers.ts
*/
import { createProvider } from '../../src/llm/providers';
async function testOpenAI() {
console.log('\n=== Testing OpenAI Provider ===');
if (!process.env.OPENAI_API_KEY) {
console.log('OPENAI_API_KEY not set, skipping');
return;
}
const provider = createProvider('openai/gpt-4o-mini');
// Test generateText
console.log('\n1. Testing generateText...');
const result = await provider.generateText({
messages: [
{ role: 'system', content: 'Be concise.' },
{ role: 'user', content: 'Say hello in one word.' },
],
maxTokens: 10,
temperature: 0,
});
console.log('Response:', result.text);
console.log('Usage:', result.usage);
// Test streaming
console.log('\n2. Testing streamText...');
let streamedText = '';
const stream = await provider.streamText({
messages: [{ role: 'user', content: 'Count 1 to 3' }],
maxTokens: 20,
onToken: (token) => process.stdout.write(token),
});
for await (const chunk of stream) {
if (chunk.text) streamedText += chunk.text;
}
console.log('\nStreamed text:', streamedText);
// Test tool calling
console.log('\n3. Testing tool calling...');
const toolResult = await provider.generateText({
messages: [{ role: 'user', content: 'What is 5+3? Use calculator.' }],
tools: [{
name: 'calculator',
description: 'Math calculator',
parameters: {
type: 'object',
properties: { expression: { type: 'string' } },
required: ['expression'],
},
}],
maxTokens: 100,
});
console.log('Tool calls:', toolResult.toolCalls);
// Test structured output
console.log('\n4. Testing structured output...');
const structuredResult = await provider.generateObject({
messages: [{ role: 'user', content: 'Extract: Alice is 25 years old' }],
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name', 'age'],
additionalProperties: false,
},
maxTokens: 100,
});
console.log('Structured output:', structuredResult.object);
console.log('\n✅ OpenAI Provider tests passed!');
}
async function testAnthropic() {
console.log('\n=== Testing Anthropic Provider ===');
if (!process.env.ANTHROPIC_API_KEY) {
console.log('ANTHROPIC_API_KEY not set, skipping');
return;
}
try {
const provider = createProvider('anthropic/claude-sonnet-4-20250514');
console.log('\n1. Testing generateText...');
const result = await provider.generateText({
messages: [
{ role: 'user', content: 'Say hello in one word.' },
],
maxTokens: 10,
});
console.log('Response:', result.text);
console.log('Usage:', result.usage);
console.log('\n✅ Anthropic Provider tests passed!');
} catch (error: any) {
console.log('Anthropic test failed (may be API key issue):', error.message);
}
}
async function testGoogle() {
console.log('\n=== Testing Google Provider ===');
if (!process.env.GOOGLE_API_KEY) {
console.log('GOOGLE_API_KEY not set, skipping');
return;
}
try {
const provider = createProvider('google/gemini-2.0-flash');
console.log('\n1. Testing generateText...');
const result = await provider.generateText({
messages: [
{ role: 'user', content: 'Say hello in one word.' },
],
maxTokens: 10,
});
console.log('Response:', result.text);
console.log('Usage:', result.usage);
console.log('\n✅ Google Provider tests passed!');
} catch (error: any) {
console.log('Google test failed:', error.message);
}
}
async function main() {
console.log('Provider Verification Script');
console.log('============================');
try {
await testOpenAI();
await testAnthropic();
await testGoogle();
console.log('\n🎉 All provider tests completed!');
} catch (error) {
console.error('\n❌ Error:', error);
process.exit(1);
}
}
main();
@@ -0,0 +1,121 @@
/**
* OpenAI Provider Integration Tests - Real API calls
* Run with: PRAISONAI_TEST_REAL_KEYS=1 npm test -- --testPathPattern=integration/llm
*/
import { describe, it, expect, beforeAll } from '@jest/globals';
import { createProvider, OpenAIProvider } from '../../../src/llm/providers';
const SKIP_REAL_TESTS = !process.env.PRAISONAI_TEST_REAL_KEYS;
const HAS_OPENAI_KEY = !!process.env.OPENAI_API_KEY;
describe('OpenAI Provider Integration', () => {
beforeAll(() => {
if (SKIP_REAL_TESTS) {
console.log('Skipping real API tests. Set PRAISONAI_TEST_REAL_KEYS=1 to enable.');
}
if (!HAS_OPENAI_KEY) {
console.log('OPENAI_API_KEY not set.');
}
});
it('should generate text', async () => {
if (SKIP_REAL_TESTS || !HAS_OPENAI_KEY) return;
const provider = createProvider('openai/gpt-4o-mini');
const result = await provider.generateText({
messages: [
{ role: 'system', content: 'You are a helpful assistant. Be concise.' },
{ role: 'user', content: 'Say "Hello" and nothing else.' },
],
maxTokens: 10,
temperature: 0,
});
expect(result.text).toBeDefined();
expect(result.text.toLowerCase()).toContain('hello');
expect(result.usage.promptTokens).toBeGreaterThan(0);
expect(result.usage.completionTokens).toBeGreaterThan(0);
console.log('OpenAI generateText result:', result.text.substring(0, 50));
});
it('should stream text', async () => {
if (SKIP_REAL_TESTS || !HAS_OPENAI_KEY) return;
const provider = createProvider('openai/gpt-4o-mini');
const tokens: string[] = [];
const stream = await provider.streamText({
messages: [
{ role: 'user', content: 'Count from 1 to 3, one number per line.' },
],
maxTokens: 20,
onToken: (token) => tokens.push(token),
});
let fullText = '';
for await (const chunk of stream) {
if (chunk.text) fullText += chunk.text;
}
expect(tokens.length).toBeGreaterThan(0);
expect(fullText).toContain('1');
console.log('OpenAI streamText tokens:', tokens.length);
});
it('should call tools', async () => {
if (SKIP_REAL_TESTS || !HAS_OPENAI_KEY) return;
const provider = createProvider('openai/gpt-4o-mini');
const result = await provider.generateText({
messages: [
{ role: 'user', content: 'What is 2 + 2? Use the calculator tool.' },
],
tools: [{
name: 'calculator',
description: 'Perform math calculations',
parameters: {
type: 'object',
properties: {
expression: { type: 'string', description: 'Math expression to evaluate' },
},
required: ['expression'],
},
}],
maxTokens: 100,
});
expect(result.toolCalls).toBeDefined();
expect(result.toolCalls!.length).toBeGreaterThan(0);
expect(result.toolCalls![0].function.name).toBe('calculator');
console.log('OpenAI tool call:', result.toolCalls![0].function);
});
it('should generate structured output', async () => {
if (SKIP_REAL_TESTS || !HAS_OPENAI_KEY) return;
const provider = createProvider('openai/gpt-4o-mini');
const result = await provider.generateObject({
messages: [
{ role: 'user', content: 'Extract: John is 30 years old and lives in New York.' },
],
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
city: { type: 'string' },
},
required: ['name', 'age', 'city'],
additionalProperties: false,
},
maxTokens: 100,
});
expect(result.object).toBeDefined();
expect(result.object.name).toBe('John');
expect(result.object.age).toBe(30);
expect(result.object.city).toBe('New York');
console.log('OpenAI structured output:', result.object);
});
});
@@ -0,0 +1,135 @@
/**
* Real Provider Integration Tests - Tests with actual API calls
* These tests require API keys and are gated by PRAISONAI_TEST_REAL_KEYS env var
*/
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
// Skip all tests if real keys not enabled
const SKIP_REAL_TESTS = !process.env.PRAISONAI_TEST_REAL_KEYS;
// These imports will be implemented
// import { createProvider } from '../../../src/llm/providers';
describe('Real Provider Integration Tests', () => {
beforeAll(() => {
if (SKIP_REAL_TESTS) {
console.log('Skipping real API tests. Set PRAISONAI_TEST_REAL_KEYS=1 to enable.');
}
});
describe('OpenAI Provider', () => {
const skipIfNoKey = !process.env.OPENAI_API_KEY;
it.skip('should generate text with OpenAI', async () => {
if (SKIP_REAL_TESTS || skipIfNoKey) return;
// const provider = createProvider('openai/gpt-4o-mini');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'Say hello in one word' }],
// maxTokens: 10,
// });
// expect(result.text).toBeDefined();
// expect(result.text.length).toBeGreaterThan(0);
// // Mask output for security
// console.log('OpenAI response length:', result.text.length);
});
it.skip('should stream text with OpenAI', async () => {
if (SKIP_REAL_TESTS || skipIfNoKey) return;
// const provider = createProvider('openai/gpt-4o-mini');
// let tokenCount = 0;
// await provider.streamText({
// messages: [{ role: 'user', content: 'Count to 3' }],
// maxTokens: 20,
// onToken: () => tokenCount++,
// });
// expect(tokenCount).toBeGreaterThan(0);
// console.log('OpenAI streamed tokens:', tokenCount);
});
it.skip('should call tools with OpenAI', async () => {
if (SKIP_REAL_TESTS || skipIfNoKey) return;
// const provider = createProvider('openai/gpt-4o-mini');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'What is 2+2? Use the calculator.' }],
// tools: [{
// name: 'calculator',
// description: 'Perform math',
// parameters: { type: 'object', properties: { expression: { type: 'string' } } },
// }],
// });
// expect(result.toolCalls).toBeDefined();
// expect(result.toolCalls.length).toBeGreaterThan(0);
});
});
describe('Anthropic Provider', () => {
const skipIfNoKey = !process.env.ANTHROPIC_API_KEY;
it.skip('should generate text with Anthropic', async () => {
if (SKIP_REAL_TESTS || skipIfNoKey) return;
// const provider = createProvider('anthropic/claude-3-5-sonnet-latest');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'Say hello in one word' }],
// maxTokens: 10,
// });
// expect(result.text).toBeDefined();
// console.log('Anthropic response length:', result.text.length);
});
it.skip('should stream text with Anthropic', async () => {
if (SKIP_REAL_TESTS || skipIfNoKey) return;
// const provider = createProvider('anthropic/claude-3-5-sonnet-latest');
// let tokenCount = 0;
// await provider.streamText({
// messages: [{ role: 'user', content: 'Count to 3' }],
// maxTokens: 20,
// onToken: () => tokenCount++,
// });
// expect(tokenCount).toBeGreaterThan(0);
});
});
describe('Google Provider', () => {
const skipIfNoKey = !process.env.GOOGLE_API_KEY;
it.skip('should generate text with Google', async () => {
if (SKIP_REAL_TESTS || skipIfNoKey) return;
// const provider = createProvider('google/gemini-2.0-flash');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'Say hello in one word' }],
// maxTokens: 10,
// });
// expect(result.text).toBeDefined();
// console.log('Google response length:', result.text.length);
});
it.skip('should stream text with Google', async () => {
if (SKIP_REAL_TESTS || skipIfNoKey) return;
// const provider = createProvider('google/gemini-2.0-flash');
// let tokenCount = 0;
// await provider.streamText({
// messages: [{ role: 'user', content: 'Count to 3' }],
// maxTokens: 20,
// onToken: () => tokenCount++,
// });
// expect(tokenCount).toBeGreaterThan(0);
});
});
describe('Token Usage Tracking', () => {
it.skip('should track token usage with real API', async () => {
if (SKIP_REAL_TESTS || !process.env.OPENAI_API_KEY) return;
// const provider = createProvider('openai/gpt-4o-mini');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'Hello' }],
// });
// expect(result.usage.promptTokens).toBeGreaterThan(0);
// expect(result.usage.completionTokens).toBeGreaterThan(0);
// console.log('Token usage:', {
// prompt: result.usage.promptTokens,
// completion: result.usage.completionTokens,
// });
});
});
});
@@ -1,116 +0,0 @@
import { Agent } from '../../../../src/agent/agent';
import { Task, Tool } from '../../../../src/types';
jest.mock('../../../../src/llm/openai');
describe('Agent', () => {
let agent: Agent;
beforeEach(() => {
agent = new Agent();
});
describe('configuration', () => {
it('should configure agent with tools', () => {
const tool: Tool = {
name: 'test-tool',
description: 'A test tool',
execute: async () => 'test result'
};
agent.configure({ tools: [tool] });
const tools = agent.getTools();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('test-tool');
});
});
describe('task execution', () => {
it('should execute simple task', async () => {
const task: Task = {
id: 'test-task',
name: 'Test Task',
description: 'A test task',
config: {
priority: 1
},
expected_output: 'test output',
agent: 'test-agent',
dependencies: [],
result: null
};
const result = await agent.execute(task);
expect(result).toBeDefined();
expect(result.id).toBe('test-task');
});
it('should execute complex task', async () => {
const task: Task = {
id: 'complex-task',
name: 'Complex Task',
description: 'A complex task',
config: {
priority: 1
},
expected_output: 'test output',
agent: 'test-agent',
dependencies: [
{
id: 'dep-task',
name: 'Dependency Task',
description: 'A dependency task',
config: {
priority: 1
},
expected_output: 'dependency output',
agent: 'test-agent',
dependencies: [],
result: 'dependency result'
}
],
result: null
};
const result = await agent.execute(task);
expect(result).toBeDefined();
expect(result.id).toBe('complex-task');
});
it('should handle task failure', async () => {
const task: Task = {
id: 'failing-task',
name: 'Failing Task',
description: 'A task that fails',
config: {
priority: 1,
shouldFail: true
},
expected_output: 'test output',
agent: 'test-agent',
dependencies: [],
result: null
};
await expect(agent.execute(task)).rejects.toThrow();
});
});
describe('tool management', () => {
it('should execute tool', async () => {
const tool: Tool = {
name: 'test-tool',
description: 'A test tool',
execute: async () => 'test result'
};
agent.configure({ tools: [tool] });
const result = await agent.executeTool('test-tool', 'test input');
expect(result).toBe('test result');
});
it('should handle unknown tool', async () => {
await expect(agent.executeTool('unknown-tool', 'test input')).rejects.toThrow();
});
});
});
@@ -0,0 +1,107 @@
/**
* Agent Handoff Tests - TDD for agent handoff support
* These tests define the expected behavior for agent-to-agent handoff
*/
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
// These imports will fail initially - TDD approach
// import { Agent, handoff, Handoff } from '../../../src/agent';
describe('Agent Handoff', () => {
describe('Handoff Creation', () => {
it.skip('should create handoff to another agent', () => {
// const targetAgent = new Agent({ name: 'specialist', instructions: 'You are a specialist' });
// const h = handoff({ agent: targetAgent, description: 'Transfer to specialist' });
// expect(h.targetAgent).toBe(targetAgent);
// expect(h.description).toBe('Transfer to specialist');
});
it.skip('should support conditional handoff', () => {
// const targetAgent = new Agent({ name: 'specialist', instructions: 'You are a specialist' });
// const h = handoff({
// agent: targetAgent,
// condition: (context) => context.topic === 'technical',
// });
// expect(h.condition).toBeDefined();
});
});
describe('Handoff Execution', () => {
it.skip('should transfer conversation to target agent', async () => {
// const mainAgent = new Agent({ name: 'main', instructions: 'You are main' });
// const specialistAgent = new Agent({ name: 'specialist', instructions: 'You are specialist' });
// mainAgent.addHandoff(handoff({ agent: specialistAgent }));
// const result = await mainAgent.chat('I need specialist help');
// expect(result.handedOffTo).toBe('specialist');
});
it.skip('should pass conversation history to target agent', async () => {
// const mainAgent = new Agent({ name: 'main', instructions: 'You are main' });
// const specialistAgent = new Agent({ name: 'specialist', instructions: 'You are specialist' });
// mainAgent.addHandoff(handoff({ agent: specialistAgent }));
// await mainAgent.chat('Context message 1');
// await mainAgent.chat('Context message 2');
// await mainAgent.chat('Transfer to specialist');
// // Specialist should have access to previous messages
});
it.skip('should support handoff with context transformation', async () => {
// const mainAgent = new Agent({ name: 'main', instructions: 'You are main' });
// const specialistAgent = new Agent({ name: 'specialist', instructions: 'You are specialist' });
// mainAgent.addHandoff(handoff({
// agent: specialistAgent,
// transformContext: (messages) => messages.slice(-5), // Only last 5 messages
// }));
});
});
describe('Handoff as Tool', () => {
it.skip('should expose handoff as tool for LLM', () => {
// const mainAgent = new Agent({ name: 'main', instructions: 'You are main' });
// const specialistAgent = new Agent({ name: 'specialist', instructions: 'You are specialist' });
// mainAgent.addHandoff(handoff({ agent: specialistAgent, name: 'transfer_to_specialist' }));
// const tools = mainAgent.getTools();
// expect(tools.find(t => t.name === 'transfer_to_specialist')).toBeDefined();
});
});
describe('Multi-Agent Handoff', () => {
it.skip('should support multiple handoff targets', () => {
// const mainAgent = new Agent({ name: 'main', instructions: 'You are main' });
// const techAgent = new Agent({ name: 'tech', instructions: 'You are tech support' });
// const salesAgent = new Agent({ name: 'sales', instructions: 'You are sales' });
// mainAgent.addHandoff(handoff({ agent: techAgent, name: 'tech_support' }));
// mainAgent.addHandoff(handoff({ agent: salesAgent, name: 'sales_inquiry' }));
// expect(mainAgent.handoffs.length).toBe(2);
});
it.skip('should support circular handoff prevention', async () => {
// const agent1 = new Agent({ name: 'agent1', instructions: 'You are agent1' });
// const agent2 = new Agent({ name: 'agent2', instructions: 'You are agent2' });
// agent1.addHandoff(handoff({ agent: agent2 }));
// agent2.addHandoff(handoff({ agent: agent1 }));
// // Should detect and prevent infinite loops
});
});
});
describe('Handoff Filters', () => {
it.skip('should filter handoffs based on context', () => {
// const mainAgent = new Agent({ name: 'main', instructions: 'You are main' });
// const techAgent = new Agent({ name: 'tech', instructions: 'You are tech' });
// mainAgent.addHandoff(handoff({
// agent: techAgent,
// filter: handoffFilters.topic('technical'),
// }));
});
it.skip('should support custom filter functions', () => {
// const mainAgent = new Agent({ name: 'main', instructions: 'You are main' });
// const vipAgent = new Agent({ name: 'vip', instructions: 'You handle VIP' });
// mainAgent.addHandoff(handoff({
// agent: vipAgent,
// filter: (context) => context.metadata?.userTier === 'vip',
// }));
});
});
@@ -0,0 +1,161 @@
/**
* Router Agent Unit Tests
*/
import { describe, it, expect } from '@jest/globals';
import { RouterAgent, createRouter, routeConditions } from '../../../src/agent/router';
// Mock EnhancedAgent for testing
const createMockAgent = (name: string) => ({
name,
chat: async (input: string) => ({ text: `Response from ${name}: ${input}` })
} as any);
describe('RouterAgent', () => {
describe('Basic Routing', () => {
it('should create router with routes', () => {
const router = createRouter({
routes: [
{ agent: createMockAgent('Agent1'), condition: () => true }
]
});
expect(router.name).toBe('Router');
});
it('should route to matching agent', async () => {
const mathAgent = createMockAgent('MathAgent');
const codeAgent = createMockAgent('CodeAgent');
const router = createRouter({
routes: [
{ agent: mathAgent, condition: routeConditions.keywords(['math', 'calculate']) },
{ agent: codeAgent, condition: routeConditions.keywords(['code', 'program']) }
]
});
const result = await router.route('Calculate 2+2');
expect(result?.agent.name).toBe('MathAgent');
});
it('should use default agent when no match', async () => {
const defaultAgent = createMockAgent('DefaultAgent');
const router = createRouter({
routes: [
{ agent: createMockAgent('SpecificAgent'), condition: () => false }
],
defaultAgent
});
const result = await router.route('Random query');
expect(result?.agent.name).toBe('DefaultAgent');
});
it('should return null when no match and no default', async () => {
const router = createRouter({
routes: [
{ agent: createMockAgent('Agent'), condition: () => false }
]
});
const result = await router.route('Random query');
expect(result).toBeNull();
});
});
describe('Priority Routing', () => {
it('should respect priority order', async () => {
const lowPriority = createMockAgent('LowPriority');
const highPriority = createMockAgent('HighPriority');
const router = createRouter({
routes: [
{ agent: lowPriority, condition: () => true, priority: 1 },
{ agent: highPriority, condition: () => true, priority: 10 }
]
});
const result = await router.route('Test');
expect(result?.agent.name).toBe('HighPriority');
});
});
describe('Dynamic Routes', () => {
it('should add routes dynamically', () => {
const router = createRouter({ routes: [] });
router.addRoute({
agent: createMockAgent('NewAgent'),
condition: () => true
});
expect(router.getRoutes().length).toBe(1);
});
});
});
describe('Route Conditions', () => {
describe('keywords', () => {
it('should match single keyword', () => {
const condition = routeConditions.keywords('math');
expect(condition('I need help with math')).toBe(true);
expect(condition('I need help with code')).toBe(false);
});
it('should match multiple keywords', () => {
const condition = routeConditions.keywords(['math', 'calculate']);
expect(condition('Calculate something')).toBe(true);
expect(condition('Math problem')).toBe(true);
expect(condition('Code problem')).toBe(false);
});
it('should be case insensitive', () => {
const condition = routeConditions.keywords('MATH');
expect(condition('math problem')).toBe(true);
});
});
describe('pattern', () => {
it('should match regex pattern', () => {
const condition = routeConditions.pattern(/\d+\s*\+\s*\d+/);
expect(condition('What is 2 + 3?')).toBe(true);
expect(condition('What is two plus three?')).toBe(false);
});
});
describe('metadata', () => {
it('should match metadata value', () => {
const condition = routeConditions.metadata('category', 'tech');
expect(condition('test', { metadata: { category: 'tech' } })).toBe(true);
expect(condition('test', { metadata: { category: 'other' } })).toBe(false);
});
});
describe('always', () => {
it('should always return true', () => {
const condition = routeConditions.always();
expect(condition()).toBe(true);
});
});
describe('and', () => {
it('should combine conditions with AND', () => {
const condition = routeConditions.and(
routeConditions.keywords('math'),
routeConditions.pattern(/\d+/)
);
expect(condition('math 123')).toBe(true);
expect(condition('math problem')).toBe(false);
});
});
describe('or', () => {
it('should combine conditions with OR', () => {
const condition = routeConditions.or(
routeConditions.keywords('math'),
routeConditions.keywords('code')
);
expect(condition('math problem')).toBe(true);
expect(condition('code problem')).toBe(true);
expect(condition('other problem')).toBe(false);
});
});
});
@@ -0,0 +1,183 @@
/**
* Session Management Tests - TDD for session/run/trace tracking
* These tests define the expected behavior for session management
*/
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
// These imports will fail initially - TDD approach
// import { Session, SessionManager } from '../../../src/session';
// import { Agent } from '../../../src/agent';
describe('Session Management', () => {
describe('Session Creation', () => {
it.skip('should create session with auto-generated ID', () => {
// const session = new Session();
// expect(session.id).toBeDefined();
// expect(typeof session.id).toBe('string');
// expect(session.id.length).toBeGreaterThan(0);
});
it.skip('should create session with custom ID', () => {
// const session = new Session({ id: 'my-session' });
// expect(session.id).toBe('my-session');
});
it.skip('should track creation timestamp', () => {
// const before = Date.now();
// const session = new Session();
// const after = Date.now();
// expect(session.createdAt).toBeGreaterThanOrEqual(before);
// expect(session.createdAt).toBeLessThanOrEqual(after);
});
it.skip('should support metadata', () => {
// const session = new Session({ metadata: { userId: 'user-123' } });
// expect(session.metadata.userId).toBe('user-123');
});
});
describe('Run Tracking', () => {
it.skip('should create run within session', () => {
// const session = new Session();
// const run = session.createRun();
// expect(run.id).toBeDefined();
// expect(run.sessionId).toBe(session.id);
});
it.skip('should track run status', () => {
// const session = new Session();
// const run = session.createRun();
// expect(run.status).toBe('pending');
// run.start();
// expect(run.status).toBe('running');
// run.complete();
// expect(run.status).toBe('completed');
});
it.skip('should track run duration', () => {
// const session = new Session();
// const run = session.createRun();
// run.start();
// // Simulate some work
// run.complete();
// expect(run.duration).toBeGreaterThan(0);
});
it.skip('should track run errors', () => {
// const session = new Session();
// const run = session.createRun();
// run.start();
// run.fail(new Error('Test error'));
// expect(run.status).toBe('failed');
// expect(run.error).toBeDefined();
});
});
describe('Trace Tracking', () => {
it.skip('should create trace within run', () => {
// const session = new Session();
// const run = session.createRun();
// const trace = run.createTrace({ name: 'llm_call' });
// expect(trace.id).toBeDefined();
// expect(trace.runId).toBe(run.id);
});
it.skip('should support nested traces (spans)', () => {
// const session = new Session();
// const run = session.createRun();
// const parentTrace = run.createTrace({ name: 'agent_execution' });
// const childTrace = parentTrace.createChild({ name: 'tool_call' });
// expect(childTrace.parentId).toBe(parentTrace.id);
});
it.skip('should track trace attributes', () => {
// const session = new Session();
// const run = session.createRun();
// const trace = run.createTrace({
// name: 'llm_call',
// attributes: { model: 'gpt-4o', tokens: 100 },
// });
// expect(trace.attributes.model).toBe('gpt-4o');
});
});
describe('Agent Integration', () => {
it.skip('should pass session to agent', () => {
// const session = new Session({ id: 'test-session' });
// const agent = new Agent({
// instructions: 'You are helpful',
// session,
// });
// expect(agent.sessionId).toBe('test-session');
});
it.skip('should auto-create session if not provided', () => {
// const agent = new Agent({ instructions: 'You are helpful' });
// expect(agent.sessionId).toBeDefined();
});
it.skip('should create run for each chat call', async () => {
// const session = new Session();
// const agent = new Agent({ instructions: 'You are helpful', session });
// await agent.chat('Hello');
// expect(session.runs.length).toBe(1);
// await agent.chat('World');
// expect(session.runs.length).toBe(2);
});
});
describe('Session Manager', () => {
it.skip('should store and retrieve sessions', () => {
// const manager = new SessionManager();
// const session = manager.create({ id: 'test' });
// const retrieved = manager.get('test');
// expect(retrieved).toBe(session);
});
it.skip('should list all sessions', () => {
// const manager = new SessionManager();
// manager.create({ id: 'session-1' });
// manager.create({ id: 'session-2' });
// const sessions = manager.list();
// expect(sessions.length).toBe(2);
});
it.skip('should delete session', () => {
// const manager = new SessionManager();
// manager.create({ id: 'test' });
// manager.delete('test');
// expect(manager.get('test')).toBeUndefined();
});
});
});
describe('Message History', () => {
it.skip('should track messages in session', async () => {
// const session = new Session();
// const agent = new Agent({ instructions: 'You are helpful', session });
// await agent.chat('Hello');
// expect(session.messages.length).toBe(2); // user + assistant
});
it.skip('should preserve message order', async () => {
// const session = new Session();
// const agent = new Agent({ instructions: 'You are helpful', session });
// await agent.chat('First');
// await agent.chat('Second');
// expect(session.messages[0].content).toBe('First');
// expect(session.messages[2].content).toBe('Second');
});
it.skip('should include tool calls in history', async () => {
// const session = new Session();
// const agent = new Agent({
// instructions: 'You are helpful',
// session,
// tools: [{ name: 'test', execute: () => 'result' }],
// });
// await agent.chat('Use the test tool');
// const toolMessages = session.messages.filter(m => m.role === 'tool');
// expect(toolMessages.length).toBeGreaterThan(0);
});
});
@@ -0,0 +1,169 @@
/**
* Structured Output Tests - TDD for JSON schema / Zod structured outputs
* These tests define the expected behavior for structured output generation
*/
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
// These imports will fail initially - TDD approach
// import { Agent } from '../../../src/agent';
// import { z } from 'zod';
describe('Structured Output', () => {
describe('JSON Schema Output', () => {
it.skip('should generate output matching JSON schema', async () => {
// const agent = new Agent({
// instructions: 'You extract information',
// outputSchema: {
// type: 'object',
// properties: {
// name: { type: 'string' },
// age: { type: 'number' },
// },
// required: ['name', 'age'],
// },
// });
// const result = await agent.chat('John is 30 years old');
// expect(result.structured).toHaveProperty('name');
// expect(result.structured).toHaveProperty('age');
// expect(typeof result.structured.name).toBe('string');
// expect(typeof result.structured.age).toBe('number');
});
it.skip('should validate output against schema', async () => {
// const agent = new Agent({
// instructions: 'You extract information',
// outputSchema: {
// type: 'object',
// properties: {
// email: { type: 'string', format: 'email' },
// },
// required: ['email'],
// },
// });
// // Should validate email format
});
});
describe('Zod Schema Output', () => {
it.skip('should generate output matching Zod schema', async () => {
// const PersonSchema = z.object({
// name: z.string(),
// age: z.number().int().positive(),
// email: z.string().email().optional(),
// });
// const agent = new Agent({
// instructions: 'You extract person information',
// outputSchema: PersonSchema,
// });
// const result = await agent.chat('John Doe is 30 years old');
// expect(result.structured.name).toBe('John Doe');
// expect(result.structured.age).toBe(30);
});
it.skip('should support nested Zod schemas', async () => {
// const AddressSchema = z.object({
// street: z.string(),
// city: z.string(),
// country: z.string(),
// });
// const PersonSchema = z.object({
// name: z.string(),
// address: AddressSchema,
// });
// const agent = new Agent({
// instructions: 'You extract person with address',
// outputSchema: PersonSchema,
// });
});
it.skip('should support array schemas', async () => {
// const ItemSchema = z.object({
// name: z.string(),
// price: z.number(),
// });
// const OrderSchema = z.object({
// items: z.array(ItemSchema),
// total: z.number(),
// });
// const agent = new Agent({
// instructions: 'You extract order information',
// outputSchema: OrderSchema,
// });
});
it.skip('should support enum schemas', async () => {
// const StatusSchema = z.object({
// status: z.enum(['pending', 'approved', 'rejected']),
// reason: z.string().optional(),
// });
// const agent = new Agent({
// instructions: 'You classify status',
// outputSchema: StatusSchema,
// });
});
});
describe('Per-call Output Schema', () => {
it.skip('should support output schema per chat call', async () => {
// const agent = new Agent({ instructions: 'You are helpful' });
// const result = await agent.chat('Extract: John is 30', {
// outputSchema: z.object({
// name: z.string(),
// age: z.number(),
// }),
// });
// expect(result.structured).toHaveProperty('name');
});
it.skip('should override agent-level schema with call-level schema', async () => {
// const agent = new Agent({
// instructions: 'You are helpful',
// outputSchema: z.object({ text: z.string() }),
// });
// const result = await agent.chat('Extract: John is 30', {
// outputSchema: z.object({ name: z.string(), age: z.number() }),
// });
// expect(result.structured).toHaveProperty('name');
// expect(result.structured).not.toHaveProperty('text');
});
});
describe('Streaming with Structured Output', () => {
it.skip('should stream partial structured output', async () => {
// const agent = new Agent({
// instructions: 'You extract information',
// outputSchema: z.object({
// name: z.string(),
// description: z.string(),
// }),
// });
// const partials: any[] = [];
// await agent.chat('Describe John', {
// stream: true,
// onPartialObject: (partial) => partials.push(partial),
// });
// expect(partials.length).toBeGreaterThan(0);
});
});
describe('Error Handling', () => {
it.skip('should retry on invalid output', async () => {
// const agent = new Agent({
// instructions: 'You extract information',
// outputSchema: z.object({ email: z.string().email() }),
// maxRetries: 3,
// });
// // Should retry if LLM returns invalid email format
});
it.skip('should throw after max retries', async () => {
// const agent = new Agent({
// instructions: 'You extract information',
// outputSchema: z.object({ impossible: z.literal('exact-match') }),
// maxRetries: 1,
// });
// await expect(agent.chat('Random text')).rejects.toThrow();
});
});
});
@@ -0,0 +1,122 @@
/**
* Evaluation Framework Unit Tests
*/
import { describe, it, expect } from '@jest/globals';
import { accuracyEval, performanceEval, reliabilityEval, EvalSuite } from '../../../src/eval';
describe('Evaluation Framework', () => {
describe('accuracyEval', () => {
it('should pass for matching output', async () => {
const result = await accuracyEval({
input: 'What is 2+2?',
expectedOutput: 'The answer is 4',
actualOutput: 'The answer is 4'
});
expect(result.passed).toBe(true);
expect(result.score).toBe(1);
});
it('should pass for similar output', async () => {
const result = await accuracyEval({
input: 'What is 2+2?',
expectedOutput: 'The answer is four',
actualOutput: 'The answer is 4',
threshold: 0.5
});
expect(result.passed).toBe(true);
});
it('should fail for different output', async () => {
const result = await accuracyEval({
input: 'What is 2+2?',
expectedOutput: 'The answer is 4',
actualOutput: 'Hello world',
threshold: 0.8
});
expect(result.passed).toBe(false);
});
});
describe('performanceEval', () => {
it('should measure execution time', async () => {
const result = await performanceEval({
func: async () => {
await new Promise(resolve => setTimeout(resolve, 10));
return 'done';
},
iterations: 3,
warmupRuns: 1
});
expect(result.passed).toBe(true);
expect(result.times.length).toBe(3);
expect(result.avgTime).toBeGreaterThan(0);
expect(result.minTime).toBeLessThanOrEqual(result.avgTime);
expect(result.maxTime).toBeGreaterThanOrEqual(result.avgTime);
});
});
describe('reliabilityEval', () => {
it('should pass when all tools called', async () => {
const result = await reliabilityEval({
expectedToolCalls: ['calculator', 'search'],
actualToolCalls: ['calculator', 'search', 'extra']
});
expect(result.passed).toBe(true);
expect(result.score).toBe(1);
});
it('should fail when tools missing', async () => {
const result = await reliabilityEval({
expectedToolCalls: ['calculator', 'search'],
actualToolCalls: ['calculator']
});
expect(result.passed).toBe(false);
expect(result.score).toBe(0.5);
});
it('should track missing and extra tools', async () => {
const result = await reliabilityEval({
expectedToolCalls: ['a', 'b'],
actualToolCalls: ['b', 'c']
});
expect(result.details?.missing).toContain('a');
expect(result.details?.extra).toContain('c');
});
});
describe('EvalSuite', () => {
it('should run multiple evaluations', async () => {
const suite = new EvalSuite();
await suite.runAccuracy('test1', {
input: 'test',
expectedOutput: 'hello world',
actualOutput: 'hello world'
});
await suite.runReliability('test2', {
expectedToolCalls: ['a'],
actualToolCalls: ['a']
});
const summary = suite.getSummary();
expect(summary.total).toBe(2);
expect(summary.passed).toBe(2);
});
it('should calculate average score', async () => {
const suite = new EvalSuite();
await suite.runAccuracy('test1', {
input: 'test',
expectedOutput: 'same',
actualOutput: 'same'
});
const summary = suite.getSummary();
expect(summary.avgScore).toBe(1);
});
});
});
@@ -0,0 +1,200 @@
/**
* Guardrails Unit Tests
*/
import { describe, it, expect } from '@jest/globals';
import { Guardrail, guardrail, GuardrailManager, builtinGuardrails } from '../../../src/guardrails';
describe('Guardrail', () => {
describe('Basic Guardrail', () => {
it('should create guardrail with config', () => {
const g = guardrail({
name: 'test',
description: 'Test guardrail',
check: () => ({ status: 'passed' }),
});
expect(g.name).toBe('test');
expect(g.description).toBe('Test guardrail');
});
it('should run check function', async () => {
const g = guardrail({
name: 'test',
check: (content) => ({
status: content.length > 0 ? 'passed' : 'failed',
}),
});
const result1 = await g.run('hello');
expect(result1.status).toBe('passed');
const result2 = await g.run('');
expect(result2.status).toBe('failed');
});
it('should handle async check', async () => {
const g = guardrail({
name: 'async-test',
check: async (content) => {
await new Promise(resolve => setTimeout(resolve, 10));
return { status: 'passed' };
},
});
const result = await g.run('test');
expect(result.status).toBe('passed');
});
it('should catch errors in check', async () => {
const g = guardrail({
name: 'error-test',
check: () => { throw new Error('Test error'); },
});
const result = await g.run('test');
expect(result.status).toBe('failed');
expect(result.message).toContain('Test error');
});
});
});
describe('GuardrailManager', () => {
it('should add guardrails', () => {
const manager = new GuardrailManager();
manager.add(guardrail({ name: 'g1', check: () => ({ status: 'passed' }) }));
manager.add(guardrail({ name: 'g2', check: () => ({ status: 'passed' }) }));
expect(manager.count).toBe(2);
});
it('should run all guardrails', async () => {
const manager = new GuardrailManager();
manager.add(guardrail({ name: 'g1', check: () => ({ status: 'passed' }) }));
manager.add(guardrail({ name: 'g2', check: () => ({ status: 'passed' }) }));
const { passed, results } = await manager.runAll('test');
expect(passed).toBe(true);
expect(results.length).toBe(2);
});
it('should fail if any guardrail fails', async () => {
const manager = new GuardrailManager();
manager.add(guardrail({ name: 'g1', check: () => ({ status: 'passed' }) }));
manager.add(guardrail({ name: 'g2', check: () => ({ status: 'failed' }) }));
const { passed } = await manager.runAll('test');
expect(passed).toBe(false);
});
it('should stop on block failure', async () => {
const manager = new GuardrailManager();
manager.add(guardrail({ name: 'g1', onFail: 'block', check: () => ({ status: 'failed' }) }));
manager.add(guardrail({ name: 'g2', check: () => ({ status: 'passed' }) }));
const { results } = await manager.runAll('test');
expect(results.length).toBe(1); // Stopped after first failure
});
});
describe('Built-in Guardrails', () => {
describe('maxLength', () => {
it('should pass for content under limit', async () => {
const g = builtinGuardrails.maxLength(10);
const result = await g.run('hello');
expect(result.status).toBe('passed');
});
it('should fail for content over limit', async () => {
const g = builtinGuardrails.maxLength(5);
const result = await g.run('hello world');
expect(result.status).toBe('failed');
});
});
describe('minLength', () => {
it('should pass for content over minimum', async () => {
const g = builtinGuardrails.minLength(3);
const result = await g.run('hello');
expect(result.status).toBe('passed');
});
it('should fail for content under minimum', async () => {
const g = builtinGuardrails.minLength(10);
const result = await g.run('hi');
expect(result.status).toBe('failed');
});
});
describe('blockedWords', () => {
it('should pass when no blocked words', async () => {
const g = builtinGuardrails.blockedWords(['bad', 'evil']);
const result = await g.run('hello world');
expect(result.status).toBe('passed');
});
it('should fail when blocked word found', async () => {
const g = builtinGuardrails.blockedWords(['bad', 'evil']);
const result = await g.run('this is bad');
expect(result.status).toBe('failed');
});
it('should be case insensitive', async () => {
const g = builtinGuardrails.blockedWords(['BAD']);
const result = await g.run('this is bad');
expect(result.status).toBe('failed');
});
});
describe('requiredWords', () => {
it('should pass when all required words present', async () => {
const g = builtinGuardrails.requiredWords(['hello', 'world']);
const result = await g.run('hello world');
expect(result.status).toBe('passed');
});
it('should fail when required word missing', async () => {
const g = builtinGuardrails.requiredWords(['hello', 'world']);
const result = await g.run('hello there');
expect(result.status).toBe('failed');
});
});
describe('pattern', () => {
it('should pass when pattern matches (mustMatch=true)', async () => {
const g = builtinGuardrails.pattern(/\d+/, true);
const result = await g.run('test123');
expect(result.status).toBe('passed');
});
it('should fail when pattern does not match (mustMatch=true)', async () => {
const g = builtinGuardrails.pattern(/\d+/, true);
const result = await g.run('test');
expect(result.status).toBe('failed');
});
it('should pass when pattern does not match (mustMatch=false)', async () => {
const g = builtinGuardrails.pattern(/\d+/, false);
const result = await g.run('test');
expect(result.status).toBe('passed');
});
it('should fail when pattern matches (mustMatch=false)', async () => {
const g = builtinGuardrails.pattern(/\d+/, false);
const result = await g.run('test123');
expect(result.status).toBe('failed');
});
});
describe('validJson', () => {
it('should pass for valid JSON', async () => {
const g = builtinGuardrails.validJson();
const result = await g.run('{"key": "value"}');
expect(result.status).toBe('passed');
});
it('should fail for invalid JSON', async () => {
const g = builtinGuardrails.validJson();
const result = await g.run('not json');
expect(result.status).toBe('failed');
});
});
});
@@ -0,0 +1,108 @@
/**
* Knowledge Base (RAG) Unit Tests
*/
import { describe, it, expect, beforeEach } from '@jest/globals';
import { KnowledgeBase, createKnowledgeBase } from '../../../src/knowledge/rag';
describe('KnowledgeBase', () => {
let kb: KnowledgeBase;
beforeEach(() => {
kb = createKnowledgeBase();
});
describe('Document Operations', () => {
it('should add document', async () => {
const doc = await kb.add({ id: 'doc1', content: 'Hello world' });
expect(doc.id).toBe('doc1');
expect(doc.content).toBe('Hello world');
});
it('should get document by id', async () => {
await kb.add({ id: 'doc1', content: 'Test content' });
const doc = kb.get('doc1');
expect(doc?.content).toBe('Test content');
});
it('should delete document', async () => {
await kb.add({ id: 'doc1', content: 'Test' });
expect(kb.delete('doc1')).toBe(true);
expect(kb.get('doc1')).toBeUndefined();
});
it('should add batch of documents', async () => {
const docs = await kb.addBatch([
{ id: 'doc1', content: 'First' },
{ id: 'doc2', content: 'Second' }
]);
expect(docs.length).toBe(2);
expect(kb.size).toBe(2);
});
it('should list all documents', async () => {
await kb.add({ id: 'doc1', content: 'First' });
await kb.add({ id: 'doc2', content: 'Second' });
const docs = kb.list();
expect(docs.length).toBe(2);
});
it('should clear all documents', async () => {
await kb.add({ id: 'doc1', content: 'Test' });
kb.clear();
expect(kb.size).toBe(0);
});
});
describe('Text Search', () => {
beforeEach(async () => {
await kb.add({ id: 'doc1', content: 'The quick brown fox jumps over the lazy dog' });
await kb.add({ id: 'doc2', content: 'A fast red fox runs through the forest' });
await kb.add({ id: 'doc3', content: 'The cat sleeps on the mat' });
});
it('should search by text', async () => {
const results = await kb.search('fox');
expect(results.length).toBeGreaterThan(0);
expect(results[0].document.content).toContain('fox');
});
it('should limit search results', async () => {
const results = await kb.search('the', 1);
expect(results.length).toBe(1);
});
it('should return empty for no match', async () => {
const results = await kb.search('elephant');
expect(results.length).toBe(0);
});
});
describe('Context Building', () => {
it('should build context from results', async () => {
await kb.add({ id: 'doc1', content: 'First document' });
await kb.add({ id: 'doc2', content: 'Second document' });
const results = await kb.search('document');
const context = kb.buildContext(results);
expect(context).toContain('[1]');
expect(context).toContain('document');
});
it('should return empty string for no results', () => {
const context = kb.buildContext([]);
expect(context).toBe('');
});
});
describe('Metadata', () => {
it('should store document metadata', async () => {
await kb.add({
id: 'doc1',
content: 'Test',
metadata: { author: 'John', category: 'test' }
});
const doc = kb.get('doc1');
expect(doc?.metadata?.author).toBe('John');
});
});
});
@@ -0,0 +1,176 @@
/**
* LLM Provider Tests - TDD for multi-provider support
* These tests define the expected behavior for the provider abstraction layer
*/
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
// These imports will fail initially - TDD approach
// import { LLMProvider, OpenAIProvider, AnthropicProvider, GoogleProvider } from '../../../src/llm/providers';
// import { createProvider, getDefaultProvider } from '../../../src/llm';
describe('LLM Provider Abstraction', () => {
describe('Provider Factory', () => {
it.skip('should create OpenAI provider from model string', () => {
// const provider = createProvider('openai/gpt-4o');
// expect(provider).toBeInstanceOf(OpenAIProvider);
// expect(provider.modelId).toBe('gpt-4o');
});
it.skip('should create Anthropic provider from model string', () => {
// const provider = createProvider('anthropic/claude-3-5-sonnet');
// expect(provider).toBeInstanceOf(AnthropicProvider);
// expect(provider.modelId).toBe('claude-3-5-sonnet');
});
it.skip('should create Google provider from model string', () => {
// const provider = createProvider('google/gemini-2.0-flash');
// expect(provider).toBeInstanceOf(GoogleProvider);
// expect(provider.modelId).toBe('gemini-2.0-flash');
});
it.skip('should default to OpenAI when no provider prefix', () => {
// const provider = createProvider('gpt-4o-mini');
// expect(provider).toBeInstanceOf(OpenAIProvider);
});
it.skip('should throw for unknown provider', () => {
// expect(() => createProvider('unknown/model')).toThrow('Unknown provider: unknown');
});
});
describe('Provider Interface', () => {
it.skip('should implement generateText method', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'Hello' }],
// temperature: 0.7,
// });
// expect(result).toHaveProperty('text');
// expect(typeof result.text).toBe('string');
});
it.skip('should implement streamText method', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const stream = await provider.streamText({
// messages: [{ role: 'user', content: 'Hello' }],
// });
// expect(stream).toHaveProperty(Symbol.asyncIterator);
});
it.skip('should implement generateObject method for structured output', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const schema = { type: 'object', properties: { name: { type: 'string' } } };
// const result = await provider.generateObject({
// messages: [{ role: 'user', content: 'Generate a name' }],
// schema,
// });
// expect(result).toHaveProperty('object');
// expect(result.object).toHaveProperty('name');
});
});
describe('Tool Calling', () => {
it.skip('should support tool definitions', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const tools = [{
// name: 'get_weather',
// description: 'Get weather for a location',
// parameters: {
// type: 'object',
// properties: { location: { type: 'string' } },
// required: ['location'],
// },
// }];
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'What is the weather in London?' }],
// tools,
// });
// expect(result).toHaveProperty('toolCalls');
});
it.skip('should validate tool call arguments', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const tools = [{
// name: 'calculate',
// parameters: {
// type: 'object',
// properties: { a: { type: 'number' }, b: { type: 'number' } },
// required: ['a', 'b'],
// },
// }];
// // Tool call validation should happen
});
});
describe('Streaming', () => {
it.skip('should yield text chunks', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const stream = await provider.streamText({
// messages: [{ role: 'user', content: 'Count to 5' }],
// });
// const chunks: string[] = [];
// for await (const chunk of stream) {
// chunks.push(chunk.text);
// }
// expect(chunks.length).toBeGreaterThan(0);
});
it.skip('should support onToken callback', async () => {
// const tokens: string[] = [];
// const provider = createProvider('openai/gpt-4o-mini');
// await provider.streamText({
// messages: [{ role: 'user', content: 'Hello' }],
// onToken: (token) => tokens.push(token),
// });
// expect(tokens.length).toBeGreaterThan(0);
});
});
describe('Error Handling', () => {
it.skip('should handle rate limiting with retry', async () => {
// const provider = createProvider('openai/gpt-4o-mini', { maxRetries: 3 });
// // Should retry on 429 errors
});
it.skip('should throw on invalid API key', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// // With invalid key, should throw AuthenticationError
});
it.skip('should handle timeout', async () => {
// const provider = createProvider('openai/gpt-4o-mini', { timeout: 1000 });
// // Should throw TimeoutError on slow responses
});
});
});
describe('Token Usage Tracking', () => {
it.skip('should track input tokens', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'Hello' }],
// });
// expect(result.usage).toHaveProperty('promptTokens');
// expect(result.usage.promptTokens).toBeGreaterThan(0);
});
it.skip('should track output tokens', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'Hello' }],
// });
// expect(result.usage).toHaveProperty('completionTokens');
// expect(result.usage.completionTokens).toBeGreaterThan(0);
});
it.skip('should calculate total tokens', async () => {
// const provider = createProvider('openai/gpt-4o-mini');
// const result = await provider.generateText({
// messages: [{ role: 'user', content: 'Hello' }],
// });
// expect(result.usage.totalTokens).toBe(
// result.usage.promptTokens + result.usage.completionTokens
// );
});
});
@@ -0,0 +1,111 @@
/**
* Observability Unit Tests
*/
import { describe, it, expect, beforeEach } from '@jest/globals';
import { MemoryObservabilityAdapter, setObservabilityAdapter, getObservabilityAdapter } from '../../../src/observability';
describe('Observability', () => {
let adapter: MemoryObservabilityAdapter;
beforeEach(() => {
adapter = new MemoryObservabilityAdapter();
});
describe('MemoryObservabilityAdapter', () => {
describe('Traces', () => {
it('should start and end trace', () => {
const trace = adapter.startTrace('test-trace', { userId: '123' });
expect(trace.traceId).toBeDefined();
trace.end('completed');
const traceData = adapter.getTrace(trace.traceId);
expect(traceData?.status).toBe('completed');
expect(traceData?.endTime).toBeDefined();
});
it('should store trace metadata', () => {
const trace = adapter.startTrace('test', { key: 'value' });
const traceData = adapter.getTrace(trace.traceId);
expect(traceData?.metadata.key).toBe('value');
});
});
describe('Spans', () => {
it('should create spans within trace', () => {
const trace = adapter.startTrace('test');
const span = trace.startSpan('llm-call', 'llm');
expect(span.spanId).toBeDefined();
expect(span.traceId).toBe(trace.traceId);
});
it('should end span with status', () => {
const trace = adapter.startTrace('test');
const span = trace.startSpan('tool-call', 'tool');
span.end('completed');
const spanData = adapter.getSpan(span.spanId);
expect(spanData?.status).toBe('completed');
});
it('should set span attributes', () => {
const trace = adapter.startTrace('test');
const span = trace.startSpan('llm-call', 'llm');
span.setAttributes({ model: 'gpt-4', tokens: 100 });
const spanData = adapter.getSpan(span.spanId);
expect(spanData?.attributes.model).toBe('gpt-4');
expect(spanData?.attributes.tokens).toBe(100);
});
it('should add events to span', () => {
const trace = adapter.startTrace('test');
const span = trace.startSpan('process', 'custom');
span.addEvent('step-1', { progress: 50 });
span.addEvent('step-2', { progress: 100 });
const spanData = adapter.getSpan(span.spanId);
expect(spanData?.events.length).toBe(2);
expect(spanData?.events[0].name).toBe('step-1');
});
});
describe('Utility Methods', () => {
it('should get all traces', () => {
adapter.startTrace('trace-1');
adapter.startTrace('trace-2');
const traces = adapter.getAllTraces();
expect(traces.length).toBe(2);
});
it('should clear all data', () => {
adapter.startTrace('test');
adapter.clear();
expect(adapter.getAllTraces().length).toBe(0);
});
});
});
describe('Global Adapter', () => {
it('should set and get global adapter', () => {
const customAdapter = new MemoryObservabilityAdapter();
setObservabilityAdapter(customAdapter);
const retrieved = getObservabilityAdapter();
expect(retrieved).toBe(customAdapter);
});
it('should create default adapter if not set', () => {
// Reset by setting null-like behavior
const adapter = getObservabilityAdapter();
expect(adapter).toBeDefined();
});
});
});
@@ -0,0 +1,121 @@
/**
* Provider Factory Unit Tests
*/
import { describe, it, expect, beforeEach } from '@jest/globals';
import {
createProvider,
parseModelString,
isProviderAvailable,
OpenAIProvider,
AnthropicProvider,
GoogleProvider
} from '../../../src/llm/providers';
describe('Provider Factory', () => {
describe('parseModelString', () => {
it('should parse provider/model format', () => {
const result = parseModelString('openai/gpt-4o');
expect(result.providerId).toBe('openai');
expect(result.modelId).toBe('gpt-4o');
});
it('should parse anthropic provider', () => {
const result = parseModelString('anthropic/claude-3-5-sonnet-latest');
expect(result.providerId).toBe('anthropic');
expect(result.modelId).toBe('claude-3-5-sonnet-latest');
});
it('should parse google provider', () => {
const result = parseModelString('google/gemini-2.0-flash');
expect(result.providerId).toBe('google');
expect(result.modelId).toBe('gemini-2.0-flash');
});
it('should default to openai for gpt- models', () => {
const result = parseModelString('gpt-4o-mini');
expect(result.providerId).toBe('openai');
expect(result.modelId).toBe('gpt-4o-mini');
});
it('should default to anthropic for claude- models', () => {
const result = parseModelString('claude-3-5-sonnet-latest');
expect(result.providerId).toBe('anthropic');
expect(result.modelId).toBe('claude-3-5-sonnet-latest');
});
it('should default to google for gemini- models', () => {
const result = parseModelString('gemini-2.0-flash');
expect(result.providerId).toBe('google');
expect(result.modelId).toBe('gemini-2.0-flash');
});
it('should default to openai for unknown models', () => {
const result = parseModelString('some-model');
expect(result.providerId).toBe('openai');
expect(result.modelId).toBe('some-model');
});
});
describe('createProvider', () => {
it('should create OpenAI provider', () => {
// Skip if no API key
if (!process.env.OPENAI_API_KEY) {
console.log('Skipping: OPENAI_API_KEY not set');
return;
}
const provider = createProvider('openai/gpt-4o-mini');
expect(provider).toBeInstanceOf(OpenAIProvider);
expect(provider.providerId).toBe('openai');
expect(provider.modelId).toBe('gpt-4o-mini');
});
it('should create Anthropic provider', () => {
// Skip if no API key
if (!process.env.ANTHROPIC_API_KEY) {
console.log('Skipping: ANTHROPIC_API_KEY not set');
return;
}
const provider = createProvider('anthropic/claude-3-5-sonnet-latest');
expect(provider).toBeInstanceOf(AnthropicProvider);
expect(provider.providerId).toBe('anthropic');
});
it('should create Google provider', () => {
// Skip if no API key
if (!process.env.GOOGLE_API_KEY) {
console.log('Skipping: GOOGLE_API_KEY not set');
return;
}
const provider = createProvider('google/gemini-2.0-flash');
expect(provider).toBeInstanceOf(GoogleProvider);
expect(provider.providerId).toBe('google');
});
it('should throw for unknown provider', () => {
expect(() => createProvider('unknown/model')).toThrow('Unknown provider: unknown');
});
});
describe('isProviderAvailable', () => {
it('should check OpenAI availability', () => {
const available = isProviderAvailable('openai');
expect(available).toBe(!!process.env.OPENAI_API_KEY);
});
it('should check Anthropic availability', () => {
const available = isProviderAvailable('anthropic');
expect(available).toBe(!!process.env.ANTHROPIC_API_KEY);
});
it('should check Google availability', () => {
const available = isProviderAvailable('google');
expect(available).toBe(!!process.env.GOOGLE_API_KEY);
});
it('should return false for unknown provider', () => {
const available = isProviderAvailable('unknown');
expect(available).toBe(false);
});
});
});
@@ -1,86 +0,0 @@
import { Tool, ToolManager } from '../../../src/tools/tools';
describe('Tool Manager', () => {
let toolManager: ToolManager;
beforeEach(() => {
toolManager = new ToolManager();
});
describe('tool registration', () => {
it('should register tools', () => {
const calculatorTool: Tool = {
name: 'calculator',
description: 'Performs calculations',
execute: async () => '42'
};
const translatorTool: Tool = {
name: 'translator',
description: 'Translates text',
execute: async () => 'translated'
};
toolManager.register(calculatorTool);
toolManager.register(translatorTool);
const registeredTools = toolManager.list();
expect(registeredTools.length).toBe(2);
expect(registeredTools.map(t => t.name)).toEqual(['calculator', 'translator']);
});
it('should prevent duplicate tool registration', () => {
const tool: Tool = {
name: 'calculator',
description: 'Performs calculations',
execute: async () => '42'
};
toolManager.register(tool);
expect(() => toolManager.register(tool)).toThrow();
});
});
describe('tool execution', () => {
it('should execute registered tool', async () => {
const tool: Tool = {
name: 'calculator',
description: 'Performs calculations',
execute: async () => '42'
};
toolManager.register(tool);
const result = await toolManager.execute('calculator', '2 + 2');
expect(result).toBe('42');
});
it('should throw error for unregistered tool', async () => {
await expect(toolManager.execute('unknown', 'input')).rejects.toThrow();
});
});
describe('tool discovery', () => {
it('should find tools by category', () => {
const calculator: Tool = {
name: 'calculator',
description: 'Performs calculations',
category: 'math',
execute: async () => '42'
};
const adder: Tool = {
name: 'adder',
description: 'Adds numbers',
category: 'math',
execute: async () => '2'
};
toolManager.register(calculator);
toolManager.register(adder);
const mathTools = toolManager.findByCategory('math');
expect(mathTools.length).toBe(2);
expect(mathTools.map(t => t.name)).toEqual(['calculator', 'adder']);
});
});
});
@@ -0,0 +1,173 @@
/**
* Session Management Unit Tests
*/
import { describe, it, expect, beforeEach } from '@jest/globals';
import { Session, Run, Trace, SessionManager, getSessionManager } from '../../../src/session';
describe('Session Management', () => {
describe('Session', () => {
it('should create session with auto-generated ID', () => {
const session = new Session();
expect(session.id).toBeDefined();
expect(typeof session.id).toBe('string');
expect(session.id.length).toBeGreaterThan(0);
});
it('should create session with custom ID', () => {
const session = new Session({ id: 'my-session' });
expect(session.id).toBe('my-session');
});
it('should track creation timestamp', () => {
const before = Date.now();
const session = new Session();
const after = Date.now();
expect(session.createdAt).toBeGreaterThanOrEqual(before);
expect(session.createdAt).toBeLessThanOrEqual(after);
});
it('should support metadata', () => {
const session = new Session({ metadata: { userId: 'user-123' } });
expect(session.metadata.userId).toBe('user-123');
});
it('should create runs', () => {
const session = new Session();
const run = session.createRun();
expect(run.sessionId).toBe(session.id);
expect(session.runs.length).toBe(1);
});
it('should add messages', () => {
const session = new Session();
const msg = session.addMessage({ role: 'user', content: 'Hello' });
expect(msg.id).toBeDefined();
expect(msg.content).toBe('Hello');
expect(session.messages.length).toBe(1);
});
it('should get messages for LLM', () => {
const session = new Session();
session.addMessage({ role: 'user', content: 'Hello' });
session.addMessage({ role: 'assistant', content: 'Hi there!' });
const messages = session.getMessagesForLLM();
expect(messages.length).toBe(2);
expect(messages[0].role).toBe('user');
expect(messages[1].role).toBe('assistant');
});
});
describe('Run', () => {
it('should create run with session ID', () => {
const session = new Session();
const run = session.createRun();
expect(run.sessionId).toBe(session.id);
expect(run.status).toBe('pending');
});
it('should track run status', () => {
const session = new Session();
const run = session.createRun();
expect(run.status).toBe('pending');
run.start();
expect(run.status).toBe('running');
run.complete();
expect(run.status).toBe('completed');
});
it('should track run duration', async () => {
const session = new Session();
const run = session.createRun();
run.start();
await new Promise(resolve => setTimeout(resolve, 10));
run.complete();
expect(run.duration).toBeGreaterThan(0);
});
it('should track run errors', () => {
const session = new Session();
const run = session.createRun();
run.start();
run.fail(new Error('Test error'));
expect(run.status).toBe('failed');
expect(run.error?.message).toBe('Test error');
});
it('should create traces', () => {
const session = new Session();
const run = session.createRun();
const trace = run.createTrace({ name: 'llm_call' });
expect(trace.runId).toBe(run.id);
expect(trace.name).toBe('llm_call');
});
});
describe('Trace', () => {
it('should create trace with run ID', () => {
const session = new Session();
const run = session.createRun();
const trace = run.createTrace({ name: 'test' });
expect(trace.runId).toBe(run.id);
});
it('should support nested traces', () => {
const session = new Session();
const run = session.createRun();
const parent = run.createTrace({ name: 'parent' });
const child = parent.createChild({ name: 'child' });
expect(child.parentId).toBe(parent.id);
});
it('should track attributes', () => {
const session = new Session();
const run = session.createRun();
const trace = run.createTrace({
name: 'llm_call',
attributes: { model: 'gpt-4o', tokens: 100 },
});
expect(trace.attributes.model).toBe('gpt-4o');
expect(trace.attributes.tokens).toBe(100);
});
it('should track duration', async () => {
const session = new Session();
const run = session.createRun();
const trace = run.createTrace({ name: 'test' }).start();
await new Promise(resolve => setTimeout(resolve, 10));
trace.complete();
expect(trace.duration).toBeGreaterThan(0);
});
});
describe('SessionManager', () => {
it('should create and retrieve sessions', () => {
const manager = new SessionManager();
const session = manager.create({ id: 'test' });
const retrieved = manager.get('test');
expect(retrieved).toBe(session);
});
it('should list all sessions', () => {
const manager = new SessionManager();
manager.create({ id: 'session-1' });
manager.create({ id: 'session-2' });
const sessions = manager.list();
expect(sessions.length).toBe(2);
});
it('should delete session', () => {
const manager = new SessionManager();
manager.create({ id: 'test' });
manager.delete('test');
expect(manager.get('test')).toBeUndefined();
});
it('should get or create session', () => {
const manager = new SessionManager();
const session1 = manager.getOrCreate('test');
const session2 = manager.getOrCreate('test');
expect(session1).toBe(session2);
});
});
});
@@ -0,0 +1,187 @@
/**
* Tool Registry Tests - TDD for tool system
* These tests define the expected behavior for the tool registry and decorator
*/
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
// These imports will fail initially - TDD approach
// import { tool, ToolRegistry, getRegistry, registerTool, getTool } from '../../../src/tools';
// import { z } from 'zod';
describe('Tool Decorator', () => {
describe('@tool decorator', () => {
it.skip('should create tool from function', () => {
// const myTool = tool({
// name: 'greet',
// description: 'Greet a person',
// parameters: z.object({ name: z.string() }),
// execute: async ({ name }) => `Hello, ${name}!`,
// });
// expect(myTool.name).toBe('greet');
// expect(myTool.description).toBe('Greet a person');
});
it.skip('should validate input parameters', async () => {
// const myTool = tool({
// name: 'add',
// parameters: z.object({ a: z.number(), b: z.number() }),
// execute: async ({ a, b }) => a + b,
// });
// await expect(myTool.execute({ a: 'not a number', b: 2 })).rejects.toThrow();
});
it.skip('should execute with valid parameters', async () => {
// const myTool = tool({
// name: 'add',
// parameters: z.object({ a: z.number(), b: z.number() }),
// execute: async ({ a, b }) => a + b,
// });
// const result = await myTool.execute({ a: 1, b: 2 });
// expect(result).toBe(3);
});
it.skip('should support optional parameters', async () => {
// const myTool = tool({
// name: 'greet',
// parameters: z.object({
// name: z.string(),
// greeting: z.string().optional().default('Hello'),
// }),
// execute: async ({ name, greeting }) => `${greeting}, ${name}!`,
// });
// const result = await myTool.execute({ name: 'World' });
// expect(result).toBe('Hello, World!');
});
it.skip('should generate JSON schema from Zod', () => {
// const myTool = tool({
// name: 'test',
// parameters: z.object({ name: z.string() }),
// execute: async () => {},
// });
// expect(myTool.jsonSchema).toEqual({
// type: 'object',
// properties: { name: { type: 'string' } },
// required: ['name'],
// });
});
});
describe('Tool from plain function', () => {
it.skip('should infer parameters from function signature', () => {
// function getWeather(location: string, units?: 'celsius' | 'fahrenheit') {
// return { temp: 20, units: units || 'celsius' };
// }
// const weatherTool = tool(getWeather, {
// description: 'Get weather for a location',
// });
// expect(weatherTool.name).toBe('getWeather');
});
});
});
describe('Tool Registry', () => {
describe('Global Registry', () => {
it.skip('should register tool globally', () => {
// const myTool = tool({
// name: 'global_tool',
// execute: async () => 'result',
// });
// registerTool(myTool);
// expect(getTool('global_tool')).toBe(myTool);
});
it.skip('should get registry singleton', () => {
// const registry1 = getRegistry();
// const registry2 = getRegistry();
// expect(registry1).toBe(registry2);
});
it.skip('should list all registered tools', () => {
// const registry = getRegistry();
// registry.clear();
// registerTool(tool({ name: 'tool1', execute: async () => {} }));
// registerTool(tool({ name: 'tool2', execute: async () => {} }));
// expect(registry.list().length).toBe(2);
});
});
describe('Scoped Registry', () => {
it.skip('should create isolated registry', () => {
// const registry = new ToolRegistry();
// const myTool = tool({ name: 'scoped', execute: async () => {} });
// registry.register(myTool);
// expect(registry.get('scoped')).toBe(myTool);
// expect(getRegistry().get('scoped')).toBeUndefined();
});
it.skip('should support tool categories', () => {
// const registry = new ToolRegistry();
// registry.register(tool({ name: 'search', category: 'web', execute: async () => {} }));
// registry.register(tool({ name: 'read', category: 'file', execute: async () => {} }));
// const webTools = registry.getByCategory('web');
// expect(webTools.length).toBe(1);
// expect(webTools[0].name).toBe('search');
});
});
describe('Tool Validation', () => {
it.skip('should validate tool has required properties', () => {
// expect(() => tool({ execute: async () => {} })).toThrow('Tool name is required');
});
it.skip('should prevent duplicate registration', () => {
// const registry = new ToolRegistry();
// registry.register(tool({ name: 'dup', execute: async () => {} }));
// expect(() => registry.register(tool({ name: 'dup', execute: async () => {} }))).toThrow();
});
it.skip('should allow overwrite with flag', () => {
// const registry = new ToolRegistry();
// registry.register(tool({ name: 'dup', execute: async () => 'v1' }));
// registry.register(tool({ name: 'dup', execute: async () => 'v2' }), { overwrite: true });
// const result = await registry.get('dup')!.execute({});
// expect(result).toBe('v2');
});
});
});
describe('Tool Execution Context', () => {
it.skip('should provide agent context to tool', async () => {
// let capturedContext: any;
// const myTool = tool({
// name: 'context_test',
// execute: async (params, context) => {
// capturedContext = context;
// return 'done';
// },
// });
// const agent = new Agent({ tools: [myTool] });
// await agent.chat('Use context_test');
// expect(capturedContext.agentName).toBeDefined();
// expect(capturedContext.sessionId).toBeDefined();
});
it.skip('should provide abort signal for cancellation', async () => {
// const myTool = tool({
// name: 'long_running',
// execute: async (params, context) => {
// // Should check context.signal.aborted
// return 'done';
// },
// });
});
});
describe('Built-in Tools', () => {
it.skip('should have arxiv search tool', () => {
// const arxiv = getTool('arxiv_search');
// expect(arxiv).toBeDefined();
});
it.skip('should have web search tool', () => {
// const webSearch = getTool('web_search');
// expect(webSearch).toBeDefined();
});
});
@@ -0,0 +1,142 @@
/**
* Tool Decorator Unit Tests
*/
import { describe, it, expect, beforeEach } from '@jest/globals';
import { tool, FunctionTool, ToolRegistry, getRegistry, registerTool, getTool } from '../../../src/tools/decorator';
describe('Tool Decorator', () => {
describe('tool function', () => {
it('should create tool from config', () => {
const myTool = tool({
name: 'greet',
description: 'Greet a person',
execute: async ({ name }: { name: string }) => `Hello, ${name}!`,
});
expect(myTool.name).toBe('greet');
expect(myTool.description).toBe('Greet a person');
});
it('should execute with parameters', async () => {
const myTool = tool({
name: 'add',
execute: async ({ a, b }: { a: number; b: number }) => a + b,
});
const result = await myTool.execute({ a: 1, b: 2 });
expect(result).toBe(3);
});
it('should have default description', () => {
const myTool = tool({
name: 'test',
execute: async () => 'result',
});
expect(myTool.description).toBe('Function test');
});
it('should support category', () => {
const myTool = tool({
name: 'search',
category: 'web',
execute: async () => 'results',
});
expect(myTool.category).toBe('web');
});
it('should generate OpenAI tool format', () => {
const myTool = tool({
name: 'test',
description: 'Test tool',
parameters: {
type: 'object',
properties: { input: { type: 'string' } },
required: ['input'],
},
execute: async () => 'result',
});
const openaiTool = myTool.toOpenAITool();
expect(openaiTool.type).toBe('function');
expect(openaiTool.function.name).toBe('test');
expect(openaiTool.function.description).toBe('Test tool');
});
});
describe('ToolRegistry', () => {
let registry: ToolRegistry;
beforeEach(() => {
registry = new ToolRegistry();
});
it('should register and retrieve tools', () => {
const myTool = tool({ name: 'test', execute: async () => 'result' });
registry.register(myTool);
expect(registry.get('test')).toBe(myTool);
});
it('should check if tool exists', () => {
const myTool = tool({ name: 'test', execute: async () => 'result' });
registry.register(myTool);
expect(registry.has('test')).toBe(true);
expect(registry.has('nonexistent')).toBe(false);
});
it('should list all tools', () => {
registry.register(tool({ name: 'tool1', execute: async () => {} }));
registry.register(tool({ name: 'tool2', execute: async () => {} }));
expect(registry.list().length).toBe(2);
});
it('should get tools by category', () => {
registry.register(tool({ name: 'search', category: 'web', execute: async () => {} }));
registry.register(tool({ name: 'read', category: 'file', execute: async () => {} }));
const webTools = registry.getByCategory('web');
expect(webTools.length).toBe(1);
expect(webTools[0].name).toBe('search');
});
it('should prevent duplicate registration', () => {
registry.register(tool({ name: 'dup', execute: async () => {} }));
expect(() => registry.register(tool({ name: 'dup', execute: async () => {} }))).toThrow();
});
it('should allow overwrite with flag', async () => {
registry.register(tool({ name: 'dup', execute: async () => 'v1' }));
registry.register(tool({ name: 'dup', execute: async () => 'v2' }), { overwrite: true });
const result = await registry.get('dup')!.execute({});
expect(result).toBe('v2');
});
it('should get definitions', () => {
registry.register(tool({ name: 'test', description: 'Test', execute: async () => {} }));
const defs = registry.getDefinitions();
expect(defs.length).toBe(1);
expect(defs[0].name).toBe('test');
});
it('should get OpenAI tools format', () => {
registry.register(tool({ name: 'test', execute: async () => {} }));
const tools = registry.toOpenAITools();
expect(tools.length).toBe(1);
expect(tools[0].type).toBe('function');
});
});
describe('Global Registry', () => {
beforeEach(() => {
getRegistry().clear();
});
it('should register globally', () => {
const myTool = tool({ name: 'global_test', execute: async () => 'result' });
registerTool(myTool);
expect(getTool('global_test')).toBe(myTool);
});
it('should return singleton', () => {
const registry1 = getRegistry();
const registry2 = getRegistry();
expect(registry1).toBe(registry2);
});
});
});
@@ -0,0 +1,232 @@
/**
* Workflow Unit Tests
*/
import { describe, it, expect } from '@jest/globals';
import { Workflow, WorkflowStep, parallel, route, loop, repeat } from '../../../src/workflows';
describe('Workflow', () => {
describe('Basic Workflow', () => {
it('should create workflow with name', () => {
const workflow = new Workflow('test-workflow');
expect(workflow.name).toBe('test-workflow');
expect(workflow.id).toBeDefined();
});
it('should add steps', () => {
const workflow = new Workflow('test')
.step('step1', async (input) => input + 1)
.step('step2', async (input) => input * 2);
expect(workflow.stepCount).toBe(2);
});
it('should run sequential steps', async () => {
const workflow = new Workflow<number, number>('math')
.step('add', async (input) => input + 10)
.step('multiply', async (input) => input * 2);
const { output, results } = await workflow.run(5);
expect(output).toBe(30); // (5 + 10) * 2
expect(results.length).toBe(2);
expect(results[0].status).toBe('completed');
expect(results[1].status).toBe('completed');
});
it('should pass context between steps', async () => {
const workflow = new Workflow<string, string>('context-test')
.addStep({
name: 'set-context',
execute: async (input, context) => {
context.set('original', input);
return input.toUpperCase();
},
})
.addStep({
name: 'use-context',
execute: async (input, context) => {
const original = context.metadata.original;
return `${input} (was: ${original})`;
},
});
const { output } = await workflow.run('hello');
expect(output).toBe('HELLO (was: hello)');
});
it('should access previous step results', async () => {
const workflow = new Workflow<number, number>('result-access')
.step('first', async (input) => input * 2)
.addStep({
name: 'second',
execute: async (input, context) => {
const firstResult = context.get<number>('first');
return input + (firstResult || 0);
},
});
const { output } = await workflow.run(5);
expect(output).toBe(20); // 10 + 10
});
});
describe('Conditional Steps', () => {
it('should skip step when condition is false', async () => {
const workflow = new Workflow<number, number>('conditional')
.step('always', async (input) => input + 1)
.addStep({
name: 'conditional',
condition: () => false,
execute: async (input) => input * 100,
})
.step('final', async (input) => input + 1);
const { output, results } = await workflow.run(0);
expect(output).toBe(2); // 0 + 1 + 1 (skipped *100)
expect(results[1].status).toBe('skipped');
});
it('should run step when condition is true', async () => {
const workflow = new Workflow<number, number>('conditional')
.addStep({
name: 'conditional',
condition: () => true,
execute: async (input) => input * 2,
});
const { output } = await workflow.run(5);
expect(output).toBe(10);
});
});
describe('Error Handling', () => {
it('should stop on error by default', async () => {
const workflow = new Workflow<number, number>('error-test')
.step('fail', async () => { throw new Error('Test error'); })
.step('never-runs', async (input) => input + 1);
const { output, results } = await workflow.run(0);
expect(output).toBeUndefined();
expect(results.length).toBe(1);
expect(results[0].status).toBe('failed');
});
it('should skip on error when configured', async () => {
const workflow = new Workflow<number, number>('skip-error')
.addStep({
name: 'fail',
onError: 'skip',
execute: async () => { throw new Error('Test error'); },
})
.step('continues', async (input) => input + 1);
const { results } = await workflow.run(0);
expect(results[0].status).toBe('skipped');
expect(results[1].status).toBe('completed');
});
it('should retry on error when configured', async () => {
let attempts = 0;
const workflow = new Workflow<number, number>('retry-test')
.addStep({
name: 'retry',
onError: 'retry',
maxRetries: 2,
execute: async (input) => {
attempts++;
if (attempts < 3) throw new Error('Retry');
return input + 1;
},
});
const { output } = await workflow.run(0);
expect(output).toBe(1);
expect(attempts).toBe(3);
});
});
describe('Step Duration', () => {
it('should track step duration', async () => {
const workflow = new Workflow<void, void>('duration-test')
.step('slow', async () => {
await new Promise(resolve => setTimeout(resolve, 50));
});
const { results } = await workflow.run();
expect(results[0].duration).toBeGreaterThanOrEqual(50);
});
});
});
describe('Parallel Helper', () => {
it('should run tasks in parallel', async () => {
const results = await parallel([
async () => 1,
async () => 2,
async () => 3,
]);
expect(results).toEqual([1, 2, 3]);
});
it('should complete faster than sequential', async () => {
const start = Date.now();
await parallel([
async () => new Promise(resolve => setTimeout(resolve, 50)),
async () => new Promise(resolve => setTimeout(resolve, 50)),
async () => new Promise(resolve => setTimeout(resolve, 50)),
]);
const duration = Date.now() - start;
expect(duration).toBeLessThan(150); // Should be ~50ms, not 150ms
});
});
describe('Route Helper', () => {
it('should execute matching condition', async () => {
const result = await route([
{ condition: () => false, execute: async () => 'a' },
{ condition: () => true, execute: async () => 'b' },
{ condition: () => true, execute: async () => 'c' },
]);
expect(result).toBe('b');
});
it('should execute default when no match', async () => {
const result = await route(
[{ condition: () => false, execute: async () => 'a' }],
async () => 'default'
);
expect(result).toBe('default');
});
it('should return undefined when no match and no default', async () => {
const result = await route([
{ condition: () => false, execute: async () => 'a' },
]);
expect(result).toBeUndefined();
});
});
describe('Loop Helper', () => {
it('should loop until condition is false', async () => {
const results = await loop(
async (i) => i,
(result, i) => i < 3
);
expect(results).toEqual([0, 1, 2, 3]);
});
it('should respect max iterations', async () => {
const results = await loop(
async (i) => i,
() => true,
5
);
expect(results.length).toBe(5);
});
});
describe('Repeat Helper', () => {
it('should repeat N times', async () => {
const results = await repeat(async (i) => i * 2, 4);
expect(results).toEqual([0, 2, 4, 6]);
});
});
+493 -162
View File
@@ -35,46 +35,108 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
---
> **Quick Paths:**
> - 🚀 **New here?** → [Quick Start](#-quick-start)
> - 📦 **Installing?** → [Installation](#-installation)
> - 💻 **Python SDK?** → [Python Examples](#-using-python-code)
> - 🎯 **CLI user?** → [CLI Reference](#-cli--no-code-interface)
> - 🔧 **Need config?** → [Configuration](#-configuration--integration)
> - 🤝 **Contributing?** → [Development](#-development)
---
## 📑 Table of Contents
### Getting Started
- [⚡ Performance](#-performance)
- [🚀 Quick Start](#-quick-start)
- [📦 Installation](#-installation)
- [Python SDK](#python-sdk)
- [JavaScript SDK](#javascript-sdk)
- [💻 Usage](#-usage)
- [Python Code Examples](#python-code-examples)
- [CLI / No-Code Interface](#cli--no-code-interface)
- [JavaScript Code Examples](#javascript-code-examples)
- [Environment Variables](#environment-variables)
### Core Concepts
- [✨ Key Features](#-key-features)
- [🌐 Supported Providers](#-supported-providers)
### Python SDK
- [📘 Python Examples](#-using-python-code)
- [1. Single Agent](#1-single-agent)
- [2. Multi Agents](#2-multi-agents)
- [3. Planning Mode](#3-agent-with-planning-mode)
- [4. Deep Research](#4-deep-research-agent)
- [5. Query Rewriter](#5-query-rewriter-agent)
- [6. Agent Memory](#6-agent-memory-zero-dependencies)
- [7. Rules & Instructions](#7-rules--instructions)
- [8. Auto-Generated Memories](#8-auto-generated-memories)
- [9. Agentic Workflows](#9-agentic-workflows)
- [10. Hooks](#10-hooks)
- [11. Shadow Git Checkpoints](#11-shadow-git-checkpoints)
- [12. Background Tasks](#12-background-tasks)
- [13. Policy Engine](#13-policy-engine)
- [14. Thinking Budgets](#14-thinking-budgets)
- [15. Output Styles](#15-output-styles)
- [16. Context Compaction](#16-context-compaction)
- [17. Field Names Reference](#17-field-names-reference-a-i-g-s)
- [18. Extended agents.yaml](#18-extended-agentsyaml-with-workflow-patterns)
- [19. MCP Protocol](#19-mcp-model-context-protocol)
- [20. A2A Protocol](#20-a2a-agent2agent-protocol)
- [🛠️ Custom Tools](#️-custom-tools)
### JavaScript SDK
- [💻 JavaScript Examples](#-using-javascript-code)
### CLI Reference
- [🎯 CLI Overview](#-cli--no-code-interface)
- [Auto Mode](#auto-mode)
- [Interactive Mode](#interactive-mode-cli)
- [Deep Research CLI](#deep-research-cli)
- [Planning Mode CLI](#planning-mode-cli)
- [Memory CLI](#memory-cli)
- [Workflow CLI](#workflow-cli)
- [Knowledge CLI](#knowledge-cli)
- [Session CLI](#session-cli)
- [Tools CLI](#tools-cli)
- [MCP Config CLI](#mcp-config-cli)
- [External Agents CLI](#external-agents-cli)
- [CLI Features Summary](#cli-features)
### Configuration
- [🔧 Configuration & Integration](#-configuration--integration)
- [Ollama Integration](#ollama-integration)
- [Groq Integration](#groq-integration)
- [100+ Models Support](#100-models-support)
- [📋 Workflows](#-workflows)
- [Workflow Patterns](#workflow-patterns)
- [YAML Configuration](#yaml-configuration)
- [Agents Playbook](#agents-playbook)
- [🧠 Memory & Context](#-memory--context)
- [📋 Agents Playbook](#-agents-playbook)
### Advanced Features
- [🔬 Advanced Features](#-advanced-features)
- [Research & Intelligence](#research--intelligence)
- [Memory & Caching](#memory--caching)
- [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)
### Architecture
- [📊 Process Types & Patterns](#-process-types--patterns)
- [Sequential Process](#sequential-process)
- [Hierarchical Process](#hierarchical-process)
- [Workflow Process](#workflow-process)
- [Agentic Patterns](#agentic-patterns)
### Data & Persistence
- [💾 Persistence (Databases)](#-persistence-databases)
- [📚 Knowledge & Retrieval (RAG)](#-knowledge--retrieval-rag)
- [🔧 Tools Table](#-tools-table)
### Learning & Community
- [🎓 Video Tutorials](#-video-tutorials)
- [⭐ Star History](#-star-history)
- [👥 Contributing](#-contributing)
- [🔧 Development](#-development)
- [❓ FAQ & Troubleshooting](#-faq--troubleshooting)
---
@@ -143,6 +205,30 @@ pip install praisonai
npm install praisonai
```
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `OPENAI_API_KEY` | Yes* | OpenAI API key |
| `ANTHROPIC_API_KEY` | No | Anthropic Claude API key |
| `GOOGLE_API_KEY` | No | Google Gemini API key |
| `GROQ_API_KEY` | No | Groq API key |
| `OPENAI_BASE_URL` | No | Custom API endpoint (for Ollama, Groq, etc.) |
> *At least one LLM provider API key is required.
```bash
# Set your API key
export OPENAI_API_KEY=your_key_here
# For Ollama (local models)
export OPENAI_BASE_URL=http://localhost:11434/v1
# For Groq
export OPENAI_API_KEY=your_groq_key
export OPENAI_BASE_URL=https://api.groq.com/openai/v1
```
---
## 💻 Usage
@@ -169,147 +255,263 @@ npm install praisonai
## ✨ Key Features
<details open>
<summary><strong>🤖 Core Agents</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| **🤖 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) |
| ↳ Agentic Routing (`route()`) | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/routing) |
| ↳ Parallel Execution (`parallel()`) | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| ↳ Loop over List/CSV (`loop()`) | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/repetitive) |
| ↳ Evaluator-Optimizer (`repeat()`) | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| ↳ Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ 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) |
| **💻 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) |
| ↳ Data Readers (PDF, DOCX, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-readers-api) |
| ↳ Vector Store Selection | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-vector-store-api) |
| ↳ Retrieval Strategies | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-retrieval-api) |
| ↳ Rerankers | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-reranker-api) |
| ↳ Index Types (Vector/Keyword/Hybrid) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-index-api) |
| ↳ Query Engines (Sub-Question, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-query-engine-api) |
| **🔬 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) |
| ↳ Middleware System | [Example](examples/middleware/basic_middleware.py) | [📖](https://docs.praison.ai/features/middleware) |
| ↳ Configurable Model | [Example](examples/middleware/configurable_model.py) | [📖](https://docs.praison.ai/features/configurable-model) |
| ↳ Rate Limiter | [Example](examples/middleware/rate_limiter.py) | [📖](https://docs.praison.ai/features/rate-limiter) |
| ↳ Injected Tool State | [Example](examples/middleware/injected_state.py) | [📖](https://docs.praison.ai/features/injected-state) |
| ↳ Shadow Git Checkpoints | [Example](#10-shadow-git-checkpoints) | [📖](https://docs.praison.ai/features/checkpoints) |
| ↳ Background Tasks | [Example](examples/background/basic_background.py) | [📖](https://docs.praison.ai/features/background-tasks) |
| ↳ Policy Engine | [Example](examples/policy/basic_policy.py) | [📖](https://docs.praison.ai/features/policy-engine) |
| ↳ Thinking Budgets | [Example](examples/thinking/basic_thinking.py) | [📖](https://docs.praison.ai/features/thinking-budgets) |
| ↳ Output Styles | [Example](examples/output/basic_output.py) | [📖](https://docs.praison.ai/features/output-styles) |
| ↳ Context Compaction | [Example](examples/compaction/basic_compaction.py) | [📖](https://docs.praison.ai/features/context-compaction) |
| **📊 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) |
| ↳ Profile/Benchmark | [Example](#profile-benchmark) | [📖](https://docs.praison.ai/docs/cli/profile) |
| ↳ Auto Mode | [Example](#auto-mode) | [📖](https://docs.praison.ai/docs/cli/auto) |
| ↳ Init | [Example](#init) | [📖](https://docs.praison.ai/docs/cli/init) |
| ↳ File Input | [Example](#file-input) | [📖](https://docs.praison.ai/docs/cli/file-input) |
| ↳ Final Agent | [Example](#final-agent) | [📖](https://docs.praison.ai/docs/cli/final-agent) |
| ↳ Max Tokens | [Example](#max-tokens) | [📖](https://docs.praison.ai/docs/cli/max-tokens) |
| **🧪 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) |
| 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) |
</details>
<details>
<summary><strong>🔄 Workflows</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
| Agentic Routing (`route()`) | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/routing) |
| Parallel Execution (`parallel()`) | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| Loop over List/CSV (`loop()`) | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/repetitive) |
| Evaluator-Optimizer (`repeat()`) | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| 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) |
</details>
<details>
<summary><strong>💻 Code & Development</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🧠 Memory & Knowledge</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
| Data Readers (PDF, DOCX, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-readers-api) |
| Vector Store Selection | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-vector-store-api) |
| Retrieval Strategies | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-retrieval-api) |
| Rerankers | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-reranker-api) |
| Index Types (Vector/Keyword/Hybrid) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-index-api) |
| Query Engines (Sub-Question, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-query-engine-api) |
</details>
<details>
<summary><strong>🔬 Research & Intelligence</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>📋 Planning & Execution</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>👥 Specialized Agents</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🎨 Media & Multimodal</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🔌 Protocols & Integration</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🛡️ Safety & Control</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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](#7-rules--instructions) | [📖](https://docs.praison.ai/features/rules) |
</details>
<details>
<summary><strong>⚙️ Advanced Features</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🛠️ Tools & Configuration</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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](#10-hooks) | [📖](https://docs.praison.ai/features/hooks) |
| Middleware System | [Example](examples/middleware/basic_middleware.py) | [📖](https://docs.praison.ai/features/middleware) |
| Configurable Model | [Example](examples/middleware/configurable_model.py) | [📖](https://docs.praison.ai/features/configurable-model) |
| Rate Limiter | [Example](examples/middleware/rate_limiter.py) | [📖](https://docs.praison.ai/features/rate-limiter) |
| Injected Tool State | [Example](examples/middleware/injected_state.py) | [📖](https://docs.praison.ai/features/injected-state) |
| Shadow Git Checkpoints | [Example](#11-shadow-git-checkpoints) | [📖](https://docs.praison.ai/features/checkpoints) |
| Background Tasks | [Example](examples/background/basic_background.py) | [📖](https://docs.praison.ai/features/background-tasks) |
| Policy Engine | [Example](examples/policy/basic_policy.py) | [📖](https://docs.praison.ai/features/policy-engine) |
| Thinking Budgets | [Example](examples/thinking/basic_thinking.py) | [📖](https://docs.praison.ai/features/thinking-budgets) |
| Output Styles | [Example](examples/output/basic_output.py) | [📖](https://docs.praison.ai/features/output-styles) |
| Context Compaction | [Example](examples/compaction/basic_compaction.py) | [📖](https://docs.praison.ai/features/context-compaction) |
</details>
<details>
<summary><strong>📊 Monitoring & Management</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🖥️ CLI Features</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
| Profile/Benchmark | [Example](#profile-benchmark) | [📖](https://docs.praison.ai/docs/cli/profile) |
| Auto Mode | [Example](#auto-mode) | [📖](https://docs.praison.ai/docs/cli/auto) |
| Init | [Example](#init) | [📖](https://docs.praison.ai/docs/cli/init) |
| File Input | [Example](#file-input) | [📖](https://docs.praison.ai/docs/cli/file-input) |
| Final Agent | [Example](#final-agent) | [📖](https://docs.praison.ai/docs/cli/final-agent) |
| Max Tokens | [Example](#max-tokens) | [📖](https://docs.praison.ai/docs/cli/max-tokens) |
</details>
<details>
<summary><strong>🧪 Evaluation</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>🎯 Agent Skills</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 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) |
</details>
<details>
<summary><strong>⏰ 24/7 Scheduling</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/docs/cli/scheduler) |
</details>
---
@@ -543,7 +745,7 @@ memory.handle_command("/memory save my_session")
| `memory="sqlite"` | Built-in | SQLite with indexing |
| `memory="chromadb"` | chromadb | Vector/semantic search |
### 6. Rules & Instructions
### 7. Rules & Instructions
PraisonAI auto-discovers instruction files from your project root and git root:
@@ -592,7 +794,7 @@ activation: always # always, glob, manual, ai_decision
- Follow PEP 8
```
### 7. Auto-Generated Memories
### 8. Auto-Generated Memories
```python
from praisonaiagents.memory import FileMemory, AutoMemory
@@ -607,7 +809,7 @@ memories = auto.process_interaction(
# Extracts: name="John", preference="Python for backend"
```
### 8. Agentic Workflows
### 9. Agentic Workflows
Create powerful multi-agent workflows with the `Workflow` class:
@@ -877,7 +1079,7 @@ callbacks:
on_workflow_complete: log_complete
```
### 9. Hooks
### 10. Hooks
Intercept and modify agent behavior at various lifecycle points:
@@ -916,7 +1118,7 @@ praisonai hooks validate hooks.json # Validate configuration
```
### 10. Shadow Git Checkpoints
### 11. Shadow Git Checkpoints
File-level undo/restore using shadow git:
@@ -953,7 +1155,7 @@ praisonai checkpoint restore abc123 # Restore to checkpoint
---
### 11. Background Tasks
### 12. Background Tasks
Run agent tasks asynchronously without blocking:
@@ -991,7 +1193,7 @@ praisonai background clear # Clear completed tasks
---
### 12. Policy Engine
### 13. Policy Engine
Control what agents can and cannot do with policy-based execution:
@@ -1032,7 +1234,7 @@ praisonai policy init # Create template
---
### 13. Thinking Budgets
### 14. Thinking Budgets
Configure token budgets for extended thinking:
@@ -1065,7 +1267,7 @@ praisonai thinking stats # Show usage statistics
---
### 14. Output Styles
### 15. Output Styles
Configure how agents format their responses:
@@ -1095,7 +1297,7 @@ praisonai output set concise # Set output style
---
### 15. Context Compaction
### 16. Context Compaction
Automatically manage context window size:
@@ -1130,7 +1332,7 @@ praisonai compaction stats # Show statistics
---
### 16. Field Names Reference (A-I-G-S)
### 17. Field Names Reference (A-I-G-S)
PraisonAI accepts both old (agents.yaml) and new (workflow.yaml) field names. Use the **canonical names** for new projects:
@@ -1164,7 +1366,7 @@ steps: # Define steps (not 'tasks')
> **Note:** The parser accepts both old and new names. Run `praisonai workflow validate <file.yaml>` to see suggestions for canonical names.
### 12. Extended agents.yaml with Workflow Patterns
### 18. Extended agents.yaml with Workflow Patterns
**Feature Parity:** Both `agents.yaml` and `workflow.yaml` now support the same features:
- All workflow patterns (route, parallel, loop, repeat)
@@ -1240,7 +1442,7 @@ Run with the same simple command:
praisonai agents.yaml
```
### 12. MCP (Model Context Protocol)
### 19. MCP (Model Context Protocol)
PraisonAI supports MCP Protocol Revision 2025-11-25 with multiple transports.
@@ -1316,7 +1518,7 @@ server.run() # stdio for Claude Desktop
| Security | Origin validation, DNS rebinding prevention |
| WebSocket | Auto-reconnect with exponential backoff |
### 13. A2A (Agent2Agent Protocol)
### 20. 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.
@@ -1364,6 +1566,22 @@ app.include_router(a2a.get_router())
PraisonAI provides a powerful CLI for no-code automation and quick prototyping.
### CLI Quick Reference
| Category | Commands |
|----------|----------|
| **Execution** | `praisonai`, `--auto`, `--interactive`, `--chat` |
| **Research** | `research`, `--query-rewrite`, `--deep-research` |
| **Planning** | `--planning`, `--planning-tools`, `--planning-reasoning` |
| **Workflows** | `workflow run`, `workflow list`, `workflow auto` |
| **Memory** | `memory show`, `memory add`, `memory search`, `memory clear` |
| **Knowledge** | `knowledge add`, `knowledge query`, `knowledge list` |
| **Sessions** | `session list`, `session resume`, `session delete` |
| **Tools** | `tools list`, `tools info`, `tools search` |
| **MCP** | `mcp list`, `mcp create`, `mcp enable` |
| **Development** | `commit`, `docs`, `checkpoint`, `hooks` |
| **Scheduling** | `schedule start`, `schedule list`, `schedule stop` |
### Auto Mode
```bash
pip install praisonai
@@ -3023,6 +3241,119 @@ cd src/praisonai && uv publish
---
## ❓ FAQ & Troubleshooting
<details>
<summary><strong>ModuleNotFoundError: No module named 'praisonaiagents'</strong></summary>
Install the package:
```bash
pip install praisonaiagents
```
</details>
<details>
<summary><strong>API key not found / Authentication error</strong></summary>
Ensure your API key is set:
```bash
export OPENAI_API_KEY=your_key_here
```
For other providers, see [Environment Variables](#environment-variables).
</details>
<details>
<summary><strong>How do I use a local model (Ollama)?</strong></summary>
```bash
# Start Ollama server first
ollama serve
# Set environment variable
export OPENAI_BASE_URL=http://localhost:11434/v1
```
See [Ollama Integration](#ollama-integration) for more details.
</details>
<details>
<summary><strong>How do I persist conversations to a database?</strong></summary>
Use the `db` parameter:
```python
from praisonaiagents import Agent, db
agent = Agent(
name="Assistant",
db=db(database_url="postgresql://localhost/mydb"),
session_id="my-session"
)
```
See [Persistence (Databases)](#-persistence-databases) for supported databases.
</details>
<details>
<summary><strong>How do I enable agent memory?</strong></summary>
```python
from praisonaiagents import Agent
agent = Agent(
name="Assistant",
memory=True, # Enables file-based memory (no extra deps!)
user_id="user123"
)
```
See [Agent Memory](#6-agent-memory-zero-dependencies) for more options.
</details>
<details>
<summary><strong>How do I run multiple agents together?</strong></summary>
```python
from praisonaiagents import Agent, PraisonAIAgents
agent1 = Agent(instructions="Research topics")
agent2 = Agent(instructions="Summarize findings")
agents = PraisonAIAgents(agents=[agent1, agent2])
agents.start()
```
See [Multi Agents](#2-multi-agents) for more examples.
</details>
<details>
<summary><strong>How do I use MCP tools?</strong></summary>
```python
from praisonaiagents import Agent, MCP
agent = Agent(
tools=MCP("npx @modelcontextprotocol/server-memory")
)
```
See [MCP Protocol](#19-mcp-model-context-protocol) for all transport options.
</details>
### Getting Help
- 📚 [Full Documentation](https://docs.praison.ai)
- 🐛 [Report Issues](https://github.com/MervinPraison/PraisonAI/issues)
- 💬 [Discussions](https://github.com/MervinPraison/PraisonAI/discussions)
---
<div align="center">
<p><strong>Made with ❤️ by the PraisonAI Team</strong></p>
<p>
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.2.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.2.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.5.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.5.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+15 -15
View File
@@ -57,13 +57,10 @@ class LLMReranker:
self._client = None
def _get_client(self):
"""Lazy load OpenAI client."""
if self._client is None:
if self.llm:
return self.llm
import openai
self._client = openai.OpenAI()
return self._client
"""Get LLM client - returns self.llm if provided, otherwise None (use litellm)."""
if self.llm:
return self.llm
return None # Will use litellm directly
def rerank(
self,
@@ -118,7 +115,7 @@ class LLMReranker:
return results
def _score_document(self, query: str, document: str) -> float:
"""Score a single document's relevance to query."""
"""Score a single document's relevance to query using litellm."""
try:
client = self._get_client()
@@ -131,18 +128,21 @@ Document: {document[:1000]}
Relevance score (0-10):"""
if hasattr(client, 'chat'):
# PraisonAI Agent or similar
# Check if client has a callable 'chat' method (PraisonAI Agent)
if client is not None and hasattr(client, 'chat') and callable(getattr(client, 'chat', None)):
# PraisonAI Agent or similar with callable chat method
response = client.chat(prompt)
else:
# OpenAI client
response = client.chat.completions.create(
# Use litellm for unified multi-provider support
import litellm
# litellm handles model-specific quirks automatically
completion = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=10,
temperature=0
max_tokens=10
)
response = response.choices[0].message.content
response = completion.choices[0].message.content
# Parse score
if response:
@@ -107,7 +107,17 @@ class ChromaVectorStore:
) -> List[str]:
"""Add vectors to ChromaDB."""
ids = ids or [str(uuid_module.uuid4()) for _ in texts]
metadatas = metadatas or [{} for _ in texts]
# ChromaDB requires non-empty metadata dicts
# Ensure each metadata dict has at least one key
if metadatas is None:
metadatas = [{"_source": "praisonai"} for _ in texts]
else:
# Ensure no empty dicts - add placeholder if empty
metadatas = [
m if m else {"_source": "praisonai"}
for m in metadatas
]
# Use different collection if namespace specified
if namespace and namespace != self.namespace:
+4 -12
View File
@@ -79,24 +79,16 @@ def start_chat_server(
_register_agent(a)
# Start the chainlit server
import sys
from pathlib import Path
# Use the built-in app file
app_file = Path(__file__).parent / "app.py"
sys.argv = [
"chainlit",
"run",
str(app_file),
"--host", config.host,
"--port", str(config.port),
]
# Set environment variables for chainlit
os.environ["CHAINLIT_HOST"] = config.host
os.environ["CHAINLIT_PORT"] = str(config.port)
if config.debug:
sys.argv.append("-w")
run_chainlit()
run_chainlit(str(app_file))
# Global registry for agents
@@ -32,6 +32,9 @@ Features:
- hooks: Hook system for intercepting and modifying agent behavior
- checkpoints: Shadow git checkpointing for file-level undo/restore
- background: Background agent task execution and management
- thinking: Thinking budget management
- compaction: Context compaction settings
- output_style: Output style configuration
"""
# Type hints available for IDE support
@@ -80,6 +83,10 @@ __all__ = [
'CheckpointsHandler',
# Background
'BackgroundHandler',
# Thinking, Compaction, Output Style
'ThinkingHandler',
'CompactionHandler',
'OutputStyleHandler',
]
def __getattr__(name):
@@ -188,4 +195,13 @@ def __getattr__(name):
elif name == 'BackgroundHandler':
from .background import BackgroundHandler
return BackgroundHandler
elif name == 'ThinkingHandler':
from .thinking import ThinkingHandler
return ThinkingHandler
elif name == 'CompactionHandler':
from .compaction import CompactionHandler
return CompactionHandler
elif name == 'OutputStyleHandler':
from .output_style import OutputStyleHandler
return OutputStyleHandler
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,294 @@
"""
Context Compaction CLI Feature for PraisonAI.
Provides CLI commands for managing context compaction.
Commands:
- praisonai compaction status # Show settings
- praisonai compaction set <strategy> # Set strategy
- praisonai compaction stats # Show statistics
"""
import os
import json
from typing import Dict, Any
class CompactionHandler:
"""
Handler for compaction CLI commands.
Provides functionality to:
- Show current compaction settings
- Set compaction strategy (truncate, sliding, summarize, smart)
- Display compaction statistics
"""
CONFIG_FILE = ".praison/compaction.json"
def __init__(self, verbose: bool = False):
self.verbose = verbose
@property
def feature_name(self) -> str:
return "compaction"
def _get_config_path(self) -> str:
"""Get path to config file."""
return os.path.join(os.getcwd(), self.CONFIG_FILE)
def _load_config(self) -> Dict[str, Any]:
"""Load config from file."""
config_path = self._get_config_path()
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
pass
return {}
def _save_config(self, config: Dict[str, Any]):
"""Save config to file."""
config_path = self._get_config_path()
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
def status(self) -> Dict[str, Any]:
"""
Show current compaction settings.
Returns:
Current compaction configuration
"""
config = self._load_config()
result = {
"strategy": config.get("strategy", "truncate"),
"max_tokens": config.get("max_tokens", 8000),
"target_tokens": config.get("target_tokens", 6000),
"preserve_system": config.get("preserve_system", True),
"preserve_recent": config.get("preserve_recent", 5),
"config_file": self._get_config_path()
}
self._print_status(result)
return result
def _print_status(self, status: Dict[str, Any]):
"""Print status in formatted output."""
try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
console = Console()
table = Table(show_header=False, box=None)
table.add_column("Setting", style="cyan")
table.add_column("Value", style="green")
table.add_row("Strategy", status["strategy"])
table.add_row("Max Tokens", f"{status['max_tokens']:,}")
table.add_row("Target Tokens", f"{status['target_tokens']:,}")
table.add_row("Preserve System", "✓" if status["preserve_system"] else "✗")
table.add_row("Preserve Recent", str(status["preserve_recent"]))
table.add_row("Config File", status["config_file"])
console.print(Panel(table, title="Context Compaction Status", border_style="blue"))
except ImportError:
print("Context Compaction Status:")
print(f" Strategy: {status['strategy']}")
print(f" Max Tokens: {status['max_tokens']:,}")
print(f" Target Tokens: {status['target_tokens']:,}")
print(f" Preserve System: {status['preserve_system']}")
print(f" Preserve Recent: {status['preserve_recent']}")
print(f" Config File: {status['config_file']}")
def set_strategy(self, strategy: str) -> Dict[str, Any]:
"""
Set compaction strategy.
Args:
strategy: Compaction strategy (truncate, sliding, summarize, smart)
Returns:
Updated configuration
"""
valid_strategies = ["truncate", "sliding", "summarize", "smart"]
strategy = strategy.lower()
if strategy not in valid_strategies:
self._print_error(f"Invalid strategy: {strategy}. Valid strategies: {', '.join(valid_strategies)}")
return {"error": f"Invalid strategy: {strategy}"}
config = self._load_config()
config["strategy"] = strategy
self._save_config(config)
descriptions = {
"truncate": "Remove oldest messages",
"sliding": "Keep sliding window of recent messages",
"summarize": "Summarize older messages",
"smart": "Intelligent compaction based on importance"
}
result = {
"strategy": strategy,
"description": descriptions[strategy],
"message": f"Compaction strategy set to '{strategy}'"
}
self._print_success(result["message"])
return result
def _print_error(self, message: str):
"""Print error message."""
try:
from rich.console import Console
Console().print(f"[red]Error: {message}[/red]")
except ImportError:
print(f"Error: {message}")
def _print_success(self, message: str):
"""Print success message."""
try:
from rich.console import Console
Console().print(f"[green]✓ {message}[/green]")
except ImportError:
print(f"✓ {message}")
def stats(self) -> Dict[str, Any]:
"""
Show compaction statistics.
Returns:
Compaction statistics
"""
config = self._load_config()
stats = config.get("stats", {})
result = {
"compaction_count": stats.get("compaction_count", 0),
"total_tokens_saved": stats.get("total_tokens_saved", 0),
"total_messages_removed": stats.get("total_messages_removed", 0),
"average_compression_ratio": stats.get("average_compression_ratio", 0.0)
}
self._print_stats(result)
return result
def _print_stats(self, stats: Dict[str, Any]):
"""Print statistics in formatted output."""
try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
console = Console()
if stats["compaction_count"] == 0:
console.print("[yellow]No compaction operations recorded yet.[/yellow]")
return
table = Table(show_header=False, box=None)
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Compactions", str(stats["compaction_count"]))
table.add_row("Tokens Saved", f"{stats['total_tokens_saved']:,}")
table.add_row("Messages Removed", str(stats["total_messages_removed"]))
table.add_row("Avg Compression", f"{stats['average_compression_ratio']:.1%}")
console.print(Panel(table, title="Compaction Statistics", border_style="blue"))
except ImportError:
print("Compaction Statistics:")
print(f" Compactions: {stats['compaction_count']}")
print(f" Tokens Saved: {stats['total_tokens_saved']:,}")
print(f" Messages Removed: {stats['total_messages_removed']}")
print(f" Avg Compression: {stats['average_compression_ratio']:.1%}")
def handle_compaction_command(args: list) -> int:
"""
Handle compaction CLI commands.
Args:
args: Command arguments
Returns:
Exit code (0 for success)
"""
if not args:
args = ["status"]
command = args[0].lower()
handler = CompactionHandler()
if command == "status":
handler.status()
return 0
elif command == "set":
if len(args) < 2:
handler._print_error("Usage: praisonai compaction set <strategy>")
print("Strategies: truncate, sliding, summarize, smart")
return 1
result = handler.set_strategy(args[1])
return 0 if "error" not in result else 1
elif command == "stats":
handler.stats()
return 0
elif command == "help":
_print_help()
return 0
else:
handler._print_error(f"Unknown command: {command}")
_print_help()
return 1
def _print_help():
"""Print help message."""
try:
from rich.console import Console
from rich.panel import Panel
console = Console()
help_text = """
[bold]Context Compaction Commands[/bold]
[cyan]praisonai compaction status[/cyan]
Show current compaction settings
[cyan]praisonai compaction set <strategy>[/cyan]
Set compaction strategy
Strategies:
• truncate - Remove oldest messages (default)
• sliding - Keep sliding window of recent messages
• summarize - Summarize older messages
• smart - Intelligent compaction based on importance
[cyan]praisonai compaction stats[/cyan]
Show compaction statistics
[cyan]praisonai compaction help[/cyan]
Show this help message
"""
console.print(Panel(help_text, title="Context Compaction Help", border_style="blue"))
except ImportError:
print("Context Compaction Commands:")
print(" praisonai compaction status - Show current settings")
print(" praisonai compaction set <strategy> - Set compaction strategy")
print(" praisonai compaction stats - Show statistics")
print(" praisonai compaction help - Show this help")
print("")
print("Strategies: truncate, sliding, summarize, smart")
@@ -68,7 +68,7 @@ class KnowledgeHandler(CommandHandler):
return "knowledge"
def get_actions(self) -> List[str]:
return ["add", "query", "search", "list", "clear", "stats", "info", "help"]
return ["add", "query", "search", "list", "clear", "stats", "info", "export", "import", "help"]
def get_help_text(self) -> str:
return """
@@ -78,6 +78,8 @@ Knowledge Commands:
praisonai knowledge list - List indexed documents
praisonai knowledge clear - Clear knowledge base
praisonai knowledge stats - Show knowledge base statistics
praisonai knowledge export <file.json> - Export knowledge base to JSON file
praisonai knowledge import <file.json> - Import knowledge base from JSON file
praisonai knowledge help - Show this help message
Options:
@@ -380,6 +382,150 @@ Examples:
"""Alias for action_stats."""
return self.action_stats(args, **kwargs)
def action_export(self, args: List[str], **kwargs) -> bool:
"""
Export knowledge base to JSON file.
Exports all documents with their metadata, embeddings, and content
for backup or migration purposes.
Args:
args: List containing output file path
Returns:
True if successful
"""
import json
from datetime import datetime
if not args:
# Default filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = os.path.join(self.workspace, f"knowledge_export_{timestamp}.json")
else:
output_file = args[0]
if not os.path.isabs(output_file):
output_file = os.path.join(self.workspace, output_file)
knowledge = self._get_knowledge()
if not knowledge:
return False
try:
export_data = {
"version": "1.0",
"exported_at": datetime.now().isoformat(),
"workspace": self.workspace,
"vector_store": self.vector_store,
"documents": []
}
# Try to get all documents from knowledge/memory
if hasattr(knowledge, 'memory') and hasattr(knowledge.memory, 'get_all'):
all_docs = knowledge.memory.get_all()
if all_docs:
for doc in all_docs:
if isinstance(doc, dict):
export_data["documents"].append(doc)
else:
export_data["documents"].append({
"content": str(doc),
"metadata": {}
})
elif hasattr(knowledge, 'get_all'):
all_docs = knowledge.get_all()
if all_docs:
for doc in all_docs:
if isinstance(doc, dict):
export_data["documents"].append(doc)
else:
export_data["documents"].append({
"content": str(doc),
"metadata": {}
})
# Ensure output directory exists
os.makedirs(os.path.dirname(output_file) or ".", exist_ok=True)
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(export_data, f, indent=2, default=str)
doc_count = len(export_data["documents"])
self.print_status(f"✅ Exported {doc_count} documents to {output_file}", "success")
return True
except Exception as e:
self.print_status(f"Export failed: {e}", "error")
return False
def action_import(self, args: List[str], **kwargs) -> bool:
"""
Import knowledge base from JSON file.
Imports documents from a previously exported JSON file.
Args:
args: List containing input file path
Returns:
True if successful
"""
import json
if not args:
self.print_status("Usage: praisonai knowledge import <file.json>", "error")
return False
input_file = args[0]
if not os.path.isabs(input_file):
input_file = os.path.join(self.workspace, input_file)
if not os.path.exists(input_file):
self.print_status(f"File not found: {input_file}", "error")
return False
knowledge = self._get_knowledge()
if not knowledge:
return False
try:
with open(input_file, 'r', encoding='utf-8') as f:
import_data = json.load(f)
# version field reserved for future schema migrations
_ = import_data.get("version", "unknown")
documents = import_data.get("documents", [])
if not documents:
self.print_status("No documents found in import file", "warning")
return True
imported_count = 0
for doc in documents:
try:
content = doc.get("content") or doc.get("memory") or doc.get("text", "")
metadata = doc.get("metadata", {})
if content:
if hasattr(knowledge, 'store'):
knowledge.store(content, metadata=metadata)
elif hasattr(knowledge, 'add'):
knowledge.add(content, metadata=metadata)
imported_count += 1
except Exception as e:
if self.verbose:
self.print_status(f"Skipped document: {e}", "warning")
self.print_status(f"✅ Imported {imported_count} documents from {input_file}", "success")
return True
except json.JSONDecodeError as e:
self.print_status(f"Invalid JSON file: {e}", "error")
return False
except Exception as e:
self.print_status(f"Import failed: {e}", "error")
return False
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
"""Execute knowledge command action."""
return super().execute(action, action_args, **kwargs)
@@ -0,0 +1,273 @@
"""
Output Styles CLI Feature for PraisonAI.
Provides CLI commands for managing output styles.
Commands:
- praisonai output status # Show current style
- praisonai output set <style> # Set output style
"""
import os
import json
from typing import Dict, Any
class OutputStyleHandler:
"""
Handler for output style CLI commands.
Provides functionality to:
- Show current output style settings
- Set output style (concise, detailed, technical, conversational, structured, minimal)
"""
CONFIG_FILE = ".praison/output.json"
def __init__(self, verbose: bool = False):
self.verbose = verbose
@property
def feature_name(self) -> str:
return "output"
def _get_config_path(self) -> str:
"""Get path to config file."""
return os.path.join(os.getcwd(), self.CONFIG_FILE)
def _load_config(self) -> Dict[str, Any]:
"""Load config from file."""
config_path = self._get_config_path()
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
pass
return {}
def _save_config(self, config: Dict[str, Any]):
"""Save config to file."""
config_path = self._get_config_path()
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
def status(self) -> Dict[str, Any]:
"""
Show current output style settings.
Returns:
Current output style configuration
"""
config = self._load_config()
result = {
"style": config.get("style", "default"),
"format": config.get("format", "markdown"),
"tone": config.get("tone", "professional"),
"verbosity": config.get("verbosity", "normal"),
"max_length": config.get("max_length"),
"config_file": self._get_config_path()
}
self._print_status(result)
return result
def _print_status(self, status: Dict[str, Any]):
"""Print status in formatted output."""
try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
console = Console()
table = Table(show_header=False, box=None)
table.add_column("Setting", style="cyan")
table.add_column("Value", style="green")
table.add_row("Style", status["style"])
table.add_row("Format", status["format"])
table.add_row("Tone", status["tone"])
table.add_row("Verbosity", status["verbosity"])
table.add_row("Max Length", str(status["max_length"]) if status["max_length"] else "None")
table.add_row("Config File", status["config_file"])
console.print(Panel(table, title="Output Style Status", border_style="blue"))
except ImportError:
print("Output Style Status:")
print(f" Style: {status['style']}")
print(f" Format: {status['format']}")
print(f" Tone: {status['tone']}")
print(f" Verbosity: {status['verbosity']}")
print(f" Max Length: {status['max_length'] or 'None'}")
print(f" Config File: {status['config_file']}")
def set_style(self, style: str) -> Dict[str, Any]:
"""
Set output style.
Args:
style: Output style (concise, detailed, technical, conversational, structured, minimal)
Returns:
Updated configuration
"""
valid_styles = ["concise", "detailed", "technical", "conversational", "structured", "minimal"]
style = style.lower()
if style not in valid_styles:
self._print_error(f"Invalid style: {style}. Valid styles: {', '.join(valid_styles)}")
return {"error": f"Invalid style: {style}"}
# Style presets
presets = {
"concise": {
"verbosity": "minimal",
"target_length": 500,
"include_examples": False
},
"detailed": {
"verbosity": "verbose",
"include_examples": True,
"include_explanations": True
},
"technical": {
"tone": "technical",
"use_code_blocks": True,
"include_examples": True
},
"conversational": {
"tone": "friendly",
"format": "plain",
"use_headers": False
},
"structured": {
"format": "markdown",
"use_headers": True,
"use_lists": True
},
"minimal": {
"verbosity": "minimal",
"target_length": 200,
"use_headers": False,
"include_examples": False
}
}
config = self._load_config()
config["style"] = style
config.update(presets.get(style, {}))
self._save_config(config)
descriptions = {
"concise": "Brief, direct responses",
"detailed": "Thorough explanations with examples",
"technical": "Developer-focused with code blocks",
"conversational": "Friendly, casual tone",
"structured": "Well-organized with headers and lists",
"minimal": "Shortest possible responses"
}
result = {
"style": style,
"description": descriptions[style],
"message": f"Output style set to '{style}'"
}
self._print_success(result["message"])
return result
def _print_error(self, message: str):
"""Print error message."""
try:
from rich.console import Console
Console().print(f"[red]Error: {message}[/red]")
except ImportError:
print(f"Error: {message}")
def _print_success(self, message: str):
"""Print success message."""
try:
from rich.console import Console
Console().print(f"[green]✓ {message}[/green]")
except ImportError:
print(f"✓ {message}")
def handle_output_command(args: list) -> int:
"""
Handle output CLI commands.
Args:
args: Command arguments
Returns:
Exit code (0 for success)
"""
if not args:
args = ["status"]
command = args[0].lower()
handler = OutputStyleHandler()
if command == "status":
handler.status()
return 0
elif command == "set":
if len(args) < 2:
handler._print_error("Usage: praisonai output set <style>")
print("Styles: concise, detailed, technical, conversational, structured, minimal")
return 1
result = handler.set_style(args[1])
return 0 if "error" not in result else 1
elif command == "help":
_print_help()
return 0
else:
handler._print_error(f"Unknown command: {command}")
_print_help()
return 1
def _print_help():
"""Print help message."""
try:
from rich.console import Console
from rich.panel import Panel
console = Console()
help_text = """
[bold]Output Style Commands[/bold]
[cyan]praisonai output status[/cyan]
Show current output style settings
[cyan]praisonai output set <style>[/cyan]
Set output style
Styles:
• concise - Brief, direct responses
• detailed - Thorough explanations with examples
• technical - Developer-focused with code blocks
• conversational - Friendly, casual tone
• structured - Well-organized with headers and lists
• minimal - Shortest possible responses
[cyan]praisonai output help[/cyan]
Show this help message
"""
console.print(Panel(help_text, title="Output Style Help", border_style="blue"))
except ImportError:
print("Output Style Commands:")
print(" praisonai output status - Show current settings")
print(" praisonai output set <style> - Set output style")
print(" praisonai output help - Show this help")
print("")
print("Styles: concise, detailed, technical, conversational, structured, minimal")
@@ -0,0 +1,305 @@
"""
Thinking Budgets CLI Feature for PraisonAI.
Provides CLI commands for managing thinking budgets.
Commands:
- praisonai thinking status # Show current budget
- praisonai thinking set <level> # Set budget level
- praisonai thinking stats # Show usage statistics
"""
import os
import json
from typing import Dict, Any
class ThinkingHandler:
"""
Handler for thinking CLI commands.
Provides functionality to:
- Show current thinking budget settings
- Set budget levels (minimal, low, medium, high, maximum)
- Display usage statistics
"""
CONFIG_FILE = ".praison/thinking.json"
def __init__(self, verbose: bool = False):
self.verbose = verbose
self._budget = None
self._tracker = None
@property
def feature_name(self) -> str:
return "thinking"
def _get_config_path(self) -> str:
"""Get path to config file."""
return os.path.join(os.getcwd(), self.CONFIG_FILE)
def _load_config(self) -> Dict[str, Any]:
"""Load config from file."""
config_path = self._get_config_path()
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
pass
return {}
def _save_config(self, config: Dict[str, Any]):
"""Save config to file."""
config_path = self._get_config_path()
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
def status(self) -> Dict[str, Any]:
"""
Show current thinking budget status.
Returns:
Current budget configuration
"""
config = self._load_config()
# Get current budget settings
level = config.get("level", "medium")
max_tokens = config.get("max_tokens", 8000)
adaptive = config.get("adaptive", True)
result = {
"level": level,
"max_tokens": max_tokens,
"adaptive": adaptive,
"config_file": self._get_config_path()
}
self._print_status(result)
return result
def _print_status(self, status: Dict[str, Any]):
"""Print status in formatted output."""
try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
console = Console()
table = Table(show_header=False, box=None)
table.add_column("Setting", style="cyan")
table.add_column("Value", style="green")
table.add_row("Level", status["level"])
table.add_row("Max Tokens", str(status["max_tokens"]))
table.add_row("Adaptive", "✓" if status["adaptive"] else "✗")
table.add_row("Config File", status["config_file"])
console.print(Panel(table, title="Thinking Budget Status", border_style="blue"))
except ImportError:
print("Thinking Budget Status:")
print(f" Level: {status['level']}")
print(f" Max Tokens: {status['max_tokens']}")
print(f" Adaptive: {status['adaptive']}")
print(f" Config File: {status['config_file']}")
def set_level(self, level: str) -> Dict[str, Any]:
"""
Set thinking budget level.
Args:
level: Budget level (minimal, low, medium, high, maximum)
Returns:
Updated configuration
"""
valid_levels = ["minimal", "low", "medium", "high", "maximum"]
level = level.lower()
if level not in valid_levels:
self._print_error(f"Invalid level: {level}. Valid levels: {', '.join(valid_levels)}")
return {"error": f"Invalid level: {level}"}
# Get token allocation for level
token_map = {
"minimal": 2000,
"low": 4000,
"medium": 8000,
"high": 16000,
"maximum": 32000
}
config = self._load_config()
config["level"] = level
config["max_tokens"] = token_map[level]
self._save_config(config)
result = {
"level": level,
"max_tokens": token_map[level],
"message": f"Thinking budget set to {level} ({token_map[level]} tokens)"
}
self._print_success(result["message"])
return result
def _print_error(self, message: str):
"""Print error message."""
try:
from rich.console import Console
Console().print(f"[red]Error: {message}[/red]")
except ImportError:
print(f"Error: {message}")
def _print_success(self, message: str):
"""Print success message."""
try:
from rich.console import Console
Console().print(f"[green]✓ {message}[/green]")
except ImportError:
print(f"✓ {message}")
def stats(self) -> Dict[str, Any]:
"""
Show thinking usage statistics.
Returns:
Usage statistics
"""
config = self._load_config()
stats = config.get("stats", {})
result = {
"session_count": stats.get("session_count", 0),
"total_tokens_used": stats.get("total_tokens_used", 0),
"total_time_seconds": stats.get("total_time_seconds", 0.0),
"average_tokens_per_session": stats.get("average_tokens_per_session", 0.0),
"average_utilization": stats.get("average_utilization", 0.0),
"over_budget_count": stats.get("over_budget_count", 0)
}
self._print_stats(result)
return result
def _print_stats(self, stats: Dict[str, Any]):
"""Print statistics in formatted output."""
try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
console = Console()
if stats["session_count"] == 0:
console.print("[yellow]No thinking sessions recorded yet.[/yellow]")
return
table = Table(show_header=False, box=None)
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Sessions", str(stats["session_count"]))
table.add_row("Total Tokens Used", f"{stats['total_tokens_used']:,}")
table.add_row("Total Time", f"{stats['total_time_seconds']:.1f}s")
table.add_row("Avg Tokens/Session", f"{stats['average_tokens_per_session']:.0f}")
table.add_row("Avg Utilization", f"{stats['average_utilization']:.1%}")
table.add_row("Over Budget", str(stats["over_budget_count"]))
console.print(Panel(table, title="Thinking Usage Statistics", border_style="blue"))
except ImportError:
print("Thinking Usage Statistics:")
print(f" Sessions: {stats['session_count']}")
print(f" Total Tokens Used: {stats['total_tokens_used']:,}")
print(f" Total Time: {stats['total_time_seconds']:.1f}s")
print(f" Avg Tokens/Session: {stats['average_tokens_per_session']:.0f}")
print(f" Avg Utilization: {stats['average_utilization']:.1%}")
print(f" Over Budget: {stats['over_budget_count']}")
def handle_thinking_command(args: list) -> int:
"""
Handle thinking CLI commands.
Args:
args: Command arguments
Returns:
Exit code (0 for success)
"""
if not args:
args = ["status"]
command = args[0].lower()
handler = ThinkingHandler()
if command == "status":
handler.status()
return 0
elif command == "set":
if len(args) < 2:
handler._print_error("Usage: praisonai thinking set <level>")
print("Levels: minimal, low, medium, high, maximum")
return 1
result = handler.set_level(args[1])
return 0 if "error" not in result else 1
elif command == "stats":
handler.stats()
return 0
elif command == "help":
_print_help()
return 0
else:
handler._print_error(f"Unknown command: {command}")
_print_help()
return 1
def _print_help():
"""Print help message."""
try:
from rich.console import Console
from rich.panel import Panel
console = Console()
help_text = """
[bold]Thinking Budget Commands[/bold]
[cyan]praisonai thinking status[/cyan]
Show current thinking budget settings
[cyan]praisonai thinking set <level>[/cyan]
Set budget level (minimal, low, medium, high, maximum)
Levels:
• minimal - 2,000 tokens
• low - 4,000 tokens
• medium - 8,000 tokens (default)
• high - 16,000 tokens
• maximum - 32,000 tokens
[cyan]praisonai thinking stats[/cyan]
Show usage statistics
[cyan]praisonai thinking help[/cyan]
Show this help message
"""
console.print(Panel(help_text, title="Thinking Budget Help", border_style="blue"))
except ImportError:
print("Thinking Budget Commands:")
print(" praisonai thinking status - Show current settings")
print(" praisonai thinking set <level> - Set budget level")
print(" praisonai thinking stats - Show usage statistics")
print(" praisonai thinking help - Show this help")
print("")
print("Levels: minimal (2K), low (4K), medium (8K), high (16K), maximum (32K)")
+19 -1
View File
@@ -668,7 +668,7 @@ class PraisonAI:
return default_args
# Define special commands
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run']
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output']
parser = argparse.ArgumentParser(prog="praisonai", description="praisonAI command-line interface")
parser.add_argument("--framework", choices=["crewai", "autogen", "praisonai"], help="Specify the framework")
@@ -1162,6 +1162,24 @@ class PraisonAI:
from .features.jobs import handle_run_command
handle_run_command(unknown_args, verbose=getattr(args, 'verbose', False))
sys.exit(0)
elif args.command == 'thinking':
# Thinking command - manage thinking budgets
from .features.thinking import handle_thinking_command
exit_code = handle_thinking_command(unknown_args)
sys.exit(exit_code)
elif args.command == 'compaction':
# Compaction command - manage context compaction
from .features.compaction import handle_compaction_command
exit_code = handle_compaction_command(unknown_args)
sys.exit(exit_code)
elif args.command == 'output':
# Output command - manage output styles
from .features.output_style import handle_output_command
exit_code = handle_output_command(unknown_args)
sys.exit(exit_code)
# Only check framework availability for agent-related operations
if not args.command and (args.init or args.auto or args.framework):
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.5.2 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.5.5 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.5.2"
__version__ = "2.5.5"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.3.2",
"praisonaiagents>=0.3.4",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -59,12 +59,12 @@ class TestKnowledgeIntegration:
}
})
# Add document
result = knowledge.add(test_file)
# Add document (mem0 requires user_id, agent_id, or run_id)
result = knowledge.add(test_file, user_id="test_user")
assert result is not None
# Search
results = knowledge.search("Who created Python?")
results = knowledge.search("Who created Python?", user_id="test_user")
assert len(results) > 0
@requires_openai
@@ -83,15 +83,17 @@ class TestKnowledgeIntegration:
}
})
# Store text
knowledge.store("The capital of France is Paris.")
knowledge.store("The capital of Germany is Berlin.")
# Store text (mem0 requires user_id, agent_id, or run_id)
knowledge.store("The capital of France is Paris.", user_id="test_user")
knowledge.store("The capital of Germany is Berlin.", user_id="test_user")
# Search
results = knowledge.search("What is the capital of France?")
assert len(results) > 0
results = knowledge.search("What is the capital of France?", user_id="test_user")
# Results is a dict with 'results' key containing list of memories
result_list = results.get("results", results) if isinstance(results, dict) else results
assert len(result_list) > 0
# Check that Paris is mentioned in results
result_text = str(results[0]).lower()
result_text = str(result_list[0]).lower()
assert "paris" in result_text or "france" in result_text
@@ -187,7 +189,7 @@ class TestRerankerIntegration:
except ImportError:
pytest.skip("Reranker not available")
reranker = LLMReranker(model="gpt-5-nano")
reranker = LLMReranker(model="gpt-4o-mini")
documents = [
"Python is a programming language.",
@@ -266,3 +266,177 @@ class TestKnowledgeHandlerEdgeCases:
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
results = handler.action_query(["test"])
assert results == []
class TestKnowledgeHandlerExportImport:
"""Tests for export/import functionality."""
def test_get_actions_includes_export_import(self):
"""Test that export and import are in available actions."""
handler = KnowledgeHandler()
actions = handler.get_actions()
assert "export" in actions
assert "import" in actions
def test_action_export_default_filename(self):
"""Test export with default filename."""
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
mock_knowledge.get_all.return_value = [
{"content": "doc1", "metadata": {}},
{"content": "doc2", "metadata": {}}
]
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_export([])
assert result is True
# Check that file was created
files = os.listdir(tmpdir)
export_files = [f for f in files if f.startswith("knowledge_export_")]
assert len(export_files) == 1
def test_action_export_custom_filename(self):
"""Test export with custom filename."""
import json
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
# Set up memory.get_all since that's checked first
mock_knowledge.memory = MagicMock()
mock_knowledge.memory.get_all.return_value = [
{"content": "test content", "metadata": {"source": "test"}}
]
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
output_file = os.path.join(tmpdir, "my_export.json")
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_export([output_file])
assert result is True
assert os.path.exists(output_file)
with open(output_file, 'r') as f:
data = json.load(f)
assert data["version"] == "1.0"
assert len(data["documents"]) == 1
def test_action_export_empty_knowledge(self):
"""Test export with empty knowledge base."""
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
mock_knowledge.get_all.return_value = []
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
output_file = os.path.join(tmpdir, "empty_export.json")
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_export([output_file])
assert result is True
def test_action_import_no_args(self):
"""Test import with no arguments."""
handler = KnowledgeHandler()
result = handler.action_import([])
assert result is False
def test_action_import_nonexistent_file(self):
"""Test import with non-existent file."""
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_import(["/nonexistent/file.json"])
assert result is False
def test_action_import_valid_file(self):
"""Test import with valid JSON file."""
import json
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
input_file = os.path.join(tmpdir, "import_test.json")
# Create test import file
import_data = {
"version": "1.0",
"documents": [
{"content": "doc1", "metadata": {}},
{"content": "doc2", "metadata": {"key": "value"}}
]
}
with open(input_file, 'w') as f:
json.dump(import_data, f)
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_import([input_file])
assert result is True
assert mock_knowledge.store.call_count == 2 or mock_knowledge.add.call_count == 2
def test_action_import_empty_documents(self):
"""Test import with empty documents list."""
import json
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
input_file = os.path.join(tmpdir, "empty_import.json")
import_data = {"version": "1.0", "documents": []}
with open(input_file, 'w') as f:
json.dump(import_data, f)
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_import([input_file])
assert result is True
def test_action_import_invalid_json(self):
"""Test import with invalid JSON file."""
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
input_file = os.path.join(tmpdir, "invalid.json")
with open(input_file, 'w') as f:
f.write("not valid json {{{")
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_import([input_file])
assert result is False
def test_export_import_roundtrip(self):
"""Test that export and import work together."""
handler = KnowledgeHandler()
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
export_file = os.path.join(tmpdir, "roundtrip.json")
# Mock for export - set up memory.get_all since that's checked first
mock_knowledge_export = MagicMock()
mock_knowledge_export.memory = MagicMock()
mock_knowledge_export.memory.get_all.return_value = [
{"content": "test doc 1", "metadata": {"id": "1"}},
{"content": "test doc 2", "metadata": {"id": "2"}}
]
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge_export):
export_result = handler.action_export([export_file])
assert export_result is True
# Mock for import
mock_knowledge_import = MagicMock()
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge_import):
import_result = handler.action_import([export_file])
assert import_result is True
# Verify documents were imported
assert mock_knowledge_import.store.call_count == 2 or mock_knowledge_import.add.call_count == 2
+4 -4
View File
@@ -4197,7 +4197,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
{ name = "praisonaiagents", specifier = ">=0.3.2" },
{ name = "praisonaiagents", specifier = ">=0.3.4" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4252,7 +4252,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.3.2"
version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -4261,9 +4261,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/8a/a33381c4f69e4ca1b639ad6b32d516c1adcd1d521ed78c8cdb2a947249fc/praisonaiagents-0.3.2.tar.gz", hash = "sha256:569fed4d865491cb67e7f3b6e2a603902f15bbeea1e104eb1691f634280b3155", size = 757144, upload-time = "2025-12-26T05:20:13.557Z" }
sdist = { url = "https://files.pythonhosted.org/packages/83/87/512452be38f96bb22d4bc734d3e1e724d2a07c0da5c7bb16e18934227790/praisonaiagents-0.3.4.tar.gz", hash = "sha256:f34e7719bbac7b31aed2f677a7306fa6412d51eab8043204bef48a99c9d5998d", size = 757654, upload-time = "2025-12-26T06:49:01.069Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/30/57526915881e5a9d1c3c24f6369c055df93613e4e5f64db6a3fc32579ce2/praisonaiagents-0.3.2-py3-none-any.whl", hash = "sha256:50f7b6aa0a825b16f001e7a09b7eb120b89f36b010743eca6b1ec6322d9662ca", size = 707842, upload-time = "2025-12-26T05:20:11.181Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/49671d346264bcd208921a345e6c38e8dcd7a2dad9ad0d049f3355b98d57/praisonaiagents-0.3.4-py3-none-any.whl", hash = "sha256:9d345c001ae7ef5e18dd1c66313231337b37abb1ecb45fa6dac17c1e7ed43247", size = 708253, upload-time = "2025-12-26T06:48:59.068Z" },
]
[[package]]