Compare commits

...
11 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
Mervin Praison ff3a4f2016 Release v2.5.2 2025-12-26 05:25:08 +00:00
Mervin Praison 0d5444fc23 fix: add missing praisonai.db module to git
- Add praisonai/db/ module that was being ignored by *.db pattern
- This fixes ModuleNotFoundError: No module named 'praisonai.db' in CI tests
2025-12-26 04:54:05 +00:00
Mervin Praison d7d5dab17b fix: add missing db module to git and update .gitignore
- Add praisonaiagents/db/ module that was being ignored by *.db pattern
- Add exception in .gitignore for the db module directory
- This fixes ModuleNotFoundError: No module named 'praisonaiagents.db' in CI
2025-12-26 04:49:31 +00:00
Mervin Praison 05678394e6 fix: remove sys.exit() at module level in test files causing pytest INTERNALERROR
- test_mini_agents_fix.py: Convert module-level sys.exit(1) to pytest.mark.skipif pattern
- test_xai_fix.py: Convert to proper pytest test function with cleanup

These sys.exit() calls at module level caused pytest to crash during test collection
with INTERNALERROR, breaking all CI pipelines.
2025-12-26 04:46:58 +00:00
Mervin Praison 54cddc09b4 Release v2.5.1 2025-12-26 04:23:51 +00:00
134 changed files with 20761 additions and 1088 deletions

No files matched your search

