mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-12 18:47:44 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08b21dabe3 | ||
|
|
8c70d840df | ||
|
|
d8e944c1d3 | ||
|
|
79b2b88f72 | ||
|
|
f5dfbd5670 | ||
|
|
aa6aa5933d | ||
|
|
ff3a4f2016 | ||
|
|
0d5444fc23 | ||
|
|
d7d5dab17b | ||
|
|
05678394e6 | ||
|
|
54cddc09b4 |
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -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]" \
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -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)
|
||||
"""
|
||||
@@ -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 "============================================================"
|
||||
@@ -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()
|
||||
@@ -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 "============================================================"
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+1
-1
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: [
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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)}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
Reference in new issue
Block a user