+609 -156
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,141 +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) |
| **🔬 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>
---
@@ -537,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:
@@ -586,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
@@ -601,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:
@@ -871,7 +1079,7 @@ callbacks:
on_workflow_complete: log_complete
```
### 9. Hooks
### 10. Hooks
Intercept and modify agent behavior at various lifecycle points:
@@ -910,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:
@@ -947,7 +1155,7 @@ praisonai checkpoint restore abc123 # Restore to checkpoint
---
### 11. Background Tasks
### 12. Background Tasks
Run agent tasks asynchronously without blocking:
@@ -985,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:
@@ -1026,7 +1234,7 @@ praisonai policy init # Create template
---
### 13. Thinking Budgets
### 14. Thinking Budgets
Configure token budgets for extended thinking:
@@ -1059,7 +1267,7 @@ praisonai thinking stats # Show usage statistics
---
### 14. Output Styles
### 15. Output Styles
Configure how agents format their responses:
@@ -1089,7 +1297,7 @@ praisonai output set concise # Set output style
---
### 15. Context Compaction
### 16. Context Compaction
Automatically manage context window size:
@@ -1124,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:
@@ -1158,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)
@@ -1234,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.
@@ -1310,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.
@@ -1358,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
@@ -2641,6 +2865,84 @@ PraisonAI provides zero-dependency persistent memory for agents. For detailed ex
---
## 📚 Knowledge & Retrieval (RAG)
PraisonAI provides a complete knowledge stack for building RAG applications with multiple vector stores, retrieval strategies, rerankers, and query modes.
### Knowledge CLI Commands
| Command | Description |
|---------|-------------|
| `praisonai knowledge add <file\|dir\|url>` | Add documents to knowledge base |
| `praisonai knowledge query <question>` | Query knowledge base with RAG |
| `praisonai knowledge list` | List indexed documents |
| `praisonai knowledge clear` | Clear knowledge base |
| `praisonai knowledge stats` | Show knowledge base statistics |
### Knowledge CLI Options
| Option | Values | Description |
|--------|--------|-------------|
| `--vector-store` | `memory`, `chroma`, `pinecone`, `qdrant`, `weaviate` | Vector store backend |
| `--retrieval` | `basic`, `fusion`, `recursive`, `auto_merge` | Retrieval strategy |
| `--reranker` | `simple`, `llm`, `cross_encoder`, `cohere` | Reranking method |
| `--index-type` | `vector`, `keyword`, `hybrid` | Index type |
| `--query-mode` | `default`, `sub_question`, `summarize` | Query mode |
### Knowledge CLI Examples
```bash
# Add documents
praisonai knowledge add ./docs/
praisonai knowledge add https://example.com/page.html
praisonai knowledge add "*.pdf"
# Query with advanced options
praisonai knowledge query "How to authenticate?" --retrieval fusion --reranker llm
# Full advanced query
praisonai knowledge query "authentication flow" \
--vector-store chroma \
--retrieval fusion \
--reranker llm \
--index-type hybrid \
--query-mode sub_question
```
### Knowledge SDK Usage
```python
from praisonaiagents import Agent, Knowledge
# Simple usage with Agent
agent = Agent(
name="Research Assistant",
knowledge=["docs/manual.pdf", "data/faq.txt"],
knowledge_config={
"vector_store": {"provider": "chroma"}
}
)
response = agent.chat("How do I authenticate?")
# Direct Knowledge usage
knowledge = Knowledge()
knowledge.add("document.pdf")
results = knowledge.search("authentication", limit=5)
```
### Knowledge Stack Features Table
| Feature | Description | SDK Docs | CLI Docs |
|---------|-------------|----------|----------|
| **Data Readers** | Load PDF, Markdown, Text, HTML, URLs | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Vector Stores** | ChromaDB, Pinecone, Qdrant, Weaviate, In-Memory | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Retrieval Strategies** | Basic, Fusion (RRF), Recursive, Auto-Merge | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Rerankers** | Simple, LLM, Cross-Encoder, Cohere | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Index Types** | Vector, Keyword (BM25), Hybrid | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Query Engines** | Default, Sub-Question, Summarize | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
---
## 🔬 Advanced Features
### Research & Intelligence
@@ -2755,6 +3057,44 @@ agent.chat("Hello!") # Auto-persists messages, runs, traces
| `praisonai persistence migrate` | Apply schema migrations |
| `praisonai persistence status` | Show schema status |
### Knowledge CLI Commands {#knowledge-cli}
| Command | Description |
|---------|-------------|
| `praisonai knowledge add <source>` | Add file, directory, URL, or glob pattern |
| `praisonai knowledge query "<question>"` | Query knowledge base with RAG |
| `praisonai knowledge list` | List indexed documents |
| `praisonai knowledge clear` | Clear knowledge base |
| `praisonai knowledge stats` | Show knowledge base statistics |
**Knowledge Query Flags:**
| Flag | Values | Default |
|------|--------|---------|
| `--vector-store` | `memory`, `chroma`, `pinecone`, `qdrant`, `weaviate` | `chroma` |
| `--retrieval-strategy` | `basic`, `fusion`, `recursive`, `auto_merge` | `basic` |
| `--reranker` | `none`, `simple`, `llm`, `cross_encoder`, `cohere` | `none` |
| `--index-type` | `vector`, `keyword`, `hybrid` | `vector` |
| `--query-mode` | `default`, `sub_question`, `summarize` | `default` |
| `--workspace` | Path to workspace directory | Current dir |
| `--session` | Session ID for persistence | - |
**Examples:**
```bash
# Add documents
praisonai knowledge add document.pdf
praisonai knowledge add ./docs/
praisonai knowledge add "*.md"
# Query with options
praisonai knowledge query "How to authenticate?" \
--vector-store chroma \
--retrieval-strategy fusion \
--reranker simple \
--query-mode sub_question
```
### Databases Table
| Database | Store Type | Install | Example | Docs |
@@ -2901,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.0" \
"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.0" \
"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.0" \
"praisonai>=2.5.5" \
"praisonai[ui]" \
"praisonai[crewai]"
+35
View File
@@ -0,0 +1,35 @@
"""
Async Runs Examples
===================
This directory contains examples for the PraisonAI Async Jobs API.
Examples:
---------
1. sdk_submit_poll_result.py - Python SDK example for submit/poll/result
2. cli_submit_poll_result.sh - Bash/curl example for submit/poll/result
3. idempotency.sh - Demonstrates idempotency key usage
4. cancel.sh - Demonstrates job cancellation
5. sse_stream.sh - Demonstrates SSE streaming
Prerequisites:
--------------
1. Set OPENAI_API_KEY environment variable
2. Start the jobs server:
cd /path/to/praisonai-package/src/praisonai
python -m uvicorn praisonai.jobs.server:create_app --port 8005 --factory
3. Run examples:
python sdk_submit_poll_result.py
bash cli_submit_poll_result.sh
bash idempotency.sh
bash cancel.sh
bash sse_stream.sh
Environment Variables:
----------------------
- OPENAI_API_KEY: Required for agent execution
- PRAISONAI_BASE_URL: API server URL (default: http://127.0.0.1:8005)
"""
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# CLI Example: Cancel a Running Job
#
# This example demonstrates how to cancel a running job.
#
# Requirements:
# - Jobs server running on http://127.0.0.1:8005
#
# Usage:
# bash cancel.sh
set -e
API_URL="${PRAISONAI_BASE_URL:-http://127.0.0.1:8005}"
echo "============================================================"
echo "PraisonAI Job Cancellation Example"
echo "============================================================"
# Submit a long-running job
echo ""
echo "1. Submitting a job..."
RESPONSE=$(curl -s -X POST "$API_URL/api/v1/runs" \
-H "Content-Type: application/json" \
-d '{"prompt": "Write a detailed essay about the history of computing."}')
JOB_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
echo " Job ID: $JOB_ID"
# Wait briefly
echo ""
echo "2. Waiting 1 second..."
sleep 1
# Cancel the job
echo ""
echo "3. Cancelling job..."
CANCEL_RESPONSE=$(curl -s -X POST "$API_URL/api/v1/runs/$JOB_ID/cancel")
STATUS=$(echo "$CANCEL_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
echo " Status after cancel: $STATUS"
# Verify cancellation
echo ""
echo "4. Verification:"
if [ "$STATUS" = "cancelled" ]; then
echo " ✓ SUCCESS: Job was cancelled"
else
echo " ℹ Job status: $STATUS (may have completed before cancel)"
fi
echo ""
echo "============================================================"
echo "Cancellation example completed!"
echo "============================================================"
@@ -0,0 +1,75 @@
#!/bin/bash
# CLI Example: Submit, Poll, and Get Result for Async Jobs
#
# This example demonstrates how to use the PraisonAI CLI to:
# 1. Submit an async job
# 2. Check job status
# 3. Get the result
#
# Requirements:
# - OPENAI_API_KEY environment variable set
# - Jobs server running on http://127.0.0.1:8005
#
# Usage:
# bash cli_submit_poll_result.sh
set -e
API_URL="${PRAISONAI_BASE_URL:-http://127.0.0.1:8005}"
echo "============================================================"
echo "PraisonAI Async Jobs CLI Example"
echo "============================================================"
# Check if server is running
echo ""
echo "Checking server health..."
if ! curl -s "$API_URL/health" > /dev/null 2>&1; then
echo "✗ Server not available at $API_URL"
echo " Start the server with: python -m uvicorn praisonai.jobs.server:create_app --port 8005 --factory"
exit 1
fi
echo "✓ Server is healthy"
# 1. Submit a job
echo ""
echo "1. Submitting job..."
RESPONSE=$(curl -s -X POST "$API_URL/api/v1/runs" \
-H "Content-Type: application/json" \
-d '{"prompt": "What is 2+2? Answer with just the number."}')
JOB_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
echo " Job ID: $JOB_ID"
# 2. Poll for status
echo ""
echo "2. Polling for status..."
while true; do
STATUS_RESPONSE=$(curl -s "$API_URL/api/v1/runs/$JOB_ID")
STATUS=$(echo "$STATUS_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
PROGRESS=$(echo "$STATUS_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['progress']['percentage'])")
echo " Status: $STATUS | Progress: ${PROGRESS}%"
if [ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] || [ "$STATUS" = "cancelled" ]; then
break
fi
sleep 2
done
# 3. Get result
echo ""
echo "3. Getting result..."
if [ "$STATUS" = "succeeded" ]; then
RESULT=$(curl -s "$API_URL/api/v1/runs/$JOB_ID/result")
echo " Result: $(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'])")"
echo " Duration: $(echo "$RESULT" | python3 -c "import sys,json; print(f\"{json.load(sys.stdin)['duration_seconds']:.2f}s\")")"
else
echo " Job did not succeed: $STATUS"
fi
echo ""
echo "============================================================"
echo "Example completed!"
echo "============================================================"
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# CLI Example: Idempotency Key Usage
#
# This example demonstrates how idempotency keys prevent duplicate job submissions.
# Submitting the same request with the same idempotency key returns the existing job.
#
# Requirements:
# - Jobs server running on http://127.0.0.1:8005
#
# Usage:
# bash idempotency.sh
set -e
API_URL="${PRAISONAI_BASE_URL:-http://127.0.0.1:8005}"
IDEM_KEY="example-idem-key-$(date +%s)"
echo "============================================================"
echo "PraisonAI Idempotency Key Example"
echo "============================================================"
# First submission
echo ""
echo "1. First submission with Idempotency-Key: $IDEM_KEY"
RESPONSE1=$(curl -s -X POST "$API_URL/api/v1/runs" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM_KEY" \
-d '{"prompt": "What is 1+1?"}')
JOB_ID1=$(echo "$RESPONSE1" | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
echo " Job ID: $JOB_ID1"
# Second submission with same key
echo ""
echo "2. Second submission with SAME Idempotency-Key: $IDEM_KEY"
RESPONSE2=$(curl -s -X POST "$API_URL/api/v1/runs" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM_KEY" \
-d '{"prompt": "Different prompt but same key"}')
JOB_ID2=$(echo "$RESPONSE2" | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
echo " Job ID: $JOB_ID2"
# Verify same job ID
echo ""
echo "3. Verification:"
if [ "$JOB_ID1" = "$JOB_ID2" ]; then
echo " ✓ SUCCESS: Same job ID returned (no duplicate created)"
else
echo " ✗ FAILURE: Different job IDs returned"
exit 1
fi
echo ""
echo "============================================================"
echo "Idempotency example completed!"
echo "============================================================"
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
SDK Example: Submit, Poll, and Get Result for Async Jobs
This example demonstrates how to use the PraisonAI Jobs SDK to:
1. Submit an async job
2. Poll for status
3. Get the result
Requirements:
- OPENAI_API_KEY environment variable set
- Jobs server running on http://127.0.0.1:8005
Usage:
python sdk_submit_poll_result.py
"""
import os
import time
import httpx
# Configuration
API_URL = os.getenv("PRAISONAI_BASE_URL", "http://127.0.0.1:8005")
POLL_INTERVAL = 2 # seconds
def submit_job(prompt: str, idempotency_key: str = None) -> dict:
"""Submit a job and return the response."""
headers = {}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
with httpx.Client(timeout=30.0) as client:
response = client.post(
f"{API_URL}/api/v1/runs",
json={"prompt": prompt},
headers=headers if headers else None
)
response.raise_for_status()
return response.json()
def get_status(job_id: str) -> dict:
"""Get job status."""
with httpx.Client(timeout=30.0) as client:
response = client.get(f"{API_URL}/api/v1/runs/{job_id}")
response.raise_for_status()
return response.json()
def get_result(job_id: str) -> dict:
"""Get job result."""
with httpx.Client(timeout=30.0) as client:
response = client.get(f"{API_URL}/api/v1/runs/{job_id}/result")
response.raise_for_status()
return response.json()
def wait_for_completion(job_id: str, max_wait: int = 120) -> dict:
"""Poll until job completes."""
start = time.time()
while time.time() - start < max_wait:
status = get_status(job_id)
print(f" Status: {status['status']} | Progress: {status['progress']['percentage']:.0f}%")
if status["status"] in ("succeeded", "failed", "cancelled"):
return status
# Honor retry_after if present
wait_time = status.get("retry_after") or POLL_INTERVAL
time.sleep(wait_time)
raise TimeoutError(f"Job {job_id} did not complete within {max_wait}s")
def main():
print("=" * 60)
print("PraisonAI Async Jobs SDK Example")
print("=" * 60)
# Check if server is running
try:
with httpx.Client(timeout=5.0) as client:
health = client.get(f"{API_URL}/health")
health.raise_for_status()
print(f"✓ Server healthy: {health.json()['status']}")
except Exception as e:
print(f"✗ Server not available at {API_URL}: {e}")
print(" Start the server with: python -m uvicorn praisonai.jobs.server:create_app --port 8005 --factory")
return
# 1. Submit a job
print("\n1. Submitting job...")
prompt = "What is the capital of France? Answer in one word."
result = submit_job(prompt)
job_id = result["job_id"]
print(f" Job ID: {job_id}")
print(f" Status: {result['status']}")
print(f" Poll URL: {result['poll_url']}")
# 2. Wait for completion
print("\n2. Waiting for completion...")
final_status = wait_for_completion(job_id)
print(f" Final status: {final_status['status']}")
# 3. Get result
if final_status["status"] == "succeeded":
print("\n3. Getting result...")
result = get_result(job_id)
print(f" Result: {result['result']}")
print(f" Duration: {result['duration_seconds']:.2f}s")
else:
print(f"\n3. Job did not succeed: {final_status.get('error', 'Unknown error')}")
print("\n" + "=" * 60)
print("Example completed successfully!")
print("=" * 60)
if __name__ == "__main__":
main()
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# CLI Example: SSE Streaming
#
# This example demonstrates how to stream job progress via Server-Sent Events.
#
# Requirements:
# - Jobs server running on http://127.0.0.1:8005
#
# Usage:
# bash sse_stream.sh
set -e
API_URL="${PRAISONAI_BASE_URL:-http://127.0.0.1:8005}"
echo "============================================================"
echo "PraisonAI SSE Streaming Example"
echo "============================================================"
# Submit a job
echo ""
echo "1. Submitting job..."
RESPONSE=$(curl -s -X POST "$API_URL/api/v1/runs" \
-H "Content-Type: application/json" \
-d '{"prompt": "Count from 1 to 5."}')
JOB_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
echo " Job ID: $JOB_ID"
# Stream progress
echo ""
echo "2. Streaming progress (Ctrl+C to stop)..."
echo " Connecting to: $API_URL/api/v1/runs/$JOB_ID/stream"
echo ""
# Use curl to stream SSE events (timeout after 60s)
curl -s -N --max-time 60 "$API_URL/api/v1/runs/$JOB_ID/stream" | while read -r line; do
if [[ "$line" == data:* ]]; then
data="${line#data: }"
if [ "$data" = "[DONE]" ]; then
echo " Stream completed"
break
fi
echo " Event: $data"
fi
done
echo ""
echo "============================================================"
echo "Streaming example completed!"
echo "============================================================"
+44 -52
View File
@@ -1,7 +1,7 @@
"""
Qdrant Knowledge Store Example for PraisonAI.
Qdrant Knowledge Store - Agent-First Example
Demonstrates vector insert + search through the knowledge store.
Demonstrates using Qdrant for knowledge-based RAG with an Agent.
Requirements:
pip install "praisonai[tools]"
@@ -13,67 +13,59 @@ Run:
python knowledge_qdrant.py
Expected Output:
=== Qdrant Knowledge Store Demo ===
Created collection: demo_knowledge
Inserted 3 documents
Search results: 2 documents found
Agent responds using knowledge from documents
"""
from praisonai.persistence.factory import create_knowledge_store
from praisonai.persistence.knowledge.base import KnowledgeDocument
import random
from praisonaiagents import Agent
import tempfile
import os
import shutil
# Create Qdrant knowledge store
store = create_knowledge_store("qdrant", url="http://localhost:6333")
# Create sample knowledge document
sample_doc = """
# AI and Programming Guide
collection_name = "demo_knowledge"
dimension = 384 # Embedding dimension
## Python Programming
Python is a versatile programming language used for web development, data science, and AI.
It has a simple syntax and extensive libraries.
# Create collection
print("=== Qdrant Knowledge Store Demo ===")
if store.collection_exists(collection_name):
store.delete_collection(collection_name)
## Machine Learning
Machine learning enables computers to learn patterns from data without explicit programming.
Common frameworks include TensorFlow, PyTorch, and scikit-learn.
store.create_collection(collection_name, dimension=dimension)
print(f"Created collection: {collection_name}")
## Databases
PostgreSQL is a powerful open-source relational database system.
It supports both SQL and JSON data types.
"""
# Create sample documents with embeddings
random.seed(42)
documents = [
KnowledgeDocument(
id="doc-1",
content="Python is a versatile programming language used for web development, data science, and AI.",
embedding=[random.random() for _ in range(dimension)],
metadata={"category": "programming", "language": "python"}
),
KnowledgeDocument(
id="doc-2",
content="Machine learning enables computers to learn patterns from data without explicit programming.",
embedding=[random.random() for _ in range(dimension)],
metadata={"category": "ai", "topic": "ml"}
),
KnowledgeDocument(
id="doc-3",
content="PostgreSQL is a powerful open-source relational database system.",
embedding=[random.random() for _ in range(dimension)],
metadata={"category": "database", "type": "sql"}
),
]
# Save to temp file
temp_dir = tempfile.mkdtemp()
doc_path = os.path.join(temp_dir, "guide.txt")
with open(doc_path, "w") as f:
f.write(sample_doc)
# Insert documents
ids = store.insert(collection_name, documents)
print(f"Inserted {len(ids)} documents")
print("=== Qdrant Knowledge Store Demo (Agent-First) ===")
# Search for similar documents
query_embedding = [random.random() for _ in range(dimension)]
results = store.search(collection_name, query_embedding, limit=2)
print(f"Search results: {len(results)} documents found")
# Agent-first approach: use knowledge parameter with Qdrant
agent = Agent(
name="KnowledgeAssistant",
instructions="You are a helpful assistant with access to technical documentation.",
knowledge=[doc_path],
knowledge_config={
"vector_store": "qdrant",
"url": "http://localhost:6333"
}
)
for doc in results:
print(f" - {doc.id}: {doc.content[:50]}...")
# Chat - agent uses knowledge for RAG
response = agent.chat("What programming language is good for AI?")
print(f"Response: {response}")
# Cleanup
store.delete_collection(collection_name)
store.close()
shutil.rmtree(temp_dir)
print("\n=== Demo Complete ===")
# --- Advanced: Direct Store Usage ---
# from praisonai.persistence.factory import create_knowledge_store
# store = create_knowledge_store("qdrant", url="http://localhost:6333")
+28 -65
View File
@@ -1,83 +1,46 @@
"""
Redis State Store
Redis State Store - Agent-First Example
Use Redis for state management, tracing, and caching.
Use Redis for state management with an Agent.
Docker Setup:
docker run -d --name redis -p 6379:6379 redis:7
Run:
python redis_state.py
Expected output:
- State operations (set/get/delete)
- Tracing operations
Agent responds with state persisted to Redis
"""
from praisonaiagents import db
from praisonaiagents import Agent, db
# Create Redis-only database (for state/tracing)
redis_db = db.RedisDB(host="localhost", port=6379)
print("=== Redis State Store (Agent-First) ===")
print("=== Redis State Store ===")
# Test state operations
print("\n1. State Operations:")
redis_db._init_stores() # Initialize stores
# Set a value
redis_db._state_store.set("user:123:preferences", {
"theme": "dark",
"language": "en"
})
print(" Set user preferences")
# Get the value
prefs = redis_db._state_store.get("user:123:preferences")
print(f" Got preferences: {prefs}")
# Set with TTL (expires in 60 seconds)
redis_db._state_store.set("temp:token", "abc123", ttl=60)
print(" Set temporary token (60s TTL)")
# Delete
redis_db._state_store.delete("user:123:preferences")
print(" Deleted preferences")
# Test tracing
print("\n2. Tracing Operations:")
# Start a trace
trace_id = "trace-example-001"
redis_db.on_trace_start(
trace_id=trace_id,
session_id="example-session",
agent_name="ExampleAgent",
metadata={"purpose": "demo"}
# Agent-first approach: use db parameter with Redis state store
my_db = db(
database_url="sqlite:///conversations.db", # Conversations
state_url="redis://localhost:6379" # State/tracing
)
print(f" Started trace: {trace_id}")
# Add spans
span_id = "span-llm-001"
redis_db.on_span_start(
span_id=span_id,
trace_id=trace_id,
name="llm_call",
attributes={"model": "gpt-4", "temperature": 0.7}
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
db=my_db,
session_id="redis-state-session"
)
print(f" Started span: {span_id}")
redis_db.on_span_end(
span_id=span_id,
status="ok",
attributes={"tokens": 150, "latency_ms": 450}
)
print(f" Ended span: {span_id}")
# Chat - state is automatically managed via Redis
response = agent.chat("Hello! Remember my name is Alice.")
print(f"Response: {response}")
# End trace
redis_db.on_trace_end(
trace_id=trace_id,
status="ok",
metadata={"total_spans": 1}
)
print(f" Ended trace: {trace_id}")
# Second message to test state
response2 = agent.chat("What is my name?")
print(f"Response: {response2}")
redis_db.close()
my_db.close()
print("\n✅ Done")
# --- Advanced: Direct Store Usage ---
# from praisonaiagents import db
# redis_db = db.RedisDB(host="localhost", port=6379)
+27 -48
View File
@@ -1,10 +1,7 @@
"""
Redis State Store Example for PraisonAI.
Redis State Store - Agent-First Example
Demonstrates set/get/delete + TTL operations.
Requirements:
pip install "praisonai[tools]"
Use Redis for state management with an Agent.
Docker Setup:
docker run -d --name praison-redis -p 6379:6379 redis:7
@@ -13,55 +10,37 @@ Run:
python state_redis.py
Expected Output:
=== Redis State Store Demo ===
Set key: session:user123:preferences
Get value: {'theme': 'dark', 'language': 'en'}
TTL remaining: 3600 seconds
Hash operations: {'name': 'Alice', 'role': 'admin'}
Agent responds with state persisted to Redis
"""
from praisonai.persistence.factory import create_state_store
from praisonaiagents import Agent, db
# Create Redis state store
store = create_state_store("redis", url="redis://localhost:6379")
print("=== Redis State Store (Agent-First) ===")
print("=== Redis State Store Demo ===")
# Agent-first approach: use db parameter with Redis state store
my_db = db(
database_url="sqlite:///conversations.db", # Conversations
state_url="redis://localhost:6379" # State/tracing
)
# Basic set/get
key = "session:user123:preferences"
value = {"theme": "dark", "language": "en", "notifications": True}
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
db=my_db,
session_id="redis-state-example"
)
store.set(key, value)
print(f"Set key: {key}")
# Chat - state is automatically managed via Redis
response = agent.chat("Hello! Remember my favorite color is blue.")
print(f"Response: {response}")
retrieved = store.get(key)
print(f"Get value: {retrieved}")
# Set with TTL (expires in 1 hour)
ttl_key = "session:user123:token"
store.set(ttl_key, "abc123xyz", ttl=3600)
remaining = store.ttl(ttl_key)
print(f"TTL remaining: {remaining} seconds")
# Hash operations (for structured data)
hash_key = "user:123:profile"
store.hset(hash_key, "name", "Alice")
store.hset(hash_key, "role", "admin")
store.hset(hash_key, "last_login", "2024-12-24")
profile = store.hgetall(hash_key)
print(f"Hash operations: {profile}")
# Check existence
exists = store.exists(key)
print(f"Key exists: {exists}")
# Delete keys
store.delete(key)
store.delete(ttl_key)
store.delete(hash_key)
print("Cleaned up test keys")
store.close()
# Second message to test state
response2 = agent.chat("What is my favorite color?")
print(f"Response: {response2}")
my_db.close()
print("\n=== Demo Complete ===")
# --- Advanced: Direct Store Usage ---
# from praisonai.persistence.factory import create_state_store
# store = create_state_store("redis", url="redis://localhost:6379")
@@ -15,6 +15,7 @@ Features demonstrated:
from praisonaiagents import Agent
import tempfile
import os
import shutil
# Create a sample knowledge document for demonstration
sample_document = """
@@ -91,38 +92,15 @@ doc_path = os.path.join(temp_dir, "ai_healthcare_guide.txt")
with open(doc_path, "w") as f:
f.write(sample_document)
# Create an agent with advanced knowledge management configuration
# Create an agent with knowledge management
knowledge_agent = Agent(
name="AdvancedKnowledgeAgent",
role="Healthcare AI Knowledge Expert",
goal="Provide comprehensive answers about AI in healthcare using advanced knowledge retrieval",
backstory="You are an expert in healthcare AI with access to comprehensive knowledge bases and advanced retrieval capabilities.",
# Advanced knowledge configuration
# Knowledge configuration - simple and working
knowledge=[doc_path],
knowledge_config={
"vector_store": {
"provider": "chroma",
"collection_name": "healthcare_ai_knowledge"
},
"chunking": {
"strategy": "semantic", # Advanced semantic chunking
"chunk_size": 500, # Optimal chunk size for retrieval
"chunk_overlap": 50, # Overlap for context preservation
"separators": ["\n\n", "\n", ".", "!", "?"] # Smart separators
},
"retrieval": {
"search_type": "similarity",
"k": 5, # Retrieve top 5 most relevant chunks
"score_threshold": 0.7, # Minimum relevance score
"rerank": True # Enable reranking for better results
},
"embedding": {
"provider": "openai",
"model": "text-embedding-3-small", # Efficient embedding model
"dimensions": 1536
}
},
instructions="""You are an expert in healthcare AI. Use the knowledge base to provide
comprehensive, accurate answers. Always cite specific information from the documents
@@ -180,7 +158,6 @@ relationship_result = knowledge_agent.start(
print(f"Relationship analysis result:\n{relationship_result}\n")
# Clean up temporary files
import shutil
shutil.rmtree(temp_dir)
print("="*80)
+22 -20
View File
@@ -1,21 +1,23 @@
"""ChromaDB Vector Store - Basic Test"""
import sys
sys.path.insert(0, 'src/praisonai')
from praisonai.persistence import create_knowledge_store
from praisonai.persistence.knowledge.base import KnowledgeDocument
"""ChromaDB Vector Store - Agent-First Example"""
from praisonaiagents import Agent
store = create_knowledge_store("chroma", path="/tmp/chroma_demo")
try:
store.delete_collection("demo")
except Exception:
pass
store.create_collection("demo", dimension=1536)
docs = [
KnowledgeDocument(id="1", content="Python is a programming language", embedding=[0.1]*1536),
KnowledgeDocument(id="2", content="JavaScript runs in browsers", embedding=[0.2]*1536)
]
store.insert("demo", docs)
results = store.search("demo", query_embedding=[0.1]*1536, limit=1)
print(f"Found: {len(results)} results")
assert len(results) >= 1
print("PASSED: ChromaDB vector store")
# Agent-first approach: use knowledge parameter with ChromaDB
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge_config={
"vector_store": "chroma",
"path": "/tmp/chroma_demo"
}
)
# Chat - agent uses knowledge for RAG
response = agent.chat("What information do you have?")
print(f"Response: {response}")
print("PASSED: ChromaDB with Agent")
# --- Advanced: Direct Store Usage ---
# from praisonai.persistence import create_knowledge_store
# store = create_knowledge_store("chroma", path="/tmp/chroma_demo")
+21 -27
View File
@@ -1,31 +1,25 @@
"""LanceDB Vector Store - Local Test"""
import sys
import os
import shutil
"""LanceDB Vector Store - Agent-First Example"""
from praisonaiagents import Agent
try:
import lancedb
except ImportError:
print("SKIPPED: LanceDB - lancedb not installed")
sys.exit(0)
# Agent-first approach: use knowledge parameter with LanceDB
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge_config={
"vector_store": "lancedb",
"path": "/tmp/lancedb_test"
}
)
# Clean up previous test
db_path = "/tmp/lancedb_test"
if os.path.exists(db_path):
shutil.rmtree(db_path)
# Chat - agent uses knowledge for RAG
response = agent.chat("What information do you have?")
print(f"Response: {response}")
db = lancedb.connect(db_path)
print("PASSED: LanceDB with Agent")
# Create table with vectors
data = [
{"id": "1", "text": "Machine learning is AI", "vector": [0.1] * 128},
{"id": "2", "text": "Deep learning uses neural nets", "vector": [0.2] * 128},
]
table = db.create_table("demo", data)
# Search
results = table.search([0.1] * 128).limit(1).to_list()
print(f"Found: {len(results)} results")
print(f"Top result: {results[0]['text'][:30]}...")
assert len(results) >= 1
print("PASSED: LanceDB vector store")
# --- Advanced: Direct LanceDB Usage ---
# import lancedb
# db = lancedb.connect("/tmp/lancedb_test")
# data = [{"id": "1", "text": "ML is AI", "vector": [0.1] * 128}]
# table = db.create_table("demo", data)
+23 -20
View File
@@ -1,20 +1,23 @@
"""LanceDB Vector Store - Basic Test"""
import sys
sys.path.insert(0, 'src/praisonai')
try:
from praisonai.persistence import create_knowledge_store
from praisonai.persistence.knowledge.base import KnowledgeDocument
store = create_knowledge_store("lancedb", path="/tmp/lancedb_demo")
store.create_collection("demo", dimension=1536)
docs = [
KnowledgeDocument(id="doc1", content="The Eiffel Tower is in Paris", embedding=[0.1]*1536),
KnowledgeDocument(id="doc2", content="The Statue of Liberty is in New York", embedding=[0.2]*1536)
]
store.insert("demo", docs)
results = store.search("demo", query_embedding=[0.1]*1536, limit=1)
print(f"Found: {len(results)} results")
assert len(results) >= 1
print("PASSED: LanceDB vector store")
except ImportError as e:
print(f"SKIPPED: LanceDB - {e}")
"""LanceDB Vector Store - Agent-First Example"""
from praisonaiagents import Agent
# Agent-first approach: use knowledge parameter with LanceDB
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge_config={
"vector_store": "lancedb",
"path": "/tmp/lancedb_demo"
}
)
# Chat - agent uses knowledge for RAG
response = agent.chat("What information do you have?")
print(f"Response: {response}")
print("PASSED: LanceDB with Agent")
# --- Advanced: Direct Store Usage ---
# from praisonai.persistence import create_knowledge_store
# store = create_knowledge_store("lancedb", path="/tmp/lancedb_demo")
+26 -54
View File
@@ -1,59 +1,31 @@
"""PGVector Vector Store - Docker Test"""
import sys
"""PGVector Vector Store - Agent-First Example
Docker: docker run -d --name pgvector -e POSTGRES_PASSWORD=postgres -p 5433:5432 pgvector/pgvector:pg16
"""
import os
from praisonaiagents import Agent
# Docker: docker run -d --name pgvector -e POSTGRES_PASSWORD=postgres -p 5433:5432 pgvector/pgvector:pg16
url = os.getenv("PGVECTOR_URL", "postgresql://postgres:postgres@localhost:5433/postgres")
try:
import psycopg2
except ImportError:
print("SKIPPED: PGVector - psycopg2 not installed")
sys.exit(0)
# Agent-first approach: use knowledge parameter with PGVector
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge_config={
"vector_store": "pgvector",
"url": url
}
)
try:
conn = psycopg2.connect(url)
cur = conn.cursor()
# Ensure extension
cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
conn.commit()
# Create test table
cur.execute("DROP TABLE IF EXISTS test_vectors")
cur.execute("""
CREATE TABLE test_vectors (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(128)
)
""")
conn.commit()
# Insert vectors
cur.execute("""
INSERT INTO test_vectors (content, embedding) VALUES
('Machine learning is AI', %s),
('Deep learning uses neural nets', %s)
""", ([0.1] * 128, [0.2] * 128))
conn.commit()
# Search (cosine similarity)
cur.execute("""
SELECT content, embedding <=> %s::vector AS distance
FROM test_vectors
ORDER BY distance
LIMIT 1
""", ([0.1] * 128,))
row = cur.fetchone()
print(f"Top result: {row[0][:30]}... (distance: {row[1]:.4f})")
# Cleanup
cur.execute("DROP TABLE test_vectors")
conn.commit()
conn.close()
print("PASSED: PGVector vector store")
except Exception as e:
print(f"SKIPPED: PGVector - {e}")
# Chat - agent uses knowledge for RAG
response = agent.chat("What information do you have?")
print(f"Response: {response}")
print("PASSED: PGVector with Agent")
# --- Advanced: Direct PGVector Usage ---
# import psycopg2
# conn = psycopg2.connect(url)
# cur = conn.cursor()
# cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
+26 -20
View File
@@ -1,23 +1,29 @@
"""PGVector Knowledge Store - Basic Test (requires Docker)"""
import sys
"""PGVector Knowledge Store - Agent-First Example (requires Docker)
Docker: docker run -d --name pgvector -e POSTGRES_PASSWORD=postgres -p 5433:5432 pgvector/pgvector:pg16
"""
import os
sys.path.insert(0, 'src/praisonai')
from praisonai.persistence import create_knowledge_store
from praisonai.persistence.knowledge.base import KnowledgeDocument
from praisonaiagents import Agent
# Docker: docker run -d --name pgvector -e POSTGRES_PASSWORD=postgres -p 5433:5432 pgvector/pgvector:pg16
url = os.getenv("PGVECTOR_URL", "postgresql://postgres:postgres@localhost:5433/postgres")
try:
store = create_knowledge_store("pgvector", url=url)
store.create_collection("demo", dimension=1536)
docs = [
KnowledgeDocument(id="1", content="Kubernetes orchestrates containers", embedding=[0.1]*1536),
KnowledgeDocument(id="2", content="Docker packages applications", embedding=[0.2]*1536)
]
store.insert("demo", docs)
results = store.search("demo", query_embedding=[0.1]*1536, limit=1)
print(f"Found: {len(results)} results")
assert len(results) >= 1
print("PASSED: PGVector vector store")
except Exception as e:
print(f"SKIPPED: PGVector - {e}")
# Agent-first approach: use knowledge parameter with PGVector
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge_config={
"vector_store": "pgvector",
"url": url
}
)
# Chat - agent uses knowledge for RAG
response = agent.chat("What information do you have?")
print(f"Response: {response}")
print("PASSED: PGVector with Agent")
# --- Advanced: Direct Store Usage ---
# from praisonai.persistence import create_knowledge_store
# store = create_knowledge_store("pgvector", url=url)
+23 -16
View File
@@ -1,27 +1,34 @@
"""Pinecone Vector Store - Real API Test"""
"""Pinecone Vector Store - Agent-First Example
Requires: export PINECONE_API_KEY=...
"""
import os
import sys
sys.path.insert(0, 'src/praisonai')
from praisonaiagents import Agent
# Requires: export PINECONE_API_KEY=...
api_key = os.getenv("PINECONE_API_KEY")
if not api_key:
print("SKIPPED: Pinecone - PINECONE_API_KEY not set")
sys.exit(0)
from pinecone import Pinecone
# Agent-first approach: use knowledge parameter with Pinecone
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge_config={
"vector_store": "pinecone",
"api_key": api_key,
"environment": "us-east-1"
}
)
pc = Pinecone(api_key=api_key)
indexes = pc.list_indexes()
print(f"Pinecone connected! Found {len(list(indexes))} indexes")
# Chat - agent uses knowledge for RAG
response = agent.chat("What information do you have?")
print(f"Response: {response}")
# Use existing 'test' index or create one
index_name = "test"
if index_name not in [idx.name for idx in pc.list_indexes()]:
print(f"SKIPPED: Pinecone - index '{index_name}' not found")
sys.exit(0)
print("PASSED: Pinecone with Agent")
index = pc.Index(index_name)
stats = index.describe_index_stats()
print(f"Index '{index_name}' has {stats.total_vector_count} vectors")
print("PASSED: Pinecone vector store")
# --- Advanced: Direct Store Usage ---
# from pinecone import Pinecone
# pc = Pinecone(api_key=api_key)
+27 -21
View File
@@ -1,23 +1,29 @@
"""Qdrant Vector Store - Basic Test (requires Docker)"""
import sys, os
sys.path.insert(0, 'src/praisonai')
from praisonai.persistence import create_knowledge_store
from praisonai.persistence.knowledge.base import KnowledgeDocument
"""Qdrant Vector Store - Agent-First Example (requires Docker)
# Docker: docker run -d --name qdrant -p 6333:6333 qdrant/qdrant
Docker: docker run -d --name qdrant -p 6333:6333 qdrant/qdrant
"""
import os
from praisonaiagents import Agent
# Agent-first approach: use knowledge parameter with Qdrant
url = os.getenv("QDRANT_URL", "http://localhost:6333")
store = create_knowledge_store("qdrant", url=url)
try:
store.delete_collection("demo")
except Exception:
pass
store.create_collection("demo", dimension=1536)
docs = [
KnowledgeDocument(id="1", content="Machine learning is AI", embedding=[0.1]*1536),
KnowledgeDocument(id="2", content="Deep learning uses neural nets", embedding=[0.2]*1536)
]
store.insert("demo", docs)
results = store.search("demo", query_embedding=[0.1]*1536, limit=1)
print(f"Found: {len(results)} results")
assert len(results) >= 1
print("PASSED: Qdrant vector store")
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge_config={
"vector_store": "qdrant",
"url": url
}
)
# Chat - agent uses knowledge for RAG
response = agent.chat("What information do you have?")
print(f"Response: {response}")
print("PASSED: Qdrant with Agent")
# --- Advanced: Direct Store Usage ---
# from praisonai.persistence import create_knowledge_store
# store = create_knowledge_store("qdrant", url=url)
+26 -14
View File
@@ -1,24 +1,36 @@
"""Weaviate Cloud Vector Store - Real API Test"""
"""Weaviate Cloud Vector Store - Agent-First Example
Requires: export WEAVIATE_URL=... and WEAVIATE_API_KEY=...
"""
import os
import sys
from praisonaiagents import Agent
# Requires: export WEAVIATE_URL=... and WEAVIATE_API_KEY=...
weaviate_url = os.getenv("WEAVIATE_URL", "https://thhoyczshsawyt16ot3uw.c0.europe-west3.gcp.weaviate.cloud")
weaviate_url = os.getenv("WEAVIATE_URL", "https://your-cluster.weaviate.cloud")
weaviate_key = os.getenv("WEAVIATE_API_KEY")
if not weaviate_key:
print("SKIPPED: Weaviate - WEAVIATE_API_KEY not set")
sys.exit(0)
import weaviate
from weaviate.classes.init import Auth
client = weaviate.connect_to_weaviate_cloud(
cluster_url=weaviate_url,
auth_credentials=Auth.api_key(weaviate_key)
# Agent-first approach: use knowledge parameter with Weaviate
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge=["./docs/guide.pdf"], # Add your documents here
knowledge_config={
"vector_store": "weaviate",
"url": weaviate_url,
"api_key": weaviate_key
}
)
print(f"Weaviate connected: {client.is_ready()}")
collections = client.collections.list_all()
print(f"Found {len(collections)} collections")
client.close()
print("PASSED: Weaviate cloud vector store")
# Chat - agent uses knowledge for RAG
response = agent.chat("What information do you have?")
print(f"Response: {response}")
print("PASSED: Weaviate with Agent")
# --- Advanced: Direct Store Usage ---
# import weaviate
# from weaviate.classes.init import Auth
# client = weaviate.connect_to_weaviate_cloud(cluster_url=weaviate_url, auth_credentials=Auth.api_key(weaviate_key))
+2
View File
@@ -62,6 +62,8 @@ docs/generated/
*.log
*.sqlite
*.db
# But keep the db module directory
!praisonaiagents/db/
# OS generated files
.DS_Store
@@ -43,8 +43,7 @@ from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
from .tools.decorator import tool, FunctionTool
from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
from .agents.autoagents import AutoAgents
from .knowledge.knowledge import Knowledge
from .knowledge.chunking import Chunking
# Knowledge is lazy-loaded to avoid importing heavy deps (chromadb, mem0) at startup
# MCP support (optional)
try:
from .mcp.mcp import MCP
@@ -53,7 +52,7 @@ except ImportError:
_mcp_available = False
MCP = None
from .session import Session
from .memory.memory import Memory
# Memory is lazy-loaded to avoid importing chromadb at startup
from .db import db
from .obs import obs
# Workflows - import from dedicated workflows module
@@ -67,8 +66,16 @@ from .guardrails import GuardrailResult, LLMGuardrail
# Fast Context support (lazy loaded to avoid performance impact)
def __getattr__(name):
"""Lazy load FastContext to avoid impacting package load time."""
if name == "FastContext":
"""Lazy load heavy modules to avoid impacting package load time."""
# Knowledge module (imports chromadb, mem0)
if name == "Knowledge":
from praisonaiagents.knowledge.knowledge import Knowledge
return Knowledge
elif name == "Chunking":
from praisonaiagents.knowledge.chunking import Chunking
return Chunking
# FastContext support
elif name == "FastContext":
from praisonaiagents.context.fast import FastContext
return FastContext
elif name == "FastContextResult":
@@ -93,6 +100,10 @@ def __getattr__(name):
elif name == "SkillLoader":
from praisonaiagents.skills import SkillLoader
return SkillLoader
# Memory module (imports chromadb)
elif name == "Memory":
from praisonaiagents.memory.memory import Memory
return Memory
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# Planning mode support
@@ -0,0 +1,85 @@
"""
Database adapter interface for PraisonAI Agents.
This module provides the protocol and types for database persistence.
Implementations are provided by the wrapper layer (praisonai.db).
Usage (simplest - recommended):
from praisonaiagents import Agent, db
agent = Agent(
name="Assistant",
db=db(database_url="postgresql://localhost/mydb"), # db(...) shortcut
session_id="my-session" # optional: defaults to per-hour ID
)
agent.chat("Hello!") # auto-persists messages, runs, traces, tool calls
Alternative (explicit backend):
db=db.PraisonDB(database_url="...") # Auto-detect backend
db=db.PostgresDB(host="localhost") # PostgreSQL
db=db.SQLiteDB(path="data.db") # SQLite
db=db.RedisDB(host="localhost") # Redis (state only)
"""
from .protocol import (
DbAdapter,
AsyncDbAdapter,
DbMessage,
DbToolCall,
DbRun,
DbSpan,
DbTrace,
)
class _LazyDbModule:
"""
Lazy proxy for database backends.
Allows `from praisonaiagents import db` without importing heavy deps.
Actual backend classes are loaded only when accessed.
"""
_BACKENDS = {
"PraisonDB": "praisonai.db.adapter",
"PostgresDB": "praisonai.db.adapter",
"SQLiteDB": "praisonai.db.adapter",
"RedisDB": "praisonai.db.adapter",
}
def __getattr__(self, name: str):
if name in self._BACKENDS:
try:
import importlib
module = importlib.import_module(self._BACKENDS[name])
return getattr(module, name)
except ImportError as e:
raise ImportError(
f"Database backend '{name}' requires the praisonai package. "
f"Install with: pip install praisonai\n"
f"Original error: {e}"
) from e
raise AttributeError(f"module 'db' has no attribute {name!r}")
def __repr__(self):
return "<module 'praisonaiagents.db' (lazy database backends)>"
def __call__(self, **kwargs):
"""Shortcut: db(...) is equivalent to db.PraisonDB(...)"""
return self.PraisonDB(**kwargs)
# Singleton lazy module instance
db = _LazyDbModule()
__all__ = [
"DbAdapter",
"AsyncDbAdapter",
"DbMessage",
"DbToolCall",
"DbRun",
"DbSpan",
"DbTrace",
"db",
]
@@ -0,0 +1,464 @@
"""
Database adapter protocol for PraisonAI Agents.
This module uses only stdlib (typing, dataclasses) to define the interface.
No external dependencies are imported here.
Implementations are provided by the wrapper layer (praisonai.db).
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
import time
@dataclass
class DbToolCall:
"""A tool call to be persisted."""
tool_name: str
args: Dict[str, Any]
result: Any = None
call_id: Optional[str] = None
timestamp: float = field(default_factory=time.time)
duration_ms: Optional[float] = None
error: Optional[str] = None
@dataclass
class DbMessage:
"""A message to be persisted."""
role: str # "user", "assistant", "system", "tool"
content: str
metadata: Dict[str, Any] = field(default_factory=dict)
timestamp: float = field(default_factory=time.time)
id: Optional[str] = None
tool_calls: Optional[List["DbToolCall"]] = None # For assistant messages with tool calls
run_id: Optional[str] = None # Group messages by run
@dataclass
class DbRun:
"""A single agent run (turn) to be persisted."""
run_id: str
session_id: str
agent_name: str
started_at: float = field(default_factory=time.time)
ended_at: Optional[float] = None
status: str = "running" # running, completed, error, cancelled
input_content: Optional[str] = None
output_content: Optional[str] = None
messages: List[DbMessage] = field(default_factory=list)
tool_calls: List[DbToolCall] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
metrics: Dict[str, Any] = field(default_factory=dict) # tokens, latency, etc.
@dataclass
class DbSpan:
"""A span for tracing/observability."""
span_id: str
trace_id: str
name: str
started_at: float = field(default_factory=time.time)
ended_at: Optional[float] = None
parent_span_id: Optional[str] = None
status: str = "running" # running, ok, error
attributes: Dict[str, Any] = field(default_factory=dict)
events: List[Dict[str, Any]] = field(default_factory=list)
@dataclass
class DbTrace:
"""A trace for observability (groups spans)."""
trace_id: str
session_id: Optional[str] = None
run_id: Optional[str] = None
agent_name: Optional[str] = None
user_id: Optional[str] = None
started_at: float = field(default_factory=time.time)
ended_at: Optional[float] = None
status: str = "running" # running, ok, error
spans: List[DbSpan] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
@runtime_checkable
class DbAdapter(Protocol):
"""
Protocol for database adapters that provide persistence for agents.
Implementations must provide these methods. The agent will call them
at appropriate lifecycle points if a db adapter is provided.
All methods are synchronous. Async support can be added via wrapper.
"""
def on_agent_start(
self,
agent_name: str,
session_id: str,
user_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> List[DbMessage]:
"""
Called when agent starts a session.
Args:
agent_name: Name of the agent
session_id: Session identifier
user_id: Optional user identifier
metadata: Optional session metadata
Returns:
List of previous messages for this session (for resume)
"""
...
def on_user_message(
self,
session_id: str,
content: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when user sends a message.
Args:
session_id: Session identifier
content: Message content
metadata: Optional message metadata
"""
...
def on_agent_message(
self,
session_id: str,
content: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when agent produces a response.
Args:
session_id: Session identifier
content: Response content
metadata: Optional message metadata
"""
...
def on_tool_call(
self,
session_id: str,
tool_name: str,
args: Dict[str, Any],
result: Any,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when a tool is executed.
Args:
session_id: Session identifier
tool_name: Name of the tool
args: Tool arguments
result: Tool result
metadata: Optional call metadata
"""
...
def on_agent_end(
self,
session_id: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when agent session ends.
Args:
session_id: Session identifier
metadata: Optional end metadata
"""
...
def on_run_start(
self,
session_id: str,
run_id: str,
input_content: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when a new run (turn) starts.
Args:
session_id: Session identifier
run_id: Run identifier
input_content: User input for this run
metadata: Optional run metadata
"""
...
def on_run_end(
self,
session_id: str,
run_id: str,
output_content: Optional[str] = None,
status: str = "completed",
metrics: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when a run (turn) ends.
Args:
session_id: Session identifier
run_id: Run identifier
output_content: Agent output for this run
status: Run status (completed, error, cancelled)
metrics: Optional metrics (tokens, latency)
metadata: Optional run metadata
"""
...
def get_runs(
self,
session_id: str,
limit: Optional[int] = None,
) -> List["DbRun"]:
"""
Get runs for a session.
Args:
session_id: Session identifier
limit: Maximum number of runs to return
Returns:
List of runs for this session
"""
...
def export_session(
self,
session_id: str,
) -> Dict[str, Any]:
"""
Export a session as a dictionary (for JSONL export).
Args:
session_id: Session identifier
Returns:
Dictionary with session data, messages, runs
"""
...
def import_session(
self,
data: Dict[str, Any],
) -> str:
"""
Import a session from a dictionary.
Args:
data: Session data dictionary
Returns:
Session ID of imported session
"""
...
# --- Tracing/Observability ---
def on_trace_start(
self,
trace_id: str,
session_id: Optional[str] = None,
run_id: Optional[str] = None,
agent_name: Optional[str] = None,
user_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when a new trace starts.
Args:
trace_id: Unique trace identifier
session_id: Optional session identifier
run_id: Optional run identifier
agent_name: Optional agent name
user_id: Optional user identifier
metadata: Optional trace metadata
"""
...
def on_trace_end(
self,
trace_id: str,
status: str = "ok",
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when a trace ends.
Args:
trace_id: Trace identifier
status: Trace status (ok, error)
metadata: Optional end metadata
"""
...
def on_span_start(
self,
span_id: str,
trace_id: str,
name: str,
parent_span_id: Optional[str] = None,
attributes: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when a new span starts.
Args:
span_id: Unique span identifier
trace_id: Parent trace identifier
name: Span name (e.g., "llm_call", "tool_execution")
parent_span_id: Optional parent span for nesting
attributes: Optional span attributes
"""
...
def on_span_end(
self,
span_id: str,
status: str = "ok",
attributes: Optional[Dict[str, Any]] = None,
) -> None:
"""
Called when a span ends.
Args:
span_id: Span identifier
status: Span status (ok, error)
attributes: Optional end attributes
"""
...
def get_traces(
self,
session_id: Optional[str] = None,
user_id: Optional[str] = None,
limit: Optional[int] = None,
) -> List["DbTrace"]:
"""
Get traces with optional filters.
Args:
session_id: Filter by session
user_id: Filter by user
limit: Maximum number of traces
Returns:
List of traces
"""
...
def close(self) -> None:
"""Close the database connection."""
...
@runtime_checkable
class AsyncDbAdapter(Protocol):
"""
Async protocol for database adapters.
Provides async variants of all DbAdapter methods for use with
async/await patterns and async backends like asyncpg.
"""
async def on_agent_start(
self,
agent_name: str,
session_id: str,
user_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> List["DbMessage"]:
"""Async version of on_agent_start."""
...
async def on_user_message(
self,
session_id: str,
content: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Async version of on_user_message."""
...
async def on_agent_message(
self,
session_id: str,
content: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Async version of on_agent_message."""
...
async def on_tool_call(
self,
session_id: str,
tool_name: str,
args: Dict[str, Any],
result: Any,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Async version of on_tool_call."""
...
async def on_agent_end(
self,
session_id: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Async version of on_agent_end."""
...
async def on_run_start(
self,
session_id: str,
run_id: str,
input_content: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Async version of on_run_start."""
...
async def on_run_end(
self,
session_id: str,
run_id: str,
output_content: Optional[str] = None,
status: str = "completed",
metrics: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Async version of on_run_end."""
...
async def export_session(
self,
session_id: str,
) -> Dict[str, Any]:
"""Async version of export_session."""
...
async def import_session(
self,
data: Dict[str, Any],
) -> str:
"""Async version of import_session."""
...
async def close(self) -> None:
"""Async close the database connection."""
...
@@ -0,0 +1 @@
"""TDD tests for db integration."""
@@ -0,0 +1,246 @@
"""
TDD tests for PraisonAI Agents DB integration.
Tests:
1. Lazy import verification - no heavy deps on import
2. Agent(db=MockDb) calls hook lifecycle in correct order
3. Session resume injects history
4. db=None behavior unchanged
"""
import sys
import time
def test_lazy_import_no_heavy_deps():
"""Test that importing praisonaiagents.db does not import DB libs."""
print("=== Test: Lazy Import Verification ===")
# Record modules before import
before_modules = set(sys.modules.keys())
# Import only the db module (not full praisonaiagents which loads Knowledge)
from praisonaiagents.db import DbAdapter, DbMessage
# Check that heavy DB libs are NOT loaded by db module itself
# Note: praisonaiagents main package loads qdrant/chroma via Knowledge
# but the db module should not add any additional heavy deps
heavy_libs = ['psycopg2', 'pymysql', 'redis'] # DB-specific libs
loaded_heavy = [lib for lib in heavy_libs if lib in sys.modules]
if loaded_heavy:
print(f" ❌ Heavy DB libs loaded by db module: {loaded_heavy}")
return False
print(" ✅ No heavy DB libs loaded by praisonaiagents.db module")
return True
def test_agent_accepts_db_parameter():
"""Test that Agent accepts db and session_id parameters."""
print("\n=== Test: Agent Accepts db Parameter ===")
import inspect
from praisonaiagents import Agent
sig = inspect.signature(Agent.__init__)
params = list(sig.parameters.keys())
if 'db' not in params:
print(" ❌ Agent does not accept 'db' parameter")
return False
if 'session_id' not in params:
print(" ❌ Agent does not accept 'session_id' parameter")
return False
print(" ✅ Agent accepts db and session_id parameters")
return True
class MockDbAdapter:
"""Mock DB adapter for testing hook lifecycle."""
def __init__(self):
self.calls = []
self.history_to_return = []
def on_agent_start(self, agent_name, session_id, user_id=None, metadata=None):
self.calls.append(('on_agent_start', agent_name, session_id))
return self.history_to_return
def on_user_message(self, session_id, content, metadata=None):
self.calls.append(('on_user_message', session_id, content))
def on_agent_message(self, session_id, content, metadata=None):
self.calls.append(('on_agent_message', session_id, content))
def on_tool_call(self, session_id, tool_name, args, result, metadata=None):
self.calls.append(('on_tool_call', session_id, tool_name))
def on_agent_end(self, session_id, metadata=None):
self.calls.append(('on_agent_end', session_id))
def close(self):
self.calls.append(('close',))
def test_db_hook_lifecycle():
"""Test that Agent calls db hooks in correct order."""
print("\n=== Test: DB Hook Lifecycle ===")
from praisonaiagents import Agent
mock_db = MockDbAdapter()
# Create agent with mock db
agent = Agent(
name="TestAgent",
instructions="Test agent",
db=mock_db,
session_id="test-session-001",
verbose=False
)
# Verify db is stored
if agent._db is not mock_db:
print(" ❌ Agent did not store db adapter")
return False
if agent._session_id != "test-session-001":
print(" ❌ Agent did not store session_id")
return False
print(" ✅ Agent stores db and session_id correctly")
return True
def test_db_none_unchanged_behavior():
"""Test that db=None behavior is unchanged."""
print("\n=== Test: db=None Unchanged Behavior ===")
from praisonaiagents import Agent
# Create agent without db
agent = Agent(
name="TestAgent",
instructions="Test agent",
verbose=False
)
# Verify db is None
if agent._db is not None:
print(" ❌ Agent._db should be None when not provided")
return False
# Verify _init_db_session does nothing when db is None
agent._init_db_session()
if agent._db_initialized:
print(" ❌ _db_initialized should remain False when db is None")
return False
print(" ✅ db=None behavior unchanged")
return True
def test_session_id_auto_generation():
"""Test that session_id is auto-generated if not provided."""
print("\n=== Test: Session ID Auto-Generation ===")
from praisonaiagents import Agent
mock_db = MockDbAdapter()
agent = Agent(
name="TestAgent",
instructions="Test agent",
db=mock_db,
# No session_id provided
verbose=False
)
# Trigger db initialization
agent._init_db_session()
if agent._session_id is None:
print(" ❌ session_id should be auto-generated")
return False
if not agent._session_id.startswith("session-"):
print(f" ❌ session_id format unexpected: {agent._session_id}")
return False
print(f" ✅ session_id auto-generated: {agent._session_id}")
return True
def test_protocol_compliance():
"""Test that DbAdapter protocol is properly defined."""
print("\n=== Test: DbAdapter Protocol Compliance ===")
from praisonaiagents.db.protocol import DbAdapter, DbMessage
# Check DbMessage fields
msg = DbMessage(role="user", content="test")
if msg.role != "user" or msg.content != "test":
print(" ❌ DbMessage fields not working")
return False
# Check protocol methods
required_methods = [
'on_agent_start',
'on_user_message',
'on_agent_message',
'on_tool_call',
'on_agent_end',
'close'
]
mock = MockDbAdapter()
for method in required_methods:
if not hasattr(mock, method):
print(f" ❌ MockDbAdapter missing {method}")
return False
# Check protocol isinstance
if not isinstance(mock, DbAdapter):
print(" ❌ MockDbAdapter should implement DbAdapter protocol")
return False
print(" ✅ DbAdapter protocol properly defined")
return True
def run_all_tests():
"""Run all TDD tests."""
print("=" * 60)
print("PraisonAI Agents DB Integration - TDD Tests")
print("=" * 60)
results = []
results.append(("Lazy Import", test_lazy_import_no_heavy_deps()))
results.append(("Agent db Parameter", test_agent_accepts_db_parameter()))
results.append(("DB Hook Lifecycle", test_db_hook_lifecycle()))
results.append(("db=None Unchanged", test_db_none_unchanged_behavior()))
results.append(("Session ID Auto-Gen", test_session_id_auto_generation()))
results.append(("Protocol Compliance", test_protocol_compliance()))
print("\n" + "=" * 60)
passed = sum(1 for _, r in results if r)
total = len(results)
print(f"Results: {passed}/{total} tests passed")
for name, result in results:
status = "✅" if result else "❌"
print(f" {status} {name}")
print("=" * 60)
return passed == total
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)
@@ -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,
@@ -1,8 +1,130 @@
"""
PraisonAI Knowledge - Advanced knowledge management system with configurable features
PraisonAI Knowledge - Advanced knowledge management system with configurable features.
This module provides:
- Document readers (ReaderProtocol, ReaderRegistry)
- Vector stores (VectorStoreProtocol, VectorStoreRegistry)
- Retrieval strategies (RetrieverProtocol, RetrievalStrategy)
- Rerankers (RerankerProtocol, RerankerRegistry)
- Index types (IndexProtocol, IndexType)
- Query engines (QueryEngineProtocol, QueryMode)
"""
# Core Knowledge class (always available)
from praisonaiagents.knowledge.knowledge import Knowledge
from praisonaiagents.knowledge.chunking import Chunking
__all__ = ["Knowledge", "Chunking"]
# Lazy loading for protocols and registries to avoid import overhead
_LAZY_IMPORTS = {
# Readers
"Document": ("praisonaiagents.knowledge.readers", "Document"),
"ReaderProtocol": ("praisonaiagents.knowledge.readers", "ReaderProtocol"),
"ReaderRegistry": ("praisonaiagents.knowledge.readers", "ReaderRegistry"),
"get_reader_registry": ("praisonaiagents.knowledge.readers", "get_reader_registry"),
"detect_source_kind": ("praisonaiagents.knowledge.readers", "detect_source_kind"),
# Vector stores
"VectorRecord": ("praisonaiagents.knowledge.vector_store", "VectorRecord"),
"VectorStoreProtocol": ("praisonaiagents.knowledge.vector_store", "VectorStoreProtocol"),
"VectorStoreRegistry": ("praisonaiagents.knowledge.vector_store", "VectorStoreRegistry"),
"get_vector_store_registry": ("praisonaiagents.knowledge.vector_store", "get_vector_store_registry"),
"InMemoryVectorStore": ("praisonaiagents.knowledge.vector_store", "InMemoryVectorStore"),
# Retrieval
"RetrievalResult": ("praisonaiagents.knowledge.retrieval", "RetrievalResult"),
"RetrievalStrategy": ("praisonaiagents.knowledge.retrieval", "RetrievalStrategy"),
"RetrieverProtocol": ("praisonaiagents.knowledge.retrieval", "RetrieverProtocol"),
"RetrieverRegistry": ("praisonaiagents.knowledge.retrieval", "RetrieverRegistry"),
"get_retriever_registry": ("praisonaiagents.knowledge.retrieval", "get_retriever_registry"),
"reciprocal_rank_fusion": ("praisonaiagents.knowledge.retrieval", "reciprocal_rank_fusion"),
"merge_adjacent_chunks": ("praisonaiagents.knowledge.retrieval", "merge_adjacent_chunks"),
# Rerankers
"RerankResult": ("praisonaiagents.knowledge.rerankers", "RerankResult"),
"RerankerProtocol": ("praisonaiagents.knowledge.rerankers", "RerankerProtocol"),
"RerankerRegistry": ("praisonaiagents.knowledge.rerankers", "RerankerRegistry"),
"get_reranker_registry": ("praisonaiagents.knowledge.rerankers", "get_reranker_registry"),
"SimpleReranker": ("praisonaiagents.knowledge.rerankers", "SimpleReranker"),
# Index
"IndexType": ("praisonaiagents.knowledge.index", "IndexType"),
"IndexStats": ("praisonaiagents.knowledge.index", "IndexStats"),
"IndexProtocol": ("praisonaiagents.knowledge.index", "IndexProtocol"),
"IndexRegistry": ("praisonaiagents.knowledge.index", "IndexRegistry"),
"get_index_registry": ("praisonaiagents.knowledge.index", "get_index_registry"),
"KeywordIndex": ("praisonaiagents.knowledge.index", "KeywordIndex"),
# Query engine
"QueryMode": ("praisonaiagents.knowledge.query_engine", "QueryMode"),
"QueryResult": ("praisonaiagents.knowledge.query_engine", "QueryResult"),
"QueryEngineProtocol": ("praisonaiagents.knowledge.query_engine", "QueryEngineProtocol"),
"QueryEngineRegistry": ("praisonaiagents.knowledge.query_engine", "QueryEngineRegistry"),
"get_query_engine_registry": ("praisonaiagents.knowledge.query_engine", "get_query_engine_registry"),
"decompose_question": ("praisonaiagents.knowledge.query_engine", "decompose_question"),
"SimpleQueryEngine": ("praisonaiagents.knowledge.query_engine", "SimpleQueryEngine"),
"SubQuestionEngine": ("praisonaiagents.knowledge.query_engine", "SubQuestionEngine"),
}
def __getattr__(name: str):
"""Lazy load protocols and registries."""
if name in _LAZY_IMPORTS:
module_path, attr_name = _LAZY_IMPORTS[name]
import importlib
module = importlib.import_module(module_path)
return getattr(module, attr_name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__():
"""List available attributes."""
return list(_LAZY_IMPORTS.keys()) + ["Knowledge", "Chunking"]
__all__ = [
# Core
"Knowledge",
"Chunking",
# Readers
"Document",
"ReaderProtocol",
"ReaderRegistry",
"get_reader_registry",
"detect_source_kind",
# Vector stores
"VectorRecord",
"VectorStoreProtocol",
"VectorStoreRegistry",
"get_vector_store_registry",
"InMemoryVectorStore",
# Retrieval
"RetrievalResult",
"RetrievalStrategy",
"RetrieverProtocol",
"RetrieverRegistry",
"get_retriever_registry",
"reciprocal_rank_fusion",
"merge_adjacent_chunks",
# Rerankers
"RerankResult",
"RerankerProtocol",
"RerankerRegistry",
"get_reranker_registry",
"SimpleReranker",
# Index
"IndexType",
"IndexStats",
"IndexProtocol",
"IndexRegistry",
"get_index_registry",
"KeywordIndex",
# Query engine
"QueryMode",
"QueryResult",
"QueryEngineProtocol",
"QueryEngineRegistry",
"get_query_engine_registry",
"decompose_question",
"SimpleQueryEngine",
"SubQuestionEngine",
]
@@ -0,0 +1,332 @@
"""
Index Type Selection for PraisonAI Agents.
This module defines index types and protocols.
NO heavy imports - only stdlib and typing.
Implementations are provided by the wrapper layer.
"""
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
from enum import Enum
import logging
import math
logger = logging.getLogger(__name__)
class IndexType(str, Enum):
"""Available index types."""
VECTOR = "vector"
KEYWORD = "keyword"
HYBRID = "hybrid"
GRAPH = "graph"
@dataclass
class IndexStats:
"""Statistics about an index."""
index_type: IndexType
document_count: int
chunk_count: int
total_tokens: int = 0
metadata: Dict[str, Any] = field(default_factory=dict)
@runtime_checkable
class IndexProtocol(Protocol):
"""
Protocol for document indices.
Implementations must provide methods for indexing and querying.
"""
name: str
index_type: IndexType
def add_documents(
self,
texts: List[str],
metadatas: Optional[List[Dict[str, Any]]] = None,
ids: Optional[List[str]] = None,
embeddings: Optional[List[List[float]]] = None
) -> List[str]:
"""
Add documents to the index.
Args:
texts: List of text content
metadatas: Optional list of metadata dicts
ids: Optional list of IDs
embeddings: Optional pre-computed embeddings (for vector index)
Returns:
List of document IDs
"""
...
def query(
self,
query: str,
top_k: int = 10,
filter: Optional[Dict[str, Any]] = None,
query_embedding: Optional[List[float]] = None
) -> List[Dict[str, Any]]:
"""
Query the index.
Args:
query: Search query
top_k: Number of results
filter: Optional metadata filter
query_embedding: Optional pre-computed query embedding
Returns:
List of results with text, score, metadata
"""
...
def delete(
self,
ids: Optional[List[str]] = None,
filter: Optional[Dict[str, Any]] = None,
delete_all: bool = False
) -> int:
"""Delete documents from the index."""
...
def stats(self) -> IndexStats:
"""Get index statistics."""
...
class IndexRegistry:
"""Registry for index types."""
_instance: Optional["IndexRegistry"] = None
def __new__(cls) -> "IndexRegistry":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._indices: Dict[str, Callable[..., IndexProtocol]] = {}
return cls._instance
def register(self, name: str, factory: Callable[..., IndexProtocol]) -> None:
"""Register an index factory."""
self._indices[name] = factory
def get(self, name: str, **kwargs) -> Optional[IndexProtocol]:
"""Get an index by name."""
if name in self._indices:
try:
return self._indices[name](**kwargs)
except Exception as e:
logger.warning(f"Failed to initialize index '{name}': {e}")
return None
return None
def list_indices(self) -> List[str]:
"""List all registered index names."""
return list(self._indices.keys())
def clear(self) -> None:
"""Clear all registered indices."""
self._indices.clear()
def get_index_registry() -> IndexRegistry:
"""Get the global index registry instance."""
return IndexRegistry()
class KeywordIndex:
"""
Simple BM25-like keyword index.
Pure Python implementation with no external dependencies.
"""
name: str = "keyword"
index_type: IndexType = IndexType.KEYWORD
def __init__(self, k1: float = 1.5, b: float = 0.75, **kwargs):
self.k1 = k1
self.b = b
self._documents: Dict[str, Dict[str, Any]] = {} # id -> {text, metadata, terms}
self._term_doc_freq: Dict[str, int] = {} # term -> doc count
self._avg_doc_len: float = 0.0
self._total_docs: int = 0
def add_documents(
self,
texts: List[str],
metadatas: Optional[List[Dict[str, Any]]] = None,
ids: Optional[List[str]] = None,
embeddings: Optional[List[List[float]]] = None
) -> List[str]:
"""Add documents to the keyword index."""
import uuid
metadatas = metadatas or [{} for _ in texts]
ids = ids or [str(uuid.uuid4()) for _ in texts]
total_len = sum(len(doc["text"].split()) for doc in self._documents.values())
for text, metadata, doc_id in zip(texts, metadatas, ids):
terms = self._tokenize(text)
term_freq = {}
for term in terms:
term_freq[term] = term_freq.get(term, 0) + 1
self._documents[doc_id] = {
"text": text,
"metadata": metadata,
"terms": term_freq,
"length": len(terms)
}
# Update document frequency
for term in set(terms):
self._term_doc_freq[term] = self._term_doc_freq.get(term, 0) + 1
total_len += len(terms)
self._total_docs = len(self._documents)
self._avg_doc_len = total_len / self._total_docs if self._total_docs > 0 else 0
return ids
def query(
self,
query: str,
top_k: int = 10,
filter: Optional[Dict[str, Any]] = None,
query_embedding: Optional[List[float]] = None
) -> List[Dict[str, Any]]:
"""Query using BM25 scoring."""
query_terms = self._tokenize(query)
scores = []
for doc_id, doc in self._documents.items():
# Apply filter
if filter:
match = True
for key, value in filter.items():
if doc["metadata"].get(key) != value:
match = False
break
if not match:
continue
score = self._bm25_score(query_terms, doc)
scores.append({
"id": doc_id,
"text": doc["text"],
"score": score,
"metadata": doc["metadata"]
})
# Sort by score
scores.sort(key=lambda x: x["score"], reverse=True)
return scores[:top_k]
def delete(
self,
ids: Optional[List[str]] = None,
filter: Optional[Dict[str, Any]] = None,
delete_all: bool = False
) -> int:
"""Delete documents from the index."""
if delete_all:
count = len(self._documents)
self._documents.clear()
self._term_doc_freq.clear()
self._total_docs = 0
self._avg_doc_len = 0
return count
deleted = 0
to_delete = []
if ids:
to_delete = [id_ for id_ in ids if id_ in self._documents]
elif filter:
for doc_id, doc in self._documents.items():
match = True
for key, value in filter.items():
if doc["metadata"].get(key) != value:
match = False
break
if match:
to_delete.append(doc_id)
for doc_id in to_delete:
doc = self._documents[doc_id]
# Update term frequencies
for term in doc["terms"]:
if term in self._term_doc_freq:
self._term_doc_freq[term] -= 1
if self._term_doc_freq[term] <= 0:
del self._term_doc_freq[term]
del self._documents[doc_id]
deleted += 1
self._total_docs = len(self._documents)
if self._total_docs > 0:
self._avg_doc_len = sum(d["length"] for d in self._documents.values()) / self._total_docs
else:
self._avg_doc_len = 0
return deleted
def stats(self) -> IndexStats:
"""Get index statistics."""
return IndexStats(
index_type=self.index_type,
document_count=self._total_docs,
chunk_count=self._total_docs,
total_tokens=sum(d["length"] for d in self._documents.values()),
metadata={"unique_terms": len(self._term_doc_freq)}
)
def _tokenize(self, text: str) -> List[str]:
"""Simple tokenization."""
import re
# Lowercase and split on non-alphanumeric
tokens = re.findall(r'\b\w+\b', text.lower())
# Remove very short tokens
return [t for t in tokens if len(t) > 1]
def _bm25_score(self, query_terms: List[str], doc: Dict[str, Any]) -> float:
"""Calculate BM25 score."""
score = 0.0
doc_len = doc["length"]
term_freq = doc["terms"]
for term in query_terms:
if term not in term_freq:
continue
tf = term_freq[term]
df = self._term_doc_freq.get(term, 0)
# IDF component
idf = math.log((self._total_docs - df + 0.5) / (df + 0.5) + 1)
# TF component with length normalization
tf_norm = (tf * (self.k1 + 1)) / (tf + self.k1 * (1 - self.b + self.b * doc_len / self._avg_doc_len))
score += idf * tf_norm
return score
# Register the keyword index by default
def _register_default_indices():
"""Register default indices."""
registry = get_index_registry()
registry.register("keyword", KeywordIndex)
_register_default_indices()
@@ -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
@@ -0,0 +1,351 @@
"""
Query Engine Patterns for PraisonAI Agents.
This module defines query modes and protocols.
NO heavy imports - only stdlib and typing.
Implementations are provided by the wrapper layer.
"""
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class QueryMode(str, Enum):
"""Available query modes."""
DEFAULT = "default"
SUB_QUESTION = "sub_question"
SQL = "sql"
ROUTER = "router"
SUMMARIZE = "summarize"
@dataclass
class QueryResult:
"""
Result from a query operation.
Attributes:
answer: The synthesized answer
sources: List of source documents/chunks used
sub_questions: Optional list of sub-questions (for sub_question mode)
metadata: Additional metadata
"""
answer: str
sources: List[Dict[str, Any]] = field(default_factory=list)
sub_questions: Optional[List[str]] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"answer": self.answer,
"sources": self.sources,
"sub_questions": self.sub_questions,
"metadata": self.metadata
}
@runtime_checkable
class QueryEngineProtocol(Protocol):
"""
Protocol for query engines.
Implementations must provide a query method that takes a question
and returns an answer with sources.
"""
name: str
mode: QueryMode
def query(
self,
question: str,
context: Optional[List[str]] = None,
**kwargs
) -> QueryResult:
"""
Query the knowledge base.
Args:
question: The user's question
context: Optional pre-retrieved context
**kwargs: Additional engine-specific options
Returns:
QueryResult with answer and sources
"""
...
async def aquery(
self,
question: str,
context: Optional[List[str]] = None,
**kwargs
) -> QueryResult:
"""Async version of query."""
...
class QueryEngineRegistry:
"""Registry for query engines."""
_instance: Optional["QueryEngineRegistry"] = None
def __new__(cls) -> "QueryEngineRegistry":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._engines: Dict[str, Callable[..., QueryEngineProtocol]] = {}
return cls._instance
def register(self, name: str, factory: Callable[..., QueryEngineProtocol]) -> None:
"""Register a query engine factory."""
self._engines[name] = factory
def get(self, name: str, **kwargs) -> Optional[QueryEngineProtocol]:
"""Get a query engine by name."""
if name in self._engines:
try:
return self._engines[name](**kwargs)
except Exception as e:
logger.warning(f"Failed to initialize query engine '{name}': {e}")
return None
return None
def list_engines(self) -> List[str]:
"""List all registered engine names."""
return list(self._engines.keys())
def clear(self) -> None:
"""Clear all registered engines."""
self._engines.clear()
def get_query_engine_registry() -> QueryEngineRegistry:
"""Get the global query engine registry instance."""
return QueryEngineRegistry()
def decompose_question(question: str) -> List[str]:
"""
Simple deterministic question decomposition.
Splits complex questions into sub-questions without LLM.
Used as fallback when LLM is not available.
Args:
question: The complex question
Returns:
List of sub-questions
"""
sub_questions = []
# Split on common conjunctions
conjunctions = [" and ", " also ", " additionally ", " furthermore ", " moreover "]
parts = [question]
for conj in conjunctions:
new_parts = []
for part in parts:
new_parts.extend(part.split(conj))
parts = new_parts
# Clean up parts
for part in parts:
part = part.strip()
if part:
# Ensure it ends with a question mark
if not part.endswith("?"):
part = part + "?"
# Capitalize first letter
part = part[0].upper() + part[1:] if len(part) > 1 else part.upper()
sub_questions.append(part)
# If no decomposition happened, return original
if len(sub_questions) <= 1:
return [question]
return sub_questions
def synthesize_answer(
question: str,
contexts: List[str],
max_context_length: int = 4000
) -> str:
"""
Simple answer synthesis without LLM.
Concatenates relevant context with the question.
Used as fallback when LLM is not available.
Args:
question: The original question
contexts: List of context strings
max_context_length: Maximum total context length
Returns:
Synthesized answer string
"""
if not contexts:
return "No relevant information found."
# Truncate contexts to fit
total_length = 0
selected_contexts = []
for ctx in contexts:
if total_length + len(ctx) > max_context_length:
remaining = max_context_length - total_length
if remaining > 100:
selected_contexts.append(ctx[:remaining] + "...")
break
selected_contexts.append(ctx)
total_length += len(ctx)
# Format as a simple answer
context_text = "\n\n".join(selected_contexts)
return f"Based on the available information:\n\n{context_text}"
class SimpleQueryEngine:
"""
Simple query engine for testing and fallback.
Uses basic context concatenation without LLM synthesis.
"""
name: str = "simple"
mode: QueryMode = QueryMode.DEFAULT
def __init__(self, **kwargs):
pass
def query(
self,
question: str,
context: Optional[List[str]] = None,
**kwargs
) -> QueryResult:
"""Simple query that returns context as answer."""
contexts = context or []
answer = synthesize_answer(question, contexts)
return QueryResult(
answer=answer,
sources=[{"text": ctx} for ctx in contexts],
metadata={"mode": self.mode.value}
)
async def aquery(
self,
question: str,
context: Optional[List[str]] = None,
**kwargs
) -> QueryResult:
"""Async version (just calls sync)."""
return self.query(question, context, **kwargs)
class SubQuestionEngine:
"""
Sub-question decomposition query engine.
Decomposes complex questions into simpler sub-questions.
Uses deterministic fallback when LLM is not available.
"""
name: str = "sub_question"
mode: QueryMode = QueryMode.SUB_QUESTION
def __init__(self, llm: Optional[Any] = None, **kwargs):
self.llm = llm
def query(
self,
question: str,
context: Optional[List[str]] = None,
retriever: Optional[Any] = None,
**kwargs
) -> QueryResult:
"""Query with sub-question decomposition."""
# Decompose question
if self.llm:
sub_questions = self._decompose_with_llm(question)
else:
sub_questions = decompose_question(question)
# Gather context for each sub-question
all_contexts = list(context or [])
sub_answers = []
for sub_q in sub_questions:
if retriever:
# Retrieve for each sub-question
try:
results = retriever.retrieve(sub_q, top_k=3)
for r in results:
if hasattr(r, 'text'):
all_contexts.append(r.text)
elif isinstance(r, dict):
all_contexts.append(r.get('text', str(r)))
except Exception as e:
logger.warning(f"Retrieval failed for sub-question: {e}")
# Simple sub-answer (would use LLM in full implementation)
sub_answers.append(f"Sub-question: {sub_q}")
# Synthesize final answer
answer = synthesize_answer(question, all_contexts)
return QueryResult(
answer=answer,
sources=[{"text": ctx} for ctx in all_contexts[:10]],
sub_questions=sub_questions,
metadata={"mode": self.mode.value, "sub_question_count": len(sub_questions)}
)
async def aquery(
self,
question: str,
context: Optional[List[str]] = None,
**kwargs
) -> QueryResult:
"""Async version (just calls sync)."""
return self.query(question, context, **kwargs)
def _decompose_with_llm(self, question: str) -> List[str]:
"""Decompose question using LLM."""
try:
prompt = f"""Break down this complex question into simpler sub-questions that can be answered independently.
Question: {question}
Return only the sub-questions, one per line, without numbering or bullets."""
response = self.llm.chat(prompt)
if response:
lines = response.strip().split("\n")
return [line.strip() for line in lines if line.strip()]
except Exception as e:
logger.warning(f"LLM decomposition failed: {e}")
return decompose_question(question)
# Register default engines
def _register_default_engines():
"""Register default query engines."""
registry = get_query_engine_registry()
registry.register("default", SimpleQueryEngine)
registry.register("simple", SimpleQueryEngine)
registry.register("sub_question", SubQuestionEngine)
_register_default_engines()
@@ -0,0 +1,286 @@
"""
Data Reader Adapter Protocol for PraisonAI Agents.
This module defines the protocol for document readers and a registry system.
NO heavy imports - only stdlib and typing.
Implementations are provided by the wrapper layer (praisonai.adapters.readers).
"""
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
import os
import logging
logger = logging.getLogger(__name__)
@dataclass
class Document:
"""
Lightweight document representation.
Attributes:
content: The text content of the document
metadata: Optional metadata dict (source, filename, page, etc.)
doc_id: Optional unique identifier
embedding: Optional pre-computed embedding
"""
content: str
metadata: Dict[str, Any] = field(default_factory=dict)
doc_id: Optional[str] = None
embedding: Optional[List[float]] = None
def __post_init__(self):
if self.doc_id is None:
import uuid
self.doc_id = str(uuid.uuid4())
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"content": self.content,
"metadata": self.metadata,
"doc_id": self.doc_id,
"embedding": self.embedding
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Document":
"""Create from dictionary."""
return cls(
content=data.get("content", ""),
metadata=data.get("metadata", {}),
doc_id=data.get("doc_id"),
embedding=data.get("embedding")
)
@runtime_checkable
class ReaderProtocol(Protocol):
"""
Protocol for document readers.
Implementations must provide:
- name: Identifier for the reader
- supported_extensions: List of file extensions this reader handles
- load(): Method to load documents from a source
"""
name: str
supported_extensions: List[str]
def load(
self,
source: str,
*,
metadata: Optional[Dict[str, Any]] = None
) -> List[Document]:
"""
Load documents from a source.
Args:
source: File path, URL, or other source identifier
metadata: Optional metadata to attach to all documents
Returns:
List of Document objects
"""
...
def can_handle(self, source: str) -> bool:
"""
Check if this reader can handle the given source.
Args:
source: File path, URL, or other source identifier
Returns:
True if this reader can handle the source
"""
...
class ReaderRegistry:
"""
Registry for document readers.
Provides lazy loading and automatic reader selection based on source type.
"""
_instance: Optional["ReaderRegistry"] = None
def __new__(cls) -> "ReaderRegistry":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._readers: Dict[str, Callable[[], ReaderProtocol]] = {}
cls._instance._extension_map: Dict[str, str] = {}
cls._instance._initialized_readers: Dict[str, ReaderProtocol] = {}
return cls._instance
def register(
self,
name: str,
factory: Callable[[], ReaderProtocol],
extensions: Optional[List[str]] = None
) -> None:
"""
Register a reader factory.
Args:
name: Unique name for the reader
factory: Callable that returns a ReaderProtocol instance
extensions: List of file extensions this reader handles
"""
self._readers[name] = factory
if extensions:
for ext in extensions:
ext_lower = ext.lower().lstrip(".")
self._extension_map[ext_lower] = name
def get(self, name: str) -> Optional[ReaderProtocol]:
"""
Get a reader by name (lazy initialization).
Args:
name: Reader name
Returns:
ReaderProtocol instance or None
"""
if name in self._initialized_readers:
return self._initialized_readers[name]
if name in self._readers:
try:
reader = self._readers[name]()
self._initialized_readers[name] = reader
return reader
except Exception as e:
logger.warning(f"Failed to initialize reader '{name}': {e}")
return None
return None
def get_for_source(self, source: str) -> Optional[ReaderProtocol]:
"""
Get the best reader for a given source.
Args:
source: File path, URL, or other source identifier
Returns:
Best matching ReaderProtocol or None
"""
source_kind = detect_source_kind(source)
if source_kind == "file":
ext = os.path.splitext(source)[1].lower().lstrip(".")
if ext in self._extension_map:
return self.get(self._extension_map[ext])
elif source_kind == "url":
# Try URL reader
if "url" in self._readers:
return self.get("url")
elif source_kind == "directory":
if "directory" in self._readers:
return self.get("directory")
# Fallback to auto reader if available
if "auto" in self._readers:
return self.get("auto")
return None
def list_readers(self) -> List[str]:
"""List all registered reader names."""
return list(self._readers.keys())
def list_extensions(self) -> Dict[str, str]:
"""List all registered extensions and their readers."""
return dict(self._extension_map)
def clear(self) -> None:
"""Clear all registered readers (mainly for testing)."""
self._readers.clear()
self._extension_map.clear()
self._initialized_readers.clear()
def get_reader_registry() -> ReaderRegistry:
"""Get the global reader registry instance."""
return ReaderRegistry()
def detect_source_kind(source: str) -> str:
"""
Detect the kind of source without importing heavy libraries.
Args:
source: File path, URL, directory, or glob pattern
Returns:
One of: "file", "url", "directory", "glob", "unknown"
"""
if not isinstance(source, str):
return "unknown"
source = source.strip()
# Check for URL
if source.startswith(("http://", "https://", "ftp://", "s3://", "gs://")):
return "url"
# Check for glob pattern
if "*" in source or "?" in source or "[" in source:
return "glob"
# Check if it's a directory
if os.path.isdir(source):
return "directory"
# Check if it's a file
if os.path.isfile(source):
return "file"
# Check if it looks like a file path (has extension)
if os.path.splitext(source)[1]:
return "file"
return "unknown"
def get_file_extension(source: str) -> str:
"""
Get the file extension from a source path.
Args:
source: File path or URL
Returns:
Lowercase extension without dot, or empty string
"""
# Handle URLs by extracting path
if source.startswith(("http://", "https://")):
from urllib.parse import urlparse
path = urlparse(source).path
return os.path.splitext(path)[1].lower().lstrip(".")
return os.path.splitext(source)[1].lower().lstrip(".")
# Default extension mappings (reader name -> extensions)
DEFAULT_EXTENSION_MAPPINGS = {
"text": ["txt", "text"],
"markdown": ["md", "markdown"],
"json": ["json", "jsonl"],
"csv": ["csv", "tsv"],
"html": ["html", "htm"],
"xml": ["xml"],
"pdf": ["pdf"],
"docx": ["docx", "doc"],
"xlsx": ["xlsx", "xls"],
"pptx": ["pptx", "ppt"],
"image": ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp"],
"audio": ["mp3", "wav", "ogg", "m4a", "flac"],
"video": ["mp4", "avi", "mov", "mkv", "webm"],
}
@@ -0,0 +1,196 @@
"""
Reranker Protocol for PraisonAI Agents.
This module defines the protocol for rerankers and a registry system.
NO heavy imports - only stdlib and typing.
Implementations are provided by praisonai-tools or wrapper layer.
"""
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
import logging
logger = logging.getLogger(__name__)
@dataclass
class RerankResult:
"""
Result from a reranking operation.
Attributes:
text: The text content
score: Reranking score (higher is more relevant)
original_index: Original position before reranking
metadata: Optional metadata
"""
text: str
score: float
original_index: int
metadata: Dict[str, Any] = None
def __post_init__(self):
if self.metadata is None:
self.metadata = {}
@runtime_checkable
class RerankerProtocol(Protocol):
"""
Protocol for rerankers.
Implementations must provide a rerank method that takes a query
and a list of documents and returns reranked results.
"""
name: str
def rerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[RerankResult]:
"""
Rerank documents based on relevance to query.
Args:
query: The search query
documents: List of document texts to rerank
top_k: Optional limit on results (None = return all)
**kwargs: Additional reranker-specific options
Returns:
List of RerankResult objects sorted by relevance
"""
...
async def arerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[RerankResult]:
"""Async version of rerank."""
...
class RerankerRegistry:
"""
Registry for rerankers.
"""
_instance: Optional["RerankerRegistry"] = None
def __new__(cls) -> "RerankerRegistry":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._rerankers: Dict[str, Callable[..., RerankerProtocol]] = {}
return cls._instance
def register(
self,
name: str,
factory: Callable[..., RerankerProtocol]
) -> None:
"""Register a reranker factory."""
self._rerankers[name] = factory
def get(
self,
name: str,
**kwargs
) -> Optional[RerankerProtocol]:
"""Get a reranker by name."""
if name in self._rerankers:
try:
return self._rerankers[name](**kwargs)
except Exception as e:
logger.warning(f"Failed to initialize reranker '{name}': {e}")
return None
return None
def list_rerankers(self) -> List[str]:
"""List all registered reranker names."""
return list(self._rerankers.keys())
def clear(self) -> None:
"""Clear all registered rerankers."""
self._rerankers.clear()
def get_reranker_registry() -> RerankerRegistry:
"""Get the global reranker registry instance."""
return RerankerRegistry()
class SimpleReranker:
"""
Simple keyword-based reranker for testing and fallback.
Uses term frequency overlap for scoring. No external dependencies.
"""
name: str = "simple"
def __init__(self, **kwargs):
pass
def rerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[RerankResult]:
"""Rerank using simple term overlap."""
query_terms = set(query.lower().split())
results = []
for i, doc in enumerate(documents):
doc_terms = set(doc.lower().split())
# Calculate Jaccard similarity
if not query_terms or not doc_terms:
score = 0.0
else:
intersection = len(query_terms & doc_terms)
union = len(query_terms | doc_terms)
score = intersection / union if union > 0 else 0.0
results.append(RerankResult(
text=doc,
score=score,
original_index=i
))
# Sort by score descending
results.sort(key=lambda x: x.score, reverse=True)
if top_k is not None:
results = results[:top_k]
return results
async def arerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[RerankResult]:
"""Async version (just calls sync)."""
return self.rerank(query, documents, top_k, **kwargs)
# Register the simple reranker by default
def _register_default_rerankers():
"""Register default rerankers."""
registry = get_reranker_registry()
registry.register("simple", SimpleReranker)
_register_default_rerankers()
@@ -0,0 +1,285 @@
"""
Retrieval Strategy Patterns for PraisonAI Agents.
This module defines protocols and strategies for document retrieval.
NO heavy imports - only stdlib and typing.
Implementations are provided by the wrapper layer.
"""
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class RetrievalStrategy(str, Enum):
"""Available retrieval strategies."""
BASIC = "basic"
FUSION = "fusion"
RECURSIVE = "recursive"
AUTO_MERGE = "auto_merge"
HYBRID = "hybrid"
@dataclass
class RetrievalResult:
"""
Result from a retrieval operation.
Attributes:
text: The retrieved text content
score: Relevance score (0-1)
metadata: Source metadata
doc_id: Document identifier
chunk_index: Index of chunk within document
"""
text: str
score: float
metadata: Dict[str, Any] = field(default_factory=dict)
doc_id: Optional[str] = None
chunk_index: Optional[int] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"text": self.text,
"score": self.score,
"metadata": self.metadata,
"doc_id": self.doc_id,
"chunk_index": self.chunk_index
}
@runtime_checkable
class RetrieverProtocol(Protocol):
"""
Protocol for document retrievers.
Implementations must provide a retrieve method that takes a query
and returns relevant documents.
"""
name: str
strategy: RetrievalStrategy
def retrieve(
self,
query: str,
top_k: int = 10,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[RetrievalResult]:
"""
Retrieve relevant documents for a query.
Args:
query: The search query
top_k: Maximum number of results
filter: Optional metadata filter
**kwargs: Additional retriever-specific options
Returns:
List of RetrievalResult objects sorted by relevance
"""
...
async def aretrieve(
self,
query: str,
top_k: int = 10,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[RetrievalResult]:
"""Async version of retrieve."""
...
class RetrieverRegistry:
"""
Registry for retrieval strategies.
"""
_instance: Optional["RetrieverRegistry"] = None
def __new__(cls) -> "RetrieverRegistry":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._retrievers: Dict[str, Callable[..., RetrieverProtocol]] = {}
return cls._instance
def register(
self,
name: str,
factory: Callable[..., RetrieverProtocol]
) -> None:
"""Register a retriever factory."""
self._retrievers[name] = factory
def get(
self,
name: str,
**kwargs
) -> Optional[RetrieverProtocol]:
"""Get a retriever by name."""
if name in self._retrievers:
try:
return self._retrievers[name](**kwargs)
except Exception as e:
logger.warning(f"Failed to initialize retriever '{name}': {e}")
return None
return None
def list_retrievers(self) -> List[str]:
"""List all registered retriever names."""
return list(self._retrievers.keys())
def clear(self) -> None:
"""Clear all registered retrievers."""
self._retrievers.clear()
def get_retriever_registry() -> RetrieverRegistry:
"""Get the global retriever registry instance."""
return RetrieverRegistry()
def reciprocal_rank_fusion(
result_lists: List[List[RetrievalResult]],
k: int = 60
) -> List[RetrievalResult]:
"""
Reciprocal Rank Fusion (RRF) algorithm for combining multiple result lists.
Pure Python implementation with no external dependencies.
Args:
result_lists: List of result lists from different retrievers
k: RRF constant (default 60)
Returns:
Fused and re-ranked list of results
"""
# Track scores by document ID
fused_scores: Dict[str, float] = {}
doc_map: Dict[str, RetrievalResult] = {}
for results in result_lists:
for rank, result in enumerate(results):
doc_key = result.doc_id or result.text[:100] # Use text prefix as fallback key
# RRF score: 1 / (k + rank)
rrf_score = 1.0 / (k + rank + 1)
if doc_key in fused_scores:
fused_scores[doc_key] += rrf_score
else:
fused_scores[doc_key] = rrf_score
doc_map[doc_key] = result
# Sort by fused score
sorted_keys = sorted(fused_scores.keys(), key=lambda x: fused_scores[x], reverse=True)
# Build result list with updated scores
fused_results = []
for key in sorted_keys:
result = doc_map[key]
fused_results.append(RetrievalResult(
text=result.text,
score=fused_scores[key],
metadata=result.metadata,
doc_id=result.doc_id,
chunk_index=result.chunk_index
))
return fused_results
def merge_adjacent_chunks(
results: List[RetrievalResult],
max_gap: int = 1
) -> List[RetrievalResult]:
"""
Merge adjacent chunks from the same document.
Args:
results: List of retrieval results
max_gap: Maximum gap between chunk indices to merge
Returns:
Merged results
"""
if not results:
return results
# Group by document ID
doc_groups: Dict[str, List[RetrievalResult]] = {}
for result in results:
doc_id = result.doc_id or "unknown"
if doc_id not in doc_groups:
doc_groups[doc_id] = []
doc_groups[doc_id].append(result)
merged_results = []
for doc_id, chunks in doc_groups.items():
# Sort by chunk index
chunks_with_index = [c for c in chunks if c.chunk_index is not None]
chunks_without_index = [c for c in chunks if c.chunk_index is None]
if not chunks_with_index:
merged_results.extend(chunks_without_index)
continue
chunks_with_index.sort(key=lambda x: x.chunk_index or 0)
# Merge adjacent chunks
current_group = [chunks_with_index[0]]
for chunk in chunks_with_index[1:]:
last_chunk = current_group[-1]
last_idx = last_chunk.chunk_index or 0
curr_idx = chunk.chunk_index or 0
if curr_idx - last_idx <= max_gap:
current_group.append(chunk)
else:
# Merge current group and start new one
merged_results.append(_merge_chunk_group(current_group))
current_group = [chunk]
# Don't forget the last group
if current_group:
merged_results.append(_merge_chunk_group(current_group))
merged_results.extend(chunks_without_index)
# Sort by score
merged_results.sort(key=lambda x: x.score, reverse=True)
return merged_results
def _merge_chunk_group(chunks: List[RetrievalResult]) -> RetrievalResult:
"""Merge a group of chunks into a single result."""
if len(chunks) == 1:
return chunks[0]
# Combine text
merged_text = "\n".join(c.text for c in chunks)
# Average score
avg_score = sum(c.score for c in chunks) / len(chunks)
# Use first chunk's metadata
metadata = chunks[0].metadata.copy()
metadata["merged_chunks"] = len(chunks)
return RetrievalResult(
text=merged_text,
score=avg_score,
metadata=metadata,
doc_id=chunks[0].doc_id,
chunk_index=chunks[0].chunk_index
)
@@ -0,0 +1,422 @@
"""
Vector Store Adapter Protocol for PraisonAI Agents.
This module defines the protocol for vector stores and a registry system.
NO heavy imports - only stdlib and typing.
Implementations are provided by praisonai-tools or wrapper layer.
"""
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
import logging
logger = logging.getLogger(__name__)
@dataclass
class VectorRecord:
"""
A record in a vector store.
Attributes:
id: Unique identifier
text: The text content
embedding: The vector embedding
metadata: Optional metadata dict
score: Optional similarity score (for query results)
"""
id: str
text: str
embedding: List[float]
metadata: Dict[str, Any] = field(default_factory=dict)
score: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"id": self.id,
"text": self.text,
"embedding": self.embedding,
"metadata": self.metadata,
"score": self.score
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "VectorRecord":
"""Create from dictionary."""
return cls(
id=data["id"],
text=data["text"],
embedding=data["embedding"],
metadata=data.get("metadata", {}),
score=data.get("score")
)
@runtime_checkable
class VectorStoreProtocol(Protocol):
"""
Protocol for vector stores.
Implementations must provide methods for:
- Adding vectors
- Querying by vector similarity
- Deleting vectors
- Getting count
"""
name: str
def add(
self,
texts: List[str],
embeddings: List[List[float]],
metadatas: Optional[List[Dict[str, Any]]] = None,
ids: Optional[List[str]] = None,
namespace: Optional[str] = None
) -> List[str]:
"""
Add vectors to the store.
Args:
texts: List of text content
embeddings: List of embedding vectors
metadatas: Optional list of metadata dicts
ids: Optional list of IDs (auto-generated if not provided)
namespace: Optional namespace for multi-tenant isolation
Returns:
List of IDs for the added vectors
"""
...
def query(
self,
embedding: List[float],
top_k: int = 10,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None
) -> List[VectorRecord]:
"""
Query vectors by similarity.
Args:
embedding: Query embedding vector
top_k: Number of results to return
namespace: Optional namespace filter
filter: Optional metadata filter
Returns:
List of VectorRecord objects sorted by similarity
"""
...
def delete(
self,
ids: Optional[List[str]] = None,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None,
delete_all: bool = False
) -> int:
"""
Delete vectors from the store.
Args:
ids: Optional list of IDs to delete
namespace: Optional namespace filter
filter: Optional metadata filter
delete_all: If True, delete all vectors
Returns:
Number of vectors deleted
"""
...
def count(self, namespace: Optional[str] = None) -> int:
"""
Get the number of vectors in the store.
Args:
namespace: Optional namespace filter
Returns:
Number of vectors
"""
...
def get(
self,
ids: List[str],
namespace: Optional[str] = None
) -> List[VectorRecord]:
"""
Get vectors by ID.
Args:
ids: List of IDs to retrieve
namespace: Optional namespace filter
Returns:
List of VectorRecord objects
"""
...
class VectorStoreRegistry:
"""
Registry for vector stores.
Provides lazy loading and configuration-based initialization.
"""
_instance: Optional["VectorStoreRegistry"] = None
def __new__(cls) -> "VectorStoreRegistry":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._stores: Dict[str, Callable[..., VectorStoreProtocol]] = {}
cls._instance._initialized_stores: Dict[str, VectorStoreProtocol] = {}
return cls._instance
def register(
self,
name: str,
factory: Callable[..., VectorStoreProtocol]
) -> None:
"""
Register a vector store factory.
Args:
name: Unique name for the store (e.g., "chroma", "pinecone")
factory: Callable that returns a VectorStoreProtocol instance
"""
self._stores[name] = factory
def get(
self,
name: str,
config: Optional[Dict[str, Any]] = None,
namespace: Optional[str] = None
) -> Optional[VectorStoreProtocol]:
"""
Get a vector store by name (lazy initialization).
Args:
name: Store name
config: Optional configuration dict
namespace: Optional namespace for multi-tenant isolation
Returns:
VectorStoreProtocol instance or None
"""
cache_key = f"{name}:{namespace or 'default'}"
if cache_key in self._initialized_stores:
return self._initialized_stores[cache_key]
if name in self._stores:
try:
store = self._stores[name](config=config, namespace=namespace)
self._initialized_stores[cache_key] = store
return store
except Exception as e:
logger.warning(f"Failed to initialize vector store '{name}': {e}")
return None
return None
def list_stores(self) -> List[str]:
"""List all registered store names."""
return list(self._stores.keys())
def clear(self) -> None:
"""Clear all registered stores (mainly for testing)."""
self._stores.clear()
self._initialized_stores.clear()
def get_vector_store_registry() -> VectorStoreRegistry:
"""Get the global vector store registry instance."""
return VectorStoreRegistry()
class InMemoryVectorStore:
"""
Pure Python in-memory vector store for testing and default usage.
Uses cosine similarity for queries. No external dependencies.
"""
name: str = "memory"
def __init__(
self,
config: Optional[Dict[str, Any]] = None,
namespace: Optional[str] = None
):
self.config = config or {}
self.default_namespace = namespace or "default"
self._data: Dict[str, Dict[str, VectorRecord]] = {} # namespace -> id -> record
def _get_namespace_data(self, namespace: Optional[str] = None) -> Dict[str, VectorRecord]:
"""Get or create namespace data."""
ns = namespace or self.default_namespace
if ns not in self._data:
self._data[ns] = {}
return self._data[ns]
def add(
self,
texts: List[str],
embeddings: List[List[float]],
metadatas: Optional[List[Dict[str, Any]]] = None,
ids: Optional[List[str]] = None,
namespace: Optional[str] = None
) -> List[str]:
"""Add vectors to the store."""
import uuid
ns_data = self._get_namespace_data(namespace)
metadatas = metadatas or [{} for _ in texts]
ids = ids or [str(uuid.uuid4()) for _ in texts]
result_ids = []
for i, (text, embedding, metadata, id_) in enumerate(zip(texts, embeddings, metadatas, ids)):
record = VectorRecord(
id=id_,
text=text,
embedding=embedding,
metadata=metadata
)
ns_data[id_] = record
result_ids.append(id_)
return result_ids
def query(
self,
embedding: List[float],
top_k: int = 10,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None
) -> List[VectorRecord]:
"""Query vectors by cosine similarity."""
ns_data = self._get_namespace_data(namespace)
if not ns_data:
return []
# Calculate cosine similarity for all records
results = []
for record in ns_data.values():
# Apply metadata filter if provided
if filter:
match = True
for key, value in filter.items():
if record.metadata.get(key) != value:
match = False
break
if not match:
continue
score = self._cosine_similarity(embedding, record.embedding)
results.append(VectorRecord(
id=record.id,
text=record.text,
embedding=record.embedding,
metadata=record.metadata,
score=score
))
# Sort by score descending and return top_k
results.sort(key=lambda x: x.score or 0, reverse=True)
return results[:top_k]
def delete(
self,
ids: Optional[List[str]] = None,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None,
delete_all: bool = False
) -> int:
"""Delete vectors from the store."""
ns = namespace or self.default_namespace
if delete_all:
if ns in self._data:
count = len(self._data[ns])
self._data[ns] = {}
return count
return 0
if ns not in self._data:
return 0
ns_data = self._data[ns]
deleted = 0
if ids:
for id_ in ids:
if id_ in ns_data:
del ns_data[id_]
deleted += 1
elif filter:
to_delete = []
for id_, record in ns_data.items():
match = True
for key, value in filter.items():
if record.metadata.get(key) != value:
match = False
break
if match:
to_delete.append(id_)
for id_ in to_delete:
del ns_data[id_]
deleted += 1
return deleted
def count(self, namespace: Optional[str] = None) -> int:
"""Get the number of vectors in the store."""
ns = namespace or self.default_namespace
if ns in self._data:
return len(self._data[ns])
return 0
def get(
self,
ids: List[str],
namespace: Optional[str] = None
) -> List[VectorRecord]:
"""Get vectors by ID."""
ns_data = self._get_namespace_data(namespace)
results = []
for id_ in ids:
if id_ in ns_data:
results.append(ns_data[id_])
return results
@staticmethod
def _cosine_similarity(a: List[float], b: List[float]) -> float:
"""Calculate cosine similarity between two vectors."""
if len(a) != len(b):
return 0.0
dot_product = sum(x * y for x, y in zip(a, b))
norm_a = sum(x * x for x in a) ** 0.5
norm_b = sum(x * x for x in b) ** 0.5
if norm_a == 0 or norm_b == 0:
return 0.0
return dot_product / (norm_a * norm_b)
# Register the in-memory store by default
def _register_default_stores():
"""Register default vector stores."""
registry = get_vector_store_registry()
registry.register("memory", InMemoryVectorStore)
_register_default_stores()
@@ -4,16 +4,20 @@ Session Management for PraisonAI Agents
A simple wrapper around existing stateful capabilities to provide a unified
session API for developers building stateful agent applications.
"""
from __future__ import annotations
import os
import uuid
import requests
import json
import time
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .memory.memory import Memory
from .knowledge.knowledge import Knowledge
from .agent import Agent
from .memory import Memory
from .knowledge import Knowledge
class Session:
@@ -112,6 +116,7 @@ class Session:
if self.is_remote:
raise ValueError("Memory operations are not available for remote agent sessions")
if self._memory is None:
from .memory.memory import Memory
self._memory = Memory(config=self.memory_config)
return self._memory
@@ -121,6 +126,7 @@ class Session:
if self.is_remote:
raise ValueError("Knowledge operations are not available for remote agent sessions")
if self._knowledge is None:
from .knowledge.knowledge import Knowledge
self._knowledge = Knowledge(config=self.knowledge_config)
return self._knowledge
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.3.0"
version = "0.3.4"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
+7 -7
View File
@@ -672,16 +672,16 @@ class TestPlanningAgent:
planner = PlanningAgent()
assert planner.llm_model == "gpt-4o-mini"
assert planner.llm_model in ["gpt-4o-mini", "gpt-5-nano"]
assert planner.read_only is True
def test_planning_agent_custom_llm(self):
"""Test PlanningAgent with custom LLM."""
from praisonaiagents.planning import PlanningAgent
planner = PlanningAgent(llm="gpt-4o")
planner = PlanningAgent(llm="gpt-5-nano")
assert planner.llm_model == "gpt-4o"
assert planner.llm_model == "gpt-5-nano"
def test_planning_agent_allowed_tools(self):
"""Test PlanningAgent has only read-only tools."""
@@ -840,10 +840,10 @@ class TestPraisonAIAgentsPlanningIntegration:
agents=[agent],
tasks=[task],
planning=True,
planning_llm="gpt-4o"
planning_llm="gpt-5-nano"
)
assert agents.planning_llm == "gpt-4o"
assert agents.planning_llm == "gpt-5-nano"
def test_agents_auto_approve_plan(self):
"""Test auto-approve plan option."""
@@ -867,7 +867,7 @@ class TestPraisonAIAgentsPlanningIntegration:
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.planning import Plan, PlanStep
agent = Agent(name="Test", role="Tester", llm="gpt-4o-mini")
agent = Agent(name="Test", role="Tester", llm="gpt-5-nano")
task = Task(description="Test task", agent=agent)
agents = PraisonAIAgents(
@@ -880,7 +880,7 @@ class TestPraisonAIAgentsPlanningIntegration:
# Test that planning is enabled and configured correctly
assert agents.planning is True
assert agents.auto_approve_plan is True
assert agents.planning_llm == "gpt-4o-mini"
assert agents.planning_llm == "gpt-5-nano"
assert agents._current_plan is None # No plan created yet
assert agents._todo_list is None
@@ -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.0"
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]);
});
});
+609 -156
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,141 +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) |
| **🔬 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>
---
@@ -537,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:
@@ -586,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
@@ -601,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:
@@ -871,7 +1079,7 @@ callbacks:
on_workflow_complete: log_complete
```
### 9. Hooks
### 10. Hooks
Intercept and modify agent behavior at various lifecycle points:
@@ -910,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:
@@ -947,7 +1155,7 @@ praisonai checkpoint restore abc123 # Restore to checkpoint
---
### 11. Background Tasks
### 12. Background Tasks
Run agent tasks asynchronously without blocking:
@@ -985,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:
@@ -1026,7 +1234,7 @@ praisonai policy init # Create template
---
### 13. Thinking Budgets
### 14. Thinking Budgets
Configure token budgets for extended thinking:
@@ -1059,7 +1267,7 @@ praisonai thinking stats # Show usage statistics
---
### 14. Output Styles
### 15. Output Styles
Configure how agents format their responses:
@@ -1089,7 +1297,7 @@ praisonai output set concise # Set output style
---
### 15. Context Compaction
### 16. Context Compaction
Automatically manage context window size:
@@ -1124,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:
@@ -1158,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)
@@ -1234,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.
@@ -1310,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.
@@ -1358,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
@@ -2641,6 +2865,84 @@ PraisonAI provides zero-dependency persistent memory for agents. For detailed ex
---
## 📚 Knowledge & Retrieval (RAG)
PraisonAI provides a complete knowledge stack for building RAG applications with multiple vector stores, retrieval strategies, rerankers, and query modes.
### Knowledge CLI Commands
| Command | Description |
|---------|-------------|
| `praisonai knowledge add <file\|dir\|url>` | Add documents to knowledge base |
| `praisonai knowledge query <question>` | Query knowledge base with RAG |
| `praisonai knowledge list` | List indexed documents |
| `praisonai knowledge clear` | Clear knowledge base |
| `praisonai knowledge stats` | Show knowledge base statistics |
### Knowledge CLI Options
| Option | Values | Description |
|--------|--------|-------------|
| `--vector-store` | `memory`, `chroma`, `pinecone`, `qdrant`, `weaviate` | Vector store backend |
| `--retrieval` | `basic`, `fusion`, `recursive`, `auto_merge` | Retrieval strategy |
| `--reranker` | `simple`, `llm`, `cross_encoder`, `cohere` | Reranking method |
| `--index-type` | `vector`, `keyword`, `hybrid` | Index type |
| `--query-mode` | `default`, `sub_question`, `summarize` | Query mode |
### Knowledge CLI Examples
```bash
# Add documents
praisonai knowledge add ./docs/
praisonai knowledge add https://example.com/page.html
praisonai knowledge add "*.pdf"
# Query with advanced options
praisonai knowledge query "How to authenticate?" --retrieval fusion --reranker llm
# Full advanced query
praisonai knowledge query "authentication flow" \
--vector-store chroma \
--retrieval fusion \
--reranker llm \
--index-type hybrid \
--query-mode sub_question
```
### Knowledge SDK Usage
```python
from praisonaiagents import Agent, Knowledge
# Simple usage with Agent
agent = Agent(
name="Research Assistant",
knowledge=["docs/manual.pdf", "data/faq.txt"],
knowledge_config={
"vector_store": {"provider": "chroma"}
}
)
response = agent.chat("How do I authenticate?")
# Direct Knowledge usage
knowledge = Knowledge()
knowledge.add("document.pdf")
results = knowledge.search("authentication", limit=5)
```
### Knowledge Stack Features Table
| Feature | Description | SDK Docs | CLI Docs |
|---------|-------------|----------|----------|
| **Data Readers** | Load PDF, Markdown, Text, HTML, URLs | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Vector Stores** | ChromaDB, Pinecone, Qdrant, Weaviate, In-Memory | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Retrieval Strategies** | Basic, Fusion (RRF), Recursive, Auto-Merge | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Rerankers** | Simple, LLM, Cross-Encoder, Cohere | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Index Types** | Vector, Keyword (BM25), Hybrid | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
| **Query Engines** | Default, Sub-Question, Summarize | [SDK](/docs/sdk/praisonaiagents/knowledge/protocols) | [CLI](/docs/cli/knowledge) |
---
## 🔬 Advanced Features
### Research & Intelligence
@@ -2755,6 +3057,44 @@ agent.chat("Hello!") # Auto-persists messages, runs, traces
| `praisonai persistence migrate` | Apply schema migrations |
| `praisonai persistence status` | Show schema status |
### Knowledge CLI Commands {#knowledge-cli}
| Command | Description |
|---------|-------------|
| `praisonai knowledge add <source>` | Add file, directory, URL, or glob pattern |
| `praisonai knowledge query "<question>"` | Query knowledge base with RAG |
| `praisonai knowledge list` | List indexed documents |
| `praisonai knowledge clear` | Clear knowledge base |
| `praisonai knowledge stats` | Show knowledge base statistics |
**Knowledge Query Flags:**
| Flag | Values | Default |
|------|--------|---------|
| `--vector-store` | `memory`, `chroma`, `pinecone`, `qdrant`, `weaviate` | `chroma` |
| `--retrieval-strategy` | `basic`, `fusion`, `recursive`, `auto_merge` | `basic` |
| `--reranker` | `none`, `simple`, `llm`, `cross_encoder`, `cohere` | `none` |
| `--index-type` | `vector`, `keyword`, `hybrid` | `vector` |
| `--query-mode` | `default`, `sub_question`, `summarize` | `default` |
| `--workspace` | Path to workspace directory | Current dir |
| `--session` | Session ID for persistence | - |
**Examples:**
```bash
# Add documents
praisonai knowledge add document.pdf
praisonai knowledge add ./docs/
praisonai knowledge add "*.md"
# Query with options
praisonai knowledge query "How to authenticate?" \
--vector-store chroma \
--retrieval-strategy fusion \
--reranker simple \
--query-mode sub_question
```
### Databases Table
| Database | Store Type | Install | Example | Docs |
@@ -2901,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.0.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.0.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]"
@@ -0,0 +1,50 @@
"""
PraisonAI Adapters - Implementations for core protocols.
This module provides concrete implementations of:
- Reader adapters (AutoReader, LlamaIndexReaderAdapter, MarkItDownReaderAdapter)
- Vector store adapters (ChromaAdapter, PineconeAdapter, etc.)
- Retriever implementations
- Reranker implementations
"""
# Lazy loading to avoid heavy imports at package load time
_LAZY_IMPORTS = {
# Readers
"AutoReader": ("praisonai.adapters.readers", "AutoReader"),
"MarkItDownReader": ("praisonai.adapters.readers", "MarkItDownReader"),
"TextReader": ("praisonai.adapters.readers", "TextReader"),
"DirectoryReader": ("praisonai.adapters.readers", "DirectoryReader"),
"register_default_readers": ("praisonai.adapters.readers", "register_default_readers"),
# Vector stores
"ChromaVectorStore": ("praisonai.adapters.vector_stores", "ChromaVectorStore"),
"register_default_vector_stores": ("praisonai.adapters.vector_stores", "register_default_vector_stores"),
# Retrievers
"BasicRetriever": ("praisonai.adapters.retrievers", "BasicRetriever"),
"FusionRetriever": ("praisonai.adapters.retrievers", "FusionRetriever"),
"register_default_retrievers": ("praisonai.adapters.retrievers", "register_default_retrievers"),
# Rerankers
"LLMReranker": ("praisonai.adapters.rerankers", "LLMReranker"),
"register_default_rerankers": ("praisonai.adapters.rerankers", "register_default_rerankers"),
}
def __getattr__(name: str):
"""Lazy load adapters."""
if name in _LAZY_IMPORTS:
module_path, attr_name = _LAZY_IMPORTS[name]
import importlib
module = importlib.import_module(module_path)
return getattr(module, attr_name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__():
"""List available attributes."""
return list(_LAZY_IMPORTS.keys())
__all__ = list(_LAZY_IMPORTS.keys())
+395
View File
@@ -0,0 +1,395 @@
"""
Reader Adapters for PraisonAI.
Provides concrete implementations of ReaderProtocol:
- TextReader: Plain text files
- MarkItDownReader: Uses markitdown for documents
- DirectoryReader: Reads all files in a directory
- AutoReader: Automatic file type detection and routing
"""
import os
import glob as glob_module
import logging
import importlib.util
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
# Lazy import flags
_MARKITDOWN_AVAILABLE = None
_LLAMA_INDEX_AVAILABLE = None
def _check_markitdown():
"""Check if markitdown is available."""
global _MARKITDOWN_AVAILABLE
if _MARKITDOWN_AVAILABLE is None:
_MARKITDOWN_AVAILABLE = importlib.util.find_spec("markitdown") is not None
return _MARKITDOWN_AVAILABLE
def _check_llama_index():
"""Check if llama_index is available."""
global _LLAMA_INDEX_AVAILABLE
if _LLAMA_INDEX_AVAILABLE is None:
_LLAMA_INDEX_AVAILABLE = importlib.util.find_spec("llama_index") is not None
return _LLAMA_INDEX_AVAILABLE
class TextReader:
"""Simple text file reader."""
name: str = "text"
supported_extensions: List[str] = ["txt", "text", "log"]
def load(
self,
source: str,
*,
metadata: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
"""Load a text file."""
from praisonaiagents.knowledge.readers import Document
try:
with open(source, 'r', encoding='utf-8') as f:
content = f.read()
doc_metadata = metadata or {}
doc_metadata.update({
"source": source,
"filename": os.path.basename(source),
"file_type": "text"
})
return [Document(content=content, metadata=doc_metadata)]
except Exception as e:
logger.error(f"Failed to read text file {source}: {e}")
return []
def can_handle(self, source: str) -> bool:
"""Check if this reader can handle the source."""
ext = os.path.splitext(source)[1].lower().lstrip(".")
return ext in self.supported_extensions
class MarkItDownReader:
"""Reader using markitdown for document conversion."""
name: str = "markitdown"
supported_extensions: List[str] = [
"pdf", "doc", "docx", "ppt", "pptx", "xls", "xlsx",
"html", "htm", "md", "markdown", "csv", "json", "xml",
"jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp",
"mp3", "wav", "ogg", "m4a", "flac"
]
def __init__(self):
self._converter = None
@property
def converter(self):
"""Lazy load markitdown converter."""
if self._converter is None:
if not _check_markitdown():
raise ImportError(
"markitdown is required for document conversion. "
"Install with: pip install markitdown"
)
from markitdown import MarkItDown
self._converter = MarkItDown()
return self._converter
def load(
self,
source: str,
*,
metadata: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
"""Load a document using markitdown."""
from praisonaiagents.knowledge.readers import Document
try:
result = self.converter.convert(source)
content = result.text_content
if not content:
logger.warning(f"No content extracted from {source}")
return []
doc_metadata = metadata or {}
doc_metadata.update({
"source": source,
"filename": os.path.basename(source),
"file_type": os.path.splitext(source)[1].lower().lstrip(".")
})
return [Document(content=content, metadata=doc_metadata)]
except Exception as e:
logger.error(f"Failed to convert document {source}: {e}")
return []
def can_handle(self, source: str) -> bool:
"""Check if this reader can handle the source."""
ext = os.path.splitext(source)[1].lower().lstrip(".")
return ext in self.supported_extensions and _check_markitdown()
class DirectoryReader:
"""Reader for directories - recursively reads all files."""
name: str = "directory"
supported_extensions: List[str] = [] # Handles directories, not extensions
def __init__(self, recursive: bool = True, exclude_patterns: Optional[List[str]] = None):
self.recursive = recursive
self.exclude_patterns = exclude_patterns or [
"*.pyc", "__pycache__", ".git", ".svn", "node_modules",
"*.egg-info", ".env", ".venv", "venv"
]
def load(
self,
source: str,
*,
metadata: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
"""Load all files from a directory."""
from praisonaiagents.knowledge.readers import get_reader_registry
if not os.path.isdir(source):
logger.error(f"Not a directory: {source}")
return []
documents = []
registry = get_reader_registry()
# Walk directory
if self.recursive:
for root, dirs, files in os.walk(source):
# Filter out excluded directories
dirs[:] = [d for d in dirs if not self._should_exclude(d)]
for file in files:
if self._should_exclude(file):
continue
file_path = os.path.join(root, file)
reader = registry.get_for_source(file_path)
if reader and reader.name != "directory":
file_metadata = (metadata or {}).copy()
file_metadata["parent_dir"] = source
loaded_docs = reader.load(file_path, metadata=file_metadata)
documents.extend(loaded_docs)
else:
for file in os.listdir(source):
if self._should_exclude(file):
continue
file_path = os.path.join(source, file)
if os.path.isfile(file_path):
reader = registry.get_for_source(file_path)
if reader and reader.name != "directory":
file_metadata = (metadata or {}).copy()
file_metadata["parent_dir"] = source
docs = reader.load(file_path, metadata=file_metadata)
documents.extend(docs)
return documents
def can_handle(self, source: str) -> bool:
"""Check if this reader can handle the source."""
return os.path.isdir(source)
def _should_exclude(self, name: str) -> bool:
"""Check if a file/directory should be excluded."""
import fnmatch
for pattern in self.exclude_patterns:
if fnmatch.fnmatch(name, pattern):
return True
return False
class GlobReader:
"""Reader for glob patterns."""
name: str = "glob"
supported_extensions: List[str] = []
def load(
self,
source: str,
*,
metadata: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
"""Load files matching a glob pattern."""
from praisonaiagents.knowledge.readers import get_reader_registry
documents = []
registry = get_reader_registry()
for file_path in glob_module.glob(source, recursive=True):
if os.path.isfile(file_path):
reader = registry.get_for_source(file_path)
if reader and reader.name not in ("glob", "directory"):
file_metadata = (metadata or {}).copy()
file_metadata["glob_pattern"] = source
docs = reader.load(file_path, metadata=file_metadata)
documents.extend(docs)
return documents
def can_handle(self, source: str) -> bool:
"""Check if this is a glob pattern."""
return "*" in source or "?" in source or "[" in source
class URLReader:
"""Reader for URLs (HTML pages)."""
name: str = "url"
supported_extensions: List[str] = []
def load(
self,
source: str,
*,
metadata: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
"""Load content from a URL."""
from praisonaiagents.knowledge.readers import Document
try:
import httpx
except ImportError:
try:
import requests as httpx
except ImportError:
logger.error("httpx or requests required for URL reading")
return []
try:
response = httpx.get(source, timeout=30, follow_redirects=True)
response.raise_for_status()
content = response.text
# Try to extract text from HTML
try:
from bs4 import BeautifulSoup
soup = BeautifulSoup(content, 'html.parser')
# Remove script and style elements
for script in soup(["script", "style"]):
script.decompose()
content = soup.get_text(separator='\n', strip=True)
except ImportError:
# Fall back to raw content
pass
doc_metadata = metadata or {}
doc_metadata.update({
"source": source,
"url": source,
"file_type": "html"
})
return [Document(content=content, metadata=doc_metadata)]
except Exception as e:
logger.error(f"Failed to fetch URL {source}: {e}")
return []
def can_handle(self, source: str) -> bool:
"""Check if this is a URL."""
return source.startswith(("http://", "https://"))
class AutoReader:
"""
Automatic reader that detects source type and routes to appropriate reader.
Selection policy:
1. If LlamaIndex reader is installed + supports the type → use it
2. Else if MarkItDown supports → use it
3. Else use simple built-in reader
"""
name: str = "auto"
supported_extensions: List[str] = [] # Handles everything
def load(
self,
source: str,
*,
metadata: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
"""Load from any source type."""
from praisonaiagents.knowledge.readers import get_reader_registry, detect_source_kind
registry = get_reader_registry()
source_kind = detect_source_kind(source)
# Route based on source kind
if source_kind == "url":
reader = registry.get("url")
elif source_kind == "directory":
reader = registry.get("directory")
elif source_kind == "glob":
reader = registry.get("glob")
elif source_kind == "file":
# Find best reader for file type
reader = registry.get_for_source(source)
if reader and reader.name == "auto":
# Avoid infinite recursion - use markitdown or text
ext = os.path.splitext(source)[1].lower().lstrip(".")
if ext in MarkItDownReader.supported_extensions and _check_markitdown():
reader = registry.get("markitdown")
else:
reader = registry.get("text")
else:
# Unknown - try markitdown then text
if _check_markitdown():
reader = registry.get("markitdown")
else:
reader = registry.get("text")
if reader:
return reader.load(source, metadata=metadata)
logger.warning(f"No reader found for source: {source}")
return []
def can_handle(self, source: str) -> bool:
"""AutoReader can handle anything."""
return True
def register_default_readers():
"""Register all default readers with the registry."""
from praisonaiagents.knowledge.readers import get_reader_registry
registry = get_reader_registry()
# Register text reader
registry.register("text", TextReader, ["txt", "text", "log"])
# Register markitdown reader
registry.register("markitdown", MarkItDownReader, MarkItDownReader.supported_extensions)
# Register directory reader
registry.register("directory", DirectoryReader)
# Register glob reader
registry.register("glob", GlobReader)
# Register URL reader
registry.register("url", URLReader)
# Register auto reader
registry.register("auto", AutoReader)
logger.debug("Registered default readers")
# Auto-register on import
register_default_readers()
@@ -0,0 +1,315 @@
"""
Reranker Implementations for PraisonAI.
Provides concrete implementations of RerankerProtocol:
- LLMReranker: Uses LLM to rerank documents
- CrossEncoderReranker: Uses sentence-transformers cross-encoder
- CohereReranker: Uses Cohere rerank API
"""
import logging
from typing import Any, List, Optional
logger = logging.getLogger(__name__)
# Lazy import flags
_SENTENCE_TRANSFORMERS_AVAILABLE = None
_COHERE_AVAILABLE = None
def _check_sentence_transformers():
"""Check if sentence-transformers is available."""
global _SENTENCE_TRANSFORMERS_AVAILABLE
if _SENTENCE_TRANSFORMERS_AVAILABLE is None:
import importlib.util
_SENTENCE_TRANSFORMERS_AVAILABLE = importlib.util.find_spec("sentence_transformers") is not None
return _SENTENCE_TRANSFORMERS_AVAILABLE
def _check_cohere():
"""Check if cohere is available."""
global _COHERE_AVAILABLE
if _COHERE_AVAILABLE is None:
import importlib.util
_COHERE_AVAILABLE = importlib.util.find_spec("cohere") is not None
return _COHERE_AVAILABLE
class LLMReranker:
"""
LLM-based reranker.
Uses an LLM to score document relevance to a query.
"""
name: str = "llm"
def __init__(
self,
llm: Optional[Any] = None,
model: Optional[str] = None,
batch_size: int = 5,
**kwargs
):
self.llm = llm
self.model = model or "gpt-4o-mini"
self.batch_size = batch_size
self._client = None
def _get_client(self):
"""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,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[Any]:
"""Rerank documents using LLM scoring."""
from praisonaiagents.knowledge.rerankers import RerankResult
results = []
# Process in batches
for i in range(0, len(documents), self.batch_size):
batch = documents[i:i + self.batch_size]
batch_results = self._score_batch(query, batch, start_idx=i)
results.extend(batch_results)
# Sort by score
results.sort(key=lambda x: x.score, reverse=True)
if top_k is not None:
results = results[:top_k]
return results
async def arerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[Any]:
"""Async version (just calls sync)."""
return self.rerank(query, documents, top_k, **kwargs)
def _score_batch(self, query: str, documents: List[str], start_idx: int = 0) -> List[Any]:
"""Score a batch of documents."""
from praisonaiagents.knowledge.rerankers import RerankResult
results = []
for i, doc in enumerate(documents):
score = self._score_document(query, doc)
results.append(RerankResult(
text=doc,
score=score,
original_index=start_idx + i
))
return results
def _score_document(self, query: str, document: str) -> float:
"""Score a single document's relevance to query using litellm."""
try:
client = self._get_client()
prompt = f"""Rate the relevance of this document to the query on a scale of 0 to 10.
Return ONLY a number between 0 and 10.
Query: {query}
Document: {document[:1000]}
Relevance score (0-10):"""
# 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:
# 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
)
response = completion.choices[0].message.content
# Parse score
if response:
try:
score = float(response.strip().split()[0])
return min(max(score / 10.0, 0.0), 1.0) # Normalize to 0-1
except (ValueError, IndexError):
pass
except Exception as e:
logger.warning(f"LLM scoring failed: {e}")
return 0.5 # Default score
class CrossEncoderReranker:
"""
Cross-encoder reranker using sentence-transformers.
Uses a cross-encoder model for accurate relevance scoring.
"""
name: str = "cross_encoder"
def __init__(
self,
model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2",
**kwargs
):
if not _check_sentence_transformers():
raise ImportError(
"sentence-transformers is required for CrossEncoderReranker. "
"Install with: pip install sentence-transformers"
)
from sentence_transformers import CrossEncoder
self.model = CrossEncoder(model_name)
def rerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[Any]:
"""Rerank documents using cross-encoder."""
from praisonaiagents.knowledge.rerankers import RerankResult
# Create query-document pairs
pairs = [[query, doc] for doc in documents]
# Get scores
scores = self.model.predict(pairs)
# Create results
results = []
for i, (doc, score) in enumerate(zip(documents, scores)):
results.append(RerankResult(
text=doc,
score=float(score),
original_index=i
))
# Sort by score
results.sort(key=lambda x: x.score, reverse=True)
if top_k is not None:
results = results[:top_k]
return results
async def arerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[Any]:
"""Async version (just calls sync)."""
return self.rerank(query, documents, top_k, **kwargs)
class CohereReranker:
"""
Cohere reranker using Cohere's rerank API.
"""
name: str = "cohere"
def __init__(
self,
api_key: Optional[str] = None,
model: str = "rerank-english-v3.0",
**kwargs
):
if not _check_cohere():
raise ImportError(
"cohere is required for CohereReranker. "
"Install with: pip install cohere"
)
import os
import cohere
self.api_key = api_key or os.environ.get("COHERE_API_KEY")
if not self.api_key:
raise ValueError("COHERE_API_KEY environment variable or api_key parameter required")
self.model = model
self._client = cohere.Client(self.api_key)
def rerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[Any]:
"""Rerank documents using Cohere API."""
from praisonaiagents.knowledge.rerankers import RerankResult
response = self._client.rerank(
query=query,
documents=documents,
top_n=top_k or len(documents),
model=self.model
)
results = []
for r in response.results:
results.append(RerankResult(
text=documents[r.index],
score=r.relevance_score,
original_index=r.index
))
return results
async def arerank(
self,
query: str,
documents: List[str],
top_k: Optional[int] = None,
**kwargs
) -> List[Any]:
"""Async version (just calls sync)."""
return self.rerank(query, documents, top_k, **kwargs)
def register_default_rerankers():
"""Register all default rerankers with the registry."""
from praisonaiagents.knowledge.rerankers import get_reranker_registry
registry = get_reranker_registry()
# LLM reranker is always available
registry.register("llm", LLMReranker)
# Cross-encoder if available
if _check_sentence_transformers():
registry.register("cross_encoder", CrossEncoderReranker)
# Cohere if available
if _check_cohere():
registry.register("cohere", CohereReranker)
logger.debug("Registered default rerankers")
# Auto-register on import
register_default_rerankers()
@@ -0,0 +1,394 @@
"""
Retriever Implementations for PraisonAI.
Provides concrete implementations of RetrieverProtocol:
- BasicRetriever: Simple vector similarity retrieval
- FusionRetriever: Multi-query with RRF fusion
- RecursiveRetriever: Depth-limited sub-retrieval
- AutoMergeRetriever: Merges adjacent chunks
"""
import logging
from typing import Any, Callable, Dict, List, Optional
logger = logging.getLogger(__name__)
class BasicRetriever:
"""
Basic vector similarity retriever.
Simply queries the vector store and returns results.
"""
name: str = "basic"
def __init__(
self,
vector_store: Any,
embedding_fn: Optional[Callable[[str], List[float]]] = None,
top_k: int = 10,
**kwargs
):
self.vector_store = vector_store
self.embedding_fn = embedding_fn
self.top_k = top_k
def retrieve(
self,
query: str,
top_k: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[Any]:
"""Retrieve documents by vector similarity."""
from praisonaiagents.knowledge.retrieval import RetrievalResult
k = top_k or self.top_k
# Get query embedding
if self.embedding_fn:
query_embedding = self.embedding_fn(query)
else:
raise ValueError("embedding_fn is required for BasicRetriever")
# Query vector store
results = self.vector_store.query(
embedding=query_embedding,
top_k=k,
filter=filter
)
# Convert to RetrievalResult
retrieval_results = []
for r in results:
retrieval_results.append(RetrievalResult(
text=r.text if hasattr(r, 'text') else str(r),
score=r.score if hasattr(r, 'score') else 1.0,
metadata=r.metadata if hasattr(r, 'metadata') else {},
doc_id=r.id if hasattr(r, 'id') else None,
chunk_index=r.metadata.get('chunk_index') if hasattr(r, 'metadata') else None
))
return retrieval_results
async def aretrieve(
self,
query: str,
top_k: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[Any]:
"""Async version (just calls sync)."""
return self.retrieve(query, top_k, filter, **kwargs)
class FusionRetriever:
"""
Fusion retriever using Reciprocal Rank Fusion.
Generates multiple query variations and fuses results.
"""
name: str = "fusion"
def __init__(
self,
vector_store: Any,
embedding_fn: Optional[Callable[[str], List[float]]] = None,
llm: Optional[Any] = None,
num_queries: int = 3,
top_k: int = 10,
rrf_k: int = 60,
**kwargs
):
self.vector_store = vector_store
self.embedding_fn = embedding_fn
self.llm = llm
self.num_queries = num_queries
self.top_k = top_k
self.rrf_k = rrf_k
def retrieve(
self,
query: str,
top_k: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[Any]:
"""Retrieve with query fusion."""
from praisonaiagents.knowledge.retrieval import RetrievalResult, reciprocal_rank_fusion
k = top_k or self.top_k
# Generate query variations
queries = self._generate_query_variations(query)
# Retrieve for each query
all_results = []
for q in queries:
if self.embedding_fn:
q_embedding = self.embedding_fn(q)
results = self.vector_store.query(
embedding=q_embedding,
top_k=k,
filter=filter
)
# Convert to RetrievalResult
query_results = []
for r in results:
query_results.append(RetrievalResult(
text=r.text if hasattr(r, 'text') else str(r),
score=r.score if hasattr(r, 'score') else 1.0,
metadata=r.metadata if hasattr(r, 'metadata') else {},
doc_id=r.id if hasattr(r, 'id') else None
))
all_results.append(query_results)
# Fuse results using RRF
fused = reciprocal_rank_fusion(all_results, k=self.rrf_k)
return fused[:k]
async def aretrieve(
self,
query: str,
top_k: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[Any]:
"""Async version (just calls sync)."""
return self.retrieve(query, top_k, filter, **kwargs)
def _generate_query_variations(self, query: str) -> List[str]:
"""Generate query variations."""
queries = [query]
# Try LLM-based variation if available
if self.llm and len(queries) < self.num_queries:
try:
prompt = f"""Generate {self.num_queries - 1} alternative phrasings of this search query.
Return only the queries, one per line.
Original query: {query}"""
response = self.llm.chat(prompt)
if response:
for line in response.strip().split("\n"):
line = line.strip()
if line and line != query:
queries.append(line)
if len(queries) >= self.num_queries:
break
except Exception as e:
logger.warning(f"LLM query variation failed: {e}")
# Fallback: simple variations
if len(queries) < self.num_queries:
# Add question form
if not query.endswith("?"):
queries.append(f"What is {query}?")
# Add keyword form
keywords = " ".join(query.split()[:5])
if keywords != query:
queries.append(keywords)
return queries[:self.num_queries]
class RecursiveRetriever:
"""
Recursive retriever with depth-limited sub-retrieval.
Retrieves, expands queries based on results, and retrieves again.
"""
name: str = "recursive"
def __init__(
self,
vector_store: Any,
embedding_fn: Optional[Callable[[str], List[float]]] = None,
llm: Optional[Any] = None,
max_depth: int = 2,
top_k: int = 10,
**kwargs
):
self.vector_store = vector_store
self.embedding_fn = embedding_fn
self.llm = llm
self.max_depth = max_depth
self.top_k = top_k
def retrieve(
self,
query: str,
top_k: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[Any]:
"""Retrieve with recursive expansion."""
from praisonaiagents.knowledge.retrieval import RetrievalResult
k = top_k or self.top_k
seen_ids = set()
all_results = []
current_queries = [query]
for depth in range(self.max_depth):
next_queries = []
for q in current_queries:
if self.embedding_fn:
q_embedding = self.embedding_fn(q)
results = self.vector_store.query(
embedding=q_embedding,
top_k=k,
filter=filter
)
for r in results:
r_id = r.id if hasattr(r, 'id') else r.text[:50]
if r_id not in seen_ids:
seen_ids.add(r_id)
all_results.append(RetrievalResult(
text=r.text if hasattr(r, 'text') else str(r),
score=r.score if hasattr(r, 'score') else 1.0,
metadata=r.metadata if hasattr(r, 'metadata') else {},
doc_id=r.id if hasattr(r, 'id') else None
))
# Generate follow-up query from result
if depth < self.max_depth - 1 and self.llm:
follow_up = self._generate_follow_up(query, r.text if hasattr(r, 'text') else str(r))
if follow_up:
next_queries.append(follow_up)
current_queries = next_queries[:3] # Limit expansion
if not current_queries:
break
# Sort by score
all_results.sort(key=lambda x: x.score, reverse=True)
return all_results[:k]
async def aretrieve(
self,
query: str,
top_k: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[Any]:
"""Async version (just calls sync)."""
return self.retrieve(query, top_k, filter, **kwargs)
def _generate_follow_up(self, original_query: str, context: str) -> Optional[str]:
"""Generate a follow-up query based on retrieved context."""
if not self.llm:
return None
try:
prompt = f"""Based on this search result, generate a follow-up query to find more relevant information.
Return only the query, nothing else.
Original query: {original_query}
Search result: {context[:500]}"""
response = self.llm.chat(prompt)
if response:
return response.strip()
except Exception as e:
logger.warning(f"Follow-up query generation failed: {e}")
return None
class AutoMergeRetriever:
"""
Auto-merge retriever that combines adjacent chunks.
Retrieves chunks and merges those from the same document.
"""
name: str = "auto_merge"
def __init__(
self,
vector_store: Any,
embedding_fn: Optional[Callable[[str], List[float]]] = None,
top_k: int = 10,
max_gap: int = 1,
**kwargs
):
self.vector_store = vector_store
self.embedding_fn = embedding_fn
self.top_k = top_k
self.max_gap = max_gap
def retrieve(
self,
query: str,
top_k: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[Any]:
"""Retrieve and merge adjacent chunks."""
from praisonaiagents.knowledge.retrieval import RetrievalResult, merge_adjacent_chunks
k = top_k or self.top_k
# Get more results than needed for merging
fetch_k = k * 3
if self.embedding_fn:
q_embedding = self.embedding_fn(query)
results = self.vector_store.query(
embedding=q_embedding,
top_k=fetch_k,
filter=filter
)
# Convert to RetrievalResult
retrieval_results = []
for r in results:
retrieval_results.append(RetrievalResult(
text=r.text if hasattr(r, 'text') else str(r),
score=r.score if hasattr(r, 'score') else 1.0,
metadata=r.metadata if hasattr(r, 'metadata') else {},
doc_id=r.metadata.get('doc_id') if hasattr(r, 'metadata') else None,
chunk_index=r.metadata.get('chunk_index') if hasattr(r, 'metadata') else None
))
# Merge adjacent chunks
merged = merge_adjacent_chunks(retrieval_results, max_gap=self.max_gap)
return merged[:k]
return []
async def aretrieve(
self,
query: str,
top_k: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
**kwargs
) -> List[Any]:
"""Async version (just calls sync)."""
return self.retrieve(query, top_k, filter, **kwargs)
def register_default_retrievers():
"""Register all default retrievers with the registry."""
from praisonaiagents.knowledge.retrieval import get_retriever_registry
registry = get_retriever_registry()
registry.register("basic", BasicRetriever)
registry.register("fusion", FusionRetriever)
registry.register("recursive", RecursiveRetriever)
registry.register("auto_merge", AutoMergeRetriever)
logger.debug("Registered default retrievers")
# Auto-register on import
register_default_retrievers()
@@ -0,0 +1,409 @@
"""
Vector Store Adapters for PraisonAI.
Provides concrete implementations of VectorStoreProtocol:
- ChromaVectorStore: Uses ChromaDB
- PineconeVectorStore: Uses Pinecone
- QdrantVectorStore: Uses Qdrant
- WeaviateVectorStore: Uses Weaviate
"""
import os
import logging
from typing import Any, Dict, List, Optional
import uuid as uuid_module
logger = logging.getLogger(__name__)
# Lazy import flags
_CHROMA_AVAILABLE = None
_PINECONE_AVAILABLE = None
_QDRANT_AVAILABLE = None
_WEAVIATE_AVAILABLE = None
def _check_chroma():
"""Check if chromadb is available."""
global _CHROMA_AVAILABLE
if _CHROMA_AVAILABLE is None:
import importlib.util
_CHROMA_AVAILABLE = importlib.util.find_spec("chromadb") is not None
return _CHROMA_AVAILABLE
def _check_pinecone():
"""Check if pinecone is available."""
global _PINECONE_AVAILABLE
if _PINECONE_AVAILABLE is None:
import importlib.util
_PINECONE_AVAILABLE = importlib.util.find_spec("pinecone") is not None
return _PINECONE_AVAILABLE
def _check_qdrant():
"""Check if qdrant_client is available."""
global _QDRANT_AVAILABLE
if _QDRANT_AVAILABLE is None:
import importlib.util
_QDRANT_AVAILABLE = importlib.util.find_spec("qdrant_client") is not None
return _QDRANT_AVAILABLE
def _check_weaviate():
"""Check if weaviate is available."""
global _WEAVIATE_AVAILABLE
if _WEAVIATE_AVAILABLE is None:
import importlib.util
_WEAVIATE_AVAILABLE = importlib.util.find_spec("weaviate") is not None
return _WEAVIATE_AVAILABLE
class ChromaVectorStore:
"""ChromaDB vector store adapter."""
name: str = "chroma"
def __init__(
self,
config: Optional[Dict[str, Any]] = None,
namespace: Optional[str] = None,
persist_directory: Optional[str] = None
):
if not _check_chroma():
raise ImportError(
"chromadb is required for ChromaVectorStore. "
"Install with: pip install chromadb"
)
import chromadb
from chromadb.config import Settings
self.config = config or {}
self.namespace = namespace or "default"
self.persist_directory = persist_directory or self.config.get("path", ".praison/chroma")
# Disable telemetry
os.environ['ANONYMIZED_TELEMETRY'] = 'False'
# Create client
self._client = chromadb.PersistentClient(
path=self.persist_directory,
settings=Settings(anonymized_telemetry=False)
)
# Get or create collection
self._collection = self._client.get_or_create_collection(
name=self.namespace,
metadata={"hnsw:space": "cosine"}
)
def add(
self,
texts: List[str],
embeddings: List[List[float]],
metadatas: Optional[List[Dict[str, Any]]] = None,
ids: Optional[List[str]] = None,
namespace: Optional[str] = None
) -> List[str]:
"""Add vectors to ChromaDB."""
ids = ids or [str(uuid_module.uuid4()) 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:
collection = self._client.get_or_create_collection(name=namespace)
else:
collection = self._collection
collection.add(
ids=ids,
embeddings=embeddings,
documents=texts,
metadatas=metadatas
)
return ids
def query(
self,
embedding: List[float],
top_k: int = 10,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None
) -> List[Any]:
"""Query ChromaDB by similarity."""
from praisonaiagents.knowledge.vector_store import VectorRecord
if namespace and namespace != self.namespace:
collection = self._client.get_or_create_collection(name=namespace)
else:
collection = self._collection
results = collection.query(
query_embeddings=[embedding],
n_results=top_k,
where=filter
)
records = []
if results and results['ids'] and results['ids'][0]:
for i, id_ in enumerate(results['ids'][0]):
records.append(VectorRecord(
id=id_,
text=results['documents'][0][i] if results.get('documents') else "",
embedding=embedding, # We don't get embeddings back from query
metadata=results['metadatas'][0][i] if results.get('metadatas') else {},
score=1 - results['distances'][0][i] if results.get('distances') else 1.0
))
return records
def delete(
self,
ids: Optional[List[str]] = None,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None,
delete_all: bool = False
) -> int:
"""Delete vectors from ChromaDB."""
if namespace and namespace != self.namespace:
collection = self._client.get_or_create_collection(name=namespace)
else:
collection = self._collection
if delete_all:
count = collection.count()
# Delete collection and recreate
self._client.delete_collection(collection.name)
self._collection = self._client.get_or_create_collection(
name=self.namespace,
metadata={"hnsw:space": "cosine"}
)
return count
if ids:
collection.delete(ids=ids)
return len(ids)
if filter:
# Get matching IDs first
results = collection.get(where=filter)
if results and results['ids']:
collection.delete(ids=results['ids'])
return len(results['ids'])
return 0
def count(self, namespace: Optional[str] = None) -> int:
"""Get count of vectors."""
if namespace and namespace != self.namespace:
collection = self._client.get_or_create_collection(name=namespace)
else:
collection = self._collection
return collection.count()
def get(
self,
ids: List[str],
namespace: Optional[str] = None
) -> List[Any]:
"""Get vectors by ID."""
from praisonaiagents.knowledge.vector_store import VectorRecord
if namespace and namespace != self.namespace:
collection = self._client.get_or_create_collection(name=namespace)
else:
collection = self._collection
results = collection.get(ids=ids, include=["documents", "metadatas", "embeddings"])
records = []
if results and results['ids']:
for i, id_ in enumerate(results['ids']):
records.append(VectorRecord(
id=id_,
text=results['documents'][i] if results.get('documents') else "",
embedding=results['embeddings'][i] if results.get('embeddings') else [],
metadata=results['metadatas'][i] if results.get('metadatas') else {}
))
return records
class PineconeVectorStore:
"""Pinecone vector store adapter."""
name: str = "pinecone"
def __init__(
self,
config: Optional[Dict[str, Any]] = None,
namespace: Optional[str] = None,
api_key: Optional[str] = None,
index_name: Optional[str] = None
):
if not _check_pinecone():
raise ImportError(
"pinecone is required for PineconeVectorStore. "
"Install with: pip install pinecone"
)
from pinecone import Pinecone
self.config = config or {}
self.namespace = namespace or "default"
self.api_key = api_key or os.environ.get("PINECONE_API_KEY")
self.index_name = index_name or self.config.get("index_name", "praisonai")
if not self.api_key:
raise ValueError("PINECONE_API_KEY environment variable or api_key parameter required")
self._pc = Pinecone(api_key=self.api_key)
self._index = self._pc.Index(self.index_name)
def add(
self,
texts: List[str],
embeddings: List[List[float]],
metadatas: Optional[List[Dict[str, Any]]] = None,
ids: Optional[List[str]] = None,
namespace: Optional[str] = None
) -> List[str]:
"""Add vectors to Pinecone."""
ids = ids or [str(uuid_module.uuid4()) for _ in texts]
metadatas = metadatas or [{} for _ in texts]
ns = namespace or self.namespace
# Add text to metadata for retrieval
vectors = []
for i, (id_, embedding, metadata) in enumerate(zip(ids, embeddings, metadatas)):
meta = metadata.copy()
meta["text"] = texts[i]
vectors.append({
"id": id_,
"values": embedding,
"metadata": meta
})
self._index.upsert(vectors=vectors, namespace=ns)
return ids
def query(
self,
embedding: List[float],
top_k: int = 10,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None
) -> List[Any]:
"""Query Pinecone by similarity."""
from praisonaiagents.knowledge.vector_store import VectorRecord
ns = namespace or self.namespace
results = self._index.query(
vector=embedding,
top_k=top_k,
namespace=ns,
filter=filter,
include_metadata=True
)
records = []
for match in results.get("matches", []):
metadata = match.get("metadata", {})
text = metadata.pop("text", "")
records.append(VectorRecord(
id=match["id"],
text=text,
embedding=embedding,
metadata=metadata,
score=match.get("score", 0.0)
))
return records
def delete(
self,
ids: Optional[List[str]] = None,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None,
delete_all: bool = False
) -> int:
"""Delete vectors from Pinecone."""
ns = namespace or self.namespace
if delete_all:
self._index.delete(delete_all=True, namespace=ns)
return -1 # Pinecone doesn't return count
if ids:
self._index.delete(ids=ids, namespace=ns)
return len(ids)
if filter:
self._index.delete(filter=filter, namespace=ns)
return -1
return 0
def count(self, namespace: Optional[str] = None) -> int:
"""Get count of vectors."""
ns = namespace or self.namespace
stats = self._index.describe_index_stats()
ns_stats = stats.get("namespaces", {}).get(ns, {})
return ns_stats.get("vector_count", 0)
def get(
self,
ids: List[str],
namespace: Optional[str] = None
) -> List[Any]:
"""Get vectors by ID."""
from praisonaiagents.knowledge.vector_store import VectorRecord
ns = namespace or self.namespace
results = self._index.fetch(ids=ids, namespace=ns)
records = []
for id_, data in results.get("vectors", {}).items():
metadata = data.get("metadata", {})
text = metadata.pop("text", "")
records.append(VectorRecord(
id=id_,
text=text,
embedding=data.get("values", []),
metadata=metadata
))
return records
def register_default_vector_stores():
"""Register all default vector stores with the registry."""
from praisonaiagents.knowledge.vector_store import get_vector_store_registry
registry = get_vector_store_registry()
# Register ChromaDB
if _check_chroma():
registry.register("chroma", ChromaVectorStore)
# Register Pinecone
if _check_pinecone():
registry.register("pinecone", PineconeVectorStore)
logger.debug("Registered default vector stores")
# Auto-register on import
register_default_vector_stores()
+106
View File
@@ -0,0 +1,106 @@
"""
PraisonAI Chat Module
This module provides the chat UI integration for PraisonAI agents.
It uses the PraisonAI Chat (based on Chainlit) for the frontend.
"""
from typing import Optional, Any
__all__ = ["start_chat_server", "ChatConfig"]
class ChatConfig:
"""Configuration for the PraisonAI Chat server."""
def __init__(
self,
host: str = "0.0.0.0",
port: int = 8000,
debug: bool = False,
auth_enabled: bool = False,
session_id: Optional[str] = None,
):
self.host = host
self.port = port
self.debug = debug
self.auth_enabled = auth_enabled
self.session_id = session_id
def start_chat_server(
agent: Optional[Any] = None,
agents: Optional[list] = None,
config: Optional[ChatConfig] = None,
port: int = 8000,
host: str = "0.0.0.0",
debug: bool = False,
) -> None:
"""
Start the PraisonAI Chat server.
Args:
agent: A single PraisonAI agent to use in the chat.
agents: A list of PraisonAI agents for multi-agent chat.
config: ChatConfig object with server settings.
port: Port to run the server on (default: 8000).
host: Host to bind to (default: 0.0.0.0).
debug: Enable debug mode (default: False).
Example:
>>> from praisonaiagents import Agent
>>> from praisonai.chat import start_chat_server
>>>
>>> agent = Agent(name="Assistant", instructions="You are helpful.")
>>> start_chat_server(agent=agent, port=8000)
"""
# Lazy import to avoid loading chainlit unless needed
try:
from chainlit.cli import run_chainlit
except ImportError:
raise ImportError(
"PraisonAI Chat requires the 'praisonai-chat' package. "
"Install it with: pip install praisonai-chat"
)
if config is None:
config = ChatConfig(host=host, port=port, debug=debug)
# Store agents in a way accessible to the chainlit app
import os
os.environ["PRAISONAI_CHAT_MODE"] = "true"
if agent is not None:
# Single agent mode
_register_agent(agent)
elif agents is not None:
# Multi-agent mode
for a in agents:
_register_agent(a)
# Start the chainlit server
from pathlib import Path
# Use the built-in app file
app_file = Path(__file__).parent / "app.py"
# Set environment variables for chainlit
os.environ["CHAINLIT_HOST"] = config.host
os.environ["CHAINLIT_PORT"] = str(config.port)
run_chainlit(str(app_file))
# Global registry for agents
_REGISTERED_AGENTS: dict = {}
def _register_agent(agent: Any) -> None:
"""Register an agent for use in the chat UI."""
agent_name = getattr(agent, "name", None) or getattr(agent, "role", "Agent")
_REGISTERED_AGENTS[agent_name] = agent
def get_registered_agents() -> dict:
"""Get all registered agents."""
return _REGISTERED_AGENTS
+125
View File
@@ -0,0 +1,125 @@
"""
PraisonAI Chat - Default Chainlit Application
This is the default chat application that runs when using `praisonai chat`.
It integrates with PraisonAI agents for multi-agent conversations.
"""
import os
import chainlit as cl
# Check if we're in PraisonAI Chat mode with registered agents
PRAISONAI_CHAT_MODE = os.environ.get("PRAISONAI_CHAT_MODE", "false").lower() == "true"
def get_agents():
"""Get registered agents from the chat module."""
try:
from praisonai.chat import get_registered_agents
return get_registered_agents()
except ImportError:
return {}
@cl.on_chat_start
async def on_chat_start():
"""Initialize the chat session."""
agents = get_agents()
if agents:
agent_names = list(agents.keys())
cl.user_session.set("agents", agents)
cl.user_session.set("current_agent", agent_names[0] if agent_names else None)
await cl.Message(
content=f"Welcome to PraisonAI Chat! Available agents: {', '.join(agent_names)}"
).send()
else:
await cl.Message(
content="Welcome to PraisonAI Chat! No agents configured. "
"Use the API to register agents or configure via YAML."
).send()
cl.user_session.set("message_history", [])
@cl.on_message
async def on_message(message: cl.Message):
"""Handle incoming messages."""
agents = cl.user_session.get("agents", {})
current_agent_name = cl.user_session.get("current_agent")
message_history = cl.user_session.get("message_history", [])
# Add user message to history
message_history.append({"role": "user", "content": message.content})
if not agents or not current_agent_name:
# No agents configured - use a simple echo response
response = f"Echo: {message.content}\n\n(No agents configured. Register agents to enable AI responses.)"
await cl.Message(content=response).send()
return
agent = agents.get(current_agent_name)
if agent is None:
await cl.Message(content=f"Agent '{current_agent_name}' not found.").send()
return
# Create a step for the agent response
async with cl.Step(name=current_agent_name, type="llm") as step:
step.input = message.content
try:
# Try to call the agent
if hasattr(agent, "chat"):
# PraisonAI Agent with chat method
response = await _call_agent_async(agent, message.content)
elif hasattr(agent, "run"):
# Agent with run method
response = await _call_agent_async(agent, message.content, method="run")
elif callable(agent):
# Callable agent
response = agent(message.content)
else:
response = f"Agent '{current_agent_name}' does not have a callable interface."
step.output = str(response)
except Exception as e:
response = f"Error calling agent: {str(e)}"
step.output = response
# Add assistant response to history
message_history.append({"role": "assistant", "content": str(response)})
cl.user_session.set("message_history", message_history)
# Send the response
await cl.Message(content=str(response)).send()
async def _call_agent_async(agent, message: str, method: str = "chat"):
"""Call an agent method, handling both sync and async."""
import asyncio
func = getattr(agent, method)
if asyncio.iscoroutinefunction(func):
return await func(message)
else:
# Run sync function in thread pool
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, func, message)
@cl.on_chat_resume
async def on_chat_resume(thread):
"""Resume a chat session."""
message_history = []
root_messages = [m for m in thread.get("steps", []) if m.get("parentId") is None]
for msg in root_messages:
if msg.get("type") == "user_message":
message_history.append({"role": "user", "content": msg.get("output", "")})
elif msg.get("type") == "assistant_message":
message_history.append({"role": "assistant", "content": msg.get("output", "")})
cl.user_session.set("message_history", message_history)
@@ -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")
@@ -0,0 +1,595 @@
"""
Jobs CLI Feature for PraisonAI.
Provides CLI commands for managing async jobs.
Commands:
- praisonai run submit <prompt> # Submit a new job
- praisonai run status <job_id> # Get job status
- praisonai run result <job_id> # Get job result
- praisonai run cancel <job_id> # Cancel a job
- praisonai run list # List jobs
"""
import sys
import json
import time
from typing import Optional, List
class JobsHandler:
"""
Handler for jobs CLI commands.
Provides functionality to:
- Submit jobs to the API
- Check job status
- Get job results
- Cancel jobs
- List jobs
"""
def __init__(
self,
api_url: str = "http://127.0.0.1:8005",
verbose: bool = False
):
self.api_url = api_url.rstrip("/")
self.verbose = verbose
@property
def feature_name(self) -> str:
return "jobs"
def _get_client(self):
"""Get HTTP client."""
try:
import httpx
return httpx.Client(timeout=30.0)
except ImportError:
raise RuntimeError("httpx is required. Install with: pip install httpx")
def submit(
self,
prompt: str,
agent_file: Optional[str] = None,
framework: str = "praisonai",
timeout: int = 3600,
wait: bool = False,
poll_interval: int = 5,
idempotency_key: Optional[str] = None,
webhook_url: Optional[str] = None,
session_id: Optional[str] = None,
output_json: bool = False
) -> dict:
"""
Submit a new job.
Args:
prompt: The prompt/task for the agent
agent_file: Optional path to agents.yaml
framework: Framework to use
timeout: Job timeout in seconds
wait: If True, wait for completion
poll_interval: Seconds between status polls
Returns:
Job submission response
"""
client = self._get_client()
payload = {
"prompt": prompt,
"framework": framework,
"timeout": timeout
}
if agent_file:
payload["agent_file"] = agent_file
if webhook_url:
payload["webhook_url"] = webhook_url
if session_id:
payload["session_id"] = session_id
if idempotency_key:
payload["idempotency_key"] = idempotency_key
headers = {}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
try:
response = client.post(
f"{self.api_url}/api/v1/runs",
json=payload,
headers=headers if headers else None
)
response.raise_for_status()
result = response.json()
if output_json:
print(json.dumps(result, indent=2))
else:
self._print_success(f"Job submitted: {result['job_id']}")
self._print_info(f"Status URL: {result['poll_url']}")
if wait:
return self._wait_for_completion(result['job_id'], poll_interval, output_json)
return result
except Exception as e:
self._print_error(f"Failed to submit job: {e}")
raise
def status(self, job_id: str) -> dict:
"""
Get job status.
Args:
job_id: Job ID
Returns:
Job status response
"""
client = self._get_client()
try:
response = client.get(f"{self.api_url}/api/v1/runs/{job_id}")
response.raise_for_status()
result = response.json()
self._print_job_status(result)
return result
except Exception as e:
self._print_error(f"Failed to get status: {e}")
raise
def result(self, job_id: str) -> dict:
"""
Get job result.
Args:
job_id: Job ID
Returns:
Job result response
"""
client = self._get_client()
try:
response = client.get(f"{self.api_url}/api/v1/runs/{job_id}/result")
response.raise_for_status()
result = response.json()
self._print_job_result(result)
return result
except Exception as e:
self._print_error(f"Failed to get result: {e}")
raise
def cancel(self, job_id: str) -> dict:
"""
Cancel a job.
Args:
job_id: Job ID
Returns:
Updated job status
"""
client = self._get_client()
try:
response = client.post(f"{self.api_url}/api/v1/runs/{job_id}/cancel")
response.raise_for_status()
result = response.json()
self._print_success(f"Job cancelled: {job_id}")
return result
except Exception as e:
self._print_error(f"Failed to cancel job: {e}")
raise
def list_jobs(
self,
status: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> dict:
"""
List jobs.
Args:
status: Optional status filter
page: Page number
page_size: Jobs per page
Returns:
Job list response
"""
client = self._get_client()
params = {"page": page, "page_size": page_size}
if status:
params["status"] = status
try:
response = client.get(
f"{self.api_url}/api/v1/runs",
params=params
)
response.raise_for_status()
result = response.json()
self._print_job_list(result)
return result
except Exception as e:
self._print_error(f"Failed to list jobs: {e}")
raise
def _wait_for_completion(self, job_id: str, poll_interval: int = 5, output_json: bool = False) -> dict:
"""Wait for a job to complete."""
client = self._get_client()
if not output_json:
self._print_info("Waiting for job completion...")
while True:
try:
response = client.get(f"{self.api_url}/api/v1/runs/{job_id}")
response.raise_for_status()
result = response.json()
status = result.get("status")
progress = result.get("progress", {}).get("percentage", 0)
if not output_json:
self._print_progress(status, progress)
if status in ("succeeded", "failed", "cancelled"):
if output_json:
# Get full result for succeeded jobs
if status == "succeeded":
res = client.get(f"{self.api_url}/api/v1/runs/{job_id}/result")
res.raise_for_status()
print(json.dumps(res.json(), indent=2))
else:
print(json.dumps(result, indent=2))
return result
else:
if status == "succeeded":
return self.result(job_id)
else:
return result
# Honor Retry-After header if present
retry_after = result.get("retry_after") or poll_interval
time.sleep(retry_after)
except KeyboardInterrupt:
if not output_json:
self._print_info("\nInterrupted. Job continues running in background.")
return result
except Exception as e:
if not output_json:
self._print_error(f"Error polling status: {e}")
time.sleep(poll_interval)
def stream(self, job_id: str, output_json: bool = False) -> None:
"""
Stream job progress via SSE.
Args:
job_id: Job ID
output_json: If True, output raw JSON events
"""
try:
import httpx
with httpx.stream(
"GET",
f"{self.api_url}/api/v1/runs/{job_id}/stream",
timeout=None
) as response:
response.raise_for_status()
for line in response.iter_lines():
if not line:
continue
if line.startswith("data:"):
data = line[5:].strip()
if data == "[DONE]":
if not output_json:
self._print_success("Stream completed")
break
try:
event = json.loads(data)
if output_json:
print(json.dumps(event))
else:
event_type = event.get("event", "unknown")
if event_type == "progress":
pct = event.get("data", {}).get("percentage", 0)
step = event.get("data", {}).get("current_step", "")
self._print_progress(f"{step}", pct)
elif event_type == "complete":
status = event.get("data", {}).get("status", "")
if status == "succeeded":
self._print_success(f"Job completed: {status}")
else:
self._print_error(f"Job ended: {status}")
elif event_type == "error":
self._print_error(event.get("data", {}).get("error", "Unknown error"))
except json.JSONDecodeError:
if not output_json:
print(data)
elif line.startswith(":"):
# Heartbeat comment, ignore
pass
except Exception as e:
self._print_error(f"Stream error: {e}")
raise
def _print_job_status(self, job: dict):
"""Print job status."""
try:
from rich.console import Console
from rich.panel import Panel
console = Console()
status = job.get("status", "unknown")
status_color = {
"queued": "dim",
"running": "yellow",
"succeeded": "green",
"failed": "red",
"cancelled": "dim"
}.get(status, "white")
progress = job.get("progress", {})
content = f"""
[bold]Job ID:[/bold] {job.get('job_id')}
[bold]Status:[/bold] [{status_color}]{status}[/{status_color}]
[bold]Progress:[/bold] {progress.get('percentage', 0):.0f}%
[bold]Step:[/bold] {progress.get('current_step') or '-'}
[bold]Created:[/bold] {job.get('created_at')}
[bold]Started:[/bold] {job.get('started_at') or '-'}
"""
if job.get("error"):
content += f"[bold]Error:[/bold] [red]{job.get('error')}[/red]\n"
console.print(Panel(content.strip(), title="Job Status"))
except ImportError:
print(f"Job: {job.get('job_id')}")
print(f" Status: {job.get('status')}")
print(f" Progress: {job.get('progress', {}).get('percentage', 0):.0f}%")
def _print_job_result(self, job: dict):
"""Print job result."""
try:
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
console = Console()
status = job.get("status")
if status == "succeeded":
result = job.get("result", "")
if isinstance(result, str):
console.print(Panel(Markdown(result), title="Result"))
else:
console.print(Panel(json.dumps(result, indent=2), title="Result"))
else:
console.print(f"[red]Job {status}: {job.get('error')}[/red]")
except ImportError:
print(f"Result: {job.get('result')}")
def _print_job_list(self, response: dict):
"""Print job list."""
try:
from rich.console import Console
from rich.table import Table
console = Console()
jobs = response.get("jobs", [])
if not jobs:
console.print("[yellow]No jobs found[/yellow]")
return
table = Table(title=f"Jobs (Page {response.get('page')}/{(response.get('total', 0) + response.get('page_size', 20) - 1) // response.get('page_size', 20)})")
table.add_column("ID", style="cyan")
table.add_column("Status", style="yellow")
table.add_column("Progress", style="blue")
table.add_column("Created", style="dim")
for job in jobs:
status = job.get("status", "unknown")
status_color = {
"queued": "dim",
"running": "yellow",
"succeeded": "green",
"failed": "red",
"cancelled": "dim"
}.get(status, "white")
progress = job.get("progress", {}).get("percentage", 0)
table.add_row(
job.get("job_id", "?"),
f"[{status_color}]{status}[/{status_color}]",
f"{progress:.0f}%",
job.get("created_at", "")[:19]
)
console.print(table)
console.print(f"Total: {response.get('total', 0)} jobs")
except ImportError:
print("Jobs:")
for job in response.get("jobs", []):
print(f" {job.get('job_id')} - {job.get('status')}")
def _print_progress(self, status: str, percentage: float):
"""Print progress update."""
try:
from rich.console import Console
console = Console()
console.print(f" Status: {status} | Progress: {percentage:.0f}%", end="\r")
except ImportError:
print(f" Status: {status} | Progress: {percentage:.0f}%", end="\r")
def _print_success(self, message: str):
"""Print success message."""
try:
from rich.console import Console
Console().print(f"[green]✓[/green] {message}")
except ImportError:
print(f"✓ {message}")
def _print_error(self, message: str):
"""Print error message."""
try:
from rich.console import Console
Console().print(f"[red]✗[/red] {message}")
except ImportError:
print(f"✗ {message}")
def _print_info(self, message: str):
"""Print info message."""
try:
from rich.console import Console
Console().print(f"[blue]ℹ[/blue] {message}")
except ImportError:
print(f"ℹ {message}")
def handle_run_command(args: List[str], verbose: bool = False):
"""
Handle run CLI commands.
Usage:
praisonai run submit "Your prompt here" [--wait] [--agent-file agents.yaml]
praisonai run status <job_id>
praisonai run result <job_id>
praisonai run cancel <job_id>
praisonai run list [--status <status>]
"""
import argparse
parser = argparse.ArgumentParser(prog="praisonai run", description="Manage async jobs")
subparsers = parser.add_subparsers(dest="subcommand", help="Available commands")
# Submit command
submit_parser = subparsers.add_parser("submit", help="Submit a new job")
submit_parser.add_argument("prompt", help="The prompt/task for the agent")
submit_parser.add_argument("--agent-file", help="Path to agents.yaml")
submit_parser.add_argument("--framework", default="praisonai", help="Framework to use")
submit_parser.add_argument("--timeout", type=int, default=3600, help="Timeout in seconds")
submit_parser.add_argument("--wait", action="store_true", help="Wait for completion")
submit_parser.add_argument("--idempotency-key", help="Idempotency key to prevent duplicates")
submit_parser.add_argument("--webhook-url", help="Webhook URL for completion callback")
submit_parser.add_argument("--session-id", help="Session ID for grouping jobs")
submit_parser.add_argument("--json", dest="output_json", action="store_true", help="Output JSON for scripting")
submit_parser.add_argument("--api-url", default="http://127.0.0.1:8005", help="API server URL")
# Status command
status_parser = subparsers.add_parser("status", help="Get job status")
status_parser.add_argument("job_id", help="Job ID")
status_parser.add_argument("--json", dest="output_json", action="store_true", help="Output JSON")
status_parser.add_argument("--api-url", default="http://127.0.0.1:8005", help="API server URL")
# Result command
result_parser = subparsers.add_parser("result", help="Get job result")
result_parser.add_argument("job_id", help="Job ID")
result_parser.add_argument("--json", dest="output_json", action="store_true", help="Output JSON")
result_parser.add_argument("--api-url", default="http://127.0.0.1:8005", help="API server URL")
# Cancel command
cancel_parser = subparsers.add_parser("cancel", help="Cancel a job")
cancel_parser.add_argument("job_id", help="Job ID")
cancel_parser.add_argument("--json", dest="output_json", action="store_true", help="Output JSON")
cancel_parser.add_argument("--api-url", default="http://127.0.0.1:8005", help="API server URL")
# List command
list_parser = subparsers.add_parser("list", help="List jobs")
list_parser.add_argument("--status", help="Filter by status")
list_parser.add_argument("--page", type=int, default=1, help="Page number")
list_parser.add_argument("--page-size", type=int, default=20, help="Jobs per page")
list_parser.add_argument("--json", dest="output_json", action="store_true", help="Output JSON")
list_parser.add_argument("--api-url", default="http://127.0.0.1:8005", help="API server URL")
# Stream command
stream_parser = subparsers.add_parser("stream", help="Stream job progress via SSE")
stream_parser.add_argument("job_id", help="Job ID")
stream_parser.add_argument("--json", dest="output_json", action="store_true", help="Output raw JSON events")
stream_parser.add_argument("--api-url", default="http://127.0.0.1:8005", help="API server URL")
if not args:
parser.print_help()
return
parsed = parser.parse_args(args)
if not parsed.subcommand:
parser.print_help()
return
handler = JobsHandler(
api_url=getattr(parsed, "api_url", "http://127.0.0.1:8005"),
verbose=verbose
)
try:
if parsed.subcommand == "submit":
handler.submit(
prompt=parsed.prompt,
agent_file=parsed.agent_file,
framework=parsed.framework,
timeout=parsed.timeout,
wait=parsed.wait,
idempotency_key=getattr(parsed, "idempotency_key", None),
webhook_url=getattr(parsed, "webhook_url", None),
session_id=getattr(parsed, "session_id", None),
output_json=getattr(parsed, "output_json", False)
)
elif parsed.subcommand == "status":
result = handler.status(parsed.job_id)
if getattr(parsed, "output_json", False):
print(json.dumps(result, indent=2))
elif parsed.subcommand == "result":
result = handler.result(parsed.job_id)
if getattr(parsed, "output_json", False):
print(json.dumps(result, indent=2))
elif parsed.subcommand == "cancel":
result = handler.cancel(parsed.job_id)
if getattr(parsed, "output_json", False):
print(json.dumps(result, indent=2))
elif parsed.subcommand == "list":
result = handler.list_jobs(
status=parsed.status,
page=parsed.page,
page_size=parsed.page_size
)
if getattr(parsed, "output_json", False):
print(json.dumps(result, indent=2))
elif parsed.subcommand == "stream":
handler.stream(
job_id=parsed.job_id,
output_json=getattr(parsed, "output_json", False)
)
except Exception:
if verbose:
import traceback
traceback.print_exc()
sys.exit(1)
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more