Compare commits

...
10 Commits
Author SHA1 Message Date
Mervin Praison f0f0f653fe Release v2.5.7 2025-12-26 13:31:45 +00:00
Mervin Praison 98a4ff8450 Release v2.5.6 2025-12-26 13:11:09 +00:00
Mervin Praison d6333aaf33 Release v1.2.1 - refactor TypeScript tools to use generic BaseTool
- Bump praisonai-ts version from 1.2.0 to 1.2.1
- Refactor BaseTool to use generics for type-safe input/output parameters
- Update ArxivSearchTool and ArxivDownloadTool to extend generic BaseTool with typed params
- Update MCPTool to extend generic BaseTool
- Change tool method from execute() to run() for consistency
- Export ToolResult, ToolValidationError, validateTool, createTool from base module
- Remove Tool interface export
2025-12-26 11:47:17 +00:00
Mervin Praison 8005094267 update 2025-12-26 08:39:22 +00:00
Mervin Praison 08b21dabe3 Release v2.5.5 2025-12-26 08:16:54 +00:00
Mervin Praison 8c70d840df docs: add knowledge export/import CLI commands to README
- Add knowledge export/import row to features table with CLI link
- Document `praisonai knowledge export <file.json>` command
- Document `praisonai knowledge import <file.json>` command
2025-12-26 06:53:20 +00:00
Mervin Praison d8e944c1d3 Release v2.5.4 2025-12-26 06:49:33 +00:00
Mervin Praison 79b2b88f72 Release v2.5.3 2025-12-26 05:54:09 +00:00
Mervin Praison f5dfbd5670 fix: resolve Core Tests failures in knowledge/reranker integration
- Fix #1: MemoryConfig Pydantic validation - add _prepare_mem0_config() to strip
  PraisonAI-specific reranker fields (enabled, default_rerank) before passing to
  mem0, which only accepts provider and config fields

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

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

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

No files matched your search

+493 -162
View File
@@ -35,46 +35,108 @@ PraisonAI is a production-ready Multi-AI Agents framework with self-reflection,
---
> **Quick Paths:**
> - 🚀 **New here?** → [Quick Start](#-quick-start)
> - 📦 **Installing?** → [Installation](#-installation)
> - 💻 **Python SDK?** → [Python Examples](#-using-python-code)
> - 🎯 **CLI user?** → [CLI Reference](#-cli--no-code-interface)
> - 🔧 **Need config?** → [Configuration](#-configuration--integration)
> - 🤝 **Contributing?** → [Development](#-development)
---
## 📑 Table of Contents
### Getting Started
- [⚡ Performance](#-performance)
- [🚀 Quick Start](#-quick-start)
- [📦 Installation](#-installation)
- [Python SDK](#python-sdk)
- [JavaScript SDK](#javascript-sdk)
- [💻 Usage](#-usage)
- [Python Code Examples](#python-code-examples)
- [CLI / No-Code Interface](#cli--no-code-interface)
- [JavaScript Code Examples](#javascript-code-examples)
- [Environment Variables](#environment-variables)
### Core Concepts
- [✨ Key Features](#-key-features)
- [🌐 Supported Providers](#-supported-providers)
### Python SDK
- [📘 Python Examples](#-using-python-code)
- [1. Single Agent](#1-single-agent)
- [2. Multi Agents](#2-multi-agents)
- [3. Planning Mode](#3-agent-with-planning-mode)
- [4. Deep Research](#4-deep-research-agent)
- [5. Query Rewriter](#5-query-rewriter-agent)
- [6. Agent Memory](#6-agent-memory-zero-dependencies)
- [7. Rules & Instructions](#7-rules--instructions)
- [8. Auto-Generated Memories](#8-auto-generated-memories)
- [9. Agentic Workflows](#9-agentic-workflows)
- [10. Hooks](#10-hooks)
- [11. Shadow Git Checkpoints](#11-shadow-git-checkpoints)
- [12. Background Tasks](#12-background-tasks)
- [13. Policy Engine](#13-policy-engine)
- [14. Thinking Budgets](#14-thinking-budgets)
- [15. Output Styles](#15-output-styles)
- [16. Context Compaction](#16-context-compaction)
- [17. Field Names Reference](#17-field-names-reference-a-i-g-s)
- [18. Extended agents.yaml](#18-extended-agentsyaml-with-workflow-patterns)
- [19. MCP Protocol](#19-mcp-model-context-protocol)
- [20. A2A Protocol](#20-a2a-agent2agent-protocol)
- [🛠️ Custom Tools](#️-custom-tools)
### JavaScript SDK
- [💻 JavaScript Examples](#-using-javascript-code)
### CLI Reference
- [🎯 CLI Overview](#-cli--no-code-interface)
- [Auto Mode](#auto-mode)
- [Interactive Mode](#interactive-mode-cli)
- [Deep Research CLI](#deep-research-cli)
- [Planning Mode CLI](#planning-mode-cli)
- [Memory CLI](#memory-cli)
- [Workflow CLI](#workflow-cli)
- [Knowledge CLI](#knowledge-cli)
- [Session CLI](#session-cli)
- [Tools CLI](#tools-cli)
- [MCP Config CLI](#mcp-config-cli)
- [External Agents CLI](#external-agents-cli)
- [CLI Features Summary](#cli-features)
### Configuration
- [🔧 Configuration & Integration](#-configuration--integration)
- [Ollama Integration](#ollama-integration)
- [Groq Integration](#groq-integration)
- [100+ Models Support](#100-models-support)
- [📋 Workflows](#-workflows)
- [Workflow Patterns](#workflow-patterns)
- [YAML Configuration](#yaml-configuration)
- [Agents Playbook](#agents-playbook)
- [🧠 Memory & Context](#-memory--context)
- [📋 Agents Playbook](#-agents-playbook)
### Advanced Features
- [🔬 Advanced Features](#-advanced-features)
- [Research & Intelligence](#research--intelligence)
- [Memory & Caching](#memory--caching)
- [Planning & Workflows](#planning--workflows)
- [Specialised Agents](#specialised-agents)
- [MCP Protocol](#mcp-protocol)
- [A2A & A2UI Protocols](#a2a--a2ui-protocols)
- [Safety & Control](#safety--control)
- [Developer Tools](#developer-tools)
### Architecture
- [📊 Process Types & Patterns](#-process-types--patterns)
- [Sequential Process](#sequential-process)
- [Hierarchical Process](#hierarchical-process)
- [Workflow Process](#workflow-process)
- [Agentic Patterns](#agentic-patterns)
### Data & Persistence
- [💾 Persistence (Databases)](#-persistence-databases)
- [📚 Knowledge & Retrieval (RAG)](#-knowledge--retrieval-rag)
- [🔧 Tools Table](#-tools-table)
### Learning & Community
- [🎓 Video Tutorials](#-video-tutorials)
- [⭐ Star History](#-star-history)
- [👥 Contributing](#-contributing)
- [🔧 Development](#-development)
- [❓ FAQ & Troubleshooting](#-faq--troubleshooting)
---
@@ -143,6 +205,30 @@ pip install praisonai
npm install praisonai
```
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `OPENAI_API_KEY` | Yes* | OpenAI API key |
| `ANTHROPIC_API_KEY` | No | Anthropic Claude API key |
| `GOOGLE_API_KEY` | No | Google Gemini API key |
| `GROQ_API_KEY` | No | Groq API key |
| `OPENAI_BASE_URL` | No | Custom API endpoint (for Ollama, Groq, etc.) |
> *At least one LLM provider API key is required.
```bash
# Set your API key
export OPENAI_API_KEY=your_key_here
# For Ollama (local models)
export OPENAI_BASE_URL=http://localhost:11434/v1
# For Groq
export OPENAI_API_KEY=your_groq_key
export OPENAI_BASE_URL=https://api.groq.com/openai/v1
```
---
## 💻 Usage
@@ -169,147 +255,263 @@ npm install praisonai
## ✨ Key Features
<details open>
<summary><strong>🤖 Core Agents</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| **🤖 Core Agents** | | |
| ↳ Single Agent | [Example](examples/python/agents/single-agent.py) | [📖](https://docs.praison.ai/agents/single) |
| ↳ Multi Agents | [Example](examples/python/general/mini_agents_example.py) | [📖](https://docs.praison.ai/concepts/agents) |
| ↳ Auto Agents | [Example](examples/python/general/auto_agents_example.py) | [📖](https://docs.praison.ai/features/autoagents) |
| ↳ Self Reflection AI Agents | [Example](examples/python/concepts/self-reflection-details.py) | [📖](https://docs.praison.ai/features/selfreflection) |
| ↳ Reasoning AI Agents | [Example](examples/python/concepts/reasoning-extraction.py) | [📖](https://docs.praison.ai/features/reasoning) |
| ↳ Multi Modal AI Agents | [Example](examples/python/general/multimodal.py) | [📖](https://docs.praison.ai/features/multimodal) |
| **🔄 Workflows** | | |
| ↳ Simple Workflow | [Example](examples/python/workflows/simple_workflow.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow with Agents | [Example](examples/python/workflows/workflow_with_agents.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Agentic Routing (`route()`) | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/routing) |
| ↳ Parallel Execution (`parallel()`) | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| ↳ Loop over List/CSV (`loop()`) | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/repetitive) |
| ↳ Evaluator-Optimizer (`repeat()`) | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| ↳ Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Branching | [Example](examples/python/workflows/workflow_branching.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Early Stop | [Example](examples/python/workflows/workflow_early_stop.py) | [📖](https://docs.praison.ai/features/workflows) |
| ↳ Workflow Checkpoints | [Example](examples/python/workflows/workflow_checkpoints.py) | [📖](https://docs.praison.ai/features/workflows) |
| **💻 Code & Development** | | |
| ↳ Code Interpreter Agents | [Example](examples/python/agents/code-agent.py) | [📖](https://docs.praison.ai/features/codeagent) |
| ↳ AI Code Editing Tools | [Example](examples/python/code/code_editing_example.py) | [📖](https://docs.praison.ai/code/editing) |
| ↳ External Agents (All) | [Example](examples/python/code/external_agents_example.py) | [📖](https://docs.praison.ai/code/external-agents) |
| ↳ Claude Code CLI | [Example](examples/python/code/claude_code_example.py) | [📖](https://docs.praison.ai/code/claude-code) |
| ↳ Gemini CLI | [Example](examples/python/code/gemini_cli_example.py) | [📖](https://docs.praison.ai/code/gemini-cli) |
| ↳ Codex CLI | [Example](examples/python/code/codex_cli_example.py) | [📖](https://docs.praison.ai/code/codex-cli) |
| ↳ Cursor CLI | [Example](examples/python/code/cursor_cli_example.py) | [📖](https://docs.praison.ai/code/cursor-cli) |
| **🧠 Memory & Knowledge** | | |
| ↳ Memory (Short & Long Term) | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| ↳ File-Based Memory | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| ↳ Claude Memory Tool | [Example](#claude-memory-tool-cli) | [📖](https://docs.praison.ai/features/claude-memory-tool) |
| ↳ Add Custom Knowledge | [Example](examples/python/concepts/knowledge-agents.py) | [📖](https://docs.praison.ai/features/knowledge) |
| ↳ RAG Agents | [Example](examples/python/concepts/rag-agents.py) | [📖](https://docs.praison.ai/features/rag) |
| ↳ Chat with PDF Agents | [Example](examples/python/concepts/chat-with-pdf.py) | [📖](https://docs.praison.ai/features/chat-with-pdf) |
| ↳ Data Readers (PDF, DOCX, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-readers-api) |
| ↳ Vector Store Selection | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-vector-store-api) |
| ↳ Retrieval Strategies | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-retrieval-api) |
| ↳ Rerankers | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-reranker-api) |
| ↳ Index Types (Vector/Keyword/Hybrid) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-index-api) |
| ↳ Query Engines (Sub-Question, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-query-engine-api) |
| **🔬 Research & Intelligence** | | |
| ↳ Deep Research Agents | [Example](examples/python/agents/research-agent.py) | [📖](https://docs.praison.ai/agents/deep-research) |
| ↳ Query Rewriter Agent | [Example](#5-query-rewriter-agent) | [📖](https://docs.praison.ai/agents/query-rewriter) |
| ↳ Native Web Search | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/agents/websearch) |
| ↳ Built-in Search Tools | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/tools/tavily) |
| ↳ Unified Web Search | [Example](src/praisonai-agents/examples/web_search_example.py) | [📖](https://docs.praison.ai/tools/web-search) |
| ↳ Web Fetch (Anthropic) | [Example](#web-search-web-fetch--prompt-caching) | [📖](https://docs.praison.ai/features/model-capabilities) |
| **📋 Planning & Execution** | | |
| ↳ Planning Mode | [Example](examples/python/agents/planning-agent.py) | [📖](https://docs.praison.ai/features/planning-mode) |
| ↳ Planning Tools | [Example](#3-agent-with-planning-mode) | [📖](https://docs.praison.ai/features/planning-mode) |
| ↳ Planning Reasoning | [Example](#3-agent-with-planning-mode) | [📖](https://docs.praison.ai/features/planning-mode) |
| ↳ Prompt Chaining | [Example](examples/python/general/prompt_chaining.py) | [📖](https://docs.praison.ai/features/promptchaining) |
| ↳ Evaluator Optimiser | [Example](examples/python/general/evaluator-optimiser.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| ↳ Orchestrator Workers | [Example](examples/python/general/orchestrator-workers.py) | [📖](https://docs.praison.ai/features/orchestrator-worker) |
| **👥 Specialized Agents** | | |
| ↳ Data Analyst Agent | [Example](examples/python/agents/data-analyst-agent.py) | [📖](https://docs.praison.ai/agents/data-analyst) |
| ↳ Finance Agent | [Example](examples/python/agents/finance-agent.py) | [📖](https://docs.praison.ai/agents/finance) |
| ↳ Shopping Agent | [Example](examples/python/agents/shopping-agent.py) | [📖](https://docs.praison.ai/agents/shopping) |
| ↳ Recommendation Agent | [Example](examples/python/agents/recommendation-agent.py) | [📖](https://docs.praison.ai/agents/recommendation) |
| ↳ Wikipedia Agent | [Example](examples/python/agents/wikipedia-agent.py) | [📖](https://docs.praison.ai/agents/wikipedia) |
| ↳ Programming Agent | [Example](examples/python/agents/programming-agent.py) | [📖](https://docs.praison.ai/agents/programming) |
| ↳ Math Agents | [Example](examples/python/agents/math-agent.py) | [📖](https://docs.praison.ai/features/mathagent) |
| ↳ Markdown Agent | [Example](examples/python/agents/markdown-agent.py) | [📖](https://docs.praison.ai/agents/markdown) |
| ↳ Prompt Expander Agent | [Example](#prompt-expansion) | [📖](https://docs.praison.ai/agents/prompt-expander) |
| **🎨 Media & Multimodal** | | |
| ↳ Image Generation Agent | [Example](examples/python/image/image-agent.py) | [📖](https://docs.praison.ai/features/image-generation) |
| ↳ Image to Text Agent | [Example](examples/python/agents/image-to-text-agent.py) | [📖](https://docs.praison.ai/agents/image-to-text) |
| ↳ Video Agent | [Example](examples/python/agents/video-agent.py) | [📖](https://docs.praison.ai/agents/video) |
| ↳ Camera Integration | [Example](examples/python/camera/) | [📖](https://docs.praison.ai/features/camera-integration) |
| **🔌 Protocols & Integration** | | |
| ↳ MCP Transports | [Example](examples/python/mcp/mcp-transports-overview.py) | [📖](https://docs.praison.ai/mcp/transports) |
| ↳ WebSocket MCP | [Example](examples/python/mcp/websocket-mcp.py) | [📖](https://docs.praison.ai/mcp/sse-transport) |
| ↳ MCP Security | [Example](examples/python/mcp/mcp-security.py) | [📖](https://docs.praison.ai/mcp/transports) |
| ↳ MCP Resumability | [Example](examples/python/mcp/mcp-resumability.py) | [📖](https://docs.praison.ai/mcp/sse-transport) |
| ↳ MCP Config Management | [Example](#mcp-config-cli) | [📖](https://docs.praison.ai/docs/cli/mcp) |
| ↳ LangChain Integrated Agents | [Example](examples/python/general/langchain_example.py) | [📖](https://docs.praison.ai/features/langchain) |
| **🛡️ Safety & Control** | | |
| ↳ Guardrails | [Example](examples/python/guardrails/comprehensive-guardrails-example.py) | [📖](https://docs.praison.ai/features/guardrails) |
| ↳ Human Approval | [Example](examples/python/general/human_approval_example.py) | [📖](https://docs.praison.ai/features/approval) |
| ↳ Rules & Instructions | [Example](#6-rules--instructions) | [📖](https://docs.praison.ai/features/rules) |
| **⚙️ Advanced Features** | | |
| ↳ Async & Parallel Processing | [Example](examples/python/general/async_example.py) | [📖](https://docs.praison.ai/features/async) |
| ↳ Parallelisation | [Example](examples/python/general/parallelisation.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| ↳ Repetitive Agents | [Example](examples/python/concepts/repetitive-agents.py) | [📖](https://docs.praison.ai/features/repetitive) |
| ↳ Agent Handoffs | [Example](examples/python/handoff/handoff_basic.py) | [📖](https://docs.praison.ai/features/handoffs) |
| ↳ Stateful Agents | [Example](examples/python/stateful/workflow-state-example.py) | [📖](https://docs.praison.ai/features/stateful-agents) |
| ↳ Autonomous Workflow | [Example](examples/python/general/autonomous-agent.py) | [📖](https://docs.praison.ai/features/autonomous-workflow) |
| ↳ Structured Output Agents | [Example](examples/python/general/structured_agents_example.py) | [📖](https://docs.praison.ai/features/structured) |
| ↳ Model Router | [Example](examples/python/agents/router-agent-cost-optimization.py) | [📖](https://docs.praison.ai/features/model-router) |
| ↳ Prompt Caching | [Example](#web-search-web-fetch--prompt-caching) | [📖](https://docs.praison.ai/features/model-capabilities) |
| ↳ Fast Context | [Example](examples/context/00_agent_fast_context_basic.py) | [📖](https://docs.praison.ai/features/fast-context) |
| **🛠️ Tools & Configuration** | | |
| ↳ 100+ Custom Tools | [Example](examples/python/general/tools_example.py) | [📖](https://docs.praison.ai/tools/tools) |
| ↳ YAML Configuration | [Example](examples/cookbooks/yaml/secondary_market_research_agents.yaml) | [📖](https://docs.praison.ai/developers/agents-playbook) |
| ↳ 100+ LLM Support | [Example](examples/python/providers/openai/openai_gpt4_example.py) | [📖](https://docs.praison.ai/models) |
| ↳ Callback Agents | [Example](examples/python/general/advanced-callback-systems.py) | [📖](https://docs.praison.ai/features/callbacks) |
| ↳ Hooks | [Example](#9-hooks) | [📖](https://docs.praison.ai/features/hooks) |
| ↳ Middleware System | [Example](examples/middleware/basic_middleware.py) | [📖](https://docs.praison.ai/features/middleware) |
| ↳ Configurable Model | [Example](examples/middleware/configurable_model.py) | [📖](https://docs.praison.ai/features/configurable-model) |
| ↳ Rate Limiter | [Example](examples/middleware/rate_limiter.py) | [📖](https://docs.praison.ai/features/rate-limiter) |
| ↳ Injected Tool State | [Example](examples/middleware/injected_state.py) | [📖](https://docs.praison.ai/features/injected-state) |
| ↳ Shadow Git Checkpoints | [Example](#10-shadow-git-checkpoints) | [📖](https://docs.praison.ai/features/checkpoints) |
| ↳ Background Tasks | [Example](examples/background/basic_background.py) | [📖](https://docs.praison.ai/features/background-tasks) |
| ↳ Policy Engine | [Example](examples/policy/basic_policy.py) | [📖](https://docs.praison.ai/features/policy-engine) |
| ↳ Thinking Budgets | [Example](examples/thinking/basic_thinking.py) | [📖](https://docs.praison.ai/features/thinking-budgets) |
| ↳ Output Styles | [Example](examples/output/basic_output.py) | [📖](https://docs.praison.ai/features/output-styles) |
| ↳ Context Compaction | [Example](examples/compaction/basic_compaction.py) | [📖](https://docs.praison.ai/features/context-compaction) |
| **📊 Monitoring & Management** | | |
| ↳ Sessions Management | [Example](examples/python/sessions/comprehensive-session-management.py) | [📖](https://docs.praison.ai/features/sessions) |
| ↳ Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| ↳ History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| ↳ Telemetry | [Example](examples/python/telemetry/production-telemetry-example.py) | [📖](https://docs.praison.ai/features/telemetry) |
| ↳ Project Docs (.praison/docs/) | [Example](#docs-cli) | [📖](https://docs.praison.ai/docs/cli/docs) |
| ↳ AI Commit Messages | [Example](#ai-commit-cli) | [📖](https://docs.praison.ai/docs/cli/commit) |
| ↳ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/docs/cli/mentions) |
| **🖥️ CLI Features** | | |
| ↳ Slash Commands | [Example](examples/python/cli/slash_commands_example.py) | [📖](https://docs.praison.ai/docs/cli/slash-commands) |
| ↳ Autonomy Modes | [Example](examples/python/cli/autonomy_modes_example.py) | [📖](https://docs.praison.ai/docs/cli/autonomy-modes) |
| ↳ Cost Tracking | [Example](examples/python/cli/cost_tracking_example.py) | [📖](https://docs.praison.ai/docs/cli/cost-tracking) |
| ↳ Repository Map | [Example](examples/python/cli/repo_map_example.py) | [📖](https://docs.praison.ai/docs/cli/repo-map) |
| ↳ Interactive TUI | [Example](examples/python/cli/interactive_tui_example.py) | [📖](https://docs.praison.ai/docs/cli/interactive-tui) |
| ↳ Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/docs/cli/git-integration) |
| ↳ Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/docs/cli/sandbox-execution) |
| ↳ CLI Compare | [Example](examples/compare/cli_compare_basic.py) | [📖](https://docs.praison.ai/docs/cli/compare) |
| ↳ Profile/Benchmark | [Example](#profile-benchmark) | [📖](https://docs.praison.ai/docs/cli/profile) |
| ↳ Auto Mode | [Example](#auto-mode) | [📖](https://docs.praison.ai/docs/cli/auto) |
| ↳ Init | [Example](#init) | [📖](https://docs.praison.ai/docs/cli/init) |
| ↳ File Input | [Example](#file-input) | [📖](https://docs.praison.ai/docs/cli/file-input) |
| ↳ Final Agent | [Example](#final-agent) | [📖](https://docs.praison.ai/docs/cli/final-agent) |
| ↳ Max Tokens | [Example](#max-tokens) | [📖](https://docs.praison.ai/docs/cli/max-tokens) |
| **🧪 Evaluation** | | |
| ↳ Accuracy Evaluation | [Example](examples/eval/accuracy_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| ↳ Performance Evaluation | [Example](examples/eval/performance_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| ↳ Reliability Evaluation | [Example](examples/eval/reliability_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| ↳ Criteria Evaluation | [Example](examples/eval/criteria_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| **🎯 Agent Skills** | | |
| ↳ Skills Management | [Example](examples/skills/basic_skill_usage.py) | [📖](https://docs.praison.ai/features/skills) |
| ↳ Custom Skills | [Example](examples/skills/custom_skill_example.py) | [📖](https://docs.praison.ai/features/skills) |
| **⏰ 24/7 Scheduling** | | |
| ↳ Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/docs/cli/scheduler) |
| Single Agent | [Example](examples/python/agents/single-agent.py) | [📖](https://docs.praison.ai/agents/single) |
| Multi Agents | [Example](examples/python/general/mini_agents_example.py) | [📖](https://docs.praison.ai/concepts/agents) |
| Auto Agents | [Example](examples/python/general/auto_agents_example.py) | [📖](https://docs.praison.ai/features/autoagents) |
| Self Reflection AI Agents | [Example](examples/python/concepts/self-reflection-details.py) | [📖](https://docs.praison.ai/features/selfreflection) |
| Reasoning AI Agents | [Example](examples/python/concepts/reasoning-extraction.py) | [📖](https://docs.praison.ai/features/reasoning) |
| Multi Modal AI Agents | [Example](examples/python/general/multimodal.py) | [📖](https://docs.praison.ai/features/multimodal) |
</details>
<details>
<summary><strong>🔄 Workflows</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Simple Workflow | [Example](examples/python/workflows/simple_workflow.py) | [📖](https://docs.praison.ai/features/workflows) |
| Workflow with Agents | [Example](examples/python/workflows/workflow_with_agents.py) | [📖](https://docs.praison.ai/features/workflows) |
| Agentic Routing (`route()`) | [Example](examples/python/workflows/workflow_routing.py) | [📖](https://docs.praison.ai/features/routing) |
| Parallel Execution (`parallel()`) | [Example](examples/python/workflows/workflow_parallel.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| Loop over List/CSV (`loop()`) | [Example](examples/python/workflows/workflow_loop_csv.py) | [📖](https://docs.praison.ai/features/repetitive) |
| Evaluator-Optimizer (`repeat()`) | [Example](examples/python/workflows/workflow_repeat.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| Conditional Steps | [Example](examples/python/workflows/workflow_conditional.py) | [📖](https://docs.praison.ai/features/workflows) |
| Workflow Branching | [Example](examples/python/workflows/workflow_branching.py) | [📖](https://docs.praison.ai/features/workflows) |
| Workflow Early Stop | [Example](examples/python/workflows/workflow_early_stop.py) | [📖](https://docs.praison.ai/features/workflows) |
| Workflow Checkpoints | [Example](examples/python/workflows/workflow_checkpoints.py) | [📖](https://docs.praison.ai/features/workflows) |
</details>
<details>
<summary><strong>💻 Code & Development</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Code Interpreter Agents | [Example](examples/python/agents/code-agent.py) | [📖](https://docs.praison.ai/features/codeagent) |
| AI Code Editing Tools | [Example](examples/python/code/code_editing_example.py) | [📖](https://docs.praison.ai/code/editing) |
| External Agents (All) | [Example](examples/python/code/external_agents_example.py) | [📖](https://docs.praison.ai/code/external-agents) |
| Claude Code CLI | [Example](examples/python/code/claude_code_example.py) | [📖](https://docs.praison.ai/code/claude-code) |
| Gemini CLI | [Example](examples/python/code/gemini_cli_example.py) | [📖](https://docs.praison.ai/code/gemini-cli) |
| Codex CLI | [Example](examples/python/code/codex_cli_example.py) | [📖](https://docs.praison.ai/code/codex-cli) |
| Cursor CLI | [Example](examples/python/code/cursor_cli_example.py) | [📖](https://docs.praison.ai/code/cursor-cli) |
</details>
<details>
<summary><strong>🧠 Memory & Knowledge</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Memory (Short & Long Term) | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| File-Based Memory | [Example](examples/python/general/memory_example.py) | [📖](https://docs.praison.ai/concepts/memory) |
| Claude Memory Tool | [Example](#claude-memory-tool-cli) | [📖](https://docs.praison.ai/features/claude-memory-tool) |
| Add Custom Knowledge | [Example](examples/python/concepts/knowledge-agents.py) | [📖](https://docs.praison.ai/features/knowledge) |
| RAG Agents | [Example](examples/python/concepts/rag-agents.py) | [📖](https://docs.praison.ai/features/rag) |
| Chat with PDF Agents | [Example](examples/python/concepts/chat-with-pdf.py) | [📖](https://docs.praison.ai/features/chat-with-pdf) |
| Data Readers (PDF, DOCX, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-readers-api) |
| Vector Store Selection | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-vector-store-api) |
| Retrieval Strategies | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-retrieval-api) |
| Rerankers | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-reranker-api) |
| Index Types (Vector/Keyword/Hybrid) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-index-api) |
| Query Engines (Sub-Question, etc.) | [CLI](#knowledge-cli) | [📖](https://docs.praison.ai/api/praisonai/knowledge-query-engine-api) |
</details>
<details>
<summary><strong>🔬 Research & Intelligence</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Deep Research Agents | [Example](examples/python/agents/research-agent.py) | [📖](https://docs.praison.ai/agents/deep-research) |
| Query Rewriter Agent | [Example](#5-query-rewriter-agent) | [📖](https://docs.praison.ai/agents/query-rewriter) |
| Native Web Search | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/agents/websearch) |
| Built-in Search Tools | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/tools/tavily) |
| Unified Web Search | [Example](src/praisonai-agents/examples/web_search_example.py) | [📖](https://docs.praison.ai/tools/web-search) |
| Web Fetch (Anthropic) | [Example](#web-search-web-fetch--prompt-caching) | [📖](https://docs.praison.ai/features/model-capabilities) |
</details>
<details>
<summary><strong>📋 Planning & Execution</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Planning Mode | [Example](examples/python/agents/planning-agent.py) | [📖](https://docs.praison.ai/features/planning-mode) |
| Planning Tools | [Example](#3-agent-with-planning-mode) | [📖](https://docs.praison.ai/features/planning-mode) |
| Planning Reasoning | [Example](#3-agent-with-planning-mode) | [📖](https://docs.praison.ai/features/planning-mode) |
| Prompt Chaining | [Example](examples/python/general/prompt_chaining.py) | [📖](https://docs.praison.ai/features/promptchaining) |
| Evaluator Optimiser | [Example](examples/python/general/evaluator-optimiser.py) | [📖](https://docs.praison.ai/features/evaluator-optimiser) |
| Orchestrator Workers | [Example](examples/python/general/orchestrator-workers.py) | [📖](https://docs.praison.ai/features/orchestrator-worker) |
</details>
<details>
<summary><strong>👥 Specialized Agents</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Data Analyst Agent | [Example](examples/python/agents/data-analyst-agent.py) | [📖](https://docs.praison.ai/agents/data-analyst) |
| Finance Agent | [Example](examples/python/agents/finance-agent.py) | [📖](https://docs.praison.ai/agents/finance) |
| Shopping Agent | [Example](examples/python/agents/shopping-agent.py) | [📖](https://docs.praison.ai/agents/shopping) |
| Recommendation Agent | [Example](examples/python/agents/recommendation-agent.py) | [📖](https://docs.praison.ai/agents/recommendation) |
| Wikipedia Agent | [Example](examples/python/agents/wikipedia-agent.py) | [📖](https://docs.praison.ai/agents/wikipedia) |
| Programming Agent | [Example](examples/python/agents/programming-agent.py) | [📖](https://docs.praison.ai/agents/programming) |
| Math Agents | [Example](examples/python/agents/math-agent.py) | [📖](https://docs.praison.ai/features/mathagent) |
| Markdown Agent | [Example](examples/python/agents/markdown-agent.py) | [📖](https://docs.praison.ai/agents/markdown) |
| Prompt Expander Agent | [Example](#prompt-expansion) | [📖](https://docs.praison.ai/agents/prompt-expander) |
</details>
<details>
<summary><strong>🎨 Media & Multimodal</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Image Generation Agent | [Example](examples/python/image/image-agent.py) | [📖](https://docs.praison.ai/features/image-generation) |
| Image to Text Agent | [Example](examples/python/agents/image-to-text-agent.py) | [📖](https://docs.praison.ai/agents/image-to-text) |
| Video Agent | [Example](examples/python/agents/video-agent.py) | [📖](https://docs.praison.ai/agents/video) |
| Camera Integration | [Example](examples/python/camera/) | [📖](https://docs.praison.ai/features/camera-integration) |
</details>
<details>
<summary><strong>🔌 Protocols & Integration</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| MCP Transports | [Example](examples/python/mcp/mcp-transports-overview.py) | [📖](https://docs.praison.ai/mcp/transports) |
| WebSocket MCP | [Example](examples/python/mcp/websocket-mcp.py) | [📖](https://docs.praison.ai/mcp/sse-transport) |
| MCP Security | [Example](examples/python/mcp/mcp-security.py) | [📖](https://docs.praison.ai/mcp/transports) |
| MCP Resumability | [Example](examples/python/mcp/mcp-resumability.py) | [📖](https://docs.praison.ai/mcp/sse-transport) |
| MCP Config Management | [Example](#mcp-config-cli) | [📖](https://docs.praison.ai/docs/cli/mcp) |
| LangChain Integrated Agents | [Example](examples/python/general/langchain_example.py) | [📖](https://docs.praison.ai/features/langchain) |
</details>
<details>
<summary><strong>🛡️ Safety & Control</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Guardrails | [Example](examples/python/guardrails/comprehensive-guardrails-example.py) | [📖](https://docs.praison.ai/features/guardrails) |
| Human Approval | [Example](examples/python/general/human_approval_example.py) | [📖](https://docs.praison.ai/features/approval) |
| Rules & Instructions | [Example](#7-rules--instructions) | [📖](https://docs.praison.ai/features/rules) |
</details>
<details>
<summary><strong>⚙️ Advanced Features</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Async & Parallel Processing | [Example](examples/python/general/async_example.py) | [📖](https://docs.praison.ai/features/async) |
| Parallelisation | [Example](examples/python/general/parallelisation.py) | [📖](https://docs.praison.ai/features/parallelisation) |
| Repetitive Agents | [Example](examples/python/concepts/repetitive-agents.py) | [📖](https://docs.praison.ai/features/repetitive) |
| Agent Handoffs | [Example](examples/python/handoff/handoff_basic.py) | [📖](https://docs.praison.ai/features/handoffs) |
| Stateful Agents | [Example](examples/python/stateful/workflow-state-example.py) | [📖](https://docs.praison.ai/features/stateful-agents) |
| Autonomous Workflow | [Example](examples/python/general/autonomous-agent.py) | [📖](https://docs.praison.ai/features/autonomous-workflow) |
| Structured Output Agents | [Example](examples/python/general/structured_agents_example.py) | [📖](https://docs.praison.ai/features/structured) |
| Model Router | [Example](examples/python/agents/router-agent-cost-optimization.py) | [📖](https://docs.praison.ai/features/model-router) |
| Prompt Caching | [Example](#web-search-web-fetch--prompt-caching) | [📖](https://docs.praison.ai/features/model-capabilities) |
| Fast Context | [Example](examples/context/00_agent_fast_context_basic.py) | [📖](https://docs.praison.ai/features/fast-context) |
</details>
<details>
<summary><strong>🛠️ Tools & Configuration</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| 100+ Custom Tools | [Example](examples/python/general/tools_example.py) | [📖](https://docs.praison.ai/tools/tools) |
| YAML Configuration | [Example](examples/cookbooks/yaml/secondary_market_research_agents.yaml) | [📖](https://docs.praison.ai/developers/agents-playbook) |
| 100+ LLM Support | [Example](examples/python/providers/openai/openai_gpt4_example.py) | [📖](https://docs.praison.ai/models) |
| Callback Agents | [Example](examples/python/general/advanced-callback-systems.py) | [📖](https://docs.praison.ai/features/callbacks) |
| Hooks | [Example](#10-hooks) | [📖](https://docs.praison.ai/features/hooks) |
| Middleware System | [Example](examples/middleware/basic_middleware.py) | [📖](https://docs.praison.ai/features/middleware) |
| Configurable Model | [Example](examples/middleware/configurable_model.py) | [📖](https://docs.praison.ai/features/configurable-model) |
| Rate Limiter | [Example](examples/middleware/rate_limiter.py) | [📖](https://docs.praison.ai/features/rate-limiter) |
| Injected Tool State | [Example](examples/middleware/injected_state.py) | [📖](https://docs.praison.ai/features/injected-state) |
| Shadow Git Checkpoints | [Example](#11-shadow-git-checkpoints) | [📖](https://docs.praison.ai/features/checkpoints) |
| Background Tasks | [Example](examples/background/basic_background.py) | [📖](https://docs.praison.ai/features/background-tasks) |
| Policy Engine | [Example](examples/policy/basic_policy.py) | [📖](https://docs.praison.ai/features/policy-engine) |
| Thinking Budgets | [Example](examples/thinking/basic_thinking.py) | [📖](https://docs.praison.ai/features/thinking-budgets) |
| Output Styles | [Example](examples/output/basic_output.py) | [📖](https://docs.praison.ai/features/output-styles) |
| Context Compaction | [Example](examples/compaction/basic_compaction.py) | [📖](https://docs.praison.ai/features/context-compaction) |
</details>
<details>
<summary><strong>📊 Monitoring & Management</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Sessions Management | [Example](examples/python/sessions/comprehensive-session-management.py) | [📖](https://docs.praison.ai/features/sessions) |
| Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| Telemetry | [Example](examples/python/telemetry/production-telemetry-example.py) | [📖](https://docs.praison.ai/features/telemetry) |
| Project Docs (.praison/docs/) | [Example](#docs-cli) | [📖](https://docs.praison.ai/docs/cli/docs) |
| AI Commit Messages | [Example](#ai-commit-cli) | [📖](https://docs.praison.ai/docs/cli/commit) |
| @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/docs/cli/mentions) |
</details>
<details>
<summary><strong>🖥️ CLI Features</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Slash Commands | [Example](examples/python/cli/slash_commands_example.py) | [📖](https://docs.praison.ai/docs/cli/slash-commands) |
| Autonomy Modes | [Example](examples/python/cli/autonomy_modes_example.py) | [📖](https://docs.praison.ai/docs/cli/autonomy-modes) |
| Cost Tracking | [Example](examples/python/cli/cost_tracking_example.py) | [📖](https://docs.praison.ai/docs/cli/cost-tracking) |
| Repository Map | [Example](examples/python/cli/repo_map_example.py) | [📖](https://docs.praison.ai/docs/cli/repo-map) |
| Interactive TUI | [Example](examples/python/cli/interactive_tui_example.py) | [📖](https://docs.praison.ai/docs/cli/interactive-tui) |
| Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/docs/cli/git-integration) |
| Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/docs/cli/sandbox-execution) |
| CLI Compare | [Example](examples/compare/cli_compare_basic.py) | [📖](https://docs.praison.ai/docs/cli/compare) |
| Profile/Benchmark | [Example](#profile-benchmark) | [📖](https://docs.praison.ai/docs/cli/profile) |
| Auto Mode | [Example](#auto-mode) | [📖](https://docs.praison.ai/docs/cli/auto) |
| Init | [Example](#init) | [📖](https://docs.praison.ai/docs/cli/init) |
| File Input | [Example](#file-input) | [📖](https://docs.praison.ai/docs/cli/file-input) |
| Final Agent | [Example](#final-agent) | [📖](https://docs.praison.ai/docs/cli/final-agent) |
| Max Tokens | [Example](#max-tokens) | [📖](https://docs.praison.ai/docs/cli/max-tokens) |
</details>
<details>
<summary><strong>🧪 Evaluation</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Accuracy Evaluation | [Example](examples/eval/accuracy_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| Performance Evaluation | [Example](examples/eval/performance_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| Reliability Evaluation | [Example](examples/eval/reliability_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
| Criteria Evaluation | [Example](examples/eval/criteria_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
</details>
<details>
<summary><strong>🎯 Agent Skills</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Skills Management | [Example](examples/skills/basic_skill_usage.py) | [📖](https://docs.praison.ai/features/skills) |
| Custom Skills | [Example](examples/skills/custom_skill_example.py) | [📖](https://docs.praison.ai/features/skills) |
</details>
<details>
<summary><strong>⏰ 24/7 Scheduling</strong></summary>
| Feature | Code | Docs |
|---------|:----:|:----:|
| Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/docs/cli/scheduler) |
</details>
---
@@ -543,7 +745,7 @@ memory.handle_command("/memory save my_session")
| `memory="sqlite"` | Built-in | SQLite with indexing |
| `memory="chromadb"` | chromadb | Vector/semantic search |
### 6. Rules & Instructions
### 7. Rules & Instructions
PraisonAI auto-discovers instruction files from your project root and git root:
@@ -592,7 +794,7 @@ activation: always # always, glob, manual, ai_decision
- Follow PEP 8
```
### 7. Auto-Generated Memories
### 8. Auto-Generated Memories
```python
from praisonaiagents.memory import FileMemory, AutoMemory
@@ -607,7 +809,7 @@ memories = auto.process_interaction(
# Extracts: name="John", preference="Python for backend"
```
### 8. Agentic Workflows
### 9. Agentic Workflows
Create powerful multi-agent workflows with the `Workflow` class:
@@ -877,7 +1079,7 @@ callbacks:
on_workflow_complete: log_complete
```
### 9. Hooks
### 10. Hooks
Intercept and modify agent behavior at various lifecycle points:
@@ -916,7 +1118,7 @@ praisonai hooks validate hooks.json # Validate configuration
```
### 10. Shadow Git Checkpoints
### 11. Shadow Git Checkpoints
File-level undo/restore using shadow git:
@@ -953,7 +1155,7 @@ praisonai checkpoint restore abc123 # Restore to checkpoint
---
### 11. Background Tasks
### 12. Background Tasks
Run agent tasks asynchronously without blocking:
@@ -991,7 +1193,7 @@ praisonai background clear # Clear completed tasks
---
### 12. Policy Engine
### 13. Policy Engine
Control what agents can and cannot do with policy-based execution:
@@ -1032,7 +1234,7 @@ praisonai policy init # Create template
---
### 13. Thinking Budgets
### 14. Thinking Budgets
Configure token budgets for extended thinking:
@@ -1065,7 +1267,7 @@ praisonai thinking stats # Show usage statistics
---
### 14. Output Styles
### 15. Output Styles
Configure how agents format their responses:
@@ -1095,7 +1297,7 @@ praisonai output set concise # Set output style
---
### 15. Context Compaction
### 16. Context Compaction
Automatically manage context window size:
@@ -1130,7 +1332,7 @@ praisonai compaction stats # Show statistics
---
### 16. Field Names Reference (A-I-G-S)
### 17. Field Names Reference (A-I-G-S)
PraisonAI accepts both old (agents.yaml) and new (workflow.yaml) field names. Use the **canonical names** for new projects:
@@ -1164,7 +1366,7 @@ steps: # Define steps (not 'tasks')
> **Note:** The parser accepts both old and new names. Run `praisonai workflow validate <file.yaml>` to see suggestions for canonical names.
### 12. Extended agents.yaml with Workflow Patterns
### 18. Extended agents.yaml with Workflow Patterns
**Feature Parity:** Both `agents.yaml` and `workflow.yaml` now support the same features:
- All workflow patterns (route, parallel, loop, repeat)
@@ -1240,7 +1442,7 @@ Run with the same simple command:
praisonai agents.yaml
```
### 12. MCP (Model Context Protocol)
### 19. MCP (Model Context Protocol)
PraisonAI supports MCP Protocol Revision 2025-11-25 with multiple transports.
@@ -1316,7 +1518,7 @@ server.run() # stdio for Claude Desktop
| Security | Origin validation, DNS rebinding prevention |
| WebSocket | Auto-reconnect with exponential backoff |
### 13. A2A (Agent2Agent Protocol)
### 20. A2A (Agent2Agent Protocol)
PraisonAI supports the [A2A Protocol](https://a2a-protocol.org) for agent-to-agent communication, enabling your agents to be discovered and collaborate with other AI agents.
@@ -1364,6 +1566,22 @@ app.include_router(a2a.get_router())
PraisonAI provides a powerful CLI for no-code automation and quick prototyping.
### CLI Quick Reference
| Category | Commands |
|----------|----------|
| **Execution** | `praisonai`, `--auto`, `--interactive`, `--chat` |
| **Research** | `research`, `--query-rewrite`, `--deep-research` |
| **Planning** | `--planning`, `--planning-tools`, `--planning-reasoning` |
| **Workflows** | `workflow run`, `workflow list`, `workflow auto` |
| **Memory** | `memory show`, `memory add`, `memory search`, `memory clear` |
| **Knowledge** | `knowledge add`, `knowledge query`, `knowledge list` |
| **Sessions** | `session list`, `session resume`, `session delete` |
| **Tools** | `tools list`, `tools info`, `tools search` |
| **MCP** | `mcp list`, `mcp create`, `mcp enable` |
| **Development** | `commit`, `docs`, `checkpoint`, `hooks` |
| **Scheduling** | `schedule start`, `schedule list`, `schedule stop` |
### Auto Mode
```bash
pip install praisonai
@@ -3023,6 +3241,119 @@ cd src/praisonai && uv publish
---
## ❓ FAQ & Troubleshooting
<details>
<summary><strong>ModuleNotFoundError: No module named 'praisonaiagents'</strong></summary>
Install the package:
```bash
pip install praisonaiagents
```
</details>
<details>
<summary><strong>API key not found / Authentication error</strong></summary>
Ensure your API key is set:
```bash
export OPENAI_API_KEY=your_key_here
```
For other providers, see [Environment Variables](#environment-variables).
</details>
<details>
<summary><strong>How do I use a local model (Ollama)?</strong></summary>
```bash
# Start Ollama server first
ollama serve
# Set environment variable
export OPENAI_BASE_URL=http://localhost:11434/v1
```
See [Ollama Integration](#ollama-integration) for more details.
</details>
<details>
<summary><strong>How do I persist conversations to a database?</strong></summary>
Use the `db` parameter:
```python
from praisonaiagents import Agent, db
agent = Agent(
name="Assistant",
db=db(database_url="postgresql://localhost/mydb"),
session_id="my-session"
)
```
See [Persistence (Databases)](#-persistence-databases) for supported databases.
</details>
<details>
<summary><strong>How do I enable agent memory?</strong></summary>
```python
from praisonaiagents import Agent
agent = Agent(
name="Assistant",
memory=True, # Enables file-based memory (no extra deps!)
user_id="user123"
)
```
See [Agent Memory](#6-agent-memory-zero-dependencies) for more options.
</details>
<details>
<summary><strong>How do I run multiple agents together?</strong></summary>
```python
from praisonaiagents import Agent, PraisonAIAgents
agent1 = Agent(instructions="Research topics")
agent2 = Agent(instructions="Summarize findings")
agents = PraisonAIAgents(agents=[agent1, agent2])
agents.start()
```
See [Multi Agents](#2-multi-agents) for more examples.
</details>
<details>
<summary><strong>How do I use MCP tools?</strong></summary>
```python
from praisonaiagents import Agent, MCP
agent = Agent(
tools=MCP("npx @modelcontextprotocol/server-memory")
)
```
See [MCP Protocol](#19-mcp-model-context-protocol) for all transport options.
</details>
### Getting Help
- 📚 [Full Documentation](https://docs.praison.ai)
- 🐛 [Report Issues](https://github.com/MervinPraison/PraisonAI/issues)
- 💬 [Discussions](https://github.com/MervinPraison/PraisonAI/discussions)
---
<div align="center">
<p><strong>Made with ❤️ by the PraisonAI Team</strong></p>
<p>
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.2" \
"praisonai>=2.5.7" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.2" \
"praisonai>=2.5.7" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.2" \
"praisonai>=2.5.7" \
"praisonai[ui]" \
"praisonai[crewai]"
+19
View File
@@ -0,0 +1,19 @@
/**
* Basic Agent Example
* Demonstrates simple agent usage with OpenAI
*/
import { Agent } from 'praisonai';
async function main() {
const agent = new Agent({
name: 'Assistant',
instructions: 'You are a helpful assistant. Be concise and informative.',
llm: 'openai/gpt-4o-mini'
});
const response = await agent.chat('What is TypeScript?');
console.log('Response:', response.text);
}
main().catch(console.error);
+47
View File
@@ -0,0 +1,47 @@
/**
* Context Agent Example
* Demonstrates RAG-enabled agent with knowledge base
*/
import { ContextAgent, createContextAgent, KnowledgeBase } from 'praisonai';
async function main() {
// Create knowledge base
const kb = new KnowledgeBase();
// Add documents
await kb.add({
id: 'doc1',
content: 'TypeScript is a strongly typed programming language that builds on JavaScript.',
metadata: { topic: 'typescript' }
});
await kb.add({
id: 'doc2',
content: 'TypeScript adds optional static typing and class-based object-oriented programming.',
metadata: { topic: 'typescript' }
});
await kb.add({
id: 'doc3',
content: 'PraisonAI is a framework for building AI agents in TypeScript and Python.',
metadata: { topic: 'praisonai' }
});
// Create context agent with knowledge base
const agent = createContextAgent({
name: 'KnowledgeAgent',
llm: 'openai/gpt-4o-mini',
knowledgeBase: kb,
maxContextDocs: 3
});
// Query with context
const response = await agent.chat('What is TypeScript?');
console.log('Response:', response.text);
// Check conversation history
console.log('\nConversation history:', agent.getHistory().length, 'messages');
}
main().catch(console.error);
+29
View File
@@ -0,0 +1,29 @@
/**
* Image Agent Example
* Demonstrates image analysis capabilities
*/
import { ImageAgent, createImageAgent } from 'praisonai';
async function main() {
const agent = createImageAgent({
name: 'VisionAgent',
llm: 'openai/gpt-4o-mini',
verbose: true
});
console.log('Image Agent created:', agent.name);
// Note: For actual image analysis, provide a real image URL
// const analysis = await agent.analyze({
// imageUrl: 'https://example.com/image.jpg',
// prompt: 'Describe this image in detail'
// });
// console.log('Analysis:', analysis);
// Chat without image
const response = await agent.chat('What types of images can you analyze?');
console.log('Response:', response);
}
main().catch(console.error);
+48
View File
@@ -0,0 +1,48 @@
/**
* Multi-Agent Example
* Demonstrates multiple agents working together
*/
import { Agent, PraisonAIAgents, Task } from 'praisonai';
async function main() {
// Create specialized agents
const researcher = new Agent({
name: 'Researcher',
role: 'Research Specialist',
goal: 'Find accurate information on topics',
instructions: 'You research topics thoroughly and provide factual information.'
});
const writer = new Agent({
name: 'Writer',
role: 'Content Writer',
goal: 'Create engaging content from research',
instructions: 'You write clear, engaging content based on provided research.'
});
// Create tasks
const researchTask = new Task({
description: 'Research the benefits of TypeScript over JavaScript',
expectedOutput: 'A list of key benefits with explanations',
agent: researcher
});
const writeTask = new Task({
description: 'Write a short article about TypeScript benefits',
expectedOutput: 'A 200-word article',
agent: writer
});
// Run agents
const agents = new PraisonAIAgents({
agents: [researcher, writer],
tasks: [researchTask, writeTask],
process: 'sequential'
});
const result = await agents.start();
console.log('Result:', result);
}
main().catch(console.error);
+33
View File
@@ -0,0 +1,33 @@
/**
* Deep Research Agent Example
* Demonstrates comprehensive research with citations
*/
import { DeepResearchAgent, createDeepResearchAgent } from 'praisonai';
async function main() {
const agent = createDeepResearchAgent({
name: 'Scholar',
llm: 'openai/gpt-4o-mini',
maxIterations: 5,
verbose: true
});
console.log('Research Agent:', agent.name);
// Conduct research
const result = await agent.research('What are the key benefits of TypeScript?');
console.log('\n=== Research Results ===');
console.log('Answer:', result.answer.substring(0, 500) + '...');
console.log('\nConfidence:', result.confidence);
console.log('Reasoning steps:', result.reasoning.length);
// Show reasoning
console.log('\n=== Reasoning Process ===');
result.reasoning.forEach(step => {
console.log(`Step ${step.step}: ${step.thought}`);
});
}
main().catch(console.error);
+57
View File
@@ -0,0 +1,57 @@
/**
* Router Agent Example
* Demonstrates routing requests to specialized agents
*/
import { RouterAgent, createRouter, routeConditions, Agent } from 'praisonai';
async function main() {
// Create specialized agents
const mathAgent = {
name: 'MathAgent',
chat: async (input: string) => ({ text: `Math answer for: ${input}` })
} as any;
const codeAgent = {
name: 'CodeAgent',
chat: async (input: string) => ({ text: `Code solution for: ${input}` })
} as any;
const generalAgent = {
name: 'GeneralAgent',
chat: async (input: string) => ({ text: `General response: ${input}` })
} as any;
// Create router with conditions
const router = createRouter({
name: 'SmartRouter',
routes: [
{
agent: mathAgent,
condition: routeConditions.keywords(['math', 'calculate', 'number', 'equation']),
priority: 10
},
{
agent: codeAgent,
condition: routeConditions.keywords(['code', 'program', 'function', 'javascript']),
priority: 10
}
],
defaultAgent: generalAgent
});
// Test routing
const queries = [
'Calculate 2 + 2',
'Write a function to sort an array',
'What is the weather today?'
];
for (const query of queries) {
const result = await router.route(query);
console.log(`Query: "${query}"`);
console.log(`Routed to: ${result?.agent.name || 'default'}\n`);
}
}
main().catch(console.error);
+59
View File
@@ -0,0 +1,59 @@
/**
* AutoAgents Example
* Demonstrates automatic agent generation from task descriptions
*/
import { AutoAgents, createAutoAgents } from 'praisonai';
async function main() {
const auto = createAutoAgents({
pattern: 'sequential',
verbose: true
});
// Analyze task complexity
console.log('=== Complexity Analysis ===');
const tasks = [
'Write a hello world function',
'Build a web scraper that extracts product data',
'Create a multi-step data pipeline with validation, transformation, and multiple output formats'
];
tasks.forEach(task => {
const complexity = auto.analyzeComplexity(task);
console.log(`"${task.substring(0, 40)}..." -> ${complexity}`);
});
// Recommend patterns
console.log('\n=== Pattern Recommendations ===');
const patternTasks = [
'Process items in parallel',
'Route requests to different handlers',
'Orchestrate multiple workers',
'Evaluate and optimize results'
];
patternTasks.forEach(task => {
const pattern = auto.recommendPattern(task);
console.log(`"${task}" -> ${pattern}`);
});
// Generate agent configuration
console.log('\n=== Generate Agent Config ===');
console.log('Generating agents for: "Build a web scraper"');
const team = await auto.generate('Build a web scraper that extracts product prices from e-commerce sites');
console.log('\nGenerated Team:');
console.log('Pattern:', team.pattern);
console.log('Agents:', team.agents.length);
team.agents.forEach(agent => {
console.log(` - ${agent.name}: ${agent.role}`);
});
console.log('Tasks:', team.tasks.length);
team.tasks.forEach(task => {
console.log(` - ${task.description.substring(0, 50)}...`);
});
}
main().catch(console.error);
+47
View File
@@ -0,0 +1,47 @@
/**
* Redis Database Example
*/
import { createMemoryRedis } from '../../../src/praisonai-ts/src';
async function main() {
// Use in-memory Redis for this example
const redis = createMemoryRedis();
await redis.connect();
console.log('Connected to Redis');
// Basic get/set
await redis.set('user:123', { name: 'John', role: 'admin' });
const user = await redis.get('user:123');
console.log('User:', user);
// Set with TTL
await redis.set('session:abc', { token: 'xyz' }, 60);
const ttl = await redis.ttl('session:abc');
console.log('Session TTL:', ttl, 'seconds');
// Hash operations
await redis.hset('profile:123', 'email', '[email protected]');
await redis.hset('profile:123', 'phone', '555-1234');
const profile = await redis.hgetall('profile:123');
console.log('Profile:', profile);
// List operations
await redis.rpush('queue:tasks', { id: 1, type: 'process' });
await redis.rpush('queue:tasks', { id: 2, type: 'notify' });
const tasks = await redis.lrange('queue:tasks', 0, -1);
console.log('Tasks:', tasks);
// Pub/Sub (memory only)
await redis.subscribe('events', (message) => {
console.log('Received event:', message);
});
await redis.publish('events', JSON.stringify({ type: 'user_joined' }));
// Cleanup
await redis.disconnect();
console.log('Done!');
}
main().catch(console.error);
+64
View File
@@ -0,0 +1,64 @@
/**
* Evaluation Framework Example
* Demonstrates accuracy, performance, and reliability evaluation
*/
import { accuracyEval, performanceEval, reliabilityEval, EvalSuite } from 'praisonai';
async function main() {
console.log('=== Accuracy Evaluation ===');
const accuracyResult = await accuracyEval({
input: 'What is 2 + 2?',
expectedOutput: '4',
actualOutput: 'The answer is 4'
});
console.log('Passed:', accuracyResult.passed);
console.log('Score:', accuracyResult.score);
console.log('Duration:', accuracyResult.duration, 'ms');
console.log('\n=== Performance Evaluation ===');
const perfResult = await performanceEval({
func: async () => {
// Simulate some work
await new Promise(r => setTimeout(r, 100));
return 'done';
},
iterations: 5,
warmupRuns: 1
});
console.log('Iterations:', perfResult.times.length);
console.log('Avg time:', perfResult.avgTime.toFixed(2), 'ms');
console.log('Min time:', perfResult.minTime.toFixed(2), 'ms');
console.log('Max time:', perfResult.maxTime.toFixed(2), 'ms');
console.log('\n=== Reliability Evaluation ===');
const reliabilityResult = await reliabilityEval({
expectedToolCalls: ['search', 'calculate'],
actualToolCalls: ['search', 'calculate']
});
console.log('Passed:', reliabilityResult.passed);
console.log('Score:', reliabilityResult.score);
console.log('\n=== Eval Suite ===');
const suite = new EvalSuite();
await suite.runAccuracy('test1', {
input: 'Hello',
expectedOutput: 'Hi',
actualOutput: 'Hi there!'
});
await suite.runAccuracy('test2', {
input: 'Goodbye',
expectedOutput: 'Bye',
actualOutput: 'Bye!'
});
const summary = suite.getSummary();
console.log('Total tests:', summary.total);
console.log('Passed:', summary.passed);
console.log('Failed:', summary.failed);
console.log('Pass rate:', (summary.passRate * 100).toFixed(1) + '%');
}
main().catch(console.error);
@@ -0,0 +1,67 @@
/**
* Content Guardrails Example
* Demonstrates input/output validation
*/
import { guardrail, GuardrailManager, builtinGuardrails, LLMGuardrail } from 'praisonai';
async function main() {
// Create custom guardrail
const noSpamGuardrail = guardrail({
name: 'no_spam',
check: (content) => {
const spamWords = ['buy now', 'free money', 'click here'];
const hasSpam = spamWords.some(word =>
content.toLowerCase().includes(word)
);
return {
status: hasSpam ? 'failed' : 'passed',
message: hasSpam ? 'Content contains spam' : undefined
};
}
});
// Use built-in guardrails
const maxLength = builtinGuardrails.maxLength(500);
const minLength = builtinGuardrails.minLength(10);
const blockedWords = builtinGuardrails.blockedWords(['badword1', 'badword2']);
// Create manager
const manager = new GuardrailManager();
manager.add(noSpamGuardrail);
manager.add(maxLength);
manager.add(minLength);
manager.add(blockedWords);
// Test content
const testCases = [
'This is a normal, helpful response about programming.',
'BUY NOW! Free money if you click here!',
'Short',
'This is a valid response that provides useful information about TypeScript and its benefits for developers.'
];
console.log('=== Guardrail Tests ===\n');
for (const content of testCases) {
console.log(`Content: "${content.substring(0, 40)}..."`);
const result = await manager.runAll(content);
console.log(` Passed: ${result.passed}`);
if (!result.passed) {
result.results
.filter(r => r.status === 'failed')
.forEach(r => console.log(` Failed: ${r.message}`));
}
console.log();
}
// LLM Guardrail example
console.log('=== LLM Guardrail ===');
const llmGuard = new LLMGuardrail({
name: 'quality_check',
criteria: 'Content must be helpful and professional'
});
console.log('LLM Guardrail created:', llmGuard.name);
}
main().catch(console.error);
+62
View File
@@ -0,0 +1,62 @@
/**
* Knowledge Base (RAG) Example
* Demonstrates retrieval-augmented generation
*/
import { KnowledgeBase, createKnowledgeBase, Chunking } from 'praisonai';
async function main() {
// Create knowledge base
const kb = createKnowledgeBase();
// Add documents
const documents = [
{
id: 'ts-intro',
content: 'TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.',
metadata: { category: 'introduction', language: 'typescript' }
},
{
id: 'ts-types',
content: 'TypeScript adds optional static typing and class-based object-oriented programming to the language.',
metadata: { category: 'features', language: 'typescript' }
},
{
id: 'ts-benefits',
content: 'Benefits of TypeScript include catching errors early, better IDE support, and improved code maintainability.',
metadata: { category: 'benefits', language: 'typescript' }
},
{
id: 'js-intro',
content: 'JavaScript is a lightweight, interpreted programming language with first-class functions.',
metadata: { category: 'introduction', language: 'javascript' }
}
];
for (const doc of documents) {
await kb.add(doc);
}
console.log('Documents added:', kb.size);
// Search
console.log('\n=== Search: "TypeScript benefits" ===');
const results = await kb.search('TypeScript benefits', 3);
results.forEach(r => {
console.log(`Score: ${r.score.toFixed(2)} - ${r.document.content.substring(0, 50)}...`);
});
// Build context
console.log('\n=== Context for LLM ===');
const context = kb.buildContext(results);
console.log(context);
// Chunking example
console.log('\n=== Chunking Example ===');
const chunker = new Chunking({ chunkSize: 100, overlap: 20 });
const longText = 'This is a long document. '.repeat(20);
const chunks = chunker.chunk(longText);
console.log(`Chunked into ${chunks.length} pieces`);
}
main().catch(console.error);
+42
View File
@@ -0,0 +1,42 @@
/**
* Conversation Memory Example
* Demonstrates memory management for conversations
*/
import { Memory, createMemory } from 'praisonai';
async function main() {
const memory = createMemory({
maxEntries: 100,
maxTokens: 10000
});
// Simulate a conversation
await memory.add('What is TypeScript?', 'user');
await memory.add('TypeScript is a strongly typed programming language that builds on JavaScript.', 'assistant');
await memory.add('What are its main benefits?', 'user');
await memory.add('Key benefits include type safety, better IDE support, and easier refactoring.', 'assistant');
await memory.add('Can you give an example?', 'user');
console.log('Memory size:', memory.size, 'entries\n');
// Get recent messages
console.log('=== Recent Messages ===');
const recent = memory.getRecent(3);
recent.forEach(entry => {
console.log(`[${entry.role}]: ${entry.content.substring(0, 50)}...`);
});
// Search memory
console.log('\n=== Search Results for "TypeScript" ===');
const results = await memory.search('TypeScript');
results.forEach(r => {
console.log(`Score: ${r.score.toFixed(2)} - ${r.entry.content.substring(0, 40)}...`);
});
// Build context
console.log('\n=== Full Context ===');
console.log(memory.buildContext());
}
main().catch(console.error);
+60
View File
@@ -0,0 +1,60 @@
/**
* Observability Example
* Demonstrates tracing and monitoring
*/
import { MemoryObservabilityAdapter, ConsoleObservabilityAdapter } from 'praisonai';
async function main() {
// Memory adapter - stores traces in memory
const memoryAdapter = new MemoryObservabilityAdapter();
// Start a trace
const trace = memoryAdapter.startTrace('agent-execution');
console.log('Trace started:', trace.traceId);
// Create spans for different operations
const llmSpan = trace.startSpan('llm-call', 'llm');
llmSpan.setAttribute('model', 'gpt-4o-mini');
llmSpan.setAttribute('tokens', 150);
await new Promise(r => setTimeout(r, 100)); // Simulate work
llmSpan.end();
const toolSpan = trace.startSpan('tool-execution', 'tool');
toolSpan.setAttribute('tool', 'calculator');
toolSpan.addEvent('tool-started', { input: '2+2' });
await new Promise(r => setTimeout(r, 50)); // Simulate work
toolSpan.addEvent('tool-completed', { output: '4' });
toolSpan.end();
// End trace
trace.end();
// Retrieve trace data
const traceData = memoryAdapter.getTrace(trace.traceId);
console.log('\n=== Trace Data ===');
console.log('Trace ID:', traceData?.traceId);
console.log('Spans:', traceData?.spans.length);
console.log('Duration:', traceData?.endTime! - traceData?.startTime!, 'ms');
// Show spans
console.log('\n=== Spans ===');
traceData?.spans.forEach(span => {
console.log(`- ${span.name} (${span.kind}): ${span.endTime! - span.startTime}ms`);
if (span.attributes) {
Object.entries(span.attributes).forEach(([k, v]) => {
console.log(` ${k}: ${v}`);
});
}
});
// Console adapter example
console.log('\n=== Console Adapter ===');
const consoleAdapter = new ConsoleObservabilityAdapter();
const trace2 = consoleAdapter.startTrace('demo-trace');
const span = trace2.startSpan('demo-span', 'custom');
span.end();
trace2.end();
}
main().catch(console.error);
+71
View File
@@ -0,0 +1,71 @@
/**
* Planning System Example
* Demonstrates plans, steps, and todo lists
*/
import { Plan, PlanStep, TodoList, TodoItem, PlanStorage, createPlan, createTodoList } from 'praisonai';
async function main() {
// Create a project plan
console.log('=== Project Plan ===');
const plan = createPlan({
name: 'Build AI Agent',
description: 'Create a fully functional AI agent'
});
plan.addStep(new PlanStep({ description: 'Design agent architecture' }));
plan.addStep(new PlanStep({ description: 'Implement core functionality' }));
plan.addStep(new PlanStep({ description: 'Add tool support' }));
plan.addStep(new PlanStep({ description: 'Write tests' }));
plan.addStep(new PlanStep({ description: 'Deploy to production' }));
console.log('Plan:', plan.name);
console.log('Steps:', plan.steps.length);
// Execute steps
plan.start();
plan.steps[0].start();
plan.steps[0].complete();
plan.steps[1].start();
plan.steps[1].complete();
const progress = plan.getProgress();
console.log(`Progress: ${progress.completed}/${progress.total} (${progress.percentage}%)`);
// Create todo list
console.log('\n=== Todo List ===');
const todos = createTodoList('Sprint Tasks');
todos.add(new TodoItem({ content: 'Review PR #123', priority: 'high' }));
todos.add(new TodoItem({ content: 'Fix bug in router', priority: 'high' }));
todos.add(new TodoItem({ content: 'Update documentation', priority: 'medium' }));
todos.add(new TodoItem({ content: 'Refactor tests', priority: 'low' }));
console.log('Total items:', todos.items.length);
console.log('High priority:', todos.getByPriority('high').length);
// Complete some items
todos.items[0].complete();
todos.items[1].start();
console.log('Pending:', todos.getPending().length);
console.log('Completed:', todos.getCompleted().length);
const todoProgress = todos.getProgress();
console.log(`Progress: ${todoProgress.completed}/${todoProgress.total} (${todoProgress.percentage}%)`);
// Use storage
console.log('\n=== Plan Storage ===');
const storage = new PlanStorage();
storage.savePlan(plan);
storage.saveTodoList(todos);
console.log('Saved plans:', storage.listPlans().length);
console.log('Saved todo lists:', storage.listTodoLists().length);
// Retrieve
const retrieved = storage.getPlan(plan.id);
console.log('Retrieved plan:', retrieved?.name);
}
main().catch(console.error);
+74
View File
@@ -0,0 +1,74 @@
/**
* Skills System Example
* Demonstrates skill management and parsing
*/
import { SkillManager, parseSkillFile, createSkillManager } from 'praisonai';
async function main() {
// Parse a skill file
const skillContent = `---
name: code-review
description: Reviews code for best practices and potential issues
license: MIT
metadata:
author: praisonai
version: "1.0"
allowed-tools: Read Grep
---
# Code Review Skill
When reviewing code:
1. Check for syntax errors
2. Look for security vulnerabilities
3. Suggest performance improvements
4. Ensure proper error handling
`;
console.log('=== Parse Skill File ===');
const skill = parseSkillFile(skillContent);
console.log('Name:', skill.metadata.name);
console.log('Description:', skill.metadata.description);
console.log('License:', skill.metadata.license);
console.log('Instructions preview:', skill.instructions.substring(0, 100) + '...');
// Create skill manager
console.log('\n=== Skill Manager ===');
const manager = createSkillManager({ paths: [] });
// Register skills manually
manager.register({
metadata: {
name: 'typescript-expert',
description: 'Expert knowledge in TypeScript development'
},
instructions: 'You are a TypeScript expert. Help with type definitions, generics, and best practices.'
});
manager.register({
metadata: {
name: 'api-designer',
description: 'Designs RESTful APIs following best practices'
},
instructions: 'You design clean, RESTful APIs with proper versioning and documentation.'
});
// List skills
console.log('Registered skills:');
manager.list().forEach(s => {
console.log(`- ${s.metadata.name}: ${s.metadata.description}`);
});
// Get specific skill
const tsSkill = manager.get('typescript-expert');
console.log('\n=== TypeScript Expert Skill ===');
console.log('Instructions:', tsSkill?.instructions);
// Generate prompt
console.log('\n=== Generated Prompt ===');
const prompt = manager.generatePrompt(['typescript-expert']);
console.log(prompt.substring(0, 200) + '...');
}
main().catch(console.error);
+61
View File
@@ -0,0 +1,61 @@
/**
* Basic Tools Example
* Demonstrates tool creation and usage
*/
import { tool, ToolRegistry, getRegistry } from 'praisonai';
async function main() {
// Create tools
const calculator = tool({
name: 'calculator',
description: 'Perform basic math calculations',
parameters: {
type: 'object',
properties: {
expression: { type: 'string', description: 'Math expression to evaluate' }
},
required: ['expression']
},
execute: async ({ expression }) => {
try {
const result = eval(expression);
return `Result: ${result}`;
} catch (e) {
return 'Error: Invalid expression';
}
}
});
const greeter = tool({
name: 'greeter',
description: 'Generate a greeting',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name to greet' }
},
required: ['name']
},
execute: async ({ name }) => `Hello, ${name}! Welcome to PraisonAI.`
});
// Create registry and register tools
const registry = new ToolRegistry();
registry.register(calculator);
registry.register(greeter);
console.log('Registered tools:', registry.list().map(t => t.name));
// Execute tools
const calcResult = await calculator.execute({ expression: '10 * 5 + 2' });
console.log('\nCalculator:', calcResult);
const greetResult = await greeter.execute({ name: 'Developer' });
console.log('Greeter:', greetResult);
// Get OpenAI format
console.log('\nOpenAI tool format:', JSON.stringify(calculator.toOpenAITool(), null, 2));
}
main().catch(console.error);
+113
View File
@@ -0,0 +1,113 @@
/**
* Custom Tool Example
* Demonstrates creating custom tools using BaseTool class (plugin pattern)
*/
import { BaseTool, createTool, tool, ToolRegistry } from 'praisonai';
// Method 1: Extend BaseTool class (recommended for complex tools)
class WeatherTool extends BaseTool<{ location: string; units?: string }, { temp: number; condition: string }> {
name = 'get_weather';
description = 'Get current weather for a location';
parameters = {
type: 'object' as const,
properties: {
location: { type: 'string', description: 'City name' },
units: { type: 'string', description: 'celsius or fahrenheit', default: 'celsius' }
},
required: ['location']
};
async run(params: { location: string; units?: string }): Promise<{ temp: number; condition: string }> {
// Simulate API call
console.log(`Getting weather for ${params.location} in ${params.units || 'celsius'}`);
return { temp: 22, condition: 'sunny' };
}
}
// Method 2: Use createTool function (quick inline tools)
const calculatorTool = createTool({
name: 'calculator',
description: 'Evaluate a math expression',
parameters: {
type: 'object',
properties: {
expression: { type: 'string', description: 'Math expression to evaluate' }
},
required: ['expression']
},
run: (params: { expression: string }) => {
try {
return eval(params.expression);
} catch {
return 'Error: Invalid expression';
}
}
});
// Method 3: Use tool() function with config object
const greeterTool = tool({
name: 'greeter',
description: 'Generate a personalized greeting',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name to greet' },
style: { type: 'string', description: 'Greeting style: formal or casual' }
},
required: ['name']
},
execute: async (params: { name: string; style?: string }) => {
if (params.style === 'formal') {
return `Good day, ${params.name}. How may I assist you?`;
}
return `Hey ${params.name}! What's up?`;
}
});
async function main() {
console.log('=== Custom Tool Examples ===\n');
// Test WeatherTool (class-based)
console.log('--- WeatherTool (class-based) ---');
const weather = new WeatherTool();
console.log('Name:', weather.name);
console.log('Description:', weather.description);
const weatherResult = await weather.run({ location: 'New York' });
console.log('Result:', weatherResult);
// Safe execution with error handling
const safeResult = await weather.safeRun({ location: 'London' });
console.log('Safe result:', safeResult);
// Get OpenAI schema
console.log('OpenAI Schema:', JSON.stringify(weather.getSchema(), null, 2));
// Test calculatorTool (createTool)
console.log('\n--- Calculator Tool (createTool) ---');
const calcResult = await calculatorTool.run({ expression: '10 * 5 + 2' });
console.log('10 * 5 + 2 =', calcResult);
// Test greeterTool (tool function)
console.log('\n--- Greeter Tool (tool function) ---');
const greeting1 = await greeterTool.execute({ name: 'Alice' });
console.log('Casual:', greeting1);
const greeting2 = await greeterTool.execute({ name: 'Mr. Smith', style: 'formal' });
console.log('Formal:', greeting2);
// Register tools in registry
console.log('\n--- Tool Registry ---');
const registry = new ToolRegistry();
registry.register(greeterTool);
console.log('Registered tools:', registry.list().map(t => t.name));
console.log('OpenAI tools format:', registry.toOpenAITools().length, 'tools');
// Validate tool
console.log('\n--- Validation ---');
console.log('WeatherTool valid:', weather.validate());
}
main().catch(console.error);
@@ -0,0 +1,57 @@
/**
* Pinecone Vector Store Example
*/
import { createPineconeStore } from 'praisonai';
async function main() {
const pinecone = createPineconeStore({
apiKey: process.env.PINECONE_API_KEY!
});
const indexName = 'test-index';
// Create index
console.log('Creating index...');
await pinecone.createIndex({
indexName,
dimension: 3,
metric: 'cosine'
});
// List indexes
const indexes = await pinecone.listIndexes();
console.log('Indexes:', indexes);
// Upsert vectors
console.log('Upserting vectors...');
await pinecone.upsert({
indexName,
vectors: [
{ id: 'vec-1', vector: [0.1, 0.2, 0.3], metadata: { category: 'A' } },
{ id: 'vec-2', vector: [0.4, 0.5, 0.6], metadata: { category: 'B' } },
{ id: 'vec-3', vector: [0.7, 0.8, 0.9], metadata: { category: 'A' } }
]
});
// Query
console.log('Querying...');
const results = await pinecone.query({
indexName,
vector: [0.1, 0.2, 0.3],
topK: 2,
includeMetadata: true
});
console.log('Results:');
for (const r of results) {
console.log(` ${r.id}: score=${r.score.toFixed(4)}, metadata=${JSON.stringify(r.metadata)}`);
}
// Cleanup
console.log('Deleting index...');
await pinecone.deleteIndex(indexName);
console.log('Done!');
}
main().catch(console.error);
+30
View File
@@ -0,0 +1,30 @@
/**
* Voice/TTS Example
*/
import { createOpenAIVoice } from '../../../src/praisonai-ts/src';
import { writeFileSync } from 'fs';
async function main() {
const voice = createOpenAIVoice({
apiKey: process.env.OPENAI_API_KEY
});
// Check available voices
const speakers = await voice.getSpeakers();
console.log('Available voices:', speakers.map(s => s.name).join(', '));
// Generate speech
console.log('Generating speech...');
const audio = await voice.speak('Hello! Welcome to PraisonAI TypeScript SDK.', {
voice: 'nova',
speed: 1.0,
format: 'mp3'
});
// Save to file
writeFileSync('output.mp3', audio);
console.log('Audio saved to output.mp3');
}
main().catch(console.error);
+29
View File
@@ -0,0 +1,29 @@
/**
* Basic Workflow Example
* Demonstrates sequential workflow execution
*/
import { Workflow, parallel, route, loop } from 'praisonai';
async function main() {
// Create a simple workflow
const workflow = new Workflow<string, string>('DataProcessor')
.step('validate', async (input) => {
console.log('Step 1: Validating input...');
return input.trim();
})
.step('transform', async (input) => {
console.log('Step 2: Transforming...');
return input.toUpperCase();
})
.step('format', async (input) => {
console.log('Step 3: Formatting...');
return `[PROCESSED] ${input}`;
});
const result = await workflow.run(' hello world ');
console.log('\nFinal output:', result.output);
console.log('Steps executed:', result.steps.length);
}
main().catch(console.error);
@@ -0,0 +1,48 @@
/**
* Conditional Workflow Example
* Demonstrates routing based on conditions
*/
import { route } from 'praisonai';
async function main() {
const processRequest = async (type: string) => {
console.log(`Processing request type: ${type}`);
const result = await route([
{
condition: () => type === 'urgent',
execute: async () => {
console.log(' -> Urgent handler activated');
return 'Processed urgently';
}
},
{
condition: () => type === 'normal',
execute: async () => {
console.log(' -> Normal handler activated');
return 'Processed normally';
}
},
{
condition: () => true, // Default
execute: async () => {
console.log(' -> Default handler activated');
return 'Processed with default handler';
}
}
]);
return result;
};
// Test different request types
const types = ['urgent', 'normal', 'unknown'];
for (const type of types) {
const result = await processRequest(type);
console.log(`Result: ${result}\n`);
}
}
main().catch(console.error);
@@ -0,0 +1,36 @@
/**
* Parallel Workflow Example
* Demonstrates parallel execution of tasks
*/
import { parallel } from 'praisonai';
async function main() {
console.log('Starting parallel tasks...\n');
const results = await parallel([
async () => {
console.log('Task 1: Starting...');
await new Promise(r => setTimeout(r, 1000));
console.log('Task 1: Complete');
return 'Result 1';
},
async () => {
console.log('Task 2: Starting...');
await new Promise(r => setTimeout(r, 500));
console.log('Task 2: Complete');
return 'Result 2';
},
async () => {
console.log('Task 3: Starting...');
await new Promise(r => setTimeout(r, 750));
console.log('Task 3: Complete');
return 'Result 3';
}
]);
console.log('\nAll tasks complete!');
console.log('Results:', results);
}
main().catch(console.error);
@@ -89,11 +89,9 @@ class AccuracyEvaluator(BaseEvaluator):
EvaluationScore with score and reasoning
"""
try:
from openai import OpenAI
import litellm
except ImportError:
raise ImportError("OpenAI package required for accuracy evaluation. Install with: pip install openai")
client = OpenAI()
raise ImportError("litellm package required for accuracy evaluation. Install with: pip install litellm")
prompt = f"""You are an expert evaluator. Compare the actual output against the expected output and provide a score from 1-10.
@@ -115,7 +113,8 @@ Respond in this exact format:
SCORE: [number 1-10]
REASONING: [brief explanation]"""
response = client.chat.completions.create(
# Use litellm for unified multi-provider support
response = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
@@ -99,11 +99,9 @@ class CriteriaEvaluator(BaseEvaluator):
CriteriaScore with score, passed status, and reasoning
"""
try:
from openai import OpenAI
import litellm
except ImportError:
raise ImportError("OpenAI package required for criteria evaluation. Install with: pip install openai")
client = OpenAI()
raise ImportError("litellm package required for criteria evaluation. Install with: pip install litellm")
if self.scoring_type == "binary":
prompt = f"""You are an expert evaluator. Evaluate the following output against the given criteria.
@@ -138,7 +136,8 @@ Respond in this exact format:
SCORE: [number 1-10]
REASONING: [brief explanation]"""
response = client.chat.completions.create(
# Use litellm for unified multi-provider support
response = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
@@ -91,19 +91,20 @@ class MongoDBMemory:
raise Exception(f"Failed to initialize MongoDB: {e}")
def _init_embedding_model(self):
"""Initialize embedding model from config."""
"""Initialize embedding model from config using litellm for unified provider support."""
try:
# Set up embedding model based on config
embedder_config = self.config.get("embedder", {})
if embedder_config.get("provider") == "openai":
import openai
self.embedding_model = openai.OpenAI()
self.embedding_model_name = embedder_config.get("config", {}).get("model", "text-embedding-3-small")
else:
# Default to OpenAI
import openai
self.embedding_model = openai.OpenAI()
self.embedding_model_name = "text-embedding-3-small"
provider = embedder_config.get("provider", "openai")
model_name = embedder_config.get("config", {}).get("model", "text-embedding-3-small")
# Store model name for later use
self.embedding_model_name = model_name
# Use litellm for embeddings - it handles all providers uniformly
# We'll use litellm.embedding() in _get_embeddings() instead of storing a client
self.embedding_provider = provider
self.embedding_model = None # Will use litellm.embedding() directly
except Exception as e:
raise Exception(f"Failed to initialize embedding model: {e}")
@@ -172,13 +173,16 @@ class MongoDBMemory:
logging.warning(f"Could not create vector search index: {e}")
def _get_embedding(self, text):
"""Get embedding for text."""
"""Get embedding for text using litellm for unified provider support."""
try:
response = self.embedding_model.embeddings.create(
input=text,
model=self.embedding_model_name
import litellm
# Use litellm.embedding() for unified multi-provider support
response = litellm.embedding(
model=self.embedding_model_name,
input=text
)
return response.data[0].embedding
return response.data[0]["embedding"]
except Exception as e:
logging.error(f"Error getting embedding: {e}")
return None
@@ -500,6 +504,33 @@ class Knowledge:
base_config["graph_store"] = self._config["graph_store"]
return base_config
def _prepare_mem0_config(self, config):
"""Prepare config for mem0 by removing PraisonAI-specific fields.
mem0's RerankerConfig only accepts 'provider' and 'config' fields.
PraisonAI adds 'enabled' and 'default_rerank' for internal use.
"""
mem0_config = config.copy()
# Strip PraisonAI-specific reranker fields that mem0 doesn't accept
if "reranker" in mem0_config:
reranker = mem0_config["reranker"]
if isinstance(reranker, dict):
# Keep only mem0-compatible fields: provider, config
mem0_reranker = {}
if "provider" in reranker:
mem0_reranker["provider"] = reranker["provider"]
if "config" in reranker:
mem0_reranker["config"] = reranker["config"]
# If no valid mem0 fields, remove reranker entirely
if mem0_reranker:
mem0_config["reranker"] = mem0_reranker
else:
del mem0_config["reranker"]
return mem0_config
@cached_property
def memory(self):
# Check if MongoDB provider is specified
@@ -511,19 +542,22 @@ class Knowledge:
# Fall back to default memory
pass
# Prepare config for mem0 (strip PraisonAI-specific fields)
mem0_config = self._prepare_mem0_config(self.config)
# Default Mem0 memory
try:
return CustomMemory.from_config(self.config)
return CustomMemory.from_config(mem0_config)
except (NotImplementedError, ValueError) as e:
if "list_collections" in str(e) or "Extra fields not allowed" in str(e):
# Keep only allowed fields
vector_store_config = {
"collection_name": self.config["vector_store"]["config"]["collection_name"],
"path": self.config["vector_store"]["config"]["path"]
"collection_name": mem0_config["vector_store"]["config"]["collection_name"],
"path": mem0_config["vector_store"]["config"]["path"]
}
self.config["vector_store"]["config"] = vector_store_config
mem0_config["vector_store"]["config"] = vector_store_config
from mem0 import Memory
return Memory.from_config(self.config)
return Memory.from_config(mem0_config)
raise
@cached_property
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.3.2"
version = "0.3.4"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
@@ -1189,16 +1189,16 @@ class TestWorkflowConfiguration:
"""Test that workflow planning setting is stored."""
def step1(ctx): return StepResult(output="Done")
workflow = Workflow(steps=[step1], planning=True, planning_llm="gpt-4o")
workflow = Workflow(steps=[step1], planning=True, planning_llm="gpt-5-nano")
assert workflow.planning == True
assert workflow.planning_llm == "gpt-4o"
assert workflow.planning_llm == "gpt-5-nano"
def test_workflow_default_llm(self):
"""Test default LLM setting."""
def step1(ctx): return StepResult(output="Done")
workflow = Workflow(steps=[step1], default_llm="gpt-4o-mini")
assert workflow.default_llm == "gpt-4o-mini"
workflow = Workflow(steps=[step1], default_llm="gpt-5-nano")
assert workflow.default_llm == "gpt-5-nano"
def test_workflow_default_agent_config(self):
"""Test default agent config."""
@@ -1393,10 +1393,10 @@ class TestPlanningAndReasoning:
workflow = Workflow(
steps=[lambda ctx: StepResult(output="test")],
planning=True,
planning_llm="gpt-4o"
planning_llm="gpt-5-nano"
)
assert workflow.planning == True
assert workflow.planning_llm == "gpt-4o"
assert workflow.planning_llm == "gpt-5-nano"
def test_workflow_reasoning_parameter_exists(self):
"""Test that Workflow accepts reasoning parameter."""
@@ -91,14 +91,11 @@ class TestAccuracyEvaluator:
def test_judge_output(self):
"""Test LLM judging of output."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 8\nREASONING: Good match"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "4"
@@ -115,14 +112,11 @@ class TestAccuracyEvaluator:
def test_run_returns_accuracy_result(self):
"""Test that run returns AccuracyResult."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 9\nREASONING: Excellent"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "4"
@@ -140,14 +134,11 @@ class TestAccuracyEvaluator:
def test_evaluate_output(self):
"""Test evaluate_output method for pre-generated outputs."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 10\nREASONING: Perfect"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "unused"
@@ -325,14 +316,11 @@ class TestCriteriaEvaluator:
def test_judge_output_numeric(self):
"""Test numeric scoring."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 8\nREASONING: Good"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "test"
@@ -350,14 +338,11 @@ class TestCriteriaEvaluator:
def test_judge_output_binary(self):
"""Test binary scoring."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "RESULT: PASS\nREASONING: Meets criteria"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "test"
@@ -374,14 +359,11 @@ class TestCriteriaEvaluator:
def test_on_fail_callback(self):
"""Test on_fail callback is called."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 3\nREASONING: Poor"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
on_fail_called = []
def on_fail(score):
@@ -403,14 +385,11 @@ class TestCriteriaEvaluator:
def test_evaluate_output_criteria(self):
"""Test evaluate_output method."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 9\nREASONING: Great"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "unused"
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.3.2"
version = "0.3.4"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -0,0 +1,229 @@
{
"version": "1.2.1",
"lastUpdated": "2024-12-26",
"implementationStatus": "Phase 4 Complete - Full Feature Parity",
"summary": {
"pythonCoreFeatures": 85,
"pythonWrapperFeatures": 45,
"typescriptFeatures": 75,
"gapCount": 10,
"priorityP0": 0,
"priorityP1": 3,
"priorityP2": 5,
"priorityP3": 2
},
"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", "ImageAgent", "ContextAgent", "DeepResearchAgent", "QueryRewriterAgent", "PromptExpanderAgent",
"RouterAgent", "Handoff", "AutoAgents", "Task", "BaseTool", "ToolResult", "tool", "FunctionTool",
"ToolRegistry", "createTool", "validateTool", "Session", "SessionManager", "Memory", "KnowledgeBase",
"Chunking", "MemoryAdapter", "createDbFactory", "Workflow", "parallel", "route", "Guardrail",
"GuardrailManager", "LLMGuardrail", "accuracyEval", "performanceEval", "reliabilityEval", "EvalSuite",
"MemoryObservabilityAdapter", "ConsoleObservabilityAdapter", "SkillManager", "TelemetryCollector",
"Plan", "PlanStep", "TodoList", "TodoItem", "PlanStorage",
"OpenAIProvider", "AnthropicProvider", "GoogleProvider", "createProvider"
],
"modules": {
"agent": ["Agent", "ImageAgent", "ContextAgent", "DeepResearchAgent", "QueryRewriterAgent", "PromptExpanderAgent", "RouterAgent", "Handoff"],
"auto": ["AutoAgents", "createAutoAgents"],
"task": ["Task"],
"tools": ["BaseTool", "ToolResult", "tool", "FunctionTool", "ToolRegistry", "createTool", "validateTool", "arxivTools", "mcpSse"],
"llm": ["OpenAIProvider", "AnthropicProvider", "GoogleProvider", "createProvider", "BaseProvider"],
"memory": ["Memory", "createMemory", "MemoryEntry"],
"knowledge": ["KnowledgeBase", "Chunking", "GraphRAG", "GraphStore", "BaseReranker", "CohereReranker", "CrossEncoderReranker", "LLMReranker"],
"db": ["MemoryAdapter", "createDbFactory", "SQLiteAdapter", "UpstashRedisAdapter", "MemoryRedisAdapter", "NeonPostgresAdapter", "MemoryPostgresAdapter", "PostgresSessionStorage"],
"session": ["Session", "SessionManager"],
"workflows": ["Workflow", "parallel", "route", "loop", "repeat", "WorkflowStep", "WorkflowContext", "parseYAMLWorkflow", "createWorkflowFromYAML"],
"guardrails": ["Guardrail", "GuardrailManager", "LLMGuardrail", "builtInGuardrails"],
"eval": ["accuracyEval", "performanceEval", "reliabilityEval", "EvalSuite"],
"observability": ["MemoryObservabilityAdapter", "ConsoleObservabilityProvider", "LangfuseObservabilityProvider", "BaseObservabilityProvider"],
"skills": ["SkillManager", "parseSkillFile", "createSkillManager"],
"telemetry": ["TelemetryCollector", "getTelemetry", "enableTelemetry", "disableTelemetry"],
"planning": ["Plan", "PlanStep", "TodoList", "TodoItem", "PlanStorage"],
"cache": ["MemoryCache", "FileCache", "BaseCache", "createMemoryCache", "createFileCache"],
"events": ["EventEmitterPubSub", "AgentEventBus", "PubSub", "createEventBus", "createPubSub", "AgentEvents"],
"vector": ["BaseVectorStore", "MemoryVectorStore", "PineconeVectorStore", "WeaviateVectorStore", "QdrantVectorStore", "ChromaVectorStore"],
"voice": ["BaseVoiceProvider", "OpenAIVoiceProvider", "ElevenLabsVoiceProvider"],
"cli": ["chat", "listProviders", "version", "help"]
}
},
"gapMatrix": {
"P0_CoreParity": [
{"feature": "Multi-provider LLM support (OpenAI, Anthropic, Google)", "python": true, "typescript": true, "priority": "P0", "effort": "high", "status": "DONE"},
{"feature": "Streaming responses", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
{"feature": "Structured outputs (JSON schema)", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
{"feature": "Tool/function calling with validation", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
{"feature": "Session management (session_id, run_id)", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
{"feature": "Multi-agent orchestration patterns", "python": true, "typescript": true, "priority": "P0", "effort": "high", "status": "DONE"},
{"feature": "Agent handoff", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
{"feature": "Tool decorator (@tool)", "python": true, "typescript": true, "priority": "P0", "effort": "low", "status": "DONE"},
{"feature": "Tool registry", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
{"feature": "Async agent execution", "python": true, "typescript": true, "priority": "P0", "effort": "low", "status": "DONE"},
{"feature": "Error handling and retries", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
{"feature": "Verbose/debug logging", "python": true, "typescript": true, "priority": "P0", "effort": "low", "status": "DONE"}
],
"P1_Persistence": [
{"feature": "Database adapter protocol", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
{"feature": "SQLite adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
{"feature": "PostgreSQL adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
{"feature": "Redis adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
{"feature": "Message persistence", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"},
{"feature": "Run/trace persistence", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"},
{"feature": "Memory persistence (vector)", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
{"feature": "Knowledge base (RAG)", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
{"feature": "Chunking strategies", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"},
{"feature": "Vector store integration", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
{"feature": "Semantic search/recall", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
{"feature": "Working memory", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"},
{"feature": "File-based memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
{"feature": "Auto-memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
{"feature": "Session export/import", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"}
],
"P2_CLI": [
{"feature": "CLI entrypoint", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
{"feature": "Slash commands", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
{"feature": "Interactive TUI", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
{"feature": "Cost tracking", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
{"feature": "Repo map", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
{"feature": "Git integration", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
{"feature": "Sandbox executor", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
{"feature": "Autonomy modes", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
{"feature": "Agent scheduler", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
{"feature": "Background jobs", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
{"feature": "Checkpoints", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
{"feature": "Flow display", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
{"feature": "Profiling", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
{"feature": "Metrics", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
{"feature": "External agent integrations", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
{"feature": "N8N integration", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
{"feature": "MCP CLI support", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
{"feature": "Skills CLI", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
{"feature": "Eval CLI", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
{"feature": "Workflow CLI", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"}
],
"P3_Advanced": [
{"feature": "Workflows/Pipelines", "python": true, "typescript": true, "priority": "P3", "effort": "high", "status": "DONE"},
{"feature": "Route pattern", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Parallel pattern", "python": true, "typescript": true, "priority": "P3", "effort": "low", "status": "DONE"},
{"feature": "Loop pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium", "status": "TODO"},
{"feature": "Repeat pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium", "status": "TODO"},
{"feature": "YAML workflow parser", "python": true, "typescript": false, "priority": "P3", "effort": "medium", "status": "TODO"},
{"feature": "Guardrails", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "LLM Guardrail", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Evaluation framework", "python": true, "typescript": true, "priority": "P3", "effort": "high", "status": "DONE"},
{"feature": "Accuracy eval", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Performance eval", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Reliability eval", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Criteria eval", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Observability protocol", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Telemetry", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Performance monitoring", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Token tracking", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
{"feature": "Skills system", "python": true, "typescript": true, "priority": "P3", "effort": "high", "status": "DONE"},
{"feature": "Fast context", "python": true, "typescript": false, "priority": "P3", "effort": "medium", "status": "TODO"},
{"feature": "Image agent", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"}
]
},
"mastraCapabilities": {
"implemented": [
"Agent core", "Tool calling", "Streaming (partial)", "Multi-agent (partial)"
],
"missing": [
"Memory with semantic recall", "Working memory", "Thread management", "Resource scoping",
"Storage abstraction", "Vector store integration", "Embeddings", "Evaluation scorers",
"Workflow engine", "Step execution", "Branching logic", "Event-driven workflows",
"MCP integration", "Voice/TTS", "Observability hooks", "Request context",
"Tool validation", "Tool suspend/resume", "Provider options per tool"
]
},
"implementationPlan": {
"phase1_P0": {
"description": "Core agent parity - streaming, structured outputs, sessions, tool system",
"tasks": [
"Implement AI SDK provider abstraction (OpenAI, Anthropic, Google)",
"Add streaming response support with proper typing",
"Implement structured output with JSON schema/Zod",
"Add session management (session_id, run_id, trace_id)",
"Implement tool decorator and registry",
"Add agent handoff support",
"Improve error handling and retries"
]
},
"phase2_P1": {
"description": "Persistence layer - db adapters, memory, knowledge",
"tasks": [
"Implement DbAdapter protocol",
"Add SQLite adapter",
"Add message/run/trace persistence",
"Implement Memory with persistence",
"Add Knowledge base (RAG) support",
"Implement vector store integration"
]
},
"phase3_P2": {
"description": "CLI parity",
"tasks": [
"Create CLI entrypoint",
"Implement slash commands",
"Add cost tracking",
"Implement interactive mode"
]
},
"phase4_P3": {
"description": "Advanced features",
"tasks": [
"Implement Workflow engine",
"Add workflow patterns (Route, Parallel, Loop, Repeat)",
"Implement Guardrails",
"Add Evaluation framework",
"Implement Observability hooks",
"Add Skills system"
]
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ const config: Config.InitialOptions = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.main.test.ts', '**/*.spec.ts'],
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
coverageDirectory: 'coverage',
collectCoverageFrom: [
+4 -2
View File
@@ -1,9 +1,12 @@
{
"name": "praisonai",
"version": "1.0.19",
"version": "1.2.2",
"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": {
+266
View File
@@ -0,0 +1,266 @@
# PraisonAI CLI Specification v1.0.0
# This is the canonical contract for CLI behavior across all runtimes (Python, TypeScript, etc.)
# Any CLI implementation MUST conform to this spec for parity.
version: "1.0.0"
spec_name: "praisonai-cli"
# Binary names per ecosystem (NO COLLISION)
binaries:
python: praisonai
typescript: praisonai-ts
# Commands (all implementations MUST support)
commands:
chat:
description: "Chat with an AI agent"
args:
- name: prompt
type: string
required: true
position: 0
flags:
- name: model
short: m
type: string
description: "Model to use (e.g., openai/gpt-4o-mini)"
- name: stream
short: s
type: boolean
default: false
description: "Enable streaming output"
- name: session
type: string
description: "Session ID for conversation continuity"
run:
description: "Run an agent with a task"
args:
- name: task
type: string
required: true
position: 0
flags:
- name: agent
short: a
type: string
description: "Agent configuration file or name"
- name: tools
short: t
type: string
description: "Comma-separated list of tools"
workflow:
description: "Execute a multi-agent workflow"
args:
- name: file
type: string
required: true
position: 0
description: "Workflow YAML file path"
flags:
- name: parallel
type: boolean
default: false
description: "Run workflow steps in parallel where possible"
eval:
description: "Evaluate agent performance"
subcommands:
accuracy:
description: "Run accuracy evaluation"
flags:
- name: input
type: string
required: true
- name: expected
type: string
required: true
- name: iterations
type: integer
default: 1
performance:
description: "Run performance benchmark"
flags:
- name: iterations
type: integer
default: 10
- name: warmup
type: integer
default: 2
reliability:
description: "Run reliability check"
flags:
- name: expected-tools
type: string
description: "Comma-separated expected tool calls"
providers:
description: "List available LLM providers"
flags: []
tools:
description: "List or manage tools"
subcommands:
list:
description: "List available tools"
info:
description: "Show tool information"
args:
- name: name
type: string
required: true
position: 0
version:
description: "Show CLI version"
flags: []
help:
description: "Show help information"
args:
- name: command
type: string
required: false
position: 0
# Global flags (apply to all commands)
global_flags:
- name: verbose
short: v
type: boolean
default: false
description: "Enable verbose output"
- name: config
short: c
type: string
description: "Path to config file"
- name: profile
short: p
type: string
description: "Profile name to use"
- name: output
short: o
type: string
enum: [json, text, pretty]
default: pretty
description: "Output format"
- name: json
type: boolean
default: false
description: "Shorthand for --output json"
# Exit codes (MUST be identical across implementations)
exit_codes:
success: 0
runtime_error: 1
invalid_arguments: 2
config_error: 3
network_error: 4
auth_error: 5
# Output schema for JSON mode (MUST be byte-compatible)
output_schema:
success:
type: object
properties:
success:
type: boolean
value: true
data:
type: object
meta:
type: object
properties:
duration_ms:
type: number
model:
type: string
tokens:
type: object
properties:
input:
type: integer
output:
type: integer
error:
type: object
properties:
success:
type: boolean
value: false
error:
type: object
properties:
code:
type: string
message:
type: string
details:
type: object
# Environment variables (MUST be respected)
environment:
PRAISONAI_MODEL:
description: "Default model to use"
example: "openai/gpt-4o-mini"
PRAISONAI_PROFILE:
description: "Default profile name"
PRAISONAI_VERBOSE:
description: "Enable verbose mode (true/false)"
PRAISONAI_CONFIG:
description: "Path to config file"
OPENAI_API_KEY:
description: "OpenAI API key"
ANTHROPIC_API_KEY:
description: "Anthropic API key"
GOOGLE_API_KEY:
description: "Google API key"
# Config file schema reference
config:
files:
- .praisonai.yaml
- .praisonai.json
resolution_order:
- cli_flags
- environment_variables
- project_config
- user_config
- defaults
schema:
model:
type: string
default: "openai/gpt-4o-mini"
verbose:
type: boolean
default: false
stream:
type: boolean
default: false
profiles:
type: object
additionalProperties:
type: object
properties:
model:
type: string
verbose:
type: boolean
stream:
type: boolean
# Tool interface contract
tool_schema:
name:
type: string
required: true
description:
type: string
required: true
inputs:
type: object
description: "JSON Schema for tool inputs"
outputs:
type: object
description: "JSON Schema for tool outputs"
+159
View File
@@ -0,0 +1,159 @@
/**
* Context Agent - Agent with enhanced context management
*/
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
import { Session } from '../session';
import { KnowledgeBase, type SearchResult } from '../knowledge/rag';
export interface ContextAgentConfig {
name?: string;
instructions: string;
llm?: string;
knowledgeBase?: KnowledgeBase;
contextWindow?: number;
maxContextTokens?: number;
verbose?: boolean;
}
export interface ContextMessage {
role: 'system' | 'user' | 'assistant';
content: string;
timestamp: number;
metadata?: Record<string, any>;
}
/**
* Context Agent - Agent with RAG and context management
*/
export class ContextAgent {
readonly name: string;
readonly instructions: string;
private provider: LLMProvider;
private knowledgeBase?: KnowledgeBase;
private contextWindow: number;
private maxContextTokens: number;
private messages: ContextMessage[] = [];
private verbose: boolean;
constructor(config: ContextAgentConfig) {
this.name = config.name || `ContextAgent_${Math.random().toString(36).substr(2, 9)}`;
this.instructions = config.instructions;
this.provider = createProvider(config.llm || 'gpt-4o-mini');
this.knowledgeBase = config.knowledgeBase;
this.contextWindow = config.contextWindow ?? 10;
this.maxContextTokens = config.maxContextTokens ?? 4000;
this.verbose = config.verbose ?? false;
}
/**
* Chat with context awareness
*/
async chat(prompt: string): Promise<{ text: string; context?: SearchResult[] }> {
// Add user message
this.messages.push({
role: 'user',
content: prompt,
timestamp: Date.now()
});
// Build context from knowledge base
let ragContext: SearchResult[] = [];
let contextString = '';
if (this.knowledgeBase) {
ragContext = await this.knowledgeBase.search(prompt);
if (ragContext.length > 0) {
contextString = this.knowledgeBase.buildContext(ragContext);
if (this.verbose) {
console.log(`[ContextAgent] Found ${ragContext.length} relevant documents`);
}
}
}
// Build messages array with context
const systemPrompt = contextString
? `${this.instructions}\n\nRelevant context:\n${contextString}`
: this.instructions;
const messages: Message[] = [
{ role: 'system', content: systemPrompt },
...this.getRecentMessages()
];
// Generate response
const result = await this.provider.generateText({ messages });
// Add assistant message
this.messages.push({
role: 'assistant',
content: result.text,
timestamp: Date.now()
});
return {
text: result.text,
context: ragContext.length > 0 ? ragContext : undefined
};
}
/**
* Get recent messages within context window
*/
private getRecentMessages(): Message[] {
const recent = this.messages.slice(-this.contextWindow);
return recent.map(m => ({
role: m.role,
content: m.content
}));
}
/**
* Add document to knowledge base
*/
async addDocument(id: string, content: string, metadata?: Record<string, any>): Promise<void> {
if (!this.knowledgeBase) {
throw new Error('No knowledge base configured');
}
await this.knowledgeBase.add({ id, content, metadata });
}
/**
* Search knowledge base
*/
async searchKnowledge(query: string, limit?: number): Promise<SearchResult[]> {
if (!this.knowledgeBase) {
return [];
}
return this.knowledgeBase.search(query, limit);
}
/**
* Clear conversation history
*/
clearHistory(): void {
this.messages = [];
}
/**
* Get conversation history
*/
getHistory(): ContextMessage[] {
return [...this.messages];
}
/**
* Set knowledge base
*/
setKnowledgeBase(kb: KnowledgeBase): void {
this.knowledgeBase = kb;
}
}
/**
* Create a context agent
*/
export function createContextAgent(config: ContextAgentConfig): ContextAgent {
return new ContextAgent(config);
}
+337
View File
@@ -0,0 +1,337 @@
/**
* Enhanced Agent - Agent with session management, provider abstraction, and tool support
*/
import { createProvider, type LLMProvider, type Message, type ToolCall, type GenerateTextResult } from '../llm/providers';
import { Session, Run, Trace, getSessionManager } from '../session';
import { FunctionTool, ToolRegistry, tool } from '../tools/decorator';
import { Logger } from '../utils/logger';
export interface EnhancedAgentConfig {
name?: string;
instructions: string;
llm?: string;
tools?: Array<FunctionTool | Function | { name: string; description?: string; parameters?: any; execute: Function }>;
session?: Session;
sessionId?: string;
verbose?: boolean;
stream?: boolean;
maxToolCalls?: number;
temperature?: number;
maxTokens?: number;
outputSchema?: any;
}
export interface ChatOptions {
stream?: boolean;
temperature?: number;
maxTokens?: number;
outputSchema?: any;
onToken?: (token: string) => void;
}
export interface ChatResult {
text: string;
structured?: any;
toolCalls?: ToolCall[];
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
runId: string;
sessionId: string;
}
/**
* Enhanced Agent with full feature support
*/
export class EnhancedAgent {
readonly name: string;
readonly instructions: string;
readonly sessionId: string;
private provider: LLMProvider;
private session: Session;
private toolRegistry: ToolRegistry;
private verbose: boolean;
private stream: boolean;
private maxToolCalls: number;
private temperature: number;
private maxTokens?: number;
private outputSchema?: any;
constructor(config: EnhancedAgentConfig) {
this.name = config.name || `Agent_${Math.random().toString(36).substr(2, 9)}`;
this.instructions = config.instructions;
this.verbose = config.verbose ?? true;
this.stream = config.stream ?? true;
this.maxToolCalls = config.maxToolCalls ?? 10;
this.temperature = config.temperature ?? 0.7;
this.maxTokens = config.maxTokens;
this.outputSchema = config.outputSchema;
// Initialize provider
this.provider = createProvider(config.llm || 'gpt-4o-mini');
// Initialize session
if (config.session) {
this.session = config.session;
} else if (config.sessionId) {
this.session = getSessionManager().getOrCreate(config.sessionId);
} else {
this.session = getSessionManager().create();
}
this.sessionId = this.session.id;
// Initialize tool registry
this.toolRegistry = new ToolRegistry();
if (config.tools) {
this.registerTools(config.tools);
}
Logger.setVerbose(this.verbose);
}
private registerTools(tools: EnhancedAgentConfig['tools']): void {
if (!tools) return;
for (const t of tools) {
if (t instanceof FunctionTool) {
this.toolRegistry.register(t);
} else if (typeof t === 'function') {
// Convert function to tool
const funcName = t.name || `function_${Math.random().toString(36).substr(2, 9)}`;
this.toolRegistry.register(tool({
name: funcName,
description: `Function ${funcName}`,
execute: t as any,
}));
} else if (typeof t === 'object' && 'execute' in t) {
// Tool config object
this.toolRegistry.register(tool({
name: t.name,
description: t.description,
parameters: t.parameters,
execute: t.execute as any,
}));
}
}
}
/**
* Add a tool to the agent
*/
addTool(t: FunctionTool | { name: string; description?: string; parameters?: any; execute: Function }): this {
if (t instanceof FunctionTool) {
this.toolRegistry.register(t);
} else {
this.toolRegistry.register(tool({
name: t.name,
description: t.description,
parameters: t.parameters,
execute: t.execute as any,
}));
}
return this;
}
/**
* Get all registered tools
*/
getTools(): FunctionTool[] {
return this.toolRegistry.list();
}
/**
* Chat with the agent
*/
async chat(prompt: string, options: ChatOptions = {}): Promise<ChatResult> {
const run = this.session.createRun().start();
const trace = run.createTrace({ name: 'chat' }).start();
try {
// Add user message to session
this.session.addMessage({ role: 'user', content: prompt });
// Build messages array
const messages: Message[] = [
{ role: 'system', content: this.instructions },
...this.session.getMessagesForLLM() as Message[],
];
// Get tool definitions
const tools = this.toolRegistry.list().length > 0
? this.toolRegistry.getDefinitions()
: undefined;
let result: GenerateTextResult;
let fullText = '';
let allToolCalls: ToolCall[] = [];
let iterations = 0;
// Tool calling loop
while (iterations < this.maxToolCalls) {
iterations++;
if (options.stream && !tools && options.onToken) {
// Streaming without tools
const stream = await this.provider.streamText({
messages,
temperature: options.temperature ?? this.temperature,
maxTokens: options.maxTokens ?? this.maxTokens,
onToken: options.onToken,
});
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
for await (const chunk of stream) {
if (chunk.text) fullText += chunk.text;
if (chunk.usage) usage = { ...usage, ...chunk.usage };
}
result = {
text: fullText,
usage,
finishReason: 'stop',
};
break;
} else if (options.outputSchema) {
// Structured output
const objResult = await this.provider.generateObject({
messages,
schema: options.outputSchema ?? this.outputSchema,
temperature: options.temperature ?? this.temperature,
maxTokens: options.maxTokens ?? this.maxTokens,
});
result = {
text: JSON.stringify(objResult.object),
usage: objResult.usage,
finishReason: 'stop',
};
fullText = result.text;
break;
} else {
// Regular generation with potential tool calls
result = await this.provider.generateText({
messages,
temperature: options.temperature ?? this.temperature,
maxTokens: options.maxTokens ?? this.maxTokens,
tools,
});
if (result.toolCalls && result.toolCalls.length > 0) {
allToolCalls.push(...result.toolCalls);
// Add assistant message with tool calls
messages.push({
role: 'assistant',
content: result.text || null,
tool_calls: result.toolCalls,
});
// Execute tool calls
for (const tc of result.toolCalls) {
const toolTrace = trace.createChild({ name: `tool:${tc.function.name}` }).start();
try {
const toolFn = this.toolRegistry.get(tc.function.name);
if (!toolFn) {
throw new Error(`Tool '${tc.function.name}' not found`);
}
const args = JSON.parse(tc.function.arguments);
const toolResult = await toolFn.execute(args, {
agentName: this.name,
sessionId: this.sessionId,
runId: run.id,
});
const resultStr = typeof toolResult === 'string'
? toolResult
: JSON.stringify(toolResult);
messages.push({
role: 'tool',
content: resultStr,
tool_call_id: tc.id,
});
toolTrace.complete({ result: resultStr });
} catch (error: any) {
messages.push({
role: 'tool',
content: `Error: ${error.message}`,
tool_call_id: tc.id,
});
toolTrace.fail(error);
}
}
// Continue loop to get final response
continue;
}
// No tool calls, we have our final response
fullText = result.text;
break;
}
}
// Add assistant message to session
this.session.addMessage({
role: 'assistant',
content: fullText,
tool_calls: allToolCalls.length > 0 ? allToolCalls : undefined,
});
trace.complete({ responseLength: fullText.length });
run.complete();
// Parse structured output if schema was provided
let structured: any;
if (options.outputSchema) {
try {
structured = JSON.parse(fullText);
} catch {
// Not valid JSON
}
}
return {
text: fullText,
structured,
toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined,
usage: result!.usage,
runId: run.id,
sessionId: this.sessionId,
};
} catch (error: any) {
trace.fail(error);
run.fail(error);
throw error;
}
}
/**
* Start method for compatibility
*/
async start(prompt: string, options?: ChatOptions): Promise<string> {
const result = await this.chat(prompt, options);
return result.text;
}
/**
* Get the session
*/
getSession(): Session {
return this.session;
}
/**
* Clear conversation history
*/
clearHistory(): void {
this.session.clearMessages();
}
}
+162
View File
@@ -0,0 +1,162 @@
/**
* Agent Handoff - Transfer conversations between agents
*/
import type { EnhancedAgent } from './enhanced';
export interface HandoffConfig {
agent: EnhancedAgent;
name?: string;
description?: string;
condition?: (context: HandoffContext) => boolean;
transformContext?: (messages: any[]) => any[];
}
export interface HandoffContext {
messages: any[];
lastMessage: string;
topic?: string;
metadata?: Record<string, any>;
}
export interface HandoffResult {
handedOffTo: string;
response: string;
context: HandoffContext;
}
/**
* Handoff class - Represents a handoff target
*/
export class Handoff {
readonly targetAgent: EnhancedAgent;
readonly name: string;
readonly description: string;
readonly condition?: (context: HandoffContext) => boolean;
readonly transformContext?: (messages: any[]) => any[];
constructor(config: HandoffConfig) {
this.targetAgent = config.agent;
this.name = config.name || `handoff_to_${config.agent.name}`;
this.description = config.description || `Transfer conversation to ${config.agent.name}`;
this.condition = config.condition;
this.transformContext = config.transformContext;
}
/**
* Check if handoff should be triggered
*/
shouldTrigger(context: HandoffContext): boolean {
if (this.condition) {
return this.condition(context);
}
return true;
}
/**
* Execute the handoff
*/
async execute(context: HandoffContext): Promise<HandoffResult> {
let messages = context.messages;
if (this.transformContext) {
messages = this.transformContext(messages);
}
// Transfer context to target agent
const response = await this.targetAgent.chat(context.lastMessage);
return {
handedOffTo: this.targetAgent.name,
response: response.text,
context: {
...context,
messages,
},
};
}
/**
* Get tool definition for LLM
*/
getToolDefinition(): { name: string; description: string; parameters: any } {
return {
name: this.name,
description: this.description,
parameters: {
type: 'object',
properties: {
reason: {
type: 'string',
description: 'Reason for the handoff',
},
},
required: [],
},
};
}
}
/**
* Create a handoff configuration
*/
export function handoff(config: HandoffConfig): Handoff {
return new Handoff(config);
}
/**
* Handoff filters - Common condition functions
*/
export const handoffFilters = {
/**
* Trigger handoff based on topic keywords
*/
topic: (keywords: string | string[]) => {
const keywordList = Array.isArray(keywords) ? keywords : [keywords];
return (context: HandoffContext): boolean => {
const lastMessage = context.lastMessage.toLowerCase();
return keywordList.some(kw => lastMessage.includes(kw.toLowerCase()));
};
},
/**
* Trigger handoff based on metadata
*/
metadata: (key: string, value: any) => {
return (context: HandoffContext): boolean => {
return context.metadata?.[key] === value;
};
},
/**
* Always trigger
*/
always: () => {
return (): boolean => true;
},
/**
* Never trigger (manual only)
*/
never: () => {
return (): boolean => false;
},
/**
* Combine multiple conditions with AND
*/
and: (...conditions: Array<(context: HandoffContext) => boolean>) => {
return (context: HandoffContext): boolean => {
return conditions.every(cond => cond(context));
};
},
/**
* Combine multiple conditions with OR
*/
or: (...conditions: Array<(context: HandoffContext) => boolean>) => {
return (context: HandoffContext): boolean => {
return conditions.some(cond => cond(context));
};
},
};
+130
View File
@@ -0,0 +1,130 @@
/**
* ImageAgent - Agent for image generation and analysis
*/
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
export interface ImageGenerationConfig {
prompt: string;
size?: '256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792';
quality?: 'standard' | 'hd';
style?: 'vivid' | 'natural';
n?: number;
}
export interface ImageAnalysisConfig {
imageUrl: string;
prompt?: string;
detail?: 'low' | 'high' | 'auto';
}
export interface ImageAgentConfig {
name?: string;
llm?: string;
imageModel?: string;
verbose?: boolean;
}
/**
* ImageAgent - Agent for image generation and analysis
*/
export class ImageAgent {
readonly name: string;
private provider: LLMProvider;
private imageModel: string;
private verbose: boolean;
constructor(config: ImageAgentConfig = {}) {
this.name = config.name || `ImageAgent_${Math.random().toString(36).substr(2, 9)}`;
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
this.imageModel = config.imageModel || 'dall-e-3';
this.verbose = config.verbose ?? false;
}
/**
* Analyze an image
*/
async analyze(config: ImageAnalysisConfig): Promise<string> {
const prompt = config.prompt || 'Describe this image in detail.';
const messages: Message[] = [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
{
type: 'image_url',
image_url: {
url: config.imageUrl,
detail: config.detail || 'auto'
}
}
] as any
}
];
const result = await this.provider.generateText({ messages });
if (this.verbose) {
console.log(`[ImageAgent] Analysis: ${result.text.substring(0, 100)}...`);
}
return result.text;
}
/**
* Generate an image (requires OpenAI DALL-E)
*/
async generate(config: ImageGenerationConfig): Promise<string[]> {
// This would require direct OpenAI API call for image generation
// For now, return a placeholder
if (this.verbose) {
console.log(`[ImageAgent] Generating image: ${config.prompt}`);
}
throw new Error('Image generation requires direct OpenAI API integration. Use the OpenAI SDK directly for DALL-E.');
}
/**
* Chat with image context
*/
async chat(prompt: string, imageUrl?: string): Promise<string> {
if (imageUrl) {
return this.analyze({ imageUrl, prompt });
}
const result = await this.provider.generateText({
messages: [{ role: 'user', content: prompt }]
});
return result.text;
}
/**
* Compare two images
*/
async compare(imageUrl1: string, imageUrl2: string, prompt?: string): Promise<string> {
const comparePrompt = prompt || 'Compare these two images and describe the differences and similarities.';
const messages: Message[] = [
{
role: 'user',
content: [
{ type: 'text', text: comparePrompt },
{ type: 'image_url', image_url: { url: imageUrl1 } },
{ type: 'image_url', image_url: { url: imageUrl2 } }
] as any
}
];
const result = await this.provider.generateText({ messages });
return result.text;
}
}
/**
* Create an ImageAgent
*/
export function createImageAgent(config?: ImageAgentConfig): ImageAgent {
return new ImageAgent(config);
}
@@ -0,0 +1,112 @@
/**
* PromptExpanderAgent - Expand and enhance prompts
*/
import { createProvider, type LLMProvider } from '../llm/providers';
export type ExpandStrategy = 'detail' | 'context' | 'examples' | 'constraints' | 'auto';
export interface ExpandResult {
original: string;
expanded: string;
strategy: ExpandStrategy;
additions: string[];
}
export interface PromptExpanderConfig {
name?: string;
llm?: string;
defaultStrategy?: ExpandStrategy;
verbose?: boolean;
}
/**
* PromptExpanderAgent - Expand prompts with more detail and context
*/
export class PromptExpanderAgent {
readonly name: string;
private provider: LLMProvider;
private defaultStrategy: ExpandStrategy;
private verbose: boolean;
constructor(config: PromptExpanderConfig = {}) {
this.name = config.name || `PromptExpander_${Math.random().toString(36).substr(2, 9)}`;
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
this.defaultStrategy = config.defaultStrategy || 'auto';
this.verbose = config.verbose ?? false;
}
/**
* Expand a prompt
*/
async expand(prompt: string, strategy?: ExpandStrategy): Promise<ExpandResult> {
const useStrategy = strategy || this.defaultStrategy;
const actualStrategy = useStrategy === 'auto' ? this.detectStrategy(prompt) : useStrategy;
const systemPrompt = this.getSystemPrompt(actualStrategy);
const result = await this.provider.generateText({
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `Expand this prompt: ${prompt}` }
]
});
const expanded = result.text;
const additions = this.extractAdditions(prompt, expanded);
if (this.verbose) {
console.log(`[PromptExpander] Strategy: ${actualStrategy}, Added ${additions.length} elements`);
}
return {
original: prompt,
expanded,
strategy: actualStrategy,
additions
};
}
/**
* Detect the best expansion strategy
*/
detectStrategy(prompt: string): ExpandStrategy {
const words = prompt.split(/\s+/).length;
const lower = prompt.toLowerCase();
if (words < 5) return 'detail';
if (lower.includes('why') || lower.includes('explain') || lower.includes('context')) return 'context';
if (lower.includes('example') || lower.includes('show') || lower.includes('demonstrate')) return 'examples';
if (lower.includes('must') || lower.includes('should') || lower.includes('require')) return 'constraints';
return 'detail';
}
private getSystemPrompt(strategy: ExpandStrategy): string {
const prompts: Record<ExpandStrategy, string> = {
detail: 'Expand this prompt by adding specific details, requirements, and clarifications. Make it more precise and actionable.',
context: 'Expand this prompt by adding relevant background context, explaining the purpose, and providing necessary information.',
examples: 'Expand this prompt by adding concrete examples, sample inputs/outputs, and demonstrations of expected behavior.',
constraints: 'Expand this prompt by adding explicit constraints, requirements, limitations, and success criteria.',
auto: 'Expand this prompt to make it clearer, more specific, and more actionable. Add relevant details and context.'
};
return prompts[strategy];
}
private extractAdditions(original: string, expanded: string): string[] {
const originalWords = new Set(original.toLowerCase().split(/\s+/));
const expandedWords = expanded.toLowerCase().split(/\s+/);
const newWords = expandedWords.filter(w => !originalWords.has(w) && w.length > 3);
const uniqueNew = [...new Set(newWords)];
return uniqueNew.slice(0, 10);
}
}
/**
* Create a PromptExpanderAgent
*/
export function createPromptExpanderAgent(config?: PromptExpanderConfig): PromptExpanderAgent {
return new PromptExpanderAgent(config);
}
@@ -0,0 +1,106 @@
/**
* QueryRewriterAgent - Rewrite and optimize queries
*/
import { createProvider, type LLMProvider } from '../llm/providers';
export type RewriteStrategy = 'expand' | 'simplify' | 'decompose' | 'rephrase' | 'auto';
export interface RewriteResult {
original: string;
rewritten: string[];
strategy: RewriteStrategy;
confidence: number;
}
export interface QueryRewriterConfig {
name?: string;
llm?: string;
defaultStrategy?: RewriteStrategy;
verbose?: boolean;
}
/**
* QueryRewriterAgent - Optimize and rewrite queries
*/
export class QueryRewriterAgent {
readonly name: string;
private provider: LLMProvider;
private defaultStrategy: RewriteStrategy;
private verbose: boolean;
constructor(config: QueryRewriterConfig = {}) {
this.name = config.name || `QueryRewriter_${Math.random().toString(36).substr(2, 9)}`;
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
this.defaultStrategy = config.defaultStrategy || 'auto';
this.verbose = config.verbose ?? false;
}
/**
* Rewrite a query
*/
async rewrite(query: string, strategy?: RewriteStrategy): Promise<RewriteResult> {
const useStrategy = strategy || this.defaultStrategy;
const actualStrategy = useStrategy === 'auto' ? this.detectStrategy(query) : useStrategy;
const prompt = this.buildPrompt(query, actualStrategy);
const result = await this.provider.generateText({
messages: [
{ role: 'system', content: this.getSystemPrompt(actualStrategy) },
{ role: 'user', content: prompt }
]
});
const rewritten = this.parseRewrittenQueries(result.text);
if (this.verbose) {
console.log(`[QueryRewriter] Strategy: ${actualStrategy}, Rewrites: ${rewritten.length}`);
}
return {
original: query,
rewritten,
strategy: actualStrategy,
confidence: rewritten.length > 0 ? 0.85 : 0.5
};
}
private detectStrategy(query: string): RewriteStrategy {
const words = query.split(/\s+/).length;
if (words < 3) return 'expand';
if (words > 20) return 'simplify';
if (query.includes(' and ') || query.includes(' or ')) return 'decompose';
return 'rephrase';
}
private getSystemPrompt(strategy: RewriteStrategy): string {
const prompts: Record<RewriteStrategy, string> = {
expand: 'Expand this query with more context and detail. Generate 3 expanded versions.',
simplify: 'Simplify this query to its core intent. Generate 3 simpler versions.',
decompose: 'Break this query into smaller, focused sub-queries. Generate 3-5 sub-queries.',
rephrase: 'Rephrase this query in different ways while keeping the same meaning. Generate 3 versions.',
auto: 'Optimize this query for better search results. Generate 3 improved versions.'
};
return prompts[strategy] + ' Output one query per line.';
}
private buildPrompt(query: string, strategy: RewriteStrategy): string {
return `Query: ${query}\n\nRewrite using ${strategy} strategy:`;
}
private parseRewrittenQueries(response: string): string[] {
return response
.split('\n')
.map(line => line.replace(/^\d+[\.\)]\s*/, '').trim())
.filter(line => line.length > 0 && line.length < 500);
}
}
/**
* Create a QueryRewriterAgent
*/
export function createQueryRewriterAgent(config?: QueryRewriterConfig): QueryRewriterAgent {
return new QueryRewriterAgent(config);
}
+162
View File
@@ -0,0 +1,162 @@
/**
* DeepResearchAgent - Agent for comprehensive research tasks
*/
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
export interface Citation {
title: string;
url: string;
snippet?: string;
}
export interface ReasoningStep {
step: number;
thought: string;
action?: string;
result?: string;
}
export interface ResearchResponse {
answer: string;
citations: Citation[];
reasoning: ReasoningStep[];
confidence: number;
}
export interface DeepResearchConfig {
name?: string;
llm?: string;
maxIterations?: number;
searchTool?: (query: string) => Promise<Citation[]>;
verbose?: boolean;
}
/**
* DeepResearchAgent - Comprehensive research with citations
*/
export class DeepResearchAgent {
readonly name: string;
private provider: LLMProvider;
private maxIterations: number;
private searchTool?: (query: string) => Promise<Citation[]>;
private verbose: boolean;
constructor(config: DeepResearchConfig = {}) {
this.name = config.name || `ResearchAgent_${Math.random().toString(36).substr(2, 9)}`;
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
this.maxIterations = config.maxIterations ?? 5;
this.searchTool = config.searchTool;
this.verbose = config.verbose ?? false;
}
/**
* Conduct deep research on a topic
*/
async research(query: string): Promise<ResearchResponse> {
const reasoning: ReasoningStep[] = [];
const citations: Citation[] = [];
let iteration = 0;
// Step 1: Analyze the query
reasoning.push({
step: ++iteration,
thought: `Analyzing research query: "${query}"`,
action: 'query_analysis'
});
// Step 2: Generate search queries
const searchQueries = await this.generateSearchQueries(query);
reasoning.push({
step: ++iteration,
thought: `Generated ${searchQueries.length} search queries`,
action: 'generate_queries',
result: searchQueries.join(', ')
});
// Step 3: Execute searches if tool available
if (this.searchTool) {
for (const sq of searchQueries.slice(0, 3)) {
try {
const results = await this.searchTool(sq);
citations.push(...results);
reasoning.push({
step: ++iteration,
thought: `Searched for: "${sq}"`,
action: 'web_search',
result: `Found ${results.length} results`
});
} catch (error) {
if (this.verbose) {
console.log(`[Research] Search failed for: ${sq}`);
}
}
}
}
// Step 4: Synthesize answer
const answer = await this.synthesizeAnswer(query, citations);
reasoning.push({
step: ++iteration,
thought: 'Synthesizing final answer from gathered information',
action: 'synthesize',
result: 'Answer generated'
});
// Calculate confidence based on citations
const confidence = Math.min(0.5 + (citations.length * 0.1), 0.95);
return {
answer,
citations,
reasoning,
confidence
};
}
private async generateSearchQueries(query: string): Promise<string[]> {
const result = await this.provider.generateText({
messages: [
{
role: 'system',
content: 'Generate 3 search queries to research this topic. Output only the queries, one per line.'
},
{ role: 'user', content: query }
]
});
return result.text.split('\n').filter(q => q.trim().length > 0).slice(0, 3);
}
private async synthesizeAnswer(query: string, citations: Citation[]): Promise<string> {
const context = citations.length > 0
? `\n\nRelevant sources:\n${citations.map(c => `- ${c.title}: ${c.snippet || ''}`).join('\n')}`
: '';
const result = await this.provider.generateText({
messages: [
{
role: 'system',
content: 'You are a research assistant. Provide a comprehensive, well-structured answer based on the available information. Cite sources when available.'
},
{ role: 'user', content: `${query}${context}` }
]
});
return result.text;
}
/**
* Set search tool
*/
setSearchTool(tool: (query: string) => Promise<Citation[]>): void {
this.searchTool = tool;
}
}
/**
* Create a DeepResearchAgent
*/
export function createDeepResearchAgent(config?: DeepResearchConfig): DeepResearchAgent {
return new DeepResearchAgent(config);
}
+146
View File
@@ -0,0 +1,146 @@
/**
* Router Agent - Route requests to specialized agents
*/
import type { EnhancedAgent } from './enhanced';
export interface RouteConfig {
agent: EnhancedAgent;
condition: (input: string, context?: RouteContext) => boolean | Promise<boolean>;
priority?: number;
}
export interface RouteContext {
history?: string[];
metadata?: Record<string, any>;
}
export interface RouterConfig {
name?: string;
routes: RouteConfig[];
defaultAgent?: EnhancedAgent;
verbose?: boolean;
}
/**
* Router Agent - Routes requests to the most appropriate agent
*/
export class RouterAgent {
readonly name: string;
private routes: RouteConfig[];
private defaultAgent?: EnhancedAgent;
private verbose: boolean;
constructor(config: RouterConfig) {
this.name = config.name || 'Router';
this.routes = config.routes.sort((a, b) => (b.priority || 0) - (a.priority || 0));
this.defaultAgent = config.defaultAgent;
this.verbose = config.verbose ?? false;
}
/**
* Route a request to the appropriate agent
*/
async route(input: string, context?: RouteContext): Promise<{ agent: EnhancedAgent; response: string } | null> {
for (const route of this.routes) {
const matches = await route.condition(input, context);
if (matches) {
if (this.verbose) {
console.log(`[Router] Routing to: ${route.agent.name}`);
}
const response = await route.agent.chat(input);
return { agent: route.agent, response: response.text };
}
}
if (this.defaultAgent) {
if (this.verbose) {
console.log(`[Router] Using default agent: ${this.defaultAgent.name}`);
}
const response = await this.defaultAgent.chat(input);
return { agent: this.defaultAgent, response: response.text };
}
return null;
}
/**
* Add a route
*/
addRoute(config: RouteConfig): this {
this.routes.push(config);
this.routes.sort((a, b) => (b.priority || 0) - (a.priority || 0));
return this;
}
/**
* Get all routes
*/
getRoutes(): RouteConfig[] {
return [...this.routes];
}
}
/**
* Route condition helpers
*/
export const routeConditions = {
/**
* Match by keywords
*/
keywords: (keywords: string | string[]) => {
const keywordList = Array.isArray(keywords) ? keywords : [keywords];
return (input: string): boolean => {
const lower = input.toLowerCase();
return keywordList.some(kw => lower.includes(kw.toLowerCase()));
};
},
/**
* Match by regex
*/
pattern: (regex: RegExp) => {
return (input: string): boolean => regex.test(input);
},
/**
* Match by metadata
*/
metadata: (key: string, value: any) => {
return (_input: string, context?: RouteContext): boolean => {
return context?.metadata?.[key] === value;
};
},
/**
* Always match (for default routes)
*/
always: () => {
return (): boolean => true;
},
/**
* Combine conditions with AND
*/
and: (...conditions: Array<(input: string, context?: RouteContext) => boolean>) => {
return (input: string, context?: RouteContext): boolean => {
return conditions.every(c => c(input, context));
};
},
/**
* Combine conditions with OR
*/
or: (...conditions: Array<(input: string, context?: RouteContext) => boolean>) => {
return (input: string, context?: RouteContext): boolean => {
return conditions.some(c => c(input, context));
};
}
};
/**
* Create a router agent
*/
export function createRouter(config: RouterConfig): RouterAgent {
return new RouterAgent(config);
}
+185
View File
@@ -0,0 +1,185 @@
/**
* AutoAgents - Automatic agent generation from task descriptions
*/
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
export interface AgentConfig {
name: string;
role: string;
goal: string;
backstory?: string;
instructions?: string;
tools?: string[];
}
export interface TaskConfig {
description: string;
expectedOutput?: string;
agent?: string;
}
export interface TeamStructure {
agents: AgentConfig[];
tasks: TaskConfig[];
pattern: 'sequential' | 'parallel' | 'hierarchical';
}
export interface AutoAgentsConfig {
llm?: string;
pattern?: 'sequential' | 'parallel' | 'routing' | 'orchestrator-workers' | 'evaluator-optimizer';
singleAgent?: boolean;
verbose?: boolean;
}
/**
* AutoAgents - Generate agent configurations from task descriptions
*/
export class AutoAgents {
private provider: LLMProvider;
private pattern: string;
private singleAgent: boolean;
private verbose: boolean;
constructor(config: AutoAgentsConfig = {}) {
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
this.pattern = config.pattern || 'sequential';
this.singleAgent = config.singleAgent ?? false;
this.verbose = config.verbose ?? false;
}
/**
* Generate agent configuration from task description
*/
async generate(taskDescription: string): Promise<TeamStructure> {
const prompt = this.buildPrompt(taskDescription);
const result = await this.provider.generateText({
messages: [
{ role: 'system', content: this.getSystemPrompt() },
{ role: 'user', content: prompt }
]
});
return this.parseResponse(result.text);
}
/**
* Recommend a pattern for the task
*/
recommendPattern(taskDescription: string): string {
const lower = taskDescription.toLowerCase();
if (lower.includes('parallel') || lower.includes('concurrent') || lower.includes('simultaneously')) {
return 'parallel';
}
if (lower.includes('route') || lower.includes('classify') || lower.includes('categorize')) {
return 'routing';
}
if (lower.includes('orchestrat') || lower.includes('coordinat') || lower.includes('manage')) {
return 'orchestrator-workers';
}
if (lower.includes('evaluat') || lower.includes('optimi') || lower.includes('improv')) {
return 'evaluator-optimizer';
}
return 'sequential';
}
/**
* Analyze task complexity
*/
analyzeComplexity(taskDescription: string): 'simple' | 'moderate' | 'complex' {
const words = taskDescription.split(/\s+/).length;
const hasMultipleSteps = /step|then|after|before|first|second|third|finally/i.test(taskDescription);
const hasMultipleAgents = /team|multiple|several|different|various/i.test(taskDescription);
if (words < 20 && !hasMultipleSteps && !hasMultipleAgents) {
return 'simple';
}
if (words > 100 || (hasMultipleSteps && hasMultipleAgents)) {
return 'complex';
}
return 'moderate';
}
private getSystemPrompt(): string {
return `You are an AI agent architect. Your job is to analyze task descriptions and generate optimal agent configurations.
Output a JSON object with the following structure:
{
"agents": [
{
"name": "AgentName",
"role": "Role description",
"goal": "Agent's goal",
"backstory": "Optional backstory",
"instructions": "Specific instructions",
"tools": ["tool1", "tool2"]
}
],
"tasks": [
{
"description": "Task description",
"expectedOutput": "What the task should produce",
"agent": "AgentName"
}
],
"pattern": "sequential|parallel|hierarchical"
}
Guidelines:
- For simple tasks, use 1 agent
- For moderate tasks, use 2-3 agents
- For complex tasks, use 3-5 agents
- Match the pattern to the task requirements
- Be specific about roles and goals`;
}
private buildPrompt(taskDescription: string): string {
const complexity = this.analyzeComplexity(taskDescription);
const recommendedPattern = this.recommendPattern(taskDescription);
return `Task Description: ${taskDescription}
Complexity: ${complexity}
Recommended Pattern: ${this.pattern || recommendedPattern}
Single Agent Mode: ${this.singleAgent}
Generate an optimal agent configuration for this task.`;
}
private parseResponse(response: string): TeamStructure {
try {
// Extract JSON from response
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
} catch (error) {
// Fallback to default structure
}
// Default fallback
return {
agents: [{
name: 'GeneralAgent',
role: 'General purpose agent',
goal: 'Complete the assigned task',
instructions: 'Follow the task description carefully'
}],
tasks: [{
description: 'Complete the task',
agent: 'GeneralAgent'
}],
pattern: 'sequential'
};
}
}
/**
* Create an AutoAgents instance
*/
export function createAutoAgents(config?: AutoAgentsConfig): AutoAgents {
return new AutoAgents(config);
}
+253
View File
@@ -0,0 +1,253 @@
/**
* Cache System - In-memory and persistent caching for agents
* Inspired by mastra's cache module
*/
export interface CacheConfig {
name?: string;
ttl?: number; // Time to live in milliseconds
maxSize?: number; // Maximum number of entries
}
export interface CacheEntry<T = any> {
value: T;
createdAt: number;
expiresAt?: number;
}
/**
* Abstract base class for cache implementations
*/
export abstract class BaseCache {
readonly name: string;
constructor(config: CacheConfig = {}) {
this.name = config.name || 'cache';
}
abstract get<T = any>(key: string): Promise<T | undefined>;
abstract set<T = any>(key: string, value: T, ttl?: number): Promise<void>;
abstract delete(key: string): Promise<boolean>;
abstract clear(): Promise<void>;
abstract has(key: string): Promise<boolean>;
abstract keys(): Promise<string[]>;
abstract size(): Promise<number>;
// List operations
abstract listPush<T = any>(key: string, value: T): Promise<void>;
abstract listLength(key: string): Promise<number>;
abstract listRange<T = any>(key: string, start: number, end?: number): Promise<T[]>;
}
/**
* In-memory cache implementation
*/
export class MemoryCache extends BaseCache {
private cache: Map<string, CacheEntry> = new Map();
private lists: Map<string, any[]> = new Map();
private ttl?: number;
private maxSize?: number;
constructor(config: CacheConfig = {}) {
super(config);
this.ttl = config.ttl;
this.maxSize = config.maxSize;
}
async get<T = any>(key: string): Promise<T | undefined> {
const entry = this.cache.get(key);
if (!entry) return undefined;
// Check expiration
if (entry.expiresAt && Date.now() > entry.expiresAt) {
this.cache.delete(key);
return undefined;
}
return entry.value as T;
}
async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {
// Enforce max size
if (this.maxSize && this.cache.size >= this.maxSize && !this.cache.has(key)) {
// Remove oldest entry
const firstKey = this.cache.keys().next().value;
if (firstKey) this.cache.delete(firstKey);
}
const effectiveTtl = ttl ?? this.ttl;
const entry: CacheEntry<T> = {
value,
createdAt: Date.now(),
expiresAt: effectiveTtl ? Date.now() + effectiveTtl : undefined
};
this.cache.set(key, entry);
}
async delete(key: string): Promise<boolean> {
const existed = this.cache.has(key);
this.cache.delete(key);
this.lists.delete(key);
return existed;
}
async clear(): Promise<void> {
this.cache.clear();
this.lists.clear();
}
async has(key: string): Promise<boolean> {
const entry = this.cache.get(key);
if (!entry) return false;
if (entry.expiresAt && Date.now() > entry.expiresAt) {
this.cache.delete(key);
return false;
}
return true;
}
async keys(): Promise<string[]> {
return Array.from(this.cache.keys());
}
async size(): Promise<number> {
return this.cache.size;
}
async listPush<T = any>(key: string, value: T): Promise<void> {
if (!this.lists.has(key)) {
this.lists.set(key, []);
}
this.lists.get(key)!.push(value);
}
async listLength(key: string): Promise<number> {
return this.lists.get(key)?.length ?? 0;
}
async listRange<T = any>(key: string, start: number, end?: number): Promise<T[]> {
const list = this.lists.get(key) ?? [];
return list.slice(start, end) as T[];
}
}
/**
* File-based cache implementation
*/
export class FileCache extends BaseCache {
private cacheDir: string;
private fs: any;
private path: any;
constructor(config: CacheConfig & { cacheDir?: string } = {}) {
super(config);
this.cacheDir = config.cacheDir || '.cache';
}
private async ensureDir(): Promise<void> {
if (!this.fs) {
this.fs = await import('fs/promises');
this.path = await import('path');
}
try {
await this.fs.mkdir(this.cacheDir, { recursive: true });
} catch {}
}
private getFilePath(key: string): string {
const safeKey = key.replace(/[^a-zA-Z0-9-_]/g, '_');
return this.path.join(this.cacheDir, `${safeKey}.json`);
}
async get<T = any>(key: string): Promise<T | undefined> {
await this.ensureDir();
try {
const data = await this.fs.readFile(this.getFilePath(key), 'utf-8');
const entry: CacheEntry<T> = JSON.parse(data);
if (entry.expiresAt && Date.now() > entry.expiresAt) {
await this.delete(key);
return undefined;
}
return entry.value;
} catch {
return undefined;
}
}
async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {
await this.ensureDir();
const entry: CacheEntry<T> = {
value,
createdAt: Date.now(),
expiresAt: ttl ? Date.now() + ttl : undefined
};
await this.fs.writeFile(this.getFilePath(key), JSON.stringify(entry));
}
async delete(key: string): Promise<boolean> {
await this.ensureDir();
try {
await this.fs.unlink(this.getFilePath(key));
return true;
} catch {
return false;
}
}
async clear(): Promise<void> {
await this.ensureDir();
try {
const files = await this.fs.readdir(this.cacheDir);
await Promise.all(
files.filter((f: string) => f.endsWith('.json'))
.map((f: string) => this.fs.unlink(this.path.join(this.cacheDir, f)))
);
} catch {}
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) !== undefined;
}
async keys(): Promise<string[]> {
await this.ensureDir();
try {
const files = await this.fs.readdir(this.cacheDir);
return files
.filter((f: string) => f.endsWith('.json'))
.map((f: string) => f.replace('.json', ''));
} catch {
return [];
}
}
async size(): Promise<number> {
return (await this.keys()).length;
}
async listPush<T = any>(key: string, value: T): Promise<void> {
const list = (await this.get<T[]>(`list_${key}`)) || [];
list.push(value);
await this.set(`list_${key}`, list);
}
async listLength(key: string): Promise<number> {
const list = (await this.get<any[]>(`list_${key}`)) || [];
return list.length;
}
async listRange<T = any>(key: string, start: number, end?: number): Promise<T[]> {
const list = (await this.get<T[]>(`list_${key}`)) || [];
return list.slice(start, end);
}
}
// Factory functions
export function createMemoryCache(config?: CacheConfig): MemoryCache {
return new MemoryCache(config);
}
export function createFileCache(config?: CacheConfig & { cacheDir?: string }): FileCache {
return new FileCache(config);
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Chat command - Chat with an AI agent
*/
import { createProvider } from '../../llm/providers';
import { Session } from '../../session';
import { resolveConfig } from '../config/resolve';
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
import * as pretty from '../output/pretty';
import { EXIT_CODES } from '../spec/cli-spec';
import { ERROR_CODES, normalizeError } from '../output/errors';
export interface ChatOptions {
model?: string;
stream?: boolean;
session?: string;
verbose?: boolean;
profile?: string;
config?: string;
output?: 'json' | 'text' | 'pretty';
json?: boolean;
}
export async function execute(args: string[], options: ChatOptions): Promise<void> {
const prompt = args[0];
if (!prompt) {
if (options.json || options.output === 'json') {
printError(ERROR_CODES.MISSING_ARG, 'Please provide a prompt');
} else {
await pretty.error('Please provide a prompt');
}
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
}
// Resolve config with precedence
const config = resolveConfig({
configPath: options.config,
profile: options.profile,
model: options.model,
verbose: options.verbose,
stream: options.stream
});
const startTime = Date.now();
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
try {
const provider = createProvider(config.model);
const session = new Session({ id: options.session });
session.addMessage({ role: 'user', content: prompt });
let responseText = '';
let inputTokens = 0;
let outputTokens = 0;
if (config.stream && outputFormat !== 'json') {
// Streaming mode (only for non-JSON output)
const stream = await provider.streamText({
messages: [{ role: 'user', content: prompt }],
onToken: (token: string) => {
process.stdout.write(token);
responseText += token;
}
});
for await (const chunk of stream) {
// Stream is consumed by onToken
}
console.log(); // New line after streaming
} else {
// Non-streaming mode
const result = await provider.generateText({
messages: [{ role: 'user', content: prompt }]
});
responseText = result.text;
inputTokens = result.usage?.promptTokens || 0;
outputTokens = result.usage?.completionTokens || 0;
if (outputFormat === 'json') {
const duration = Date.now() - startTime;
outputJson(formatSuccess(
{ response: responseText },
{
duration_ms: duration,
model: config.model,
tokens: {
input: inputTokens,
output: outputTokens
}
}
));
} else {
console.log(responseText);
}
}
session.addMessage({ role: 'assistant', content: responseText });
} catch (error) {
const cliError = normalizeError(error);
if (outputFormat === 'json') {
outputJson(formatError(cliError.code, cliError.message, cliError.details));
} else {
await pretty.error(cliError.message);
if (config.verbose && error instanceof Error && error.stack) {
await pretty.dim(error.stack);
}
}
process.exit(cliError.exitCode);
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Config utilities index
*/
export * from './schema';
export * from './load';
export * from './resolve';
+213
View File
@@ -0,0 +1,213 @@
/**
* Config file loading
*/
import * as fs from 'fs';
import * as path from 'path';
import { CONFIG_FILES } from '../spec/cli-spec';
import { FullConfig, validateConfig, DEFAULT_CONFIG } from './schema';
export interface LoadConfigOptions {
configPath?: string;
cwd?: string;
}
/**
* Find config file in directory hierarchy
*/
function findConfigFile(startDir: string): string | null {
let currentDir = startDir;
while (true) {
for (const filename of CONFIG_FILES) {
const filePath = path.join(currentDir, filename);
if (fs.existsSync(filePath)) {
return filePath;
}
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
break; // Reached root
}
currentDir = parentDir;
}
return null;
}
/**
* Find user config file
*/
function findUserConfigFile(): string | null {
const homeDir = process.env.HOME || process.env.USERPROFILE;
if (!homeDir) return null;
for (const filename of CONFIG_FILES) {
const filePath = path.join(homeDir, filename);
if (fs.existsSync(filePath)) {
return filePath;
}
}
return null;
}
/**
* Parse config file content
*/
function parseConfigFile(filePath: string): unknown {
const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath).toLowerCase();
if (ext === '.json') {
return JSON.parse(content);
}
if (ext === '.yaml' || ext === '.yml') {
// Simple YAML parsing for basic config
// For complex YAML, we'd need a proper parser
return parseSimpleYaml(content);
}
throw new Error(`Unsupported config file format: ${ext}`);
}
/**
* Simple YAML parser for basic key-value configs
* Handles: strings, booleans, numbers, simple objects
*/
function parseSimpleYaml(content: string): Record<string, unknown> {
const result: Record<string, unknown> = {};
const lines = content.split('\n');
let currentObject: Record<string, unknown> | null = null;
let currentKey: string | null = null;
let baseIndent = 0;
for (const line of lines) {
// Skip empty lines and comments
if (!line.trim() || line.trim().startsWith('#')) {
continue;
}
const indent = line.search(/\S/);
const trimmed = line.trim();
// Check for key: value pattern
const colonIndex = trimmed.indexOf(':');
if (colonIndex === -1) continue;
const key = trimmed.slice(0, colonIndex).trim();
const value = trimmed.slice(colonIndex + 1).trim();
if (indent === 0) {
// Top-level key
if (value === '' || value === '{}') {
// Start of nested object
currentObject = {};
currentKey = key;
baseIndent = indent;
result[key] = currentObject;
} else {
result[key] = parseYamlValue(value);
currentObject = null;
currentKey = null;
}
} else if (currentObject !== null && indent > baseIndent) {
// Nested key
if (value === '' || value === '{}') {
// Nested object within nested object
const nestedObj: Record<string, unknown> = {};
currentObject[key] = nestedObj;
// For simplicity, we only support one level of nesting in profiles
} else {
currentObject[key] = parseYamlValue(value);
}
}
}
return result;
}
function parseYamlValue(value: string): string | number | boolean {
// Remove quotes
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
return value.slice(1, -1);
}
// Boolean
if (value === 'true') return true;
if (value === 'false') return false;
// Number
const num = Number(value);
if (!isNaN(num) && value !== '') return num;
return value;
}
/**
* Load config from file
*/
export function loadConfigFile(filePath: string): FullConfig {
try {
const parsed = parseConfigFile(filePath);
if (!validateConfig(parsed)) {
throw new Error('Invalid config format');
}
return parsed;
} catch (error) {
throw new Error(`Failed to load config from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Load config with resolution order:
* 1. Explicit config path
* 2. Project config (current dir and parents)
* 3. User config (~/.praisonai.yaml)
* 4. Defaults
*/
export function loadConfig(options: LoadConfigOptions = {}): FullConfig {
const configs: Partial<FullConfig>[] = [DEFAULT_CONFIG];
// User config
const userConfigPath = findUserConfigFile();
if (userConfigPath) {
try {
configs.push(loadConfigFile(userConfigPath));
} catch {
// Ignore user config errors
}
}
// Project config
const cwd = options.cwd || process.cwd();
const projectConfigPath = findConfigFile(cwd);
if (projectConfigPath) {
try {
configs.push(loadConfigFile(projectConfigPath));
} catch {
// Ignore project config errors
}
}
// Explicit config path
if (options.configPath) {
configs.push(loadConfigFile(options.configPath));
}
// Merge all configs
const result: FullConfig = { ...DEFAULT_CONFIG };
for (const config of configs) {
if (config.model !== undefined) result.model = config.model;
if (config.verbose !== undefined) result.verbose = config.verbose;
if (config.stream !== undefined) result.stream = config.stream;
if (config.profiles !== undefined) {
result.profiles = { ...result.profiles, ...config.profiles };
}
}
return result;
}
@@ -0,0 +1,80 @@
/**
* Config resolution with profiles and CLI overrides
*/
import { loadConfig } from './load';
import { loadEnvConfig } from '../runtime/env';
import { FullConfig, ProfileConfig, DEFAULT_CONFIG } from './schema';
export interface ResolvedConfig {
model: string;
verbose: boolean;
stream: boolean;
}
export interface ResolveOptions {
configPath?: string;
profile?: string;
model?: string;
verbose?: boolean;
stream?: boolean;
}
/**
* Resolve final config with precedence:
* 1. CLI flags (highest)
* 2. Environment variables
* 3. Profile settings
* 4. Config file
* 5. Defaults (lowest)
*/
export function resolveConfig(options: ResolveOptions = {}): ResolvedConfig {
// Load base config from files
const fileConfig = loadConfig({ configPath: options.configPath });
// Load env config
const envConfig = loadEnvConfig();
// Get profile config if specified
let profileConfig: ProfileConfig = {};
const profileName = options.profile || envConfig.profile;
if (profileName && fileConfig.profiles?.[profileName]) {
profileConfig = fileConfig.profiles[profileName];
}
// Resolve with precedence
const resolved: ResolvedConfig = {
model: DEFAULT_CONFIG.model!,
verbose: DEFAULT_CONFIG.verbose!,
stream: DEFAULT_CONFIG.stream!
};
// Apply file config
if (fileConfig.model !== undefined) resolved.model = fileConfig.model;
if (fileConfig.verbose !== undefined) resolved.verbose = fileConfig.verbose;
if (fileConfig.stream !== undefined) resolved.stream = fileConfig.stream;
// Apply profile config
if (profileConfig.model !== undefined) resolved.model = profileConfig.model;
if (profileConfig.verbose !== undefined) resolved.verbose = profileConfig.verbose;
if (profileConfig.stream !== undefined) resolved.stream = profileConfig.stream;
// Apply env config
if (envConfig.model !== undefined) resolved.model = envConfig.model;
if (envConfig.verbose !== undefined) resolved.verbose = envConfig.verbose;
// Apply CLI flags (highest precedence)
if (options.model !== undefined) resolved.model = options.model;
if (options.verbose !== undefined) resolved.verbose = options.verbose;
if (options.stream !== undefined) resolved.stream = options.stream;
return resolved;
}
/**
* Get available profile names
*/
export function getAvailableProfiles(configPath?: string): string[] {
const config = loadConfig({ configPath });
return Object.keys(config.profiles || {});
}
+80
View File
@@ -0,0 +1,80 @@
/**
* Config schema definitions
*/
import { ConfigSchema } from '../spec/cli-spec';
export interface ProfileConfig {
model?: string;
verbose?: boolean;
stream?: boolean;
}
export interface FullConfig extends ConfigSchema {
profiles?: Record<string, ProfileConfig>;
}
export const DEFAULT_CONFIG: FullConfig = {
model: 'openai/gpt-4o-mini',
verbose: false,
stream: false,
profiles: {}
};
export function validateConfig(config: unknown): config is FullConfig {
if (typeof config !== 'object' || config === null) {
return false;
}
const c = config as Record<string, unknown>;
if (c.model !== undefined && typeof c.model !== 'string') {
return false;
}
if (c.verbose !== undefined && typeof c.verbose !== 'boolean') {
return false;
}
if (c.stream !== undefined && typeof c.stream !== 'boolean') {
return false;
}
if (c.profiles !== undefined) {
if (typeof c.profiles !== 'object' || c.profiles === null) {
return false;
}
for (const profile of Object.values(c.profiles as Record<string, unknown>)) {
if (typeof profile !== 'object' || profile === null) {
return false;
}
const p = profile as Record<string, unknown>;
if (p.model !== undefined && typeof p.model !== 'string') {
return false;
}
if (p.verbose !== undefined && typeof p.verbose !== 'boolean') {
return false;
}
if (p.stream !== undefined && typeof p.stream !== 'boolean') {
return false;
}
}
}
return true;
}
export function mergeConfigs(...configs: Partial<FullConfig>[]): FullConfig {
const result: FullConfig = { ...DEFAULT_CONFIG };
for (const config of configs) {
if (config.model !== undefined) result.model = config.model;
if (config.verbose !== undefined) result.verbose = config.verbose;
if (config.stream !== undefined) result.stream = config.stream;
if (config.profiles !== undefined) {
result.profiles = { ...result.profiles, ...config.profiles };
}
}
return result;
}
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* PraisonAI TypeScript CLI
*/
import { createProvider, isProviderAvailable, getAvailableProviders } from '../llm/providers';
import { Session } from '../session';
import { tool, ToolRegistry } from '../tools/decorator';
interface CLIOptions {
model?: string;
stream?: boolean;
verbose?: boolean;
sessionId?: string;
}
async function chat(prompt: string, options: CLIOptions = {}): Promise<string> {
const model = options.model || process.env.PRAISONAI_MODEL || 'openai/gpt-4o-mini';
const provider = createProvider(model);
const session = new Session({ id: options.sessionId });
session.addMessage({ role: 'user', content: prompt });
if (options.stream) {
let fullText = '';
const stream = await provider.streamText({
messages: [{ role: 'user', content: prompt }],
onToken: (token) => {
process.stdout.write(token);
fullText += token;
}
});
for await (const chunk of stream) {
// Stream is consumed by onToken
}
console.log(); // New line after streaming
return fullText;
} else {
const result = await provider.generateText({
messages: [{ role: 'user', content: prompt }]
});
console.log(result.text);
return result.text;
}
}
async function listProviders(): Promise<void> {
console.log('Available Providers:');
const available = getAvailableProviders();
const providers = ['openai', 'anthropic', 'google'];
for (const p of providers) {
const status = available.includes(p) ? '✅' : '❌';
console.log(` ${status} ${p}`);
}
}
async function version(): Promise<void> {
const pkg = require('../../package.json');
console.log(`praisonai v${pkg.version}`);
}
async function help(): Promise<void> {
console.log(`
PraisonAI TypeScript CLI
Usage:
praisonai-ts chat <prompt> Chat with an AI agent
praisonai-ts providers List available providers
praisonai-ts version Show version
praisonai-ts help Show this help
Options:
--model, -m <model> Model to use (e.g., openai/gpt-4o-mini)
--stream, -s Enable streaming output
--verbose, -v Verbose output
--session <id> Session ID for conversation continuity
Environment Variables:
OPENAI_API_KEY OpenAI API key
ANTHROPIC_API_KEY Anthropic API key
GOOGLE_API_KEY Google API key
PRAISONAI_MODEL Default model
Examples:
praisonai-ts chat "Hello, how are you?"
praisonai-ts chat "Write a poem" --stream
praisonai-ts chat "Explain AI" -m anthropic/claude-sonnet-4-20250514
praisonai-ts providers
`);
}
function parseArgs(args: string[]): { command: string; prompt: string; options: CLIOptions } {
const options: CLIOptions = {};
let command = 'help';
let prompt = '';
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg === '--model' || arg === '-m') {
options.model = args[++i];
} else if (arg === '--stream' || arg === '-s') {
options.stream = true;
} else if (arg === '--verbose' || arg === '-v') {
options.verbose = true;
} else if (arg === '--session') {
options.sessionId = args[++i];
} else if (!arg.startsWith('-')) {
if (!command || command === 'help') {
command = arg;
} else {
prompt = arg;
}
}
i++;
}
return { command, prompt, options };
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
const { command, prompt, options } = parseArgs(args);
try {
switch (command) {
case 'chat':
if (!prompt) {
console.error('Error: Please provide a prompt');
process.exit(1);
}
await chat(prompt, options);
break;
case 'providers':
await listProviders();
break;
case 'version':
await version();
break;
case 'help':
default:
await help();
break;
}
} catch (error: any) {
console.error('Error:', error.message);
if (options.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
// Export for programmatic use
export { chat, listProviders, version, help, parseArgs };
// Run CLI if executed directly
if (require.main === module) {
main();
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Environment variable handling for CLI
*/
import { ENV_VARS } from '../spec/cli-spec';
export interface EnvConfig {
model?: string;
profile?: string;
verbose?: boolean;
configPath?: string;
}
export function loadEnvConfig(): EnvConfig {
const config: EnvConfig = {};
const model = process.env[ENV_VARS.PRAISONAI_MODEL];
if (model) config.model = model;
const profile = process.env[ENV_VARS.PRAISONAI_PROFILE];
if (profile) config.profile = profile;
const verbose = process.env[ENV_VARS.PRAISONAI_VERBOSE];
if (verbose) config.verbose = verbose.toLowerCase() === 'true';
const configPath = process.env[ENV_VARS.PRAISONAI_CONFIG];
if (configPath) config.configPath = configPath;
return config;
}
export function hasApiKey(provider: 'openai' | 'anthropic' | 'google'): boolean {
switch (provider) {
case 'openai':
return !!process.env[ENV_VARS.OPENAI_API_KEY];
case 'anthropic':
return !!process.env[ENV_VARS.ANTHROPIC_API_KEY];
case 'google':
return !!process.env[ENV_VARS.GOOGLE_API_KEY];
default:
return false;
}
}
export function getApiKey(provider: 'openai' | 'anthropic' | 'google'): string | undefined {
switch (provider) {
case 'openai':
return process.env[ENV_VARS.OPENAI_API_KEY];
case 'anthropic':
return process.env[ENV_VARS.ANTHROPIC_API_KEY];
case 'google':
return process.env[ENV_VARS.GOOGLE_API_KEY];
default:
return undefined;
}
}
+48
View File
@@ -0,0 +1,48 @@
/**
* Exit code handling for CLI
*/
import { EXIT_CODES, ExitCode } from '../spec/cli-spec';
export function exit(code: ExitCode): never {
process.exit(code);
}
export function exitSuccess(): never {
exit(EXIT_CODES.SUCCESS);
}
export function exitError(message?: string): never {
if (message) {
console.error(message);
}
exit(EXIT_CODES.RUNTIME_ERROR);
}
export function exitInvalidArgs(message?: string): never {
if (message) {
console.error(`Error: ${message}`);
}
exit(EXIT_CODES.INVALID_ARGUMENTS);
}
export function exitConfigError(message?: string): never {
if (message) {
console.error(`Config Error: ${message}`);
}
exit(EXIT_CODES.CONFIG_ERROR);
}
export function exitNetworkError(message?: string): never {
if (message) {
console.error(`Network Error: ${message}`);
}
exit(EXIT_CODES.NETWORK_ERROR);
}
export function exitAuthError(message?: string): never {
if (message) {
console.error(`Auth Error: ${message}`);
}
exit(EXIT_CODES.AUTH_ERROR);
}
@@ -0,0 +1,7 @@
/**
* Runtime utilities index
*/
export * from './env';
export * from './exit';
export * from './lazy';
+56
View File
@@ -0,0 +1,56 @@
/**
* Lazy loading utilities for CLI commands
* Ensures startup time stays <100ms by only loading command code when invoked
*/
import { validateCommand } from '../spec/cli-spec';
export type CommandModule = {
execute: (args: string[], options: Record<string, unknown>) => Promise<void>;
};
const commandCache = new Map<string, CommandModule>();
export async function loadCommand(command: string): Promise<CommandModule> {
if (!validateCommand(command)) {
throw new Error(`Unknown command: ${command}`);
}
// Check cache first
if (commandCache.has(command)) {
return commandCache.get(command)!;
}
// Lazy import the command module
const module = await import(`../commands/${command}`);
commandCache.set(command, module);
return module;
}
export function clearCommandCache(): void {
commandCache.clear();
}
/**
* Lazy load optional dependencies
* Returns undefined if not available
*/
export async function tryLoadOptional<T>(moduleName: string): Promise<T | undefined> {
try {
return await import(moduleName);
} catch {
return undefined;
}
}
/**
* Check if an optional dependency is available
*/
export async function hasOptionalDep(moduleName: string): Promise<boolean> {
try {
await import(moduleName);
return true;
} catch {
return false;
}
}
+204
View File
@@ -0,0 +1,204 @@
/**
* PraisonAI CLI Specification - TypeScript Runtime
* Parsed and validated spec from cli-spec.v1.yaml
*/
export const CLI_SPEC_VERSION = '1.0.0';
export interface CommandArg {
name: string;
type: 'string' | 'integer' | 'boolean';
required: boolean;
position?: number;
description?: string;
}
export interface CommandFlag {
name: string;
short?: string;
type: 'string' | 'integer' | 'boolean';
default?: string | number | boolean;
enum?: string[];
description?: string;
required?: boolean;
}
export interface Subcommand {
description: string;
args?: CommandArg[];
flags?: CommandFlag[];
}
export interface Command {
description: string;
args?: CommandArg[];
flags?: CommandFlag[];
subcommands?: Record<string, Subcommand>;
}
export const COMMANDS: Record<string, Command> = {
chat: {
description: 'Chat with an AI agent',
args: [
{ name: 'prompt', type: 'string', required: true, position: 0 }
],
flags: [
{ name: 'model', short: 'm', type: 'string', description: 'Model to use (e.g., openai/gpt-4o-mini)' },
{ name: 'stream', short: 's', type: 'boolean', default: false, description: 'Enable streaming output' },
{ name: 'session', type: 'string', description: 'Session ID for conversation continuity' }
]
},
run: {
description: 'Run an agent with a task',
args: [
{ name: 'task', type: 'string', required: true, position: 0 }
],
flags: [
{ name: 'agent', short: 'a', type: 'string', description: 'Agent configuration file or name' },
{ name: 'tools', short: 't', type: 'string', description: 'Comma-separated list of tools' }
]
},
workflow: {
description: 'Execute a multi-agent workflow',
args: [
{ name: 'file', type: 'string', required: true, position: 0, description: 'Workflow YAML file path' }
],
flags: [
{ name: 'parallel', type: 'boolean', default: false, description: 'Run workflow steps in parallel where possible' }
]
},
eval: {
description: 'Evaluate agent performance',
subcommands: {
accuracy: {
description: 'Run accuracy evaluation',
flags: [
{ name: 'input', type: 'string', required: true },
{ name: 'expected', type: 'string', required: true },
{ name: 'iterations', type: 'integer', default: 1 }
]
},
performance: {
description: 'Run performance benchmark',
flags: [
{ name: 'iterations', type: 'integer', default: 10 },
{ name: 'warmup', type: 'integer', default: 2 }
]
},
reliability: {
description: 'Run reliability check',
flags: [
{ name: 'expected-tools', type: 'string', description: 'Comma-separated expected tool calls' }
]
}
}
},
providers: {
description: 'List available LLM providers',
flags: []
},
tools: {
description: 'List or manage tools',
subcommands: {
list: {
description: 'List available tools'
},
info: {
description: 'Show tool information',
args: [
{ name: 'name', type: 'string', required: true, position: 0 }
]
}
}
},
version: {
description: 'Show CLI version',
flags: []
},
help: {
description: 'Show help information',
args: [
{ name: 'command', type: 'string', required: false, position: 0 }
]
}
};
export const GLOBAL_FLAGS: CommandFlag[] = [
{ name: 'verbose', short: 'v', type: 'boolean', default: false, description: 'Enable verbose output' },
{ name: 'config', short: 'c', type: 'string', description: 'Path to config file' },
{ name: 'profile', short: 'p', type: 'string', description: 'Profile name to use' },
{ name: 'output', short: 'o', type: 'string', enum: ['json', 'text', 'pretty'], default: 'pretty', description: 'Output format' },
{ name: 'json', type: 'boolean', default: false, description: 'Shorthand for --output json' }
];
export const EXIT_CODES = {
SUCCESS: 0,
RUNTIME_ERROR: 1,
INVALID_ARGUMENTS: 2,
CONFIG_ERROR: 3,
NETWORK_ERROR: 4,
AUTH_ERROR: 5
} as const;
export type ExitCode = typeof EXIT_CODES[keyof typeof EXIT_CODES];
export interface SuccessOutput<T = unknown> {
success: true;
data: T;
meta?: {
duration_ms?: number;
model?: string;
tokens?: {
input?: number;
output?: number;
};
};
}
export interface ErrorOutput {
success: false;
error: {
code: string;
message: string;
details?: Record<string, unknown>;
};
}
export type CLIOutput<T = unknown> = SuccessOutput<T> | ErrorOutput;
export const ENV_VARS = {
PRAISONAI_MODEL: 'PRAISONAI_MODEL',
PRAISONAI_PROFILE: 'PRAISONAI_PROFILE',
PRAISONAI_VERBOSE: 'PRAISONAI_VERBOSE',
PRAISONAI_CONFIG: 'PRAISONAI_CONFIG',
OPENAI_API_KEY: 'OPENAI_API_KEY',
ANTHROPIC_API_KEY: 'ANTHROPIC_API_KEY',
GOOGLE_API_KEY: 'GOOGLE_API_KEY'
} as const;
export const CONFIG_FILES = ['.praisonai.yaml', '.praisonai.json'] as const;
export interface ConfigSchema {
model?: string;
verbose?: boolean;
stream?: boolean;
profiles?: Record<string, {
model?: string;
verbose?: boolean;
stream?: boolean;
}>;
}
export function validateCommand(command: string): command is keyof typeof COMMANDS {
return command in COMMANDS;
}
export function getCommandSpec(command: string): Command | undefined {
return COMMANDS[command];
}
export function getAllFlags(command: string): CommandFlag[] {
const cmdSpec = COMMANDS[command];
const cmdFlags = cmdSpec?.flags || [];
return [...GLOBAL_FLAGS, ...cmdFlags];
}
+207
View File
@@ -0,0 +1,207 @@
/**
* Evaluation Framework - Accuracy, Performance, and Reliability evaluation
*/
export interface EvalResult {
passed: boolean;
score: number;
message?: string;
details?: Record<string, any>;
duration: number;
}
export interface AccuracyEvalConfig {
input: string;
expectedOutput: string;
actualOutput: string;
threshold?: number;
}
export interface PerformanceEvalConfig {
func: () => Promise<any>;
iterations?: number;
warmupRuns?: number;
}
export interface PerformanceResult extends EvalResult {
avgTime: number;
minTime: number;
maxTime: number;
p95Time: number;
times: number[];
}
export interface ReliabilityEvalConfig {
expectedToolCalls: string[];
actualToolCalls: string[];
}
/**
* Accuracy Evaluation - Compare actual output to expected
*/
export async function accuracyEval(config: AccuracyEvalConfig): Promise<EvalResult> {
const start = Date.now();
const threshold = config.threshold ?? 0.8;
const similarity = calculateSimilarity(config.expectedOutput, config.actualOutput);
const passed = similarity >= threshold;
return {
passed,
score: similarity,
message: passed ? 'Output matches expected' : 'Output does not match expected',
details: {
expected: config.expectedOutput,
actual: config.actualOutput,
threshold
},
duration: Date.now() - start
};
}
/**
* Performance Evaluation - Measure execution time
*/
export async function performanceEval(config: PerformanceEvalConfig): Promise<PerformanceResult> {
const iterations = config.iterations ?? 10;
const warmupRuns = config.warmupRuns ?? 2;
const times: number[] = [];
// Warmup runs
for (let i = 0; i < warmupRuns; i++) {
await config.func();
}
// Actual runs
const start = Date.now();
for (let i = 0; i < iterations; i++) {
const runStart = Date.now();
await config.func();
times.push(Date.now() - runStart);
}
const sortedTimes = [...times].sort((a, b) => a - b);
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = sortedTimes[0];
const maxTime = sortedTimes[sortedTimes.length - 1];
const p95Index = Math.floor(sortedTimes.length * 0.95);
const p95Time = sortedTimes[p95Index] || maxTime;
return {
passed: true,
score: 1,
avgTime,
minTime,
maxTime,
p95Time,
times,
duration: Date.now() - start,
details: {
iterations,
warmupRuns
}
};
}
/**
* Reliability Evaluation - Check tool call accuracy
*/
export async function reliabilityEval(config: ReliabilityEvalConfig): Promise<EvalResult> {
const start = Date.now();
const expected = new Set(config.expectedToolCalls);
const actual = new Set(config.actualToolCalls);
const matched = config.expectedToolCalls.filter(t => actual.has(t));
const missing = config.expectedToolCalls.filter(t => !actual.has(t));
const extra = config.actualToolCalls.filter(t => !expected.has(t));
const score = expected.size > 0 ? matched.length / expected.size : 1;
const passed = missing.length === 0;
return {
passed,
score,
message: passed ? 'All expected tool calls made' : `Missing tool calls: ${missing.join(', ')}`,
details: {
matched,
missing,
extra,
expected: config.expectedToolCalls,
actual: config.actualToolCalls
},
duration: Date.now() - start
};
}
/**
* Calculate text similarity (simple Jaccard similarity)
*/
function calculateSimilarity(a: string, b: string): number {
const wordsA = new Set(a.toLowerCase().split(/\s+/));
const wordsB = new Set(b.toLowerCase().split(/\s+/));
const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
const union = new Set([...wordsA, ...wordsB]);
return union.size > 0 ? intersection.size / union.size : 0;
}
/**
* Eval Suite - Run multiple evaluations
*/
export class EvalSuite {
private results: Map<string, EvalResult> = new Map();
async runAccuracy(name: string, config: AccuracyEvalConfig): Promise<EvalResult> {
const result = await accuracyEval(config);
this.results.set(name, result);
return result;
}
async runPerformance(name: string, config: PerformanceEvalConfig): Promise<PerformanceResult> {
const result = await performanceEval(config);
this.results.set(name, result);
return result;
}
async runReliability(name: string, config: ReliabilityEvalConfig): Promise<EvalResult> {
const result = await reliabilityEval(config);
this.results.set(name, result);
return result;
}
getResults(): Map<string, EvalResult> {
return new Map(this.results);
}
getSummary(): { total: number; passed: number; failed: number; avgScore: number } {
const results = Array.from(this.results.values());
const passed = results.filter(r => r.passed).length;
const avgScore = results.length > 0
? results.reduce((a, b) => a + b.score, 0) / results.length
: 0;
return {
total: results.length,
passed,
failed: results.length - passed,
avgScore
};
}
printSummary(): void {
const summary = this.getSummary();
console.log('\n=== Evaluation Summary ===');
console.log(`Total: ${summary.total}`);
console.log(`Passed: ${summary.passed}`);
console.log(`Failed: ${summary.failed}`);
console.log(`Avg Score: ${(summary.avgScore * 100).toFixed(1)}%`);
console.log('\nResults:');
for (const [name, result] of this.results) {
const status = result.passed ? '✅' : '❌';
console.log(` ${status} ${name}: ${(result.score * 100).toFixed(1)}%`);
}
}
}
+184
View File
@@ -0,0 +1,184 @@
/**
* Event System - PubSub and Event Emitter for agent communication
* Inspired by mastra's events module
*/
import { EventEmitter } from 'events';
export interface Event {
id: string;
topic: string;
data: any;
createdAt: Date;
metadata?: Record<string, any>;
}
export type EventHandler = (event: Event, ack?: () => Promise<void>) => void | Promise<void>;
/**
* Abstract PubSub base class
*/
export abstract class PubSub {
abstract publish(topic: string, data: any, metadata?: Record<string, any>): Promise<void>;
abstract subscribe(topic: string, handler: EventHandler): Promise<void>;
abstract unsubscribe(topic: string, handler: EventHandler): Promise<void>;
abstract close(): Promise<void>;
}
/**
* In-memory EventEmitter-based PubSub implementation
*/
export class EventEmitterPubSub extends PubSub {
private emitter: EventEmitter;
private handlers: Map<string, Set<EventHandler>> = new Map();
constructor(existingEmitter?: EventEmitter) {
super();
this.emitter = existingEmitter ?? new EventEmitter();
this.emitter.setMaxListeners(100); // Allow many listeners
}
async publish(topic: string, data: any, metadata?: Record<string, any>): Promise<void> {
const event: Event = {
id: crypto.randomUUID(),
topic,
data,
createdAt: new Date(),
metadata
};
this.emitter.emit(topic, event);
}
async subscribe(topic: string, handler: EventHandler): Promise<void> {
if (!this.handlers.has(topic)) {
this.handlers.set(topic, new Set());
}
this.handlers.get(topic)!.add(handler);
this.emitter.on(topic, handler);
}
async unsubscribe(topic: string, handler: EventHandler): Promise<void> {
this.handlers.get(topic)?.delete(handler);
this.emitter.off(topic, handler);
}
async close(): Promise<void> {
this.emitter.removeAllListeners();
this.handlers.clear();
}
/**
* Wait for a specific event with optional timeout
*/
async waitFor(topic: string, timeout?: number): Promise<Event> {
return new Promise((resolve, reject) => {
const timer = timeout ? setTimeout(() => {
this.emitter.off(topic, handler);
reject(new Error(`Timeout waiting for event: ${topic}`));
}, timeout) : null;
const handler = (event: Event) => {
if (timer) clearTimeout(timer);
this.emitter.off(topic, handler);
resolve(event);
};
this.emitter.once(topic, handler);
});
}
/**
* Get the underlying EventEmitter
*/
getEmitter(): EventEmitter {
return this.emitter;
}
}
/**
* Agent Event Bus - Specialized event system for agent communication
*/
export class AgentEventBus {
private pubsub: PubSub;
private agentId: string;
constructor(agentId: string, pubsub?: PubSub) {
this.agentId = agentId;
this.pubsub = pubsub ?? new EventEmitterPubSub();
}
/**
* Emit an agent event
*/
async emit(eventType: string, data: any): Promise<void> {
await this.pubsub.publish(`agent:${this.agentId}:${eventType}`, data, {
agentId: this.agentId,
eventType
});
}
/**
* Listen for agent events
*/
async on(eventType: string, handler: (data: any) => void | Promise<void>): Promise<void> {
await this.pubsub.subscribe(`agent:${this.agentId}:${eventType}`, (event) => {
handler(event.data);
});
}
/**
* Broadcast to all agents
*/
async broadcast(eventType: string, data: any): Promise<void> {
await this.pubsub.publish(`broadcast:${eventType}`, data, {
sourceAgentId: this.agentId,
eventType
});
}
/**
* Listen for broadcast events
*/
async onBroadcast(eventType: string, handler: (data: any, sourceAgentId: string) => void | Promise<void>): Promise<void> {
await this.pubsub.subscribe(`broadcast:${eventType}`, (event) => {
handler(event.data, event.metadata?.sourceAgentId);
});
}
/**
* Send message to specific agent
*/
async sendTo(targetAgentId: string, eventType: string, data: any): Promise<void> {
await this.pubsub.publish(`agent:${targetAgentId}:${eventType}`, data, {
sourceAgentId: this.agentId,
targetAgentId,
eventType
});
}
async close(): Promise<void> {
await this.pubsub.close();
}
}
// Standard event types
export const AgentEvents = {
STARTED: 'started',
COMPLETED: 'completed',
ERROR: 'error',
TOOL_CALLED: 'tool_called',
TOOL_RESULT: 'tool_result',
MESSAGE_RECEIVED: 'message_received',
MESSAGE_SENT: 'message_sent',
HANDOFF_INITIATED: 'handoff_initiated',
HANDOFF_COMPLETED: 'handoff_completed'
} as const;
// Factory functions
export function createEventBus(agentId: string): AgentEventBus {
return new AgentEventBus(agentId);
}
export function createPubSub(): EventEmitterPubSub {
return new EventEmitterPubSub();
}
+244
View File
@@ -0,0 +1,244 @@
/**
* Guardrails - Input/output validation and safety checks
*/
export type GuardrailStatus = 'passed' | 'failed' | 'warning';
export interface GuardrailResult {
status: GuardrailStatus;
message?: string;
details?: Record<string, any>;
modifiedContent?: string;
}
export type GuardrailFunction = (
content: string,
context?: GuardrailContext
) => Promise<GuardrailResult> | GuardrailResult;
export interface GuardrailContext {
role: 'input' | 'output';
agentName?: string;
sessionId?: string;
metadata?: Record<string, any>;
}
export interface GuardrailConfig {
name: string;
description?: string;
check: GuardrailFunction;
onFail?: 'block' | 'warn' | 'modify';
}
/**
* Guardrail class
*/
export class Guardrail {
readonly name: string;
readonly description: string;
readonly check: GuardrailFunction;
readonly onFail: 'block' | 'warn' | 'modify';
constructor(config: GuardrailConfig) {
this.name = config.name;
this.description = config.description || `Guardrail: ${config.name}`;
this.check = config.check;
this.onFail = config.onFail || 'block';
}
async run(content: string, context?: GuardrailContext): Promise<GuardrailResult> {
try {
return await this.check(content, context);
} catch (error: any) {
return {
status: 'failed',
message: `Guardrail error: ${error.message}`,
details: { error: error.message },
};
}
}
}
/**
* Create a guardrail
*/
export function guardrail(config: GuardrailConfig): Guardrail {
return new Guardrail(config);
}
/**
* Guardrail Manager - Run multiple guardrails
*/
export class GuardrailManager {
private guardrails: Guardrail[] = [];
add(g: Guardrail): this {
this.guardrails.push(g);
return this;
}
async runAll(
content: string,
context?: GuardrailContext
): Promise<{ passed: boolean; results: Array<{ name: string; result: GuardrailResult }> }> {
const results: Array<{ name: string; result: GuardrailResult }> = [];
let passed = true;
for (const g of this.guardrails) {
const result = await g.run(content, context);
results.push({ name: g.name, result });
if (result.status === 'failed') {
passed = false;
if (g.onFail === 'block') {
break;
}
}
}
return { passed, results };
}
get count(): number {
return this.guardrails.length;
}
}
/**
* Built-in guardrails
*/
export const builtinGuardrails = {
/**
* Check for maximum length
*/
maxLength: (maxChars: number): Guardrail => {
return guardrail({
name: 'max_length',
description: `Ensure content is under ${maxChars} characters`,
check: (content) => {
if (content.length > maxChars) {
return {
status: 'failed',
message: `Content exceeds maximum length of ${maxChars} characters`,
details: { length: content.length, max: maxChars },
};
}
return { status: 'passed' };
},
});
},
/**
* Check for minimum length
*/
minLength: (minChars: number): Guardrail => {
return guardrail({
name: 'min_length',
description: `Ensure content is at least ${minChars} characters`,
check: (content) => {
if (content.length < minChars) {
return {
status: 'failed',
message: `Content is below minimum length of ${minChars} characters`,
details: { length: content.length, min: minChars },
};
}
return { status: 'passed' };
},
});
},
/**
* Check for blocked words
*/
blockedWords: (words: string[]): Guardrail => {
return guardrail({
name: 'blocked_words',
description: 'Check for blocked words',
check: (content) => {
const lowerContent = content.toLowerCase();
const found = words.filter(w => lowerContent.includes(w.toLowerCase()));
if (found.length > 0) {
return {
status: 'failed',
message: `Content contains blocked words`,
details: { blockedWords: found },
};
}
return { status: 'passed' };
},
});
},
/**
* Check for required words
*/
requiredWords: (words: string[]): Guardrail => {
return guardrail({
name: 'required_words',
description: 'Check for required words',
check: (content) => {
const lowerContent = content.toLowerCase();
const missing = words.filter(w => !lowerContent.includes(w.toLowerCase()));
if (missing.length > 0) {
return {
status: 'failed',
message: `Content missing required words`,
details: { missingWords: missing },
};
}
return { status: 'passed' };
},
});
},
/**
* Regex pattern check
*/
pattern: (regex: RegExp, mustMatch: boolean = true): Guardrail => {
return guardrail({
name: 'pattern',
description: `Check content against pattern: ${regex}`,
check: (content) => {
const matches = regex.test(content);
if (mustMatch && !matches) {
return {
status: 'failed',
message: `Content does not match required pattern`,
details: { pattern: regex.toString() },
};
}
if (!mustMatch && matches) {
return {
status: 'failed',
message: `Content matches forbidden pattern`,
details: { pattern: regex.toString() },
};
}
return { status: 'passed' };
},
});
},
/**
* JSON validity check
*/
validJson: (): Guardrail => {
return guardrail({
name: 'valid_json',
description: 'Ensure content is valid JSON',
check: (content) => {
try {
JSON.parse(content);
return { status: 'passed' };
} catch (e: any) {
return {
status: 'failed',
message: 'Content is not valid JSON',
details: { error: e.message },
};
}
},
});
},
};
@@ -0,0 +1,116 @@
/**
* LLMGuardrail - LLM-based content validation
*/
import { createProvider, type LLMProvider } from '../llm/providers';
export interface LLMGuardrailConfig {
name: string;
criteria: string;
llm?: string;
threshold?: number;
verbose?: boolean;
}
export interface LLMGuardrailResult {
status: 'passed' | 'failed' | 'warning';
score: number;
message?: string;
reasoning?: string;
}
/**
* LLMGuardrail - Use LLM to validate content against criteria
*/
export class LLMGuardrail {
readonly name: string;
readonly criteria: string;
private provider: LLMProvider;
private threshold: number;
private verbose: boolean;
constructor(config: LLMGuardrailConfig) {
this.name = config.name;
this.criteria = config.criteria;
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
this.threshold = config.threshold ?? 0.7;
this.verbose = config.verbose ?? false;
}
/**
* Check content against criteria
*/
async check(content: string): Promise<LLMGuardrailResult> {
const prompt = `Evaluate the following content against this criteria:
Criteria: ${this.criteria}
Content: ${content}
Respond with a JSON object containing:
- score: number from 0 to 1 (1 = fully meets criteria)
- passed: boolean
- reasoning: brief explanation
JSON response:`;
try {
const result = await this.provider.generateText({
messages: [{ role: 'user', content: prompt }]
});
const parsed = this.parseResponse(result.text);
if (this.verbose) {
console.log(`[LLMGuardrail:${this.name}] Score: ${parsed.score}, Passed: ${parsed.status}`);
}
return parsed;
} catch (error: any) {
return {
status: 'warning',
score: 0.5,
message: `Guardrail check failed: ${error.message}`
};
}
}
/**
* Run guardrail (alias for check)
*/
async run(content: string): Promise<LLMGuardrailResult> {
return this.check(content);
}
private parseResponse(response: string): LLMGuardrailResult {
try {
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
const score = typeof parsed.score === 'number' ? parsed.score : 0.5;
return {
status: score >= this.threshold ? 'passed' : 'failed',
score,
reasoning: parsed.reasoning
};
}
} catch (e) {
// Parse failed
}
// Fallback parsing
const hasPositive = /pass|good|valid|accept|meets/i.test(response);
return {
status: hasPositive ? 'passed' : 'failed',
score: hasPositive ? 0.8 : 0.3,
reasoning: response.substring(0, 200)
};
}
}
/**
* Create an LLMGuardrail
*/
export function createLLMGuardrail(config: LLMGuardrailConfig): LLMGuardrail {
return new LLMGuardrail(config);
}
+165 -1
View File
@@ -4,4 +4,168 @@ export * from './knowledge';
export * from './llm';
export * from './memory';
export * from './process';
export * from './tools';
// Export tools (excluding conflicting types)
export {
BaseTool, ToolResult, ToolValidationError, validateTool, createTool,
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 Memory
export { Memory, createMemory, type MemoryEntry, type MemoryConfig, type SearchResult as MemorySearchResult } from './memory/memory';
// Export Telemetry
export { TelemetryCollector, getTelemetry, enableTelemetry, disableTelemetry, cleanupTelemetry, type TelemetryEvent, type TelemetryConfig } from './telemetry';
// Export AutoAgents
export { AutoAgents, createAutoAgents, type AgentConfig, type TaskConfig, type TeamStructure, type AutoAgentsConfig } from './auto';
// Export ImageAgent
export { ImageAgent, createImageAgent, type ImageAgentConfig, type ImageGenerationConfig, type ImageAnalysisConfig } from './agent/image';
// Export DeepResearchAgent
export { DeepResearchAgent, createDeepResearchAgent, type DeepResearchConfig, type ResearchResponse, type Citation, type ReasoningStep } from './agent/research';
// Export QueryRewriterAgent
export { QueryRewriterAgent, createQueryRewriterAgent, type QueryRewriterConfig, type RewriteResult, type RewriteStrategy } from './agent/query-rewriter';
// Export PromptExpanderAgent
export { PromptExpanderAgent, createPromptExpanderAgent, type PromptExpanderConfig, type ExpandResult, type ExpandStrategy } from './agent/prompt-expander';
// Export Chunking
export { Chunking, createChunking, type ChunkingConfig, type Chunk, type ChunkStrategy } from './knowledge/chunking';
// Export LLMGuardrail
export { LLMGuardrail, createLLMGuardrail, type LLMGuardrailConfig, type LLMGuardrailResult } from './guardrails/llm-guardrail';
// Export Planning
export { Plan, PlanStep, TodoList, TodoItem, PlanStorage, createPlan, createTodoList, createPlanStorage, type PlanConfig, type PlanStepConfig, type TodoItemConfig, type PlanStatus, type TodoStatus } from './planning';
// Export Cache
export { BaseCache, MemoryCache, FileCache, createMemoryCache, createFileCache, type CacheConfig, type CacheEntry } from './cache';
// Export Events
export { PubSub, EventEmitterPubSub, AgentEventBus, AgentEvents, createEventBus, createPubSub, type Event, type EventHandler } from './events';
// Export YAML Workflow Parser
export { parseYAMLWorkflow, createWorkflowFromYAML, loadWorkflowFromFile, validateWorkflowDefinition, type YAMLWorkflowDefinition, type YAMLStepDefinition, type ParsedWorkflow } from './workflows/yaml-parser';
// Export SQLite Adapter
export { SQLiteAdapter, createSQLiteAdapter, type SQLiteConfig, type DbMessage, type DbRun, type DbTrace } from './db/sqlite';
// Export Redis Adapter
export {
UpstashRedisAdapter, MemoryRedisAdapter,
createUpstashRedis, createMemoryRedis,
type RedisConfig, type RedisAdapter
} from './db/redis';
// Export PostgreSQL Adapter
export {
NeonPostgresAdapter, MemoryPostgresAdapter, PostgresSessionStorage,
createNeonPostgres, createMemoryPostgres, createPostgresSessionStorage,
type PostgresConfig, type PostgresAdapter
} from './db/postgres';
// Export Integrations - Vector Stores
export {
BaseVectorStore, MemoryVectorStore, createMemoryVectorStore,
PineconeVectorStore, createPineconeStore,
WeaviateVectorStore, createWeaviateStore,
QdrantVectorStore, createQdrantStore,
ChromaVectorStore, createChromaStore,
type VectorDocument, type QueryResult as VectorQueryResult, type IndexStats
} from './integrations/vector';
// Export Integrations - Observability
export {
BaseObservabilityProvider, ConsoleObservabilityProvider, MemoryObservabilityProvider,
LangfuseObservabilityProvider,
createConsoleObservability, createMemoryObservability, createLangfuseObservability,
type Span, type TraceContext as ObservabilityTraceContext, type LogEntry, type Metric
} from './integrations/observability';
// Export Integrations - Voice
export {
BaseVoiceProvider, OpenAIVoiceProvider, ElevenLabsVoiceProvider,
createOpenAIVoice, createElevenLabsVoice,
type VoiceConfig, type SpeakOptions, type ListenOptions, type Speaker
} from './integrations/voice';
// Export Reranker
export {
BaseReranker, CohereReranker, CrossEncoderReranker, LLMReranker,
createCohereReranker, createCrossEncoderReranker, createLLMReranker,
type RerankResult, type RerankConfig
} from './knowledge/reranker';
// Export Graph RAG
export {
GraphStore, GraphRAG, createGraphRAG,
type GraphNode, type GraphEdge, type GraphQueryResult, type GraphRAGConfig
} from './knowledge/graph-rag';
// 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,13 @@
/**
* PraisonAI Integrations
* Extensions for vector stores, observability, and voice
*/
// Vector Store Integrations
export * from './vector';
// Observability Integrations
export * from './observability';
// Voice Integrations
export * from './voice';
@@ -0,0 +1,290 @@
/**
* Base Observability - Abstract class for observability integrations
* Supports tracing, logging, and metrics
*/
export interface Span {
id: string;
traceId: string;
parentId?: string;
name: string;
startTime: number;
endTime?: number;
status: 'ok' | 'error' | 'unset';
attributes: Record<string, any>;
events: SpanEvent[];
}
export interface SpanEvent {
name: string;
timestamp: number;
attributes?: Record<string, any>;
}
export interface TraceContext {
traceId: string;
spanId: string;
parentSpanId?: string;
}
export interface LogEntry {
level: 'debug' | 'info' | 'warn' | 'error';
message: string;
timestamp: number;
traceId?: string;
spanId?: string;
attributes?: Record<string, any>;
}
export interface Metric {
name: string;
value: number;
type: 'counter' | 'gauge' | 'histogram';
timestamp: number;
tags?: Record<string, string>;
}
/**
* Abstract base class for observability providers
*/
export abstract class BaseObservabilityProvider {
readonly name: string;
constructor(name: string) {
this.name = name;
}
/**
* Start a new span
*/
abstract startSpan(name: string, attributes?: Record<string, any>, parentContext?: TraceContext): Span;
/**
* End a span
*/
abstract endSpan(span: Span, status?: 'ok' | 'error', error?: Error): void;
/**
* Add event to span
*/
abstract addSpanEvent(span: Span, name: string, attributes?: Record<string, any>): void;
/**
* Log a message
*/
abstract log(entry: LogEntry): void;
/**
* Record a metric
*/
abstract recordMetric(metric: Metric): void;
/**
* Flush all pending data
*/
abstract flush(): Promise<void>;
/**
* Shutdown the provider
*/
abstract shutdown(): Promise<void>;
/**
* Helper to create trace context
*/
protected createTraceContext(span: Span): TraceContext {
return {
traceId: span.traceId,
spanId: span.id,
parentSpanId: span.parentId
};
}
/**
* Generate unique ID
*/
protected generateId(): string {
return crypto.randomUUID().replace(/-/g, '');
}
}
/**
* Console-based observability provider for development
*/
export class ConsoleObservabilityProvider extends BaseObservabilityProvider {
private spans: Map<string, Span> = new Map();
private verbose: boolean;
constructor(config: { verbose?: boolean } = {}) {
super('ConsoleObservability');
this.verbose = config.verbose ?? false;
}
startSpan(name: string, attributes?: Record<string, any>, parentContext?: TraceContext): Span {
const span: Span = {
id: this.generateId(),
traceId: parentContext?.traceId || this.generateId(),
parentId: parentContext?.spanId,
name,
startTime: Date.now(),
status: 'unset',
attributes: attributes || {},
events: []
};
this.spans.set(span.id, span);
if (this.verbose) {
console.log(`[SPAN START] ${name}`, { traceId: span.traceId, spanId: span.id });
}
return span;
}
endSpan(span: Span, status: 'ok' | 'error' = 'ok', error?: Error): void {
span.endTime = Date.now();
span.status = status;
if (error) {
span.attributes.error = error.message;
span.attributes.errorStack = error.stack;
}
const duration = span.endTime - span.startTime;
if (this.verbose) {
console.log(`[SPAN END] ${span.name}`, {
duration: `${duration}ms`,
status,
...(error && { error: error.message })
});
}
this.spans.delete(span.id);
}
addSpanEvent(span: Span, name: string, attributes?: Record<string, any>): void {
span.events.push({
name,
timestamp: Date.now(),
attributes
});
if (this.verbose) {
console.log(`[SPAN EVENT] ${span.name} -> ${name}`, attributes);
}
}
log(entry: LogEntry): void {
const prefix = `[${entry.level.toUpperCase()}]`;
const context = entry.traceId ? ` [trace:${entry.traceId.slice(0, 8)}]` : '';
switch (entry.level) {
case 'debug':
console.debug(`${prefix}${context} ${entry.message}`, entry.attributes);
break;
case 'info':
console.info(`${prefix}${context} ${entry.message}`, entry.attributes);
break;
case 'warn':
console.warn(`${prefix}${context} ${entry.message}`, entry.attributes);
break;
case 'error':
console.error(`${prefix}${context} ${entry.message}`, entry.attributes);
break;
}
}
recordMetric(metric: Metric): void {
if (this.verbose) {
console.log(`[METRIC] ${metric.name}: ${metric.value} (${metric.type})`, metric.tags);
}
}
async flush(): Promise<void> {
// Console provider doesn't need flushing
}
async shutdown(): Promise<void> {
this.spans.clear();
}
}
/**
* Memory-based observability provider for testing
*/
export class MemoryObservabilityProvider extends BaseObservabilityProvider {
spans: Span[] = [];
logs: LogEntry[] = [];
metrics: Metric[] = [];
constructor() {
super('MemoryObservability');
}
startSpan(name: string, attributes?: Record<string, any>, parentContext?: TraceContext): Span {
const span: Span = {
id: this.generateId(),
traceId: parentContext?.traceId || this.generateId(),
parentId: parentContext?.spanId,
name,
startTime: Date.now(),
status: 'unset',
attributes: attributes || {},
events: []
};
this.spans.push(span);
return span;
}
endSpan(span: Span, status: 'ok' | 'error' = 'ok', error?: Error): void {
span.endTime = Date.now();
span.status = status;
if (error) {
span.attributes.error = error.message;
}
}
addSpanEvent(span: Span, name: string, attributes?: Record<string, any>): void {
span.events.push({ name, timestamp: Date.now(), attributes });
}
log(entry: LogEntry): void {
this.logs.push(entry);
}
recordMetric(metric: Metric): void {
this.metrics.push(metric);
}
async flush(): Promise<void> {}
async shutdown(): Promise<void> {
this.spans = [];
this.logs = [];
this.metrics = [];
}
// Helper methods for testing
getSpansByName(name: string): Span[] {
return this.spans.filter(s => s.name === name);
}
getLogsByLevel(level: LogEntry['level']): LogEntry[] {
return this.logs.filter(l => l.level === level);
}
getMetricsByName(name: string): Metric[] {
return this.metrics.filter(m => m.name === name);
}
}
// Factory functions
export function createConsoleObservability(config?: { verbose?: boolean }): ConsoleObservabilityProvider {
return new ConsoleObservabilityProvider(config);
}
export function createMemoryObservability(): MemoryObservabilityProvider {
return new MemoryObservabilityProvider();
}
@@ -0,0 +1,11 @@
/**
* Observability Integrations
* Provides adapters for various observability platforms
*/
export * from './base';
export * from './langfuse';
// Re-export factory functions
export { createConsoleObservability, createMemoryObservability } from './base';
export { createLangfuseObservability } from './langfuse';
@@ -0,0 +1,208 @@
/**
* Langfuse Observability Integration
*/
import { BaseObservabilityProvider, Span, TraceContext, LogEntry, Metric } from './base';
export interface LangfuseConfig {
publicKey: string;
secretKey: string;
baseUrl?: string;
flushAt?: number;
flushInterval?: number;
}
export class LangfuseObservabilityProvider extends BaseObservabilityProvider {
private publicKey: string;
private secretKey: string;
private baseUrl: string;
private queue: any[] = [];
private flushAt: number;
private flushInterval: number;
private flushTimer?: NodeJS.Timeout;
constructor(config: LangfuseConfig) {
super('LangfuseObservability');
this.publicKey = config.publicKey;
this.secretKey = config.secretKey;
this.baseUrl = config.baseUrl || 'https://cloud.langfuse.com';
this.flushAt = config.flushAt || 20;
this.flushInterval = config.flushInterval || 10000;
this.startFlushTimer();
}
private startFlushTimer(): void {
this.flushTimer = setInterval(() => {
if (this.queue.length > 0) {
this.flush();
}
}, this.flushInterval);
}
private async request(path: string, body: any): Promise<any> {
const auth = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString('base64');
const response = await fetch(`${this.baseUrl}${path}`, {
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Langfuse API error: ${response.status} - ${error}`);
}
return response.json();
}
private enqueue(event: any): void {
this.queue.push(event);
if (this.queue.length >= this.flushAt) {
this.flush();
}
}
startSpan(name: string, attributes?: Record<string, any>, parentContext?: TraceContext): Span {
const span: Span = {
id: this.generateId(),
traceId: parentContext?.traceId || this.generateId(),
parentId: parentContext?.spanId,
name,
startTime: Date.now(),
status: 'unset',
attributes: attributes || {},
events: []
};
// Create trace if this is root span
if (!parentContext) {
this.enqueue({
type: 'trace-create',
body: {
id: span.traceId,
name,
metadata: attributes,
timestamp: new Date(span.startTime).toISOString()
}
});
}
// Create span/generation
this.enqueue({
type: 'span-create',
body: {
id: span.id,
traceId: span.traceId,
parentObservationId: span.parentId,
name,
startTime: new Date(span.startTime).toISOString(),
metadata: attributes
}
});
return span;
}
endSpan(span: Span, status: 'ok' | 'error' = 'ok', error?: Error): void {
span.endTime = Date.now();
span.status = status;
const updateBody: any = {
spanId: span.id,
endTime: new Date(span.endTime).toISOString(),
level: status === 'error' ? 'ERROR' : 'DEFAULT'
};
if (error) {
updateBody.statusMessage = error.message;
}
this.enqueue({
type: 'span-update',
body: updateBody
});
}
addSpanEvent(span: Span, name: string, attributes?: Record<string, any>): void {
span.events.push({
name,
timestamp: Date.now(),
attributes
});
this.enqueue({
type: 'event-create',
body: {
traceId: span.traceId,
observationId: span.id,
name,
metadata: attributes,
timestamp: new Date().toISOString()
}
});
}
log(entry: LogEntry): void {
// Langfuse doesn't have direct log support, use events instead
if (entry.traceId) {
this.enqueue({
type: 'event-create',
body: {
traceId: entry.traceId,
observationId: entry.spanId,
name: `log:${entry.level}`,
metadata: {
message: entry.message,
...entry.attributes
},
timestamp: new Date(entry.timestamp).toISOString()
}
});
}
}
recordMetric(metric: Metric): void {
// Langfuse uses scores for metrics
this.enqueue({
type: 'score-create',
body: {
name: metric.name,
value: metric.value,
timestamp: new Date(metric.timestamp).toISOString(),
comment: JSON.stringify(metric.tags)
}
});
}
async flush(): Promise<void> {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.queue.length);
try {
await this.request('/api/public/ingestion', {
batch
});
} catch (error) {
// Re-add failed items to queue
this.queue.unshift(...batch);
console.error('Langfuse flush error:', error);
}
}
async shutdown(): Promise<void> {
if (this.flushTimer) {
clearInterval(this.flushTimer);
}
await this.flush();
}
}
export function createLangfuseObservability(config: LangfuseConfig): LangfuseObservabilityProvider {
return new LangfuseObservabilityProvider(config);
}
@@ -0,0 +1,270 @@
/**
* Base Vector Store - Abstract class for vector database integrations
* Matches mastra's MastraVector pattern
*/
export interface VectorDocument {
id: string;
vector: number[];
metadata?: Record<string, any>;
content?: string;
}
export interface QueryResult {
id: string;
score: number;
metadata?: Record<string, any>;
content?: string;
vector?: number[];
}
export interface IndexStats {
dimension: number;
count: number;
metric: 'cosine' | 'euclidean' | 'dotProduct';
}
export interface CreateIndexParams {
indexName: string;
dimension: number;
metric?: 'cosine' | 'euclidean' | 'dotProduct';
}
export interface UpsertParams {
indexName: string;
vectors: VectorDocument[];
}
export interface QueryParams {
indexName: string;
vector: number[];
topK?: number;
filter?: Record<string, any>;
includeMetadata?: boolean;
includeVectors?: boolean;
}
export interface DeleteParams {
indexName: string;
ids?: string[];
filter?: Record<string, any>;
}
/**
* Abstract base class for vector store implementations
*/
export abstract class BaseVectorStore {
readonly id: string;
readonly name: string;
constructor(config: { id: string; name?: string }) {
this.id = config.id;
this.name = config.name || 'VectorStore';
}
/**
* Create a new index
*/
abstract createIndex(params: CreateIndexParams): Promise<void>;
/**
* List all indexes
*/
abstract listIndexes(): Promise<string[]>;
/**
* Get index statistics
*/
abstract describeIndex(indexName: string): Promise<IndexStats>;
/**
* Delete an index
*/
abstract deleteIndex(indexName: string): Promise<void>;
/**
* Upsert vectors into an index
*/
abstract upsert(params: UpsertParams): Promise<string[]>;
/**
* Query vectors by similarity
*/
abstract query(params: QueryParams): Promise<QueryResult[]>;
/**
* Delete vectors by ID or filter
*/
abstract delete(params: DeleteParams): Promise<void>;
/**
* Update vector metadata
*/
abstract update(indexName: string, id: string, metadata: Record<string, any>): Promise<void>;
}
/**
* In-memory vector store for testing and development
*/
export class MemoryVectorStore extends BaseVectorStore {
private indexes: Map<string, {
dimension: number;
metric: 'cosine' | 'euclidean' | 'dotProduct';
vectors: Map<string, VectorDocument>;
}> = new Map();
constructor(config: { id: string } = { id: 'memory' }) {
super({ ...config, name: 'MemoryVectorStore' });
}
async createIndex(params: CreateIndexParams): Promise<void> {
if (this.indexes.has(params.indexName)) {
return; // Index already exists
}
this.indexes.set(params.indexName, {
dimension: params.dimension,
metric: params.metric || 'cosine',
vectors: new Map()
});
}
async listIndexes(): Promise<string[]> {
return Array.from(this.indexes.keys());
}
async describeIndex(indexName: string): Promise<IndexStats> {
const index = this.indexes.get(indexName);
if (!index) {
throw new Error(`Index ${indexName} not found`);
}
return {
dimension: index.dimension,
count: index.vectors.size,
metric: index.metric
};
}
async deleteIndex(indexName: string): Promise<void> {
this.indexes.delete(indexName);
}
async upsert(params: UpsertParams): Promise<string[]> {
const index = this.indexes.get(params.indexName);
if (!index) {
throw new Error(`Index ${params.indexName} not found`);
}
const ids: string[] = [];
for (const doc of params.vectors) {
index.vectors.set(doc.id, doc);
ids.push(doc.id);
}
return ids;
}
async query(params: QueryParams): Promise<QueryResult[]> {
const index = this.indexes.get(params.indexName);
if (!index) {
throw new Error(`Index ${params.indexName} not found`);
}
const results: QueryResult[] = [];
for (const [id, doc] of index.vectors) {
// Apply filter if provided
if (params.filter && !this.matchesFilter(doc.metadata || {}, params.filter)) {
continue;
}
const score = this.calculateSimilarity(params.vector, doc.vector, index.metric);
results.push({
id,
score,
metadata: params.includeMetadata ? doc.metadata : undefined,
content: doc.content,
vector: params.includeVectors ? doc.vector : undefined
});
}
// Sort by score descending and limit
results.sort((a, b) => b.score - a.score);
return results.slice(0, params.topK || 10);
}
async delete(params: DeleteParams): Promise<void> {
const index = this.indexes.get(params.indexName);
if (!index) {
throw new Error(`Index ${params.indexName} not found`);
}
if (params.ids) {
for (const id of params.ids) {
index.vectors.delete(id);
}
} else if (params.filter) {
for (const [id, doc] of index.vectors) {
if (this.matchesFilter(doc.metadata || {}, params.filter)) {
index.vectors.delete(id);
}
}
}
}
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
const index = this.indexes.get(indexName);
if (!index) {
throw new Error(`Index ${indexName} not found`);
}
const doc = index.vectors.get(id);
if (doc) {
doc.metadata = { ...doc.metadata, ...metadata };
}
}
private calculateSimilarity(a: number[], b: number[], metric: string): number {
if (a.length !== b.length) {
throw new Error('Vector dimensions must match');
}
switch (metric) {
case 'cosine':
return this.cosineSimilarity(a, b);
case 'euclidean':
return 1 / (1 + this.euclideanDistance(a, b));
case 'dotProduct':
return this.dotProduct(a, b);
default:
return this.cosineSimilarity(a, b);
}
}
private cosineSimilarity(a: number[], b: number[]): number {
const dot = this.dotProduct(a, b);
const normA = Math.sqrt(a.reduce((sum, x) => sum + x * x, 0));
const normB = Math.sqrt(b.reduce((sum, x) => sum + x * x, 0));
return dot / (normA * normB);
}
private euclideanDistance(a: number[], b: number[]): number {
return Math.sqrt(a.reduce((sum, x, i) => sum + Math.pow(x - b[i], 2), 0));
}
private dotProduct(a: number[], b: number[]): number {
return a.reduce((sum, x, i) => sum + x * b[i], 0);
}
private matchesFilter(metadata: Record<string, any>, filter: Record<string, any>): boolean {
for (const [key, value] of Object.entries(filter)) {
if (metadata[key] !== value) {
return false;
}
}
return true;
}
}
// Factory function
export function createMemoryVectorStore(id?: string): MemoryVectorStore {
return new MemoryVectorStore({ id: id || 'memory' });
}
@@ -0,0 +1,167 @@
/**
* Chroma Vector Store Integration
*/
import { BaseVectorStore, CreateIndexParams, UpsertParams, QueryParams, DeleteParams, QueryResult, IndexStats } from './base';
export interface ChromaConfig {
host?: string;
port?: number;
path?: string;
}
export class ChromaVectorStore extends BaseVectorStore {
private baseUrl: string;
constructor(config: ChromaConfig & { id?: string } = {}) {
super({ id: config.id || 'chroma', name: 'ChromaVectorStore' });
const host = config.host || 'localhost';
const port = config.port || 8000;
this.baseUrl = config.path || `http://${host}:${port}`;
}
private async request(path: string, options: RequestInit = {}): Promise<any> {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers as Record<string, string>
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Chroma API error: ${response.status} - ${error}`);
}
const text = await response.text();
return text ? JSON.parse(text) : {};
}
async createIndex(params: CreateIndexParams): Promise<void> {
const metadataMap: Record<string, string> = {
cosine: 'cosine',
euclidean: 'l2',
dotProduct: 'ip'
};
await this.request('/api/v1/collections', {
method: 'POST',
body: JSON.stringify({
name: params.indexName,
metadata: {
'hnsw:space': metadataMap[params.metric || 'cosine'],
dimension: params.dimension
}
})
});
}
async listIndexes(): Promise<string[]> {
const response = await this.request('/api/v1/collections');
return (response || []).map((c: any) => c.name);
}
async describeIndex(indexName: string): Promise<IndexStats> {
const response = await this.request(`/api/v1/collections/${indexName}`);
const spaceMap: Record<string, 'cosine' | 'euclidean' | 'dotProduct'> = {
'cosine': 'cosine',
'l2': 'euclidean',
'ip': 'dotProduct'
};
return {
dimension: response.metadata?.dimension || 0,
count: response.count || 0,
metric: spaceMap[response.metadata?.['hnsw:space']] || 'cosine'
};
}
async deleteIndex(indexName: string): Promise<void> {
await this.request(`/api/v1/collections/${indexName}`, { method: 'DELETE' });
}
async upsert(params: UpsertParams): Promise<string[]> {
const ids = params.vectors.map(v => v.id);
const embeddings = params.vectors.map(v => v.vector);
const metadatas = params.vectors.map(v => v.metadata || {});
const documents = params.vectors.map(v => v.content || '');
await this.request(`/api/v1/collections/${params.indexName}/upsert`, {
method: 'POST',
body: JSON.stringify({
ids,
embeddings,
metadatas,
documents
})
});
return ids;
}
async query(params: QueryParams): Promise<QueryResult[]> {
let where: any = undefined;
if (params.filter) {
where = params.filter;
}
const response = await this.request(`/api/v1/collections/${params.indexName}/query`, {
method: 'POST',
body: JSON.stringify({
query_embeddings: [params.vector],
n_results: params.topK || 10,
where,
include: [
'documents',
'metadatas',
'distances',
...(params.includeVectors ? ['embeddings'] : [])
]
})
});
const ids = response.ids?.[0] || [];
const distances = response.distances?.[0] || [];
const metadatas = response.metadatas?.[0] || [];
const documents = response.documents?.[0] || [];
const embeddings = response.embeddings?.[0] || [];
return ids.map((id: string, i: number) => ({
id,
score: 1 - (distances[i] || 0), // Convert distance to similarity
metadata: metadatas[i],
content: documents[i],
vector: embeddings[i]
}));
}
async delete(params: DeleteParams): Promise<void> {
if (params.ids) {
await this.request(`/api/v1/collections/${params.indexName}/delete`, {
method: 'POST',
body: JSON.stringify({ ids: params.ids })
});
} else if (params.filter) {
await this.request(`/api/v1/collections/${params.indexName}/delete`, {
method: 'POST',
body: JSON.stringify({ where: params.filter })
});
}
}
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
await this.request(`/api/v1/collections/${indexName}/update`, {
method: 'POST',
body: JSON.stringify({
ids: [id],
metadatas: [metadata]
})
});
}
}
export function createChromaStore(config?: ChromaConfig): ChromaVectorStore {
return new ChromaVectorStore(config);
}
@@ -0,0 +1,17 @@
/**
* Vector Store Integrations
* Provides adapters for various vector databases
*/
export * from './base';
export * from './pinecone';
export * from './weaviate';
export * from './qdrant';
export * from './chroma';
// Re-export factory functions for convenience
export { createMemoryVectorStore } from './base';
export { createPineconeStore } from './pinecone';
export { createWeaviateStore } from './weaviate';
export { createQdrantStore } from './qdrant';
export { createChromaStore } from './chroma';
@@ -0,0 +1,200 @@
/**
* Pinecone Vector Store Integration
*/
import { BaseVectorStore, CreateIndexParams, UpsertParams, QueryParams, DeleteParams, QueryResult, IndexStats } from './base';
export interface PineconeConfig {
apiKey: string;
environment?: string;
projectId?: string;
}
export class PineconeVectorStore extends BaseVectorStore {
private apiKey: string;
private baseUrl: string;
private indexHosts: Map<string, string> = new Map();
constructor(config: PineconeConfig & { id?: string }) {
super({ id: config.id || 'pinecone', name: 'PineconeVectorStore' });
this.apiKey = config.apiKey;
this.baseUrl = 'https://api.pinecone.io';
}
private async request(path: string, options: RequestInit = {}): Promise<any> {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'Api-Key': this.apiKey,
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Pinecone API error: ${response.status} - ${error}`);
}
const text = await response.text();
return text ? JSON.parse(text) : {};
}
private async indexRequest(indexName: string, path: string, options: RequestInit = {}): Promise<any> {
let host = this.indexHosts.get(indexName);
if (!host) {
const info = await this.request(`/indexes/${indexName}`);
host = info.host as string;
this.indexHosts.set(indexName, host);
}
const response = await fetch(`https://${host}${path}`, {
...options,
headers: {
'Api-Key': this.apiKey,
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Pinecone Index API error: ${response.status} - ${error}`);
}
const text = await response.text();
return text ? JSON.parse(text) : {};
}
async createIndex(params: CreateIndexParams): Promise<void> {
const metricMap: Record<string, string> = {
cosine: 'cosine',
euclidean: 'euclidean',
dotProduct: 'dotproduct'
};
await this.request('/indexes', {
method: 'POST',
body: JSON.stringify({
name: params.indexName,
dimension: params.dimension,
metric: metricMap[params.metric || 'cosine'],
spec: {
serverless: {
cloud: 'aws',
region: 'us-east-1'
}
}
})
});
// Wait for index to be ready
let ready = false;
let attempts = 0;
while (!ready && attempts < 60) {
await new Promise(r => setTimeout(r, 2000));
try {
const info = await this.request(`/indexes/${params.indexName}`);
ready = info.status?.ready === true;
} catch {
// Index not ready yet
}
attempts++;
}
}
async listIndexes(): Promise<string[]> {
const response = await this.request('/indexes');
return (response.indexes || []).map((idx: any) => idx.name);
}
async describeIndex(indexName: string): Promise<IndexStats> {
const info = await this.request(`/indexes/${indexName}`);
const stats = await this.indexRequest(indexName, '/describe_index_stats', { method: 'POST', body: '{}' });
const metricMap: Record<string, 'cosine' | 'euclidean' | 'dotProduct'> = {
cosine: 'cosine',
euclidean: 'euclidean',
dotproduct: 'dotProduct'
};
return {
dimension: info.dimension,
count: stats.totalVectorCount || 0,
metric: metricMap[info.metric] || 'cosine'
};
}
async deleteIndex(indexName: string): Promise<void> {
await this.request(`/indexes/${indexName}`, { method: 'DELETE' });
this.indexHosts.delete(indexName);
}
async upsert(params: UpsertParams): Promise<string[]> {
const vectors = params.vectors.map(v => ({
id: v.id,
values: v.vector,
metadata: v.metadata
}));
// Batch upsert in chunks of 100
const batchSize = 100;
const ids: string[] = [];
for (let i = 0; i < vectors.length; i += batchSize) {
const batch = vectors.slice(i, i + batchSize);
await this.indexRequest(params.indexName, '/vectors/upsert', {
method: 'POST',
body: JSON.stringify({ vectors: batch })
});
ids.push(...batch.map(v => v.id));
}
return ids;
}
async query(params: QueryParams): Promise<QueryResult[]> {
const response = await this.indexRequest(params.indexName, '/query', {
method: 'POST',
body: JSON.stringify({
vector: params.vector,
topK: params.topK || 10,
filter: params.filter,
includeMetadata: params.includeMetadata !== false,
includeValues: params.includeVectors
})
});
return (response.matches || []).map((match: any) => ({
id: match.id,
score: match.score,
metadata: match.metadata,
vector: match.values
}));
}
async delete(params: DeleteParams): Promise<void> {
if (params.ids) {
await this.indexRequest(params.indexName, '/vectors/delete', {
method: 'POST',
body: JSON.stringify({ ids: params.ids })
});
} else if (params.filter) {
await this.indexRequest(params.indexName, '/vectors/delete', {
method: 'POST',
body: JSON.stringify({ filter: params.filter })
});
}
}
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
await this.indexRequest(indexName, '/vectors/update', {
method: 'POST',
body: JSON.stringify({ id, setMetadata: metadata })
});
}
}
export function createPineconeStore(config: PineconeConfig): PineconeVectorStore {
return new PineconeVectorStore(config);
}
@@ -0,0 +1,171 @@
/**
* Qdrant Vector Store Integration
*/
import { BaseVectorStore, CreateIndexParams, UpsertParams, QueryParams, DeleteParams, QueryResult, IndexStats } from './base';
export interface QdrantConfig {
url: string;
apiKey?: string;
}
export class QdrantVectorStore extends BaseVectorStore {
private url: string;
private apiKey?: string;
constructor(config: QdrantConfig & { id?: string }) {
super({ id: config.id || 'qdrant', name: 'QdrantVectorStore' });
this.url = config.url.replace(/\/$/, '');
this.apiKey = config.apiKey;
}
private async request(path: string, options: RequestInit = {}): Promise<any> {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (this.apiKey) {
headers['api-key'] = this.apiKey;
}
const response = await fetch(`${this.url}${path}`, {
...options,
headers: { ...headers, ...options.headers as Record<string, string> }
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Qdrant API error: ${response.status} - ${error}`);
}
const text = await response.text();
return text ? JSON.parse(text) : {};
}
async createIndex(params: CreateIndexParams): Promise<void> {
const distanceMap: Record<string, string> = {
cosine: 'Cosine',
euclidean: 'Euclid',
dotProduct: 'Dot'
};
await this.request(`/collections/${params.indexName}`, {
method: 'PUT',
body: JSON.stringify({
vectors: {
size: params.dimension,
distance: distanceMap[params.metric || 'cosine']
}
})
});
}
async listIndexes(): Promise<string[]> {
const response = await this.request('/collections');
return (response.result?.collections || []).map((c: any) => c.name);
}
async describeIndex(indexName: string): Promise<IndexStats> {
const response = await this.request(`/collections/${indexName}`);
const config = response.result?.config?.params?.vectors;
const distanceMap: Record<string, 'cosine' | 'euclidean' | 'dotProduct'> = {
'Cosine': 'cosine',
'Euclid': 'euclidean',
'Dot': 'dotProduct'
};
return {
dimension: config?.size || 0,
count: response.result?.points_count || 0,
metric: distanceMap[config?.distance] || 'cosine'
};
}
async deleteIndex(indexName: string): Promise<void> {
await this.request(`/collections/${indexName}`, { method: 'DELETE' });
}
async upsert(params: UpsertParams): Promise<string[]> {
const points = params.vectors.map(v => ({
id: v.id,
vector: v.vector,
payload: {
...v.metadata,
_content: v.content
}
}));
await this.request(`/collections/${params.indexName}/points`, {
method: 'PUT',
body: JSON.stringify({ points })
});
return params.vectors.map(v => v.id);
}
async query(params: QueryParams): Promise<QueryResult[]> {
let filter: any = undefined;
if (params.filter) {
filter = {
must: Object.entries(params.filter).map(([key, value]) => ({
key,
match: { value }
}))
};
}
const response = await this.request(`/collections/${params.indexName}/points/search`, {
method: 'POST',
body: JSON.stringify({
vector: params.vector,
limit: params.topK || 10,
filter,
with_payload: params.includeMetadata !== false,
with_vector: params.includeVectors
})
});
return (response.result || []).map((r: any) => ({
id: r.id,
score: r.score,
metadata: r.payload ? { ...r.payload, _content: undefined } : undefined,
content: r.payload?._content,
vector: r.vector
}));
}
async delete(params: DeleteParams): Promise<void> {
if (params.ids) {
await this.request(`/collections/${params.indexName}/points/delete`, {
method: 'POST',
body: JSON.stringify({ points: params.ids })
});
} else if (params.filter) {
const filter = {
must: Object.entries(params.filter).map(([key, value]) => ({
key,
match: { value }
}))
};
await this.request(`/collections/${params.indexName}/points/delete`, {
method: 'POST',
body: JSON.stringify({ filter })
});
}
}
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
await this.request(`/collections/${indexName}/points/payload`, {
method: 'POST',
body: JSON.stringify({
points: [id],
payload: metadata
})
});
}
}
export function createQdrantStore(config: QdrantConfig): QdrantVectorStore {
return new QdrantVectorStore(config);
}
@@ -0,0 +1,243 @@
/**
* Weaviate Vector Store Integration
*/
import { BaseVectorStore, CreateIndexParams, UpsertParams, QueryParams, DeleteParams, QueryResult, IndexStats } from './base';
export interface WeaviateConfig {
host: string;
apiKey?: string;
scheme?: 'http' | 'https';
}
export class WeaviateVectorStore extends BaseVectorStore {
private host: string;
private apiKey?: string;
private scheme: string;
constructor(config: WeaviateConfig & { id?: string }) {
super({ id: config.id || 'weaviate', name: 'WeaviateVectorStore' });
this.host = config.host;
this.apiKey = config.apiKey;
this.scheme = config.scheme || 'https';
}
private get baseUrl(): string {
return `${this.scheme}://${this.host}`;
}
private async request(path: string, options: RequestInit = {}): Promise<any> {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
}
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: { ...headers, ...options.headers as Record<string, string> }
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Weaviate API error: ${response.status} - ${error}`);
}
const text = await response.text();
return text ? JSON.parse(text) : {};
}
private async graphql(query: string, variables?: Record<string, any>): Promise<any> {
const response = await this.request('/v1/graphql', {
method: 'POST',
body: JSON.stringify({ query, variables })
});
return response;
}
private toClassName(indexName: string): string {
// Weaviate class names must start with uppercase
return indexName.charAt(0).toUpperCase() + indexName.slice(1);
}
async createIndex(params: CreateIndexParams): Promise<void> {
const className = this.toClassName(params.indexName);
const vectorIndexConfig: Record<string, any> = {
distance: params.metric === 'euclidean' ? 'l2-squared' :
params.metric === 'dotProduct' ? 'dot' : 'cosine'
};
await this.request('/v1/schema', {
method: 'POST',
body: JSON.stringify({
class: className,
vectorIndexConfig,
properties: [
{
name: 'content',
dataType: ['text']
},
{
name: 'metadata',
dataType: ['object']
}
]
})
});
}
async listIndexes(): Promise<string[]> {
const response = await this.request('/v1/schema');
return (response.classes || []).map((c: any) => c.class.toLowerCase());
}
async describeIndex(indexName: string): Promise<IndexStats> {
const className = this.toClassName(indexName);
const response = await this.request(`/v1/schema/${className}`);
// Get count via aggregate
const countQuery = `{
Aggregate {
${className} {
meta { count }
}
}
}`;
const countResponse = await this.graphql(countQuery);
const count = countResponse.data?.Aggregate?.[className]?.[0]?.meta?.count || 0;
const distanceMap: Record<string, 'cosine' | 'euclidean' | 'dotProduct'> = {
'cosine': 'cosine',
'l2-squared': 'euclidean',
'dot': 'dotProduct'
};
return {
dimension: response.vectorIndexConfig?.dimensions || 0,
count,
metric: distanceMap[response.vectorIndexConfig?.distance] || 'cosine'
};
}
async deleteIndex(indexName: string): Promise<void> {
const className = this.toClassName(indexName);
await this.request(`/v1/schema/${className}`, { method: 'DELETE' });
}
async upsert(params: UpsertParams): Promise<string[]> {
const className = this.toClassName(params.indexName);
const ids: string[] = [];
// Batch upsert
const objects = params.vectors.map(v => ({
class: className,
id: v.id,
vector: v.vector,
properties: {
content: v.content || '',
metadata: v.metadata || {}
}
}));
// Batch in chunks of 100
const batchSize = 100;
for (let i = 0; i < objects.length; i += batchSize) {
const batch = objects.slice(i, i + batchSize);
await this.request('/v1/batch/objects', {
method: 'POST',
body: JSON.stringify({ objects: batch })
});
ids.push(...batch.map(o => o.id));
}
return ids;
}
async query(params: QueryParams): Promise<QueryResult[]> {
const className = this.toClassName(params.indexName);
const topK = params.topK || 10;
let whereClause = '';
if (params.filter) {
const conditions = Object.entries(params.filter).map(([key, value]) => {
return `{ path: ["metadata", "${key}"], operator: Equal, valueString: "${value}" }`;
});
if (conditions.length > 0) {
whereClause = `where: { operator: And, operands: [${conditions.join(', ')}] }`;
}
}
const query = `{
Get {
${className}(
nearVector: { vector: [${params.vector.join(',')}] }
limit: ${topK}
${whereClause}
) {
_additional {
id
distance
${params.includeVectors ? 'vector' : ''}
}
content
metadata
}
}
}`;
const response = await this.graphql(query);
const results = response.data?.Get?.[className] || [];
return results.map((r: any) => ({
id: r._additional.id,
score: 1 - (r._additional.distance || 0), // Convert distance to similarity
metadata: r.metadata,
content: r.content,
vector: r._additional.vector
}));
}
async delete(params: DeleteParams): Promise<void> {
const className = this.toClassName(params.indexName);
if (params.ids) {
for (const id of params.ids) {
await this.request(`/v1/objects/${className}/${id}`, { method: 'DELETE' });
}
} else if (params.filter) {
// Delete by filter using batch delete
const conditions = Object.entries(params.filter).map(([key, value]) => ({
path: ['metadata', key],
operator: 'Equal',
valueString: String(value)
}));
await this.request('/v1/batch/objects', {
method: 'DELETE',
body: JSON.stringify({
match: {
class: className,
where: { operator: 'And', operands: conditions }
}
})
});
}
}
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
const className = this.toClassName(indexName);
await this.request(`/v1/objects/${className}/${id}`, {
method: 'PATCH',
body: JSON.stringify({
properties: { metadata }
})
});
}
}
export function createWeaviateStore(config: WeaviateConfig): WeaviateVectorStore {
return new WeaviateVectorStore(config);
}
@@ -0,0 +1,242 @@
/**
* Base Voice - Abstract class for voice/TTS integrations
* Matches mastra's MastraVoice pattern
*/
export interface VoiceConfig {
apiKey?: string;
model?: string;
voice?: string;
}
export interface SpeakOptions {
voice?: string;
speed?: number;
pitch?: number;
format?: 'mp3' | 'wav' | 'ogg' | 'opus';
}
export interface ListenOptions {
language?: string;
model?: string;
}
export interface Speaker {
id: string;
name: string;
language?: string;
gender?: 'male' | 'female' | 'neutral';
}
/**
* Abstract base class for voice providers
*/
export abstract class BaseVoiceProvider {
readonly name: string;
protected apiKey?: string;
protected defaultVoice?: string;
constructor(config: VoiceConfig & { name?: string }) {
this.name = config.name || 'VoiceProvider';
this.apiKey = config.apiKey;
this.defaultVoice = config.voice;
}
/**
* Convert text to speech
*/
abstract speak(text: string, options?: SpeakOptions): Promise<Buffer | ReadableStream>;
/**
* Convert speech to text
*/
abstract listen(audio: Buffer | ReadableStream, options?: ListenOptions): Promise<string>;
/**
* Get available voices/speakers
*/
abstract getSpeakers(): Promise<Speaker[]>;
/**
* Check if provider is available
*/
abstract isAvailable(): Promise<boolean>;
}
/**
* OpenAI Voice Provider (TTS and Whisper)
*/
export class OpenAIVoiceProvider extends BaseVoiceProvider {
private baseUrl: string;
constructor(config: VoiceConfig = {}) {
super({ ...config, name: 'OpenAIVoice' });
this.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
this.defaultVoice = config.voice || 'alloy';
this.baseUrl = 'https://api.openai.com/v1';
}
async speak(text: string, options?: SpeakOptions): Promise<Buffer> {
if (!this.apiKey) {
throw new Error('OpenAI API key required for TTS');
}
const response = await fetch(`${this.baseUrl}/audio/speech`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'tts-1',
input: text,
voice: options?.voice || this.defaultVoice,
speed: options?.speed || 1.0,
response_format: options?.format || 'mp3'
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`OpenAI TTS error: ${response.status} - ${error}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
}
async listen(audio: Buffer | ReadableStream, options?: ListenOptions): Promise<string> {
if (!this.apiKey) {
throw new Error('OpenAI API key required for Whisper');
}
const formData = new FormData();
// Convert Buffer to Blob
const audioBlob = audio instanceof Buffer
? new Blob([new Uint8Array(audio)], { type: 'audio/mp3' })
: audio;
formData.append('file', audioBlob as Blob, 'audio.mp3');
formData.append('model', options?.model || 'whisper-1');
if (options?.language) {
formData.append('language', options.language);
}
const response = await fetch(`${this.baseUrl}/audio/transcriptions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`
},
body: formData
});
if (!response.ok) {
const error = await response.text();
throw new Error(`OpenAI Whisper error: ${response.status} - ${error}`);
}
const result = await response.json();
return result.text;
}
async getSpeakers(): Promise<Speaker[]> {
return [
{ id: 'alloy', name: 'Alloy', gender: 'neutral' },
{ id: 'echo', name: 'Echo', gender: 'male' },
{ id: 'fable', name: 'Fable', gender: 'neutral' },
{ id: 'onyx', name: 'Onyx', gender: 'male' },
{ id: 'nova', name: 'Nova', gender: 'female' },
{ id: 'shimmer', name: 'Shimmer', gender: 'female' }
];
}
async isAvailable(): Promise<boolean> {
return !!this.apiKey;
}
}
/**
* ElevenLabs Voice Provider
*/
export class ElevenLabsVoiceProvider extends BaseVoiceProvider {
private baseUrl: string;
constructor(config: VoiceConfig = {}) {
super({ ...config, name: 'ElevenLabsVoice' });
this.apiKey = config.apiKey || process.env.ELEVENLABS_API_KEY;
this.baseUrl = 'https://api.elevenlabs.io/v1';
}
async speak(text: string, options?: SpeakOptions): Promise<Buffer> {
if (!this.apiKey) {
throw new Error('ElevenLabs API key required');
}
const voiceId = options?.voice || this.defaultVoice || '21m00Tcm4TlvDq8ikWAM'; // Rachel
const response = await fetch(`${this.baseUrl}/text-to-speech/${voiceId}`, {
method: 'POST',
headers: {
'xi-api-key': this.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
model_id: 'eleven_monolingual_v1',
voice_settings: {
stability: 0.5,
similarity_boost: 0.5
}
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`ElevenLabs error: ${response.status} - ${error}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
}
async listen(_audio: Buffer | ReadableStream, _options?: ListenOptions): Promise<string> {
throw new Error('ElevenLabs does not support speech-to-text');
}
async getSpeakers(): Promise<Speaker[]> {
if (!this.apiKey) {
return [];
}
const response = await fetch(`${this.baseUrl}/voices`, {
headers: {
'xi-api-key': this.apiKey
}
});
if (!response.ok) {
return [];
}
const data = await response.json();
return (data.voices || []).map((v: any) => ({
id: v.voice_id,
name: v.name,
gender: v.labels?.gender
}));
}
async isAvailable(): Promise<boolean> {
return !!this.apiKey;
}
}
// Factory functions
export function createOpenAIVoice(config?: VoiceConfig): OpenAIVoiceProvider {
return new OpenAIVoiceProvider(config);
}
export function createElevenLabsVoice(config?: VoiceConfig): ElevenLabsVoiceProvider {
return new ElevenLabsVoiceProvider(config);
}
@@ -0,0 +1,9 @@
/**
* Voice Integrations
* Provides adapters for TTS and STT services
*/
export * from './base';
// Re-export factory functions
export { createOpenAIVoice, createElevenLabsVoice } from './base';
+193
View File
@@ -0,0 +1,193 @@
/**
* Chunking - Text chunking utilities for RAG
*/
export type ChunkStrategy = 'size' | 'sentence' | 'paragraph' | 'semantic';
export interface ChunkingConfig {
chunkSize?: number;
overlap?: number;
strategy?: ChunkStrategy;
separator?: string;
}
export interface Chunk {
content: string;
index: number;
startOffset: number;
endOffset: number;
metadata?: Record<string, any>;
}
/**
* Chunking class for splitting text into chunks
*/
export class Chunking {
private chunkSize: number;
private overlap: number;
private strategy: ChunkStrategy;
private separator: string;
constructor(config: ChunkingConfig = {}) {
this.chunkSize = config.chunkSize ?? 500;
this.overlap = config.overlap ?? 50;
this.strategy = config.strategy ?? 'size';
this.separator = config.separator ?? ' ';
}
/**
* Chunk text based on configured strategy
*/
chunk(text: string): Chunk[] {
switch (this.strategy) {
case 'sentence':
return this.chunkBySentence(text);
case 'paragraph':
return this.chunkByParagraph(text);
case 'semantic':
return this.chunkBySemantic(text);
default:
return this.chunkBySize(text);
}
}
/**
* Chunk by fixed size with overlap
*/
chunkBySize(text: string): Chunk[] {
const chunks: Chunk[] = [];
let startOffset = 0;
let index = 0;
while (startOffset < text.length) {
const endOffset = Math.min(startOffset + this.chunkSize, text.length);
const content = text.slice(startOffset, endOffset);
chunks.push({
content,
index,
startOffset,
endOffset
});
startOffset = endOffset - this.overlap;
if (startOffset >= text.length - this.overlap) break;
index++;
}
return chunks;
}
/**
* Chunk by sentences
*/
chunkBySentence(text: string): Chunk[] {
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
return sentences.map((content, index) => {
const startOffset = text.indexOf(content);
return {
content: content.trim(),
index,
startOffset,
endOffset: startOffset + content.length
};
});
}
/**
* Chunk by paragraphs
*/
chunkByParagraph(text: string): Chunk[] {
const paragraphs = text.split(/\n\n+/).filter(p => p.trim().length > 0);
let offset = 0;
return paragraphs.map((content, index) => {
const startOffset = text.indexOf(content, offset);
offset = startOffset + content.length;
return {
content: content.trim(),
index,
startOffset,
endOffset: offset
};
});
}
/**
* Chunk by semantic boundaries (simplified)
*/
chunkBySemantic(text: string): Chunk[] {
// Simplified semantic chunking - split on headers, lists, etc.
const patterns = [
/^#{1,6}\s+.+$/gm, // Headers
/^\s*[-*]\s+/gm, // List items
/^\d+\.\s+/gm // Numbered lists
];
let chunks: Chunk[] = [];
let lastEnd = 0;
let index = 0;
// Find semantic boundaries
const boundaries: number[] = [0];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(text)) !== null) {
boundaries.push(match.index);
}
}
boundaries.push(text.length);
boundaries.sort((a, b) => a - b);
// Create chunks from boundaries
for (let i = 0; i < boundaries.length - 1; i++) {
const start = boundaries[i];
const end = boundaries[i + 1];
const content = text.slice(start, end).trim();
if (content.length > 0) {
chunks.push({
content,
index: index++,
startOffset: start,
endOffset: end
});
}
}
return chunks.length > 0 ? chunks : this.chunkBySize(text);
}
/**
* Merge small chunks
*/
mergeSmallChunks(chunks: Chunk[], minSize: number = 100): Chunk[] {
const merged: Chunk[] = [];
let current: Chunk | null = null;
for (const chunk of chunks) {
if (!current) {
current = { ...chunk };
} else if (current.content.length < minSize) {
current.content += '\n' + chunk.content;
current.endOffset = chunk.endOffset;
} else {
merged.push(current);
current = { ...chunk };
}
}
if (current) {
merged.push(current);
}
return merged.map((c, i) => ({ ...c, index: i }));
}
}
/**
* Create a Chunking instance
*/
export function createChunking(config?: ChunkingConfig): Chunking {
return new Chunking(config);
}
+366
View File
@@ -0,0 +1,366 @@
/**
* Graph RAG - Graph-based Retrieval Augmented Generation
* Inspired by mastra's graph-rag module
*/
export interface GraphNode {
id: string;
type: string;
content: string;
metadata?: Record<string, any>;
embedding?: number[];
}
export interface GraphEdge {
id: string;
source: string;
target: string;
type: string;
weight?: number;
metadata?: Record<string, any>;
}
export interface GraphQueryResult {
nodes: GraphNode[];
edges: GraphEdge[];
paths: GraphNode[][];
score: number;
}
export interface GraphRAGConfig {
maxDepth?: number;
maxNodes?: number;
minScore?: number;
includeEdges?: boolean;
}
/**
* In-memory Graph Store for Graph RAG
*/
export class GraphStore {
private nodes: Map<string, GraphNode> = new Map();
private edges: Map<string, GraphEdge> = new Map();
private adjacencyList: Map<string, Set<string>> = new Map();
private reverseAdjacencyList: Map<string, Set<string>> = new Map();
/**
* Add a node to the graph
*/
addNode(node: GraphNode): void {
this.nodes.set(node.id, node);
if (!this.adjacencyList.has(node.id)) {
this.adjacencyList.set(node.id, new Set());
}
if (!this.reverseAdjacencyList.has(node.id)) {
this.reverseAdjacencyList.set(node.id, new Set());
}
}
/**
* Add an edge to the graph
*/
addEdge(edge: GraphEdge): void {
this.edges.set(edge.id, edge);
// Update adjacency lists
if (!this.adjacencyList.has(edge.source)) {
this.adjacencyList.set(edge.source, new Set());
}
this.adjacencyList.get(edge.source)!.add(edge.target);
if (!this.reverseAdjacencyList.has(edge.target)) {
this.reverseAdjacencyList.set(edge.target, new Set());
}
this.reverseAdjacencyList.get(edge.target)!.add(edge.source);
}
/**
* Get a node by ID
*/
getNode(id: string): GraphNode | undefined {
return this.nodes.get(id);
}
/**
* Get all nodes
*/
getAllNodes(): GraphNode[] {
return Array.from(this.nodes.values());
}
/**
* Get neighbors of a node
*/
getNeighbors(nodeId: string, direction: 'outgoing' | 'incoming' | 'both' = 'both'): GraphNode[] {
const neighborIds = new Set<string>();
if (direction === 'outgoing' || direction === 'both') {
const outgoing = this.adjacencyList.get(nodeId);
if (outgoing) {
outgoing.forEach(id => neighborIds.add(id));
}
}
if (direction === 'incoming' || direction === 'both') {
const incoming = this.reverseAdjacencyList.get(nodeId);
if (incoming) {
incoming.forEach(id => neighborIds.add(id));
}
}
return Array.from(neighborIds)
.map(id => this.nodes.get(id))
.filter((n): n is GraphNode => n !== undefined);
}
/**
* Get edges between two nodes
*/
getEdgesBetween(sourceId: string, targetId: string): GraphEdge[] {
return Array.from(this.edges.values()).filter(
e => (e.source === sourceId && e.target === targetId) ||
(e.source === targetId && e.target === sourceId)
);
}
/**
* Find paths between two nodes using BFS
*/
findPaths(startId: string, endId: string, maxDepth: number = 3): GraphNode[][] {
const paths: GraphNode[][] = [];
const queue: Array<{ path: string[]; visited: Set<string> }> = [
{ path: [startId], visited: new Set([startId]) }
];
while (queue.length > 0) {
const { path, visited } = queue.shift()!;
const currentId = path[path.length - 1];
if (currentId === endId) {
paths.push(path.map(id => this.nodes.get(id)!).filter(n => n));
continue;
}
if (path.length >= maxDepth) {
continue;
}
const neighbors = this.adjacencyList.get(currentId) || new Set();
for (const neighborId of neighbors) {
if (!visited.has(neighborId)) {
const newVisited = new Set(visited);
newVisited.add(neighborId);
queue.push({ path: [...path, neighborId], visited: newVisited });
}
}
}
return paths;
}
/**
* Get subgraph around a node
*/
getSubgraph(nodeId: string, depth: number = 2): { nodes: GraphNode[]; edges: GraphEdge[] } {
const visitedNodes = new Set<string>();
const resultEdges: GraphEdge[] = [];
const queue: Array<{ id: string; currentDepth: number }> = [{ id: nodeId, currentDepth: 0 }];
while (queue.length > 0) {
const { id, currentDepth } = queue.shift()!;
if (visitedNodes.has(id) || currentDepth > depth) {
continue;
}
visitedNodes.add(id);
if (currentDepth < depth) {
const neighbors = this.getNeighbors(id);
for (const neighbor of neighbors) {
if (!visitedNodes.has(neighbor.id)) {
queue.push({ id: neighbor.id, currentDepth: currentDepth + 1 });
// Add edges
const edges = this.getEdgesBetween(id, neighbor.id);
resultEdges.push(...edges);
}
}
}
}
const resultNodes = Array.from(visitedNodes)
.map(id => this.nodes.get(id))
.filter((n): n is GraphNode => n !== undefined);
return { nodes: resultNodes, edges: resultEdges };
}
/**
* Clear the graph
*/
clear(): void {
this.nodes.clear();
this.edges.clear();
this.adjacencyList.clear();
this.reverseAdjacencyList.clear();
}
/**
* Get graph statistics
*/
getStats(): { nodeCount: number; edgeCount: number } {
return {
nodeCount: this.nodes.size,
edgeCount: this.edges.size
};
}
}
/**
* Graph RAG - Combines graph traversal with vector similarity
*/
export class GraphRAG {
private graphStore: GraphStore;
private embedFn?: (text: string) => Promise<number[]>;
constructor(config: { embedFn?: (text: string) => Promise<number[]> } = {}) {
this.graphStore = new GraphStore();
this.embedFn = config.embedFn;
}
/**
* Add a document as a node
*/
async addDocument(doc: { id: string; content: string; type?: string; metadata?: Record<string, any> }): Promise<void> {
const embedding = this.embedFn ? await this.embedFn(doc.content) : undefined;
this.graphStore.addNode({
id: doc.id,
type: doc.type || 'document',
content: doc.content,
metadata: doc.metadata,
embedding
});
}
/**
* Add a relationship between documents
*/
addRelationship(sourceId: string, targetId: string, type: string, weight?: number): void {
this.graphStore.addEdge({
id: `${sourceId}-${type}-${targetId}`,
source: sourceId,
target: targetId,
type,
weight
});
}
/**
* Query the graph with a natural language query
*/
async query(query: string, config: GraphRAGConfig = {}): Promise<GraphQueryResult> {
const { maxDepth = 2, maxNodes = 10, minScore = 0, includeEdges = true } = config;
// Get all nodes and score them
const allNodes = this.graphStore.getAllNodes();
let scoredNodes: Array<{ node: GraphNode; score: number }>;
if (this.embedFn) {
// Use embedding similarity
const queryEmbedding = await this.embedFn(query);
scoredNodes = allNodes
.filter(n => n.embedding)
.map(node => ({
node,
score: this.cosineSimilarity(queryEmbedding, node.embedding!)
}));
} else {
// Use keyword matching
const queryTerms = query.toLowerCase().split(/\s+/);
scoredNodes = allNodes.map(node => {
const content = node.content.toLowerCase();
let score = 0;
for (const term of queryTerms) {
if (content.includes(term)) score += 1;
}
return { node, score: score / queryTerms.length };
});
}
// Filter and sort by score
scoredNodes = scoredNodes
.filter(s => s.score >= minScore)
.sort((a, b) => b.score - a.score);
// Get top nodes and expand graph
const topNodes = scoredNodes.slice(0, Math.min(3, maxNodes));
const resultNodes: GraphNode[] = [];
const resultEdges: GraphEdge[] = [];
const seenNodeIds = new Set<string>();
for (const { node } of topNodes) {
const subgraph = this.graphStore.getSubgraph(node.id, maxDepth);
for (const n of subgraph.nodes) {
if (!seenNodeIds.has(n.id) && resultNodes.length < maxNodes) {
seenNodeIds.add(n.id);
resultNodes.push(n);
}
}
if (includeEdges) {
resultEdges.push(...subgraph.edges);
}
}
// Find paths between top nodes
const paths: GraphNode[][] = [];
if (topNodes.length >= 2) {
for (let i = 0; i < topNodes.length - 1; i++) {
const foundPaths = this.graphStore.findPaths(
topNodes[i].node.id,
topNodes[i + 1].node.id,
maxDepth
);
paths.push(...foundPaths.slice(0, 3));
}
}
return {
nodes: resultNodes,
edges: resultEdges,
paths,
score: topNodes.length > 0 ? topNodes[0].score : 0
};
}
/**
* Get context string from query results
*/
getContext(result: GraphQueryResult): string {
const nodeContents = result.nodes.map(n => n.content);
return nodeContents.join('\n\n---\n\n');
}
/**
* Get the underlying graph store
*/
getGraphStore(): GraphStore {
return this.graphStore;
}
private cosineSimilarity(a: number[], b: number[]): number {
const dot = a.reduce((sum, x, i) => sum + x * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, x) => sum + x * x, 0));
const normB = Math.sqrt(b.reduce((sum, x) => sum + x * x, 0));
return dot / (normA * normB);
}
}
// Factory function
export function createGraphRAG(config?: { embedFn?: (text: string) => Promise<number[]> }): GraphRAG {
return new GraphRAG(config);
}
+6
View File
@@ -47,3 +47,9 @@ export class BaseKnowledgeBase implements KnowledgeBase {
return this.knowledge.delete(id);
}
}
// Export Reranker
export * from './reranker';
// Export Graph RAG
export * from './graph-rag';
+193
View File
@@ -0,0 +1,193 @@
/**
* Knowledge Base (RAG) - Retrieval Augmented Generation
*/
export interface Document {
id: string;
content: string;
metadata?: Record<string, any>;
embedding?: number[];
}
export interface SearchResult {
document: Document;
score: number;
}
export interface EmbeddingProvider {
embed(text: string): Promise<number[]>;
embedBatch(texts: string[]): Promise<number[][]>;
}
export interface KnowledgeBaseConfig {
embeddingProvider?: EmbeddingProvider;
similarityThreshold?: number;
maxResults?: number;
}
/**
* Simple in-memory vector store for RAG
*/
export class KnowledgeBase {
private documents: Map<string, Document> = new Map();
private embeddingProvider?: EmbeddingProvider;
private similarityThreshold: number;
private maxResults: number;
constructor(config: KnowledgeBaseConfig = {}) {
this.embeddingProvider = config.embeddingProvider;
this.similarityThreshold = config.similarityThreshold ?? 0.7;
this.maxResults = config.maxResults ?? 5;
}
/**
* Add a document to the knowledge base
*/
async add(doc: Omit<Document, 'embedding'>): Promise<Document> {
const document: Document = { ...doc };
if (this.embeddingProvider) {
document.embedding = await this.embeddingProvider.embed(doc.content);
}
this.documents.set(doc.id, document);
return document;
}
/**
* Add multiple documents
*/
async addBatch(docs: Array<Omit<Document, 'embedding'>>): Promise<Document[]> {
if (this.embeddingProvider && docs.length > 0) {
const embeddings = await this.embeddingProvider.embedBatch(docs.map(d => d.content));
return Promise.all(docs.map(async (doc, i) => {
const document: Document = { ...doc, embedding: embeddings[i] };
this.documents.set(doc.id, document);
return document;
}));
}
return Promise.all(docs.map(doc => this.add(doc)));
}
/**
* Get a document by ID
*/
get(id: string): Document | undefined {
return this.documents.get(id);
}
/**
* Delete a document
*/
delete(id: string): boolean {
return this.documents.delete(id);
}
/**
* Search for similar documents
*/
async search(query: string, limit?: number): Promise<SearchResult[]> {
const maxResults = limit ?? this.maxResults;
if (!this.embeddingProvider) {
// Fallback to simple text matching
return this.textSearch(query, maxResults);
}
const queryEmbedding = await this.embeddingProvider.embed(query);
const results: SearchResult[] = [];
for (const doc of this.documents.values()) {
if (doc.embedding) {
const score = this.cosineSimilarity(queryEmbedding, doc.embedding);
if (score >= this.similarityThreshold) {
results.push({ document: doc, score });
}
}
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, maxResults);
}
/**
* Simple text-based search fallback
*/
private textSearch(query: string, limit: number): SearchResult[] {
const queryLower = query.toLowerCase();
const results: SearchResult[] = [];
for (const doc of this.documents.values()) {
const contentLower = doc.content.toLowerCase();
if (contentLower.includes(queryLower)) {
const score = queryLower.length / contentLower.length;
results.push({ document: doc, score: Math.min(score * 10, 1) });
}
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
/**
* Calculate cosine similarity between two vectors
*/
private cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) return 0;
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator === 0 ? 0 : dotProduct / denominator;
}
/**
* Get all documents
*/
list(): Document[] {
return Array.from(this.documents.values());
}
/**
* Clear all documents
*/
clear(): void {
this.documents.clear();
}
/**
* Get document count
*/
get size(): number {
return this.documents.size;
}
/**
* Build context from search results for RAG
*/
buildContext(results: SearchResult[]): string {
if (results.length === 0) return '';
return results
.map((r, i) => `[${i + 1}] ${r.document.content}`)
.join('\n\n');
}
}
/**
* Create a knowledge base
*/
export function createKnowledgeBase(config?: KnowledgeBaseConfig): KnowledgeBase {
return new KnowledgeBase(config);
}
+265
View File
@@ -0,0 +1,265 @@
/**
* Reranker - Rerank search results for improved relevance
* Inspired by mastra's rerank module
*/
export interface RerankResult {
id: string;
score: number;
content: string;
metadata?: Record<string, any>;
originalRank: number;
newRank: number;
}
export interface RerankConfig {
model?: string;
topK?: number;
threshold?: number;
}
/**
* Abstract base class for rerankers
*/
export abstract class BaseReranker {
readonly name: string;
constructor(name: string) {
this.name = name;
}
abstract rerank(
query: string,
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
config?: RerankConfig
): Promise<RerankResult[]>;
}
/**
* Cohere Reranker - Uses Cohere's rerank API
*/
export class CohereReranker extends BaseReranker {
private apiKey: string;
private model: string;
constructor(config: { apiKey?: string; model?: string } = {}) {
super('CohereReranker');
this.apiKey = config.apiKey || process.env.COHERE_API_KEY || '';
this.model = config.model || 'rerank-english-v3.0';
}
async rerank(
query: string,
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
config?: RerankConfig
): Promise<RerankResult[]> {
if (!this.apiKey) {
throw new Error('Cohere API key required for reranking');
}
const response = await fetch('https://api.cohere.ai/v1/rerank', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: config?.model || this.model,
query,
documents: documents.map(d => d.content),
top_n: config?.topK || documents.length,
return_documents: true
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Cohere rerank error: ${response.status} - ${error}`);
}
const data = await response.json();
const threshold = config?.threshold || 0;
return (data.results || [])
.filter((r: any) => r.relevance_score >= threshold)
.map((r: any, newRank: number) => ({
id: documents[r.index].id,
score: r.relevance_score,
content: documents[r.index].content,
metadata: documents[r.index].metadata,
originalRank: r.index,
newRank
}));
}
}
/**
* Cross-Encoder Reranker - Uses sentence similarity for reranking
* This is a simple implementation using cosine similarity
*/
export class CrossEncoderReranker extends BaseReranker {
private embedFn?: (texts: string[]) => Promise<number[][]>;
constructor(config: { embedFn?: (texts: string[]) => Promise<number[][]> } = {}) {
super('CrossEncoderReranker');
this.embedFn = config.embedFn;
}
async rerank(
query: string,
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
config?: RerankConfig
): Promise<RerankResult[]> {
if (!this.embedFn) {
// Fallback to simple keyword matching
return this.keywordRerank(query, documents, config);
}
// Get embeddings for query and all documents
const texts = [query, ...documents.map(d => d.content)];
const embeddings = await this.embedFn(texts);
const queryEmbedding = embeddings[0];
const docEmbeddings = embeddings.slice(1);
// Calculate similarity scores
const scores = docEmbeddings.map((docEmb, i) => ({
index: i,
score: this.cosineSimilarity(queryEmbedding, docEmb)
}));
// Sort by score descending
scores.sort((a, b) => b.score - a.score);
const threshold = config?.threshold || 0;
const topK = config?.topK || documents.length;
return scores
.filter(s => s.score >= threshold)
.slice(0, topK)
.map((s, newRank) => ({
id: documents[s.index].id,
score: s.score,
content: documents[s.index].content,
metadata: documents[s.index].metadata,
originalRank: s.index,
newRank
}));
}
private keywordRerank(
query: string,
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
config?: RerankConfig
): RerankResult[] {
const queryTerms = query.toLowerCase().split(/\s+/);
const scores = documents.map((doc, i) => {
const content = doc.content.toLowerCase();
let score = 0;
for (const term of queryTerms) {
if (content.includes(term)) {
score += 1;
}
}
// Normalize by query length
score = score / queryTerms.length;
return { index: i, score };
});
scores.sort((a, b) => b.score - a.score);
const threshold = config?.threshold || 0;
const topK = config?.topK || documents.length;
return scores
.filter(s => s.score >= threshold)
.slice(0, topK)
.map((s, newRank) => ({
id: documents[s.index].id,
score: s.score,
content: documents[s.index].content,
metadata: documents[s.index].metadata,
originalRank: s.index,
newRank
}));
}
private cosineSimilarity(a: number[], b: number[]): number {
const dot = a.reduce((sum, x, i) => sum + x * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, x) => sum + x * x, 0));
const normB = Math.sqrt(b.reduce((sum, x) => sum + x * x, 0));
return dot / (normA * normB);
}
}
/**
* LLM Reranker - Uses an LLM to score relevance
*/
export class LLMReranker extends BaseReranker {
private generateFn: (prompt: string) => Promise<string>;
constructor(config: { generateFn: (prompt: string) => Promise<string> }) {
super('LLMReranker');
this.generateFn = config.generateFn;
}
async rerank(
query: string,
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
config?: RerankConfig
): Promise<RerankResult[]> {
const scores: Array<{ index: number; score: number }> = [];
// Score each document
for (let i = 0; i < documents.length; i++) {
const doc = documents[i];
const prompt = `Rate the relevance of the following document to the query on a scale of 0 to 10.
Query: ${query}
Document: ${doc.content.slice(0, 500)}
Respond with only a number between 0 and 10.`;
try {
const response = await this.generateFn(prompt);
const score = parseFloat(response.trim()) / 10;
scores.push({ index: i, score: isNaN(score) ? 0 : Math.min(1, Math.max(0, score)) });
} catch {
scores.push({ index: i, score: 0 });
}
}
scores.sort((a, b) => b.score - a.score);
const threshold = config?.threshold || 0;
const topK = config?.topK || documents.length;
return scores
.filter(s => s.score >= threshold)
.slice(0, topK)
.map((s, newRank) => ({
id: documents[s.index].id,
score: s.score,
content: documents[s.index].content,
metadata: documents[s.index].metadata,
originalRank: s.index,
newRank
}));
}
}
// Factory functions
export function createCohereReranker(config?: { apiKey?: string; model?: string }): CohereReranker {
return new CohereReranker(config);
}
export function createCrossEncoderReranker(config?: { embedFn?: (texts: string[]) => Promise<number[][]> }): CrossEncoderReranker {
return new CrossEncoderReranker(config);
}
export function createLLMReranker(config: { generateFn: (prompt: string) => Promise<string> }): LLMReranker {
return new LLMReranker(config);
}
@@ -0,0 +1,334 @@
/**
* Anthropic Provider - Implementation for Anthropic Claude API
*/
import { BaseProvider } from './base';
import type {
ProviderConfig,
GenerateTextOptions,
GenerateTextResult,
StreamTextOptions,
StreamChunk,
GenerateObjectOptions,
GenerateObjectResult,
Message,
ToolDefinition,
ToolCall,
} from './types';
interface AnthropicMessage {
role: 'user' | 'assistant';
content: string | Array<{ type: string; text?: string; tool_use_id?: string; content?: string }>;
}
interface AnthropicTool {
name: string;
description: string;
input_schema: Record<string, any>;
}
export class AnthropicProvider extends BaseProvider {
readonly providerId = 'anthropic';
private apiKey: string;
private baseUrl: string;
constructor(modelId: string, config: ProviderConfig = {}) {
super(modelId, config);
this.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY || '';
this.baseUrl = config.baseUrl || 'https://api.anthropic.com';
if (!this.apiKey) {
throw new Error('ANTHROPIC_API_KEY is required');
}
}
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
return this.withRetry(async () => {
const { systemPrompt, messages } = this.extractSystemPrompt(options.messages);
const response = await fetch(`${this.baseUrl}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: this.modelId,
max_tokens: options.maxTokens || 4096,
system: systemPrompt,
messages: this.formatMessages(messages),
temperature: options.temperature ?? 0.7,
tools: options.tools ? this.formatTools(options.tools) : undefined,
stop_sequences: options.stop,
top_p: options.topP,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw Object.assign(new Error(error.error?.message || `Anthropic API error: ${response.status}`), {
status: response.status,
});
}
const data = await response.json();
let text = '';
const toolCalls: ToolCall[] = [];
for (const block of data.content || []) {
if (block.type === 'text') {
text += block.text;
} else if (block.type === 'tool_use') {
toolCalls.push({
id: block.id,
type: 'function',
function: {
name: block.name,
arguments: JSON.stringify(block.input),
},
});
}
}
return {
text,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: {
promptTokens: data.usage?.input_tokens || 0,
completionTokens: data.usage?.output_tokens || 0,
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
},
finishReason: this.mapStopReason(data.stop_reason),
raw: data,
};
});
}
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
const self = this;
const { systemPrompt, messages } = this.extractSystemPrompt(options.messages);
return {
async *[Symbol.asyncIterator]() {
const response = await fetch(`${self.baseUrl}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': self.apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: self.modelId,
max_tokens: options.maxTokens || 4096,
system: systemPrompt,
messages: self.formatMessages(messages),
temperature: options.temperature ?? 0.7,
tools: options.tools ? self.formatTools(options.tools) : undefined,
stream: true,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error?.message || `Anthropic API error: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
const toolCalls: ToolCall[] = [];
let currentToolCall: Partial<ToolCall> | null = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const event = JSON.parse(data);
if (event.type === 'content_block_delta') {
if (event.delta?.type === 'text_delta') {
const text = event.delta.text;
if (options.onToken) options.onToken(text);
yield { text };
} else if (event.delta?.type === 'input_json_delta') {
if (currentToolCall) {
currentToolCall.function!.arguments += event.delta.partial_json;
}
}
} else if (event.type === 'content_block_start') {
if (event.content_block?.type === 'tool_use') {
currentToolCall = {
id: event.content_block.id,
type: 'function',
function: {
name: event.content_block.name,
arguments: '',
},
};
}
} else if (event.type === 'content_block_stop') {
if (currentToolCall) {
toolCalls.push(currentToolCall as ToolCall);
currentToolCall = null;
}
} else if (event.type === 'message_delta') {
yield {
finishReason: self.mapStopReason(event.delta?.stop_reason),
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: event.usage ? {
promptTokens: 0,
completionTokens: event.usage.output_tokens,
totalTokens: event.usage.output_tokens,
} : undefined,
};
}
} catch (e) {
// Skip malformed JSON
}
}
}
},
};
}
async generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
const { systemPrompt, messages } = this.extractSystemPrompt(options.messages);
// Add JSON instruction to system prompt
const jsonSystemPrompt = `${systemPrompt}\n\nYou must respond with valid JSON matching this schema:\n${JSON.stringify(options.schema, null, 2)}`;
return this.withRetry(async () => {
const response = await fetch(`${this.baseUrl}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: this.modelId,
max_tokens: options.maxTokens || 4096,
system: jsonSystemPrompt,
messages: this.formatMessages(messages),
temperature: options.temperature ?? 0.7,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error?.message || `Anthropic API error: ${response.status}`);
}
const data = await response.json();
let text = '';
for (const block of data.content || []) {
if (block.type === 'text') {
text += block.text;
}
}
// Extract JSON from response
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error(`No JSON found in response: ${text}`);
}
let parsed: T;
try {
parsed = JSON.parse(jsonMatch[0]);
} catch (e) {
throw new Error(`Failed to parse JSON: ${jsonMatch[0]}`);
}
return {
object: parsed,
usage: {
promptTokens: data.usage?.input_tokens || 0,
completionTokens: data.usage?.output_tokens || 0,
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
},
raw: data,
};
});
}
private extractSystemPrompt(messages: Message[]): { systemPrompt: string; messages: Message[] } {
const systemMessages = messages.filter(m => m.role === 'system');
const otherMessages = messages.filter(m => m.role !== 'system');
const systemPrompt = systemMessages.map(m => m.content).join('\n');
return { systemPrompt, messages: otherMessages };
}
protected formatMessages(messages: Message[]): AnthropicMessage[] {
const result: AnthropicMessage[] = [];
for (const msg of messages) {
if (msg.role === 'system') continue; // Handled separately
if (msg.role === 'tool') {
// Tool results need to be part of user message in Anthropic
result.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: msg.tool_call_id || '',
content: msg.content || '',
}],
});
} else if (msg.role === 'assistant' && msg.tool_calls) {
const content: any[] = [];
if (msg.content) {
content.push({ type: 'text', text: msg.content });
}
for (const tc of msg.tool_calls) {
content.push({
type: 'tool_use',
id: tc.id,
name: tc.function.name,
input: JSON.parse(tc.function.arguments),
});
}
result.push({ role: 'assistant', content });
} else {
result.push({
role: msg.role as 'user' | 'assistant',
content: msg.content || '',
});
}
}
return result;
}
protected formatTools(tools: ToolDefinition[]): AnthropicTool[] {
return tools.map(tool => ({
name: tool.name,
description: tool.description || `Function ${tool.name}`,
input_schema: tool.parameters || { type: 'object', properties: {} },
}));
}
private mapStopReason(reason: string | null): GenerateTextResult['finishReason'] {
switch (reason) {
case 'end_turn': return 'stop';
case 'max_tokens': return 'length';
case 'tool_use': return 'tool_calls';
default: return 'stop';
}
}
}
@@ -0,0 +1,77 @@
/**
* Base Provider - Abstract base class for LLM providers
*/
import type {
LLMProvider,
ProviderConfig,
GenerateTextOptions,
GenerateTextResult,
StreamTextOptions,
StreamChunk,
GenerateObjectOptions,
GenerateObjectResult,
Message,
ToolDefinition,
} from './types';
export abstract class BaseProvider implements LLMProvider {
abstract readonly providerId: string;
readonly modelId: string;
protected config: ProviderConfig;
constructor(modelId: string, config: ProviderConfig = {}) {
this.modelId = modelId;
this.config = {
maxRetries: 3,
timeout: 60000,
...config,
};
}
abstract generateText(options: GenerateTextOptions): Promise<GenerateTextResult>;
abstract streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>>;
abstract generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>>;
/**
* Convert tool definitions to provider-specific format
*/
protected abstract formatTools(tools: ToolDefinition[]): any[];
/**
* Convert messages to provider-specific format
*/
protected abstract formatMessages(messages: Message[]): any[];
/**
* Retry logic with exponential backoff
*/
protected async withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = this.config.maxRetries || 3
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
// Check if error is retryable (rate limit, server error)
const isRetryable =
error.status === 429 ||
error.status >= 500 ||
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT';
if (!isRetryable || attempt === maxRetries) {
throw error;
}
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
}
@@ -0,0 +1,322 @@
/**
* Google Provider - Implementation for Google Gemini API
*/
import { BaseProvider } from './base';
import type {
ProviderConfig,
GenerateTextOptions,
GenerateTextResult,
StreamTextOptions,
StreamChunk,
GenerateObjectOptions,
GenerateObjectResult,
Message,
ToolDefinition,
ToolCall,
} from './types';
interface GeminiContent {
role: 'user' | 'model';
parts: Array<{ text?: string; functionCall?: any; functionResponse?: any }>;
}
export class GoogleProvider extends BaseProvider {
readonly providerId = 'google';
private apiKey: string;
private baseUrl: string;
constructor(modelId: string, config: ProviderConfig = {}) {
super(modelId, config);
this.apiKey = config.apiKey || process.env.GOOGLE_API_KEY || '';
this.baseUrl = config.baseUrl || 'https://generativelanguage.googleapis.com/v1beta';
if (!this.apiKey) {
throw new Error('GOOGLE_API_KEY is required');
}
}
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
return this.withRetry(async () => {
const { systemInstruction, contents } = this.formatRequest(options.messages);
const response = await fetch(
`${this.baseUrl}/models/${this.modelId}:generateContent?key=${this.apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
contents,
generationConfig: {
temperature: options.temperature ?? 0.7,
maxOutputTokens: options.maxTokens,
topP: options.topP,
stopSequences: options.stop,
},
tools: options.tools ? this.formatTools(options.tools) : undefined,
}),
}
);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw Object.assign(new Error(error.error?.message || `Google API error: ${response.status}`), {
status: response.status,
});
}
const data = await response.json();
const candidate = data.candidates?.[0];
let text = '';
const toolCalls: ToolCall[] = [];
for (const part of candidate?.content?.parts || []) {
if (part.text) {
text += part.text;
} else if (part.functionCall) {
toolCalls.push({
id: `call_${Math.random().toString(36).substr(2, 9)}`,
type: 'function',
function: {
name: part.functionCall.name,
arguments: JSON.stringify(part.functionCall.args || {}),
},
});
}
}
return {
text,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: {
promptTokens: data.usageMetadata?.promptTokenCount || 0,
completionTokens: data.usageMetadata?.candidatesTokenCount || 0,
totalTokens: data.usageMetadata?.totalTokenCount || 0,
},
finishReason: this.mapFinishReason(candidate?.finishReason),
raw: data,
};
});
}
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
const self = this;
const { systemInstruction, contents } = this.formatRequest(options.messages);
return {
async *[Symbol.asyncIterator]() {
const response = await fetch(
`${self.baseUrl}/models/${self.modelId}:streamGenerateContent?key=${self.apiKey}&alt=sse`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
contents,
generationConfig: {
temperature: options.temperature ?? 0.7,
maxOutputTokens: options.maxTokens,
},
tools: options.tools ? self.formatTools(options.tools) : undefined,
}),
}
);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error?.message || `Google API error: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
const toolCalls: ToolCall[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const event = JSON.parse(data);
const candidate = event.candidates?.[0];
for (const part of candidate?.content?.parts || []) {
if (part.text) {
if (options.onToken) options.onToken(part.text);
yield { text: part.text };
} else if (part.functionCall) {
toolCalls.push({
id: `call_${Math.random().toString(36).substr(2, 9)}`,
type: 'function',
function: {
name: part.functionCall.name,
arguments: JSON.stringify(part.functionCall.args || {}),
},
});
}
}
if (candidate?.finishReason) {
yield {
finishReason: self.mapFinishReason(candidate.finishReason),
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: event.usageMetadata ? {
promptTokens: event.usageMetadata.promptTokenCount || 0,
completionTokens: event.usageMetadata.candidatesTokenCount || 0,
totalTokens: event.usageMetadata.totalTokenCount || 0,
} : undefined,
};
}
} catch (e) {
// Skip malformed JSON
}
}
}
},
};
}
async generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
const { systemInstruction, contents } = this.formatRequest(options.messages);
return this.withRetry(async () => {
const response = await fetch(
`${this.baseUrl}/models/${this.modelId}:generateContent?key=${this.apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: systemInstruction
? { parts: [{ text: `${systemInstruction}\n\nRespond with valid JSON matching this schema:\n${JSON.stringify(options.schema, null, 2)}` }] }
: { parts: [{ text: `Respond with valid JSON matching this schema:\n${JSON.stringify(options.schema, null, 2)}` }] },
contents,
generationConfig: {
temperature: options.temperature ?? 0.7,
maxOutputTokens: options.maxTokens,
responseMimeType: 'application/json',
},
}),
}
);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error?.message || `Google API error: ${response.status}`);
}
const data = await response.json();
const candidate = data.candidates?.[0];
let text = '';
for (const part of candidate?.content?.parts || []) {
if (part.text) text += part.text;
}
let parsed: T;
try {
parsed = JSON.parse(text);
} catch (e) {
// Try to extract JSON from text
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error(`No JSON found: ${text}`);
parsed = JSON.parse(jsonMatch[0]);
}
return {
object: parsed,
usage: {
promptTokens: data.usageMetadata?.promptTokenCount || 0,
completionTokens: data.usageMetadata?.candidatesTokenCount || 0,
totalTokens: data.usageMetadata?.totalTokenCount || 0,
},
raw: data,
};
});
}
private formatRequest(messages: Message[]): { systemInstruction: string | null; contents: GeminiContent[] } {
const systemMessages = messages.filter(m => m.role === 'system');
const otherMessages = messages.filter(m => m.role !== 'system');
const systemInstruction = systemMessages.length > 0
? systemMessages.map(m => m.content).join('\n')
: null;
return {
systemInstruction,
contents: this.formatMessages(otherMessages),
};
}
protected formatMessages(messages: Message[]): GeminiContent[] {
const result: GeminiContent[] = [];
for (const msg of messages) {
if (msg.role === 'system') continue;
const role = msg.role === 'assistant' ? 'model' : 'user';
if (msg.role === 'tool') {
result.push({
role: 'user',
parts: [{
functionResponse: {
name: msg.name || 'function',
response: { result: msg.content },
},
}],
});
} else if (msg.role === 'assistant' && msg.tool_calls) {
const parts: any[] = [];
if (msg.content) parts.push({ text: msg.content });
for (const tc of msg.tool_calls) {
parts.push({
functionCall: {
name: tc.function.name,
args: JSON.parse(tc.function.arguments),
},
});
}
result.push({ role: 'model', parts });
} else {
result.push({
role,
parts: [{ text: msg.content || '' }],
});
}
}
return result;
}
protected formatTools(tools: ToolDefinition[]): any[] {
return [{
functionDeclarations: tools.map(tool => ({
name: tool.name,
description: tool.description || `Function ${tool.name}`,
parameters: tool.parameters || { type: 'object', properties: {} },
})),
}];
}
private mapFinishReason(reason: string | null): GenerateTextResult['finishReason'] {
switch (reason) {
case 'STOP': return 'stop';
case 'MAX_TOKENS': return 'length';
case 'SAFETY': return 'content_filter';
default: return 'stop';
}
}
}
+104
View File
@@ -0,0 +1,104 @@
/**
* LLM Providers - Factory and exports for multi-provider support
*/
export * from './types';
export * from './base';
export { OpenAIProvider } from './openai';
export { AnthropicProvider } from './anthropic';
export { GoogleProvider } from './google';
import type { LLMProvider, ProviderConfig } from './types';
import { OpenAIProvider } from './openai';
import { AnthropicProvider } from './anthropic';
import { GoogleProvider } from './google';
/**
* Provider registry for dynamic provider loading
*/
const PROVIDER_MAP: Record<string, new (modelId: string, config?: ProviderConfig) => LLMProvider> = {
openai: OpenAIProvider,
anthropic: AnthropicProvider,
google: GoogleProvider,
gemini: GoogleProvider, // Alias
};
/**
* Parse model string into provider and model ID
* Supports formats:
* - "provider/model" (e.g., "openai/gpt-4o")
* - "model" (defaults to OpenAI, e.g., "gpt-4o-mini")
*/
export function parseModelString(model: string): { providerId: string; modelId: string } {
if (model.includes('/')) {
const [providerId, ...rest] = model.split('/');
return { providerId: providerId.toLowerCase(), modelId: rest.join('/') };
}
// Default to OpenAI for common model prefixes
if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3')) {
return { providerId: 'openai', modelId: model };
}
if (model.startsWith('claude-')) {
return { providerId: 'anthropic', modelId: model };
}
if (model.startsWith('gemini-')) {
return { providerId: 'google', modelId: model };
}
// Default to OpenAI
return { providerId: 'openai', modelId: model };
}
/**
* Create a provider instance from a model string
*
* @example
* ```typescript
* const provider = createProvider('openai/gpt-4o');
* const provider = createProvider('anthropic/claude-3-5-sonnet-latest');
* const provider = createProvider('google/gemini-2.0-flash');
* const provider = createProvider('gpt-4o-mini'); // Defaults to OpenAI
* ```
*/
export function createProvider(model: string, config?: ProviderConfig): LLMProvider {
const { providerId, modelId } = parseModelString(model);
const ProviderClass = PROVIDER_MAP[providerId];
if (!ProviderClass) {
throw new Error(`Unknown provider: ${providerId}. Available providers: ${Object.keys(PROVIDER_MAP).join(', ')}`);
}
return new ProviderClass(modelId, config);
}
/**
* Get the default provider (OpenAI with gpt-4o-mini)
*/
export function getDefaultProvider(config?: ProviderConfig): LLMProvider {
return createProvider('openai/gpt-4o-mini', config);
}
/**
* Check if a provider is available (has required API key)
*/
export function isProviderAvailable(providerId: string): boolean {
switch (providerId.toLowerCase()) {
case 'openai':
return !!process.env.OPENAI_API_KEY;
case 'anthropic':
return !!process.env.ANTHROPIC_API_KEY;
case 'google':
case 'gemini':
return !!process.env.GOOGLE_API_KEY;
default:
return false;
}
}
/**
* Get list of available providers
*/
export function getAvailableProviders(): string[] {
return Object.keys(PROVIDER_MAP).filter(isProviderAvailable);
}
@@ -0,0 +1,227 @@
/**
* OpenAI Provider - Implementation for OpenAI API
*/
import OpenAI from 'openai';
import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';
import { BaseProvider } from './base';
import type {
ProviderConfig,
GenerateTextOptions,
GenerateTextResult,
StreamTextOptions,
StreamChunk,
GenerateObjectOptions,
GenerateObjectResult,
Message,
ToolDefinition,
ToolCall,
} from './types';
export class OpenAIProvider extends BaseProvider {
readonly providerId = 'openai';
private client: OpenAI;
constructor(modelId: string, config: ProviderConfig = {}) {
super(modelId, config);
this.client = new OpenAI({
apiKey: config.apiKey || process.env.OPENAI_API_KEY,
baseURL: config.baseUrl,
timeout: config.timeout || 60000,
maxRetries: 0, // We handle retries ourselves
});
}
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
return this.withRetry(async () => {
const response = await this.client.chat.completions.create({
model: this.modelId,
messages: this.formatMessages(options.messages),
temperature: options.temperature ?? 0.7,
max_tokens: options.maxTokens,
tools: options.tools ? this.formatTools(options.tools) : undefined,
tool_choice: options.toolChoice as any,
stop: options.stop,
top_p: options.topP,
frequency_penalty: options.frequencyPenalty,
presence_penalty: options.presencePenalty,
});
const choice = response.choices[0];
const message = choice.message;
return {
text: message.content || '',
toolCalls: message.tool_calls?.map(tc => ({
id: tc.id,
type: 'function' as const,
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
})),
usage: {
promptTokens: response.usage?.prompt_tokens || 0,
completionTokens: response.usage?.completion_tokens || 0,
totalTokens: response.usage?.total_tokens || 0,
},
finishReason: this.mapFinishReason(choice.finish_reason),
raw: response,
};
});
}
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
const self = this;
return {
async *[Symbol.asyncIterator]() {
const stream = await self.client.chat.completions.create({
model: self.modelId,
messages: self.formatMessages(options.messages),
temperature: options.temperature ?? 0.7,
max_tokens: options.maxTokens,
tools: options.tools ? self.formatTools(options.tools) : undefined,
tool_choice: options.toolChoice as any,
stop: options.stop,
stream: true,
});
let toolCalls: ToolCall[] = [];
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (delta?.content) {
if (options.onToken) {
options.onToken(delta.content);
}
yield { text: delta.content };
}
if (delta?.tool_calls) {
for (const tc of delta.tool_calls) {
if (tc.index !== undefined) {
if (!toolCalls[tc.index]) {
toolCalls[tc.index] = {
id: tc.id || '',
type: 'function',
function: { name: '', arguments: '' },
};
}
if (tc.id) toolCalls[tc.index].id = tc.id;
if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
}
}
}
if (chunk.choices[0]?.finish_reason) {
yield {
finishReason: chunk.choices[0].finish_reason,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
usage: chunk.usage ? {
promptTokens: chunk.usage.prompt_tokens,
completionTokens: chunk.usage.completion_tokens,
totalTokens: chunk.usage.total_tokens,
} : undefined,
};
}
}
},
};
}
async generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
return this.withRetry(async () => {
const response = await this.client.chat.completions.create({
model: this.modelId,
messages: this.formatMessages(options.messages),
temperature: options.temperature ?? 0.7,
max_tokens: options.maxTokens,
response_format: {
type: 'json_schema',
json_schema: {
name: 'response',
schema: this.normalizeSchema(options.schema),
strict: true,
},
},
});
const choice = response.choices[0];
const content = choice.message.content || '{}';
let parsed: T;
try {
parsed = JSON.parse(content);
} catch (e) {
throw new Error(`Failed to parse JSON response: ${content}`);
}
return {
object: parsed,
usage: {
promptTokens: response.usage?.prompt_tokens || 0,
completionTokens: response.usage?.completion_tokens || 0,
totalTokens: response.usage?.total_tokens || 0,
},
raw: response,
};
});
}
protected formatMessages(messages: Message[]): ChatCompletionMessageParam[] {
return messages.map(msg => {
if (msg.role === 'tool') {
return {
role: 'tool' as const,
content: msg.content || '',
tool_call_id: msg.tool_call_id || '',
};
}
if (msg.role === 'assistant' && msg.tool_calls) {
return {
role: 'assistant' as const,
content: msg.content,
tool_calls: msg.tool_calls,
};
}
return {
role: msg.role as 'system' | 'user' | 'assistant',
content: msg.content || '',
};
});
}
protected formatTools(tools: ToolDefinition[]): ChatCompletionTool[] {
return tools.map(tool => ({
type: 'function' as const,
function: {
name: tool.name,
description: tool.description || `Function ${tool.name}`,
parameters: tool.parameters || { type: 'object', properties: {} },
},
}));
}
private mapFinishReason(reason: string | null): GenerateTextResult['finishReason'] {
switch (reason) {
case 'stop': return 'stop';
case 'length': return 'length';
case 'tool_calls': return 'tool_calls';
case 'content_filter': return 'content_filter';
default: return 'stop';
}
}
private normalizeSchema(schema: any): any {
// If it's a Zod schema, convert to JSON schema
if (schema && typeof schema.parse === 'function' && typeof schema._def === 'object') {
// This is a Zod schema - we need zod-to-json-schema
// For now, throw an error suggesting to use JSON schema directly
throw new Error('Zod schemas require zod-to-json-schema. Please use JSON schema directly or install zod-to-json-schema.');
}
return schema;
}
}
+103
View File
@@ -0,0 +1,103 @@
/**
* LLM Provider Types - Core type definitions for the provider abstraction layer
*/
export interface Message {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string | null;
name?: string;
tool_call_id?: string;
tool_calls?: ToolCall[];
}
export interface ToolCall {
id: string;
type: 'function';
function: {
name: string;
arguments: string;
};
}
export interface ToolDefinition {
name: string;
description?: string;
parameters?: Record<string, any>;
}
export interface TokenUsage {
promptTokens: number;
completionTokens: number;
totalTokens: number;
}
export interface GenerateTextOptions {
messages: Message[];
temperature?: number;
maxTokens?: number;
tools?: ToolDefinition[];
toolChoice?: 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } };
stop?: string[];
topP?: number;
frequencyPenalty?: number;
presencePenalty?: number;
}
export interface GenerateTextResult {
text: string;
toolCalls?: ToolCall[];
usage: TokenUsage;
finishReason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error';
raw?: any;
}
export interface StreamTextOptions extends GenerateTextOptions {
onToken?: (token: string) => void;
onToolCall?: (toolCall: ToolCall) => void;
}
export interface StreamChunk {
text?: string;
toolCalls?: ToolCall[];
usage?: Partial<TokenUsage>;
finishReason?: string;
}
export interface GenerateObjectOptions<T = any> {
messages: Message[];
schema: Record<string, any> | any; // JSON Schema or Zod schema
temperature?: number;
maxTokens?: number;
maxRetries?: number;
}
export interface GenerateObjectResult<T = any> {
object: T;
usage: TokenUsage;
raw?: any;
}
export interface ProviderConfig {
apiKey?: string;
baseUrl?: string;
maxRetries?: number;
timeout?: number;
defaultModel?: string;
}
/**
* Base interface for all LLM providers
*/
export interface LLMProvider {
readonly providerId: string;
readonly modelId: string;
generateText(options: GenerateTextOptions): Promise<GenerateTextResult>;
streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>>;
generateObject<T = any>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>>;
}
/**
* Provider factory function type
*/
export type ProviderFactory = (modelId: string, config?: ProviderConfig) => LLMProvider;
+221
View File
@@ -0,0 +1,221 @@
/**
* Memory System - Conversation and context memory management
*/
export interface MemoryEntry {
id: string;
content: string;
role: 'user' | 'assistant' | 'system';
timestamp: number;
metadata?: Record<string, any>;
embedding?: number[];
}
export interface MemoryConfig {
maxEntries?: number;
maxTokens?: number;
embeddingProvider?: EmbeddingProvider;
}
export interface EmbeddingProvider {
embed(text: string): Promise<number[]>;
embedBatch(texts: string[]): Promise<number[][]>;
}
export interface SearchResult {
entry: MemoryEntry;
score: number;
}
/**
* Memory class for managing conversation history and context
*/
export class Memory {
private entries: Map<string, MemoryEntry> = new Map();
private maxEntries: number;
private maxTokens: number;
private embeddingProvider?: EmbeddingProvider;
constructor(config: MemoryConfig = {}) {
this.maxEntries = config.maxEntries ?? 1000;
this.maxTokens = config.maxTokens ?? 100000;
this.embeddingProvider = config.embeddingProvider;
}
/**
* Add a memory entry
*/
async add(content: string, role: 'user' | 'assistant' | 'system', metadata?: Record<string, any>): Promise<MemoryEntry> {
const id = this.generateId();
const entry: MemoryEntry = {
id,
content,
role,
timestamp: Date.now(),
metadata
};
if (this.embeddingProvider) {
entry.embedding = await this.embeddingProvider.embed(content);
}
this.entries.set(id, entry);
this.enforceLimit();
return entry;
}
/**
* Get a memory entry by ID
*/
get(id: string): MemoryEntry | undefined {
return this.entries.get(id);
}
/**
* Get all entries
*/
getAll(): MemoryEntry[] {
return Array.from(this.entries.values())
.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* Get recent entries
*/
getRecent(count: number): MemoryEntry[] {
return this.getAll().slice(-count);
}
/**
* Search memory by text similarity
*/
async search(query: string, limit: number = 5): Promise<SearchResult[]> {
if (!this.embeddingProvider) {
// Fallback to simple text matching
return this.textSearch(query, limit);
}
const queryEmbedding = await this.embeddingProvider.embed(query);
const results: SearchResult[] = [];
for (const entry of this.entries.values()) {
if (entry.embedding) {
const score = this.cosineSimilarity(queryEmbedding, entry.embedding);
results.push({ entry, score });
}
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
/**
* Simple text search fallback
*/
private textSearch(query: string, limit: number): SearchResult[] {
const queryLower = query.toLowerCase();
const results: SearchResult[] = [];
for (const entry of this.entries.values()) {
const contentLower = entry.content.toLowerCase();
if (contentLower.includes(queryLower)) {
const score = queryLower.length / contentLower.length;
results.push({ entry, score: Math.min(score * 10, 1) });
}
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
/**
* Calculate cosine similarity
*/
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;
}
/**
* Delete a memory entry
*/
delete(id: string): boolean {
return this.entries.delete(id);
}
/**
* Clear all memory
*/
clear(): void {
this.entries.clear();
}
/**
* Get memory size
*/
get size(): number {
return this.entries.size;
}
/**
* Build context string from recent memory
*/
buildContext(count?: number): string {
const entries = count ? this.getRecent(count) : this.getAll();
return entries
.map(e => `${e.role}: ${e.content}`)
.join('\n');
}
/**
* Export memory to JSON
*/
toJSON(): MemoryEntry[] {
return this.getAll();
}
/**
* Import memory from JSON
*/
fromJSON(entries: MemoryEntry[]): void {
this.entries.clear();
for (const entry of entries) {
this.entries.set(entry.id, entry);
}
}
private generateId(): string {
return `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private enforceLimit(): void {
while (this.entries.size > this.maxEntries) {
const oldest = this.getAll()[0];
if (oldest) {
this.entries.delete(oldest.id);
}
}
}
}
/**
* Create a memory instance
*/
export function createMemory(config?: MemoryConfig): Memory {
return new Memory(config);
}
+238
View File
@@ -0,0 +1,238 @@
/**
* Observability - Tracing, logging, and metrics hooks
*/
export type SpanKind = 'llm' | 'tool' | 'agent' | 'workflow' | 'custom';
export type SpanStatus = 'pending' | 'running' | 'completed' | 'failed';
export interface SpanData {
id: string;
traceId: string;
parentId?: string;
name: string;
kind: SpanKind;
status: SpanStatus;
startTime: number;
endTime?: number;
attributes: Record<string, any>;
events: SpanEvent[];
}
export interface SpanEvent {
name: string;
timestamp: number;
attributes?: Record<string, any>;
}
export interface TraceData {
id: string;
name: string;
startTime: number;
endTime?: number;
status: SpanStatus;
spans: SpanData[];
metadata: Record<string, any>;
}
/**
* Observability Adapter Protocol
*/
export interface ObservabilityAdapter {
startTrace(name: string, metadata?: Record<string, any>): TraceContext;
endTrace(traceId: string, status?: SpanStatus): void;
startSpan(traceId: string, name: string, kind: SpanKind, parentId?: string): SpanContext;
endSpan(spanId: string, status?: SpanStatus, attributes?: Record<string, any>): void;
addEvent(spanId: string, name: string, attributes?: Record<string, any>): void;
flush(): Promise<void>;
}
export interface TraceContext {
traceId: string;
startSpan(name: string, kind: SpanKind): SpanContext;
end(status?: SpanStatus): void;
}
export interface SpanContext {
spanId: string;
traceId: string;
addEvent(name: string, attributes?: Record<string, any>): void;
setAttributes(attributes: Record<string, any>): void;
end(status?: SpanStatus): void;
}
/**
* In-memory observability adapter for development
*/
export class MemoryObservabilityAdapter implements ObservabilityAdapter {
private traces: Map<string, TraceData> = new Map();
private spans: Map<string, SpanData> = new Map();
startTrace(name: string, metadata: Record<string, any> = {}): TraceContext {
const traceId = this.generateId();
const trace: TraceData = {
id: traceId,
name,
startTime: Date.now(),
status: 'running',
spans: [],
metadata
};
this.traces.set(traceId, trace);
const self = this;
return {
traceId,
startSpan(spanName: string, kind: SpanKind): SpanContext {
return self.startSpan(traceId, spanName, kind);
},
end(status: SpanStatus = 'completed'): void {
self.endTrace(traceId, status);
}
};
}
endTrace(traceId: string, status: SpanStatus = 'completed'): void {
const trace = this.traces.get(traceId);
if (trace) {
trace.endTime = Date.now();
trace.status = status;
}
}
startSpan(traceId: string, name: string, kind: SpanKind, parentId?: string): SpanContext {
const spanId = this.generateId();
const span: SpanData = {
id: spanId,
traceId,
parentId,
name,
kind,
status: 'running',
startTime: Date.now(),
attributes: {},
events: []
};
this.spans.set(spanId, span);
const trace = this.traces.get(traceId);
if (trace) {
trace.spans.push(span);
}
const self = this;
return {
spanId,
traceId,
addEvent(eventName: string, attributes?: Record<string, any>): void {
self.addEvent(spanId, eventName, attributes);
},
setAttributes(attributes: Record<string, any>): void {
const s = self.spans.get(spanId);
if (s) {
s.attributes = { ...s.attributes, ...attributes };
}
},
end(status: SpanStatus = 'completed'): void {
self.endSpan(spanId, status);
}
};
}
endSpan(spanId: string, status: SpanStatus = 'completed', attributes?: Record<string, any>): void {
const span = this.spans.get(spanId);
if (span) {
span.endTime = Date.now();
span.status = status;
if (attributes) {
span.attributes = { ...span.attributes, ...attributes };
}
}
}
addEvent(spanId: string, name: string, attributes?: Record<string, any>): void {
const span = this.spans.get(spanId);
if (span) {
span.events.push({
name,
timestamp: Date.now(),
attributes
});
}
}
async flush(): Promise<void> {
// No-op for memory adapter
}
// Utility methods
getTrace(traceId: string): TraceData | undefined {
return this.traces.get(traceId);
}
getSpan(spanId: string): SpanData | undefined {
return this.spans.get(spanId);
}
getAllTraces(): TraceData[] {
return Array.from(this.traces.values());
}
clear(): void {
this.traces.clear();
this.spans.clear();
}
private generateId(): string {
return Math.random().toString(36).substring(2, 15);
}
}
/**
* Console observability adapter for debugging
*/
export class ConsoleObservabilityAdapter implements ObservabilityAdapter {
private memory = new MemoryObservabilityAdapter();
startTrace(name: string, metadata?: Record<string, any>): TraceContext {
console.log(`[TRACE START] ${name}`, metadata || '');
return this.memory.startTrace(name, metadata);
}
endTrace(traceId: string, status?: SpanStatus): void {
console.log(`[TRACE END] ${traceId} - ${status || 'completed'}`);
this.memory.endTrace(traceId, status);
}
startSpan(traceId: string, name: string, kind: SpanKind, parentId?: string): SpanContext {
console.log(` [SPAN START] ${name} (${kind})`);
return this.memory.startSpan(traceId, name, kind, parentId);
}
endSpan(spanId: string, status?: SpanStatus, attributes?: Record<string, any>): void {
console.log(` [SPAN END] ${spanId} - ${status || 'completed'}`, attributes || '');
this.memory.endSpan(spanId, status, attributes);
}
addEvent(spanId: string, name: string, attributes?: Record<string, any>): void {
console.log(` [EVENT] ${name}`, attributes || '');
this.memory.addEvent(spanId, name, attributes);
}
async flush(): Promise<void> {
await this.memory.flush();
}
}
// Global observability instance
let globalAdapter: ObservabilityAdapter | null = null;
export function setObservabilityAdapter(adapter: ObservabilityAdapter): void {
globalAdapter = adapter;
}
export function getObservabilityAdapter(): ObservabilityAdapter {
if (!globalAdapter) {
globalAdapter = new MemoryObservabilityAdapter();
}
return globalAdapter;
}
+307
View File
@@ -0,0 +1,307 @@
/**
* Planning System - Plans, Steps, and TodoLists
*/
export type PlanStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
export type TodoStatus = 'pending' | 'in_progress' | 'completed';
export interface PlanConfig {
name: string;
description?: string;
metadata?: Record<string, any>;
}
export interface PlanStepConfig {
description: string;
status?: PlanStatus;
order?: number;
dependencies?: string[];
metadata?: Record<string, any>;
}
export interface TodoItemConfig {
content: string;
priority?: 'low' | 'medium' | 'high';
status?: TodoStatus;
dueDate?: Date;
metadata?: Record<string, any>;
}
/**
* PlanStep - A single step in a plan
*/
export class PlanStep {
readonly id: string;
description: string;
status: PlanStatus;
order: number;
dependencies: string[];
metadata: Record<string, any>;
startedAt?: number;
completedAt?: number;
constructor(config: PlanStepConfig) {
this.id = `step_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.description = config.description;
this.status = config.status ?? 'pending';
this.order = config.order ?? 0;
this.dependencies = config.dependencies ?? [];
this.metadata = config.metadata ?? {};
}
start(): this {
this.status = 'in_progress';
this.startedAt = Date.now();
return this;
}
complete(): this {
this.status = 'completed';
this.completedAt = Date.now();
return this;
}
fail(): this {
this.status = 'failed';
this.completedAt = Date.now();
return this;
}
cancel(): this {
this.status = 'cancelled';
return this;
}
}
/**
* Plan - A collection of steps to accomplish a goal
*/
export class Plan {
readonly id: string;
name: string;
description?: string;
steps: PlanStep[] = [];
status: PlanStatus = 'pending';
metadata: Record<string, any>;
createdAt: number;
updatedAt: number;
constructor(config: PlanConfig) {
this.id = `plan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.name = config.name;
this.description = config.description;
this.metadata = config.metadata ?? {};
this.createdAt = Date.now();
this.updatedAt = Date.now();
}
addStep(step: PlanStep): this {
step.order = this.steps.length;
this.steps.push(step);
this.updatedAt = Date.now();
return this;
}
removeStep(stepId: string): boolean {
const index = this.steps.findIndex(s => s.id === stepId);
if (index >= 0) {
this.steps.splice(index, 1);
this.updatedAt = Date.now();
return true;
}
return false;
}
getStep(stepId: string): PlanStep | undefined {
return this.steps.find(s => s.id === stepId);
}
getNextStep(): PlanStep | undefined {
return this.steps.find(s => s.status === 'pending');
}
getProgress(): { completed: number; total: number; percentage: number } {
const completed = this.steps.filter(s => s.status === 'completed').length;
const total = this.steps.length;
return {
completed,
total,
percentage: total > 0 ? Math.round((completed / total) * 100) : 0
};
}
start(): this {
this.status = 'in_progress';
this.updatedAt = Date.now();
return this;
}
complete(): this {
this.status = 'completed';
this.updatedAt = Date.now();
return this;
}
}
/**
* TodoItem - A single todo item
*/
export class TodoItem {
readonly id: string;
content: string;
priority: 'low' | 'medium' | 'high';
status: TodoStatus;
dueDate?: Date;
metadata: Record<string, any>;
createdAt: number;
completedAt?: number;
constructor(config: TodoItemConfig) {
this.id = `todo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.content = config.content;
this.priority = config.priority ?? 'medium';
this.status = config.status ?? 'pending';
this.dueDate = config.dueDate;
this.metadata = config.metadata ?? {};
this.createdAt = Date.now();
}
start(): this {
this.status = 'in_progress';
return this;
}
complete(): this {
this.status = 'completed';
this.completedAt = Date.now();
return this;
}
reset(): this {
this.status = 'pending';
this.completedAt = undefined;
return this;
}
}
/**
* TodoList - A collection of todo items
*/
export class TodoList {
readonly id: string;
name: string;
items: TodoItem[] = [];
createdAt: number;
constructor(name: string = 'Todo List') {
this.id = `todolist_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.name = name;
this.createdAt = Date.now();
}
add(item: TodoItem): this {
this.items.push(item);
return this;
}
remove(itemId: string): boolean {
const index = this.items.findIndex(i => i.id === itemId);
if (index >= 0) {
this.items.splice(index, 1);
return true;
}
return false;
}
get(itemId: string): TodoItem | undefined {
return this.items.find(i => i.id === itemId);
}
getPending(): TodoItem[] {
return this.items.filter(i => i.status === 'pending');
}
getCompleted(): TodoItem[] {
return this.items.filter(i => i.status === 'completed');
}
getByPriority(priority: 'low' | 'medium' | 'high'): TodoItem[] {
return this.items.filter(i => i.priority === priority);
}
getProgress(): { completed: number; total: number; percentage: number } {
const completed = this.items.filter(i => i.status === 'completed').length;
const total = this.items.length;
return {
completed,
total,
percentage: total > 0 ? Math.round((completed / total) * 100) : 0
};
}
clear(): this {
this.items = [];
return this;
}
}
/**
* PlanStorage - Persist plans (in-memory implementation)
*/
export class PlanStorage {
private plans: Map<string, Plan> = new Map();
private todoLists: Map<string, TodoList> = new Map();
savePlan(plan: Plan): void {
this.plans.set(plan.id, plan);
}
getPlan(planId: string): Plan | undefined {
return this.plans.get(planId);
}
deletePlan(planId: string): boolean {
return this.plans.delete(planId);
}
listPlans(): Plan[] {
return Array.from(this.plans.values());
}
saveTodoList(list: TodoList): void {
this.todoLists.set(list.id, list);
}
getTodoList(listId: string): TodoList | undefined {
return this.todoLists.get(listId);
}
deleteTodoList(listId: string): boolean {
return this.todoLists.delete(listId);
}
listTodoLists(): TodoList[] {
return Array.from(this.todoLists.values());
}
}
/**
* Create a Plan
*/
export function createPlan(config: PlanConfig): Plan {
return new Plan(config);
}
/**
* Create a TodoList
*/
export function createTodoList(name?: string): TodoList {
return new TodoList(name);
}
/**
* Create a PlanStorage
*/
export function createPlanStorage(): PlanStorage {
return new PlanStorage();
}
+321
View File
@@ -0,0 +1,321 @@
/**
* Session Management - Session, Run, and Trace tracking
*/
import { randomUUID } from 'crypto';
export interface SessionConfig {
id?: string;
metadata?: Record<string, any>;
}
export interface RunConfig {
id?: string;
metadata?: Record<string, any>;
}
export interface TraceConfig {
id?: string;
name: string;
attributes?: Record<string, any>;
}
export type RunStatus = 'pending' | 'running' | 'completed' | 'failed';
export type TraceStatus = 'pending' | 'running' | 'completed' | 'failed';
export interface Message {
id: string;
role: 'system' | 'user' | 'assistant' | 'tool';
content: string | null;
name?: string;
tool_call_id?: string;
tool_calls?: any[];
timestamp: number;
runId?: string;
}
/**
* Trace represents a span within a run (e.g., LLM call, tool call)
*/
export class Trace {
readonly id: string;
readonly runId: string;
readonly name: string;
readonly parentId: string | null;
readonly startTime: number;
endTime: number | null = null;
status: TraceStatus = 'pending';
attributes: Record<string, any>;
private children: Trace[] = [];
constructor(runId: string, config: TraceConfig, parentId: string | null = null) {
this.id = config.id || randomUUID();
this.runId = runId;
this.name = config.name;
this.parentId = parentId;
this.startTime = Date.now();
this.attributes = config.attributes || {};
}
start(): this {
this.status = 'running';
return this;
}
complete(attributes?: Record<string, any>): this {
this.status = 'completed';
this.endTime = Date.now();
if (attributes) {
this.attributes = { ...this.attributes, ...attributes };
}
return this;
}
fail(error?: Error): this {
this.status = 'failed';
this.endTime = Date.now();
if (error) {
this.attributes.error = error.message;
this.attributes.errorStack = error.stack;
}
return this;
}
get duration(): number | null {
if (this.endTime === null) return null;
return this.endTime - this.startTime;
}
createChild(config: TraceConfig): Trace {
const child = new Trace(this.runId, config, this.id);
this.children.push(child);
return child;
}
getChildren(): Trace[] {
return [...this.children];
}
toJSON(): Record<string, any> {
return {
id: this.id,
runId: this.runId,
name: this.name,
parentId: this.parentId,
startTime: this.startTime,
endTime: this.endTime,
duration: this.duration,
status: this.status,
attributes: this.attributes,
children: this.children.map(c => c.toJSON()),
};
}
}
/**
* Run represents a single execution within a session (e.g., one chat call)
*/
export class Run {
readonly id: string;
readonly sessionId: string;
readonly startTime: number;
endTime: number | null = null;
status: RunStatus = 'pending';
error: Error | null = null;
metadata: Record<string, any>;
private traces: Trace[] = [];
private messages: Message[] = [];
constructor(sessionId: string, config: RunConfig = {}) {
this.id = config.id || randomUUID();
this.sessionId = sessionId;
this.startTime = Date.now();
this.metadata = config.metadata || {};
}
start(): this {
this.status = 'running';
return this;
}
complete(metadata?: Record<string, any>): this {
this.status = 'completed';
this.endTime = Date.now();
if (metadata) {
this.metadata = { ...this.metadata, ...metadata };
}
return this;
}
fail(error: Error): this {
this.status = 'failed';
this.endTime = Date.now();
this.error = error;
return this;
}
get duration(): number | null {
if (this.endTime === null) return null;
return this.endTime - this.startTime;
}
createTrace(config: TraceConfig): Trace {
const trace = new Trace(this.id, config);
this.traces.push(trace);
return trace;
}
addMessage(message: Omit<Message, 'id' | 'timestamp' | 'runId'>): Message {
const msg: Message = {
...message,
id: randomUUID(),
timestamp: Date.now(),
runId: this.id,
};
this.messages.push(msg);
return msg;
}
getTraces(): Trace[] {
return [...this.traces];
}
getMessages(): Message[] {
return [...this.messages];
}
toJSON(): Record<string, any> {
return {
id: this.id,
sessionId: this.sessionId,
startTime: this.startTime,
endTime: this.endTime,
duration: this.duration,
status: this.status,
error: this.error?.message,
metadata: this.metadata,
traces: this.traces.map(t => t.toJSON()),
messageCount: this.messages.length,
};
}
}
/**
* Session represents a conversation session with message history
*/
export class Session {
readonly id: string;
readonly createdAt: number;
metadata: Record<string, any>;
private _runs: Run[] = [];
private _messages: Message[] = [];
constructor(config: SessionConfig = {}) {
this.id = config.id || randomUUID();
this.createdAt = Date.now();
this.metadata = config.metadata || {};
}
createRun(config: RunConfig = {}): Run {
const run = new Run(this.id, config);
this._runs.push(run);
return run;
}
get runs(): Run[] {
return [...this._runs];
}
get messages(): Message[] {
return [...this._messages];
}
addMessage(message: Omit<Message, 'id' | 'timestamp'>): Message {
const msg: Message = {
...message,
id: randomUUID(),
timestamp: Date.now(),
};
this._messages.push(msg);
return msg;
}
getMessagesForLLM(): Array<{ role: string; content: string | null; tool_call_id?: string; tool_calls?: any[] }> {
return this._messages.map(m => ({
role: m.role,
content: m.content,
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}),
...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
}));
}
clearMessages(): void {
this._messages = [];
}
toJSON(): Record<string, any> {
return {
id: this.id,
createdAt: this.createdAt,
metadata: this.metadata,
runCount: this._runs.length,
messageCount: this._messages.length,
runs: this._runs.map(r => r.toJSON()),
};
}
}
/**
* SessionManager for managing multiple sessions
*/
export class SessionManager {
private sessions: Map<string, Session> = new Map();
create(config: SessionConfig = {}): Session {
const session = new Session(config);
this.sessions.set(session.id, session);
return session;
}
get(id: string): Session | undefined {
return this.sessions.get(id);
}
getOrCreate(id: string, config: Omit<SessionConfig, 'id'> = {}): Session {
let session = this.sessions.get(id);
if (!session) {
session = new Session({ ...config, id });
this.sessions.set(id, session);
}
return session;
}
list(): Session[] {
return Array.from(this.sessions.values());
}
delete(id: string): boolean {
return this.sessions.delete(id);
}
clear(): void {
this.sessions.clear();
}
toJSON(): Record<string, any> {
return {
sessionCount: this.sessions.size,
sessions: this.list().map(s => s.toJSON()),
};
}
}
// Default session manager instance
let defaultManager: SessionManager | null = null;
export function getSessionManager(): SessionManager {
if (!defaultManager) {
defaultManager = new SessionManager();
}
return defaultManager;
}
+246
View File
@@ -0,0 +1,246 @@
/**
* Skills System - Agent Skills standard implementation
*/
import * as fs from 'fs';
import * as path from 'path';
export interface SkillMetadata {
name: string;
description: string;
license?: string;
compatibility?: string;
allowedTools?: string[];
metadata?: Record<string, string>;
}
export interface Skill {
metadata: SkillMetadata;
instructions: string;
path?: string;
}
export interface SkillDiscoveryOptions {
paths?: string[];
includeSystem?: boolean;
includeUser?: boolean;
includeProject?: boolean;
}
/**
* Parse SKILL.md file
*/
export function parseSkillFile(content: string): Skill {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) {
throw new Error('Invalid SKILL.md format: missing frontmatter');
}
const [, frontmatter, instructions] = frontmatterMatch;
const metadata = parseYamlFrontmatter(frontmatter);
if (!metadata.name || !metadata.description) {
throw new Error('Invalid SKILL.md: name and description are required');
}
return {
metadata: {
name: metadata.name,
description: metadata.description,
license: metadata.license,
compatibility: metadata.compatibility,
allowedTools: metadata['allowed-tools']?.split(/\s+/),
metadata: metadata.metadata
},
instructions: instructions.trim()
};
}
/**
* Simple YAML frontmatter parser
*/
function parseYamlFrontmatter(yaml: string): Record<string, any> {
const result: Record<string, any> = {};
const lines = yaml.split('\n');
let currentKey = '';
let inMetadata = false;
const metadataObj: Record<string, string> = {};
for (const line of lines) {
if (line.startsWith('metadata:')) {
inMetadata = true;
continue;
}
if (inMetadata && line.startsWith(' ')) {
const match = line.match(/^\s+(\w+):\s*(.*)$/);
if (match) {
metadataObj[match[1]] = match[2].replace(/^["']|["']$/g, '');
}
continue;
} else if (inMetadata && !line.startsWith(' ')) {
inMetadata = false;
result.metadata = metadataObj;
}
const match = line.match(/^(\S+):\s*(.*)$/);
if (match) {
currentKey = match[1];
result[currentKey] = match[2].replace(/^["']|["']$/g, '');
}
}
if (inMetadata) {
result.metadata = metadataObj;
}
return result;
}
/**
* Skill Manager - Load and manage skills
*/
export class SkillManager {
private skills: Map<string, Skill> = new Map();
private discoveryPaths: string[] = [];
constructor(options: SkillDiscoveryOptions = {}) {
this.setupDiscoveryPaths(options);
}
private setupDiscoveryPaths(options: SkillDiscoveryOptions): void {
if (options.paths) {
this.discoveryPaths.push(...options.paths);
}
if (options.includeProject !== false) {
this.discoveryPaths.push('./.praison/skills');
this.discoveryPaths.push('./.claude/skills');
}
if (options.includeUser !== false) {
const home = process.env.HOME || process.env.USERPROFILE || '';
if (home) {
this.discoveryPaths.push(path.join(home, '.praison/skills'));
}
}
if (options.includeSystem) {
this.discoveryPaths.push('/etc/praison/skills');
}
}
/**
* Discover and load skills from configured paths
*/
async discover(): Promise<Skill[]> {
const discovered: Skill[] = [];
for (const basePath of this.discoveryPaths) {
if (!fs.existsSync(basePath)) continue;
const entries = fs.readdirSync(basePath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const skillPath = path.join(basePath, entry.name, 'SKILL.md');
if (fs.existsSync(skillPath)) {
try {
const skill = await this.loadSkill(skillPath);
skill.path = path.join(basePath, entry.name);
discovered.push(skill);
this.skills.set(skill.metadata.name, skill);
} catch (e) {
// Skip invalid skills
}
}
}
}
}
return discovered;
}
/**
* Load a skill from a SKILL.md file
*/
async loadSkill(skillPath: string): Promise<Skill> {
const content = fs.readFileSync(skillPath, 'utf-8');
return parseSkillFile(content);
}
/**
* Register a skill manually
*/
register(skill: Skill): void {
this.skills.set(skill.metadata.name, skill);
}
/**
* Get a skill by name
*/
get(name: string): Skill | undefined {
return this.skills.get(name);
}
/**
* List all loaded skills
*/
list(): Skill[] {
return Array.from(this.skills.values());
}
/**
* Generate XML prompt for skills
*/
generatePrompt(skillNames?: string[]): string {
const skillsToInclude = skillNames
? skillNames.map(n => this.skills.get(n)).filter(Boolean) as Skill[]
: this.list();
if (skillsToInclude.length === 0) return '';
const parts = ['<skills>'];
for (const skill of skillsToInclude) {
parts.push(` <skill name="${skill.metadata.name}">`);
parts.push(` <description>${skill.metadata.description}</description>`);
parts.push(` <instructions>${skill.instructions}</instructions>`);
parts.push(' </skill>');
}
parts.push('</skills>');
return parts.join('\n');
}
/**
* Validate a skill
*/
validate(skill: Skill): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!skill.metadata.name) {
errors.push('Missing required field: name');
} else if (!/^[a-z0-9-]{1,64}$/.test(skill.metadata.name)) {
errors.push('Invalid name: must be 1-64 lowercase chars, numbers, or hyphens');
}
if (!skill.metadata.description) {
errors.push('Missing required field: description');
} else if (skill.metadata.description.length > 1024) {
errors.push('Description too long: max 1024 characters');
}
if (skill.instructions.length > 5000) {
errors.push('Instructions too long: max 5000 characters');
}
return { valid: errors.length === 0, errors };
}
}
/**
* Create a skill manager
*/
export function createSkillManager(options?: SkillDiscoveryOptions): SkillManager {
return new SkillManager(options);
}
+227
View File
@@ -0,0 +1,227 @@
/**
* Telemetry - Usage tracking and analytics
*/
export interface TelemetryEvent {
name: string;
timestamp: number;
properties?: Record<string, any>;
userId?: string;
sessionId?: string;
}
export interface TelemetryConfig {
enabled?: boolean;
endpoint?: string;
batchSize?: number;
flushInterval?: number;
}
/**
* Telemetry collector for tracking usage
*/
export class TelemetryCollector {
private events: TelemetryEvent[] = [];
private enabled: boolean;
private endpoint?: string;
private batchSize: number;
private flushInterval: number;
private flushTimer?: NodeJS.Timeout;
private userId?: string;
private sessionId: string;
constructor(config: TelemetryConfig = {}) {
this.enabled = config.enabled ?? this.checkEnabled();
this.endpoint = config.endpoint;
this.batchSize = config.batchSize ?? 100;
this.flushInterval = config.flushInterval ?? 60000;
this.sessionId = this.generateSessionId();
if (this.enabled && this.flushInterval > 0) {
this.startFlushTimer();
}
}
private checkEnabled(): boolean {
const disabled = process.env.PRAISONAI_TELEMETRY_DISABLED === 'true' ||
process.env.PRAISONAI_DISABLE_TELEMETRY === 'true' ||
process.env.DO_NOT_TRACK === 'true';
return !disabled;
}
/**
* Track an event
*/
track(name: string, properties?: Record<string, any>): void {
if (!this.enabled) return;
const event: TelemetryEvent = {
name,
timestamp: Date.now(),
properties,
userId: this.userId,
sessionId: this.sessionId
};
this.events.push(event);
if (this.events.length >= this.batchSize) {
this.flush();
}
}
/**
* Track feature usage
*/
trackFeatureUsage(feature: string, metadata?: Record<string, any>): void {
this.track('feature_usage', { feature, ...metadata });
}
/**
* Track agent execution
*/
trackAgentExecution(agentName: string, duration: number, success: boolean): void {
this.track('agent_execution', { agentName, duration, success });
}
/**
* Track tool call
*/
trackToolCall(toolName: string, duration: number, success: boolean): void {
this.track('tool_call', { toolName, duration, success });
}
/**
* Track LLM call
*/
trackLLMCall(provider: string, model: string, tokens: number, duration: number): void {
this.track('llm_call', { provider, model, tokens, duration });
}
/**
* Track error
*/
trackError(error: string, context?: Record<string, any>): void {
this.track('error', { error, ...context });
}
/**
* Set user ID
*/
setUserId(userId: string): void {
this.userId = userId;
}
/**
* Flush events
*/
async flush(): Promise<void> {
if (this.events.length === 0) return;
const eventsToSend = [...this.events];
this.events = [];
if (this.endpoint) {
try {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events: eventsToSend })
});
} catch (error) {
// Silently fail - never break user applications
}
}
}
/**
* Enable telemetry
*/
enable(): void {
this.enabled = true;
this.startFlushTimer();
}
/**
* Disable telemetry
*/
disable(): void {
this.enabled = false;
this.stopFlushTimer();
this.events = [];
}
/**
* Check if telemetry is enabled
*/
isEnabled(): boolean {
return this.enabled;
}
/**
* Get pending events count
*/
getPendingCount(): number {
return this.events.length;
}
/**
* Cleanup resources
*/
cleanup(): void {
this.stopFlushTimer();
this.flush();
}
private startFlushTimer(): void {
if (this.flushTimer) return;
this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
}
private stopFlushTimer(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = undefined;
}
}
private generateSessionId(): string {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
// Global telemetry instance
let globalTelemetry: TelemetryCollector | null = null;
/**
* Get global telemetry instance
*/
export function getTelemetry(): TelemetryCollector {
if (!globalTelemetry) {
globalTelemetry = new TelemetryCollector();
}
return globalTelemetry;
}
/**
* Enable telemetry
*/
export function enableTelemetry(): void {
getTelemetry().enable();
}
/**
* Disable telemetry
*/
export function disableTelemetry(): void {
getTelemetry().disable();
}
/**
* Cleanup telemetry resources
*/
export function cleanupTelemetry(): void {
if (globalTelemetry) {
globalTelemetry.cleanup();
}
}
+12 -15
View File
@@ -1,6 +1,6 @@
import axios from 'axios';
import { Tool, BaseTool } from './index';
import { XMLParser } from 'fast-xml-parser';
import { BaseTool } from './base';
export interface ArxivPaper {
id: string;
@@ -12,21 +12,21 @@ export interface ArxivPaper {
link: string;
}
export class ArxivSearchTool extends BaseTool {
export class ArxivSearchTool extends BaseTool<{ query: string; maxResults?: number }, ArxivPaper[]> {
name = 'arxiv-search';
description = 'Search for academic papers on arXiv';
private parser: XMLParser;
constructor() {
super(
'arxiv-search',
'Search for academic papers on arXiv'
);
super();
this.parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_'
});
}
async execute(query: string, maxResults: number = 10): Promise<ArxivPaper[]> {
async run(params: { query: string; maxResults?: number }): Promise<ArxivPaper[]> {
const { query, maxResults = 10 } = params;
try {
const response = await axios.get(`http://export.arxiv.org/api/query`, {
params: {
@@ -67,15 +67,12 @@ export class ArxivSearchTool extends BaseTool {
}
}
export class ArxivDownloadTool extends BaseTool {
constructor() {
super(
'arxiv-download',
'Download PDF of an arXiv paper'
);
}
export class ArxivDownloadTool extends BaseTool<{ paperId: string }, Buffer> {
name = 'arxiv-download';
description = 'Download PDF of an arXiv paper';
async execute(paperId: string): Promise<Buffer> {
async run(params: { paperId: string }): Promise<Buffer> {
const { paperId } = params;
try {
const response = await axios.get(`https://arxiv.org/pdf/${paperId}.pdf`, {
responseType: 'arraybuffer'
+189
View File
@@ -0,0 +1,189 @@
/**
* BaseTool - Abstract base class for creating custom tools (plugins)
*
* This provides the same extensibility pattern as Python's BaseTool.
* External developers can create plugins by extending BaseTool.
*
* Usage:
* import { BaseTool } from 'praisonai';
*
* class MyTool extends BaseTool {
* name = 'my_tool';
* description = 'Does something useful';
*
* async run(params: { query: string }): Promise<string> {
* return `Result for ${params.query}`;
* }
* }
*/
export interface ToolResult<T = any> {
output: T;
success: boolean;
error?: string;
metadata?: Record<string, any>;
}
export class ToolValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ToolValidationError';
}
}
export interface ToolParameters {
type: 'object';
properties: Record<string, {
type: string;
description?: string;
enum?: string[];
default?: any;
}>;
required?: string[];
}
/**
* Abstract base class for all PraisonAI tools.
*
* Subclass this to create custom tools that can be:
* - Used directly by agents
* - Distributed as npm packages (plugins)
* - Auto-discovered via package.json
*/
export abstract class BaseTool<TParams = any, TResult = any> {
/** Unique identifier for the tool */
abstract name: string;
/** Human-readable description (used by LLM) */
abstract description: string;
/** Tool version string */
version: string = '1.0.0';
/** JSON Schema for parameters */
parameters?: ToolParameters;
/**
* Execute the tool with given arguments.
* This method must be implemented by subclasses.
*/
abstract run(params: TParams): Promise<TResult> | TResult;
/**
* Allow tool to be called directly like a function.
*/
async execute(params: TParams): Promise<TResult> {
return this.run(params);
}
/**
* Execute tool with error handling, returning ToolResult.
*/
async safeRun(params: TParams): Promise<ToolResult<TResult>> {
try {
const output = await this.run(params);
return { output, success: true };
} catch (error: any) {
return {
output: null as any,
success: false,
error: error.message
};
}
}
/**
* Get OpenAI-compatible function schema for this tool.
*/
getSchema(): { type: 'function'; function: { name: string; description: string; parameters: any } } {
return {
type: 'function',
function: {
name: this.name,
description: this.description,
parameters: this.parameters || { type: 'object', properties: {}, required: [] }
}
};
}
/**
* Validate the tool configuration.
*/
validate(): boolean {
const errors: string[] = [];
if (!this.name || typeof this.name !== 'string') {
errors.push("Tool must have a non-empty string 'name'");
}
if (!this.description || typeof this.description !== 'string') {
errors.push("Tool must have a non-empty string 'description'");
}
if (errors.length > 0) {
throw new ToolValidationError(`Tool '${this.name}' validation failed: ${errors.join('; ')}`);
}
return true;
}
toString(): string {
return `${this.constructor.name}(name='${this.name}')`;
}
}
/**
* Validate any tool-like object.
*/
export function validateTool(tool: any): boolean {
if (tool instanceof BaseTool) {
return tool.validate();
}
if (typeof tool === 'function' || (tool && typeof tool.run === 'function')) {
const name = tool.name || tool.__name__;
if (!name) {
throw new ToolValidationError('Tool must have a name');
}
return true;
}
throw new ToolValidationError(`Invalid tool type: ${typeof tool}`);
}
/**
* Create a simple tool from a function (alternative to class-based approach)
*/
export function createTool<TParams = any, TResult = any>(config: {
name: string;
description: string;
parameters?: ToolParameters;
run: (params: TParams) => Promise<TResult> | TResult;
}): BaseTool<TParams, TResult> {
return {
name: config.name,
description: config.description,
version: '1.0.0',
parameters: config.parameters,
run: config.run,
execute: async (params: TParams) => config.run(params),
safeRun: async (params: TParams) => {
try {
const output = await config.run(params);
return { output, success: true };
} catch (error: any) {
return { output: null as any, success: false, error: error.message };
}
},
getSchema: () => ({
type: 'function' as const,
function: {
name: config.name,
description: config.description,
parameters: config.parameters || { type: 'object', properties: {}, required: [] }
}
}),
validate: () => true,
toString: () => `Tool(name='${config.name}')`
} as BaseTool<TParams, TResult>;
}
+225
View File
@@ -0,0 +1,225 @@
/**
* Tool Decorator and Registry - Type-safe tool creation and management
*/
export interface ToolParameters {
type: 'object';
properties: Record<string, {
type: string;
description?: string;
enum?: string[];
default?: any;
}>;
required?: string[];
}
export interface ToolConfig<TParams = any, TResult = any> {
name: string;
description?: string;
parameters?: ToolParameters | any; // Support Zod schemas
category?: string;
execute: (params: TParams, context?: ToolContext) => Promise<TResult> | TResult;
}
export interface ToolContext {
agentName?: string;
sessionId?: string;
runId?: string;
signal?: AbortSignal;
}
export interface ToolDefinition {
name: string;
description: string;
parameters: ToolParameters;
category?: string;
}
/**
* Tool class - Represents an executable tool
*/
export class FunctionTool<TParams = any, TResult = any> {
readonly name: string;
readonly description: string;
readonly parameters: ToolParameters;
readonly category?: string;
private readonly executeFn: (params: TParams, context?: ToolContext) => Promise<TResult> | TResult;
constructor(config: ToolConfig<TParams, TResult>) {
this.name = config.name;
this.description = config.description || `Function ${config.name}`;
this.parameters = this.normalizeParameters(config.parameters);
this.category = config.category;
this.executeFn = config.execute;
}
private normalizeParameters(params: any): ToolParameters {
if (!params) {
return { type: 'object', properties: {}, required: [] };
}
// Check if it's a Zod schema
if (params && typeof params.parse === 'function' && typeof params._def === 'object') {
return this.zodToJsonSchema(params);
}
// Already a JSON schema
return params;
}
private zodToJsonSchema(zodSchema: any): ToolParameters {
// Basic Zod to JSON Schema conversion
// For full support, use zod-to-json-schema package
const def = zodSchema._def;
if (def.typeName === 'ZodObject') {
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(def.shape())) {
const fieldDef = (value as any)._def;
properties[key] = this.zodFieldToJsonSchema(fieldDef);
// Check if field is required (not optional)
if (fieldDef.typeName !== 'ZodOptional' && fieldDef.typeName !== 'ZodDefault') {
required.push(key);
}
}
return { type: 'object', properties, required };
}
return { type: 'object', properties: {}, required: [] };
}
private zodFieldToJsonSchema(def: any): any {
const typeName = def.typeName;
switch (typeName) {
case 'ZodString':
return { type: 'string', description: def.description };
case 'ZodNumber':
return { type: 'number', description: def.description };
case 'ZodBoolean':
return { type: 'boolean', description: def.description };
case 'ZodArray':
return { type: 'array', items: this.zodFieldToJsonSchema(def.type._def) };
case 'ZodEnum':
return { type: 'string', enum: def.values };
case 'ZodOptional':
return this.zodFieldToJsonSchema(def.innerType._def);
case 'ZodDefault':
const inner = this.zodFieldToJsonSchema(def.innerType._def);
inner.default = def.defaultValue();
return inner;
default:
return { type: 'string' };
}
}
async execute(params: TParams, context?: ToolContext): Promise<TResult> {
// Validate parameters if we have a schema
// For now, just execute - validation can be added later
return this.executeFn(params, context);
}
/**
* Get the tool definition for LLM
*/
getDefinition(): ToolDefinition {
return {
name: this.name,
description: this.description,
parameters: this.parameters,
category: this.category,
};
}
/**
* Get OpenAI-compatible tool format
*/
toOpenAITool(): { type: 'function'; function: { name: string; description: string; parameters: any } } {
return {
type: 'function',
function: {
name: this.name,
description: this.description,
parameters: this.parameters,
},
};
}
}
/**
* Create a tool from a configuration object
*/
export function tool<TParams = any, TResult = any>(
config: ToolConfig<TParams, TResult>
): FunctionTool<TParams, TResult> {
return new FunctionTool(config);
}
/**
* Tool Registry - Manages tool registration and lookup
*/
export class ToolRegistry {
private tools: Map<string, FunctionTool> = new Map();
register(tool: FunctionTool, options?: { overwrite?: boolean }): this {
if (this.tools.has(tool.name) && !options?.overwrite) {
throw new Error(`Tool '${tool.name}' is already registered. Use { overwrite: true } to replace.`);
}
this.tools.set(tool.name, tool);
return this;
}
get(name: string): FunctionTool | undefined {
return this.tools.get(name);
}
has(name: string): boolean {
return this.tools.has(name);
}
list(): FunctionTool[] {
return Array.from(this.tools.values());
}
getByCategory(category: string): FunctionTool[] {
return this.list().filter(t => t.category === category);
}
getDefinitions(): ToolDefinition[] {
return this.list().map(t => t.getDefinition());
}
toOpenAITools(): Array<{ type: 'function'; function: any }> {
return this.list().map(t => t.toOpenAITool());
}
delete(name: string): boolean {
return this.tools.delete(name);
}
clear(): void {
this.tools.clear();
}
}
// Global registry instance
let globalRegistry: ToolRegistry | null = null;
export function getRegistry(): ToolRegistry {
if (!globalRegistry) {
globalRegistry = new ToolRegistry();
}
return globalRegistry;
}
export function registerTool(tool: FunctionTool, options?: { overwrite?: boolean }): void {
getRegistry().register(tool, options);
}
export function getTool(name: string): FunctionTool | undefined {
return getRegistry().get(name);
}
+4 -19
View File
@@ -1,23 +1,8 @@
// Export base tool interfaces
export interface Tool {
name: string;
description: string;
execute(...args: any[]): Promise<any>;
}
// Export base tool interfaces and classes
export { BaseTool, ToolResult, ToolValidationError, validateTool, createTool, type ToolParameters } from './base';
export class BaseTool implements Tool {
name: string;
description: string;
constructor(name: string, description: string) {
this.name = name;
this.description = description;
}
async execute(...args: any[]): Promise<any> {
throw new Error('Method not implemented.');
}
}
// Export decorator and registry
export * from './decorator';
// Export all tool modules
export * from './arxivTools';
+8 -4
View File
@@ -1,6 +1,6 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { BaseTool } from './index';
import { BaseTool } from './base';
export interface MCPToolInfo {
name: string;
@@ -8,12 +8,16 @@ export interface MCPToolInfo {
inputSchema?: any;
}
export class MCPTool extends BaseTool {
export class MCPTool extends BaseTool<any, any> {
name: string;
description: string;
private client: Client;
private inputSchema: any;
constructor(info: MCPToolInfo, client: Client) {
super(info.name, info.description || `Call the ${info.name} tool`);
super();
this.name = info.name;
this.description = info.description || `Call the ${info.name} tool`;
this.client = client;
this.inputSchema = info.inputSchema || { type: 'object', properties: {}, required: [] };
}
@@ -22,7 +26,7 @@ export class MCPTool extends BaseTool {
return this.inputSchema?.properties;
}
async execute(args: any = {}): Promise<any> {
async run(args: any = {}): Promise<any> {
try {
const result: any = await this.client.callTool({ name: this.name, arguments: args });
if (result.structuredContent) {
+300
View File
@@ -0,0 +1,300 @@
/**
* Workflows - Pipeline and orchestration patterns
*/
import { randomUUID } from 'crypto';
export type StepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
export interface StepResult<T = any> {
stepId: string;
stepName: string;
status: StepStatus;
output?: T;
error?: Error;
duration: number;
startedAt: number;
completedAt?: number;
}
export interface WorkflowContext {
workflowId: string;
stepResults: Map<string, StepResult>;
metadata: Record<string, any>;
get<T = any>(stepName: string): T | undefined;
set(key: string, value: any): void;
}
export type StepFunction<TInput = any, TOutput = any> = (
input: TInput,
context: WorkflowContext
) => Promise<TOutput> | TOutput;
export interface WorkflowStepConfig<TInput = any, TOutput = any> {
name: string;
execute: StepFunction<TInput, TOutput>;
condition?: (context: WorkflowContext) => boolean;
onError?: 'fail' | 'skip' | 'retry';
maxRetries?: number;
timeout?: number;
}
/**
* Workflow Step
*/
export class WorkflowStep<TInput = any, TOutput = any> {
readonly id: string;
readonly name: string;
readonly execute: StepFunction<TInput, TOutput>;
readonly condition?: (context: WorkflowContext) => boolean;
readonly onError: 'fail' | 'skip' | 'retry';
readonly maxRetries: number;
readonly timeout?: number;
constructor(config: WorkflowStepConfig<TInput, TOutput>) {
this.id = randomUUID();
this.name = config.name;
this.execute = config.execute;
this.condition = config.condition;
this.onError = config.onError || 'fail';
this.maxRetries = config.maxRetries || 0;
this.timeout = config.timeout;
}
async run(input: TInput, context: WorkflowContext): Promise<StepResult<TOutput>> {
const startedAt = Date.now();
// Check condition
if (this.condition && !this.condition(context)) {
return {
stepId: this.id,
stepName: this.name,
status: 'skipped',
duration: 0,
startedAt,
completedAt: Date.now(),
};
}
let lastError: Error | undefined;
let attempts = 0;
while (attempts <= this.maxRetries) {
attempts++;
try {
const output = await this.executeWithTimeout(input, context);
const completedAt = Date.now();
return {
stepId: this.id,
stepName: this.name,
status: 'completed',
output,
duration: completedAt - startedAt,
startedAt,
completedAt,
};
} catch (error: any) {
lastError = error;
if (this.onError === 'skip') {
return {
stepId: this.id,
stepName: this.name,
status: 'skipped',
error: lastError,
duration: Date.now() - startedAt,
startedAt,
completedAt: Date.now(),
};
}
if (this.onError !== 'retry' || attempts > this.maxRetries) {
break;
}
}
}
return {
stepId: this.id,
stepName: this.name,
status: 'failed',
error: lastError,
duration: Date.now() - startedAt,
startedAt,
completedAt: Date.now(),
};
}
private async executeWithTimeout(input: TInput, context: WorkflowContext): Promise<TOutput> {
if (!this.timeout) {
return this.execute(input, context);
}
return Promise.race([
this.execute(input, context),
new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Step ${this.name} timed out after ${this.timeout}ms`)), this.timeout);
}),
]);
}
}
/**
* Create a workflow context
*/
function createContext(workflowId: string): WorkflowContext {
const stepResults = new Map<string, StepResult>();
const metadata: Record<string, any> = {};
return {
workflowId,
stepResults,
metadata,
get<T = any>(stepName: string): T | undefined {
const result = stepResults.get(stepName);
return result?.output as T | undefined;
},
set(key: string, value: any): void {
metadata[key] = value;
},
};
}
/**
* Workflow - Sequential pipeline execution
*/
export class Workflow<TInput = any, TOutput = any> {
readonly id: string;
readonly name: string;
private steps: WorkflowStep[] = [];
constructor(name: string) {
this.id = randomUUID();
this.name = name;
}
/**
* Add a step to the workflow
*/
addStep<TStepInput = any, TStepOutput = any>(
config: WorkflowStepConfig<TStepInput, TStepOutput>
): this {
this.steps.push(new WorkflowStep(config));
return this;
}
/**
* Add a step using a simpler syntax
*/
step<TStepInput = any, TStepOutput = any>(
name: string,
execute: StepFunction<TStepInput, TStepOutput>
): this {
return this.addStep({ name, execute });
}
/**
* Run the workflow
*/
async run(input: TInput): Promise<{ output: TOutput | undefined; results: StepResult[]; context: WorkflowContext }> {
const context = createContext(this.id);
const results: StepResult[] = [];
let currentInput: any = input;
for (const step of this.steps) {
const result = await step.run(currentInput, context);
results.push(result);
context.stepResults.set(step.name, result);
if (result.status === 'failed') {
return { output: undefined, results, context };
}
if (result.status === 'completed') {
currentInput = result.output;
}
}
const lastResult = results[results.length - 1];
return {
output: lastResult?.output as TOutput | undefined,
results,
context,
};
}
/**
* Get step count
*/
get stepCount(): number {
return this.steps.length;
}
}
/**
* Pipeline - Alias for Workflow
*/
export const Pipeline = Workflow;
/**
* Parallel execution helper
*/
export async function parallel<T>(
tasks: Array<() => Promise<T>>
): Promise<T[]> {
return Promise.all(tasks.map(task => task()));
}
/**
* Route helper - Execute based on condition
*/
export async function route<T>(
conditions: Array<{ condition: () => boolean; execute: () => Promise<T> }>,
defaultExecute?: () => Promise<T>
): Promise<T | undefined> {
for (const { condition, execute } of conditions) {
if (condition()) {
return execute();
}
}
return defaultExecute?.();
}
/**
* Loop helper - Repeat until condition
*/
export async function loop<T>(
execute: (iteration: number) => Promise<T>,
shouldContinue: (result: T, iteration: number) => boolean,
maxIterations: number = 100
): Promise<T[]> {
const results: T[] = [];
let iteration = 0;
while (iteration < maxIterations) {
const result = await execute(iteration);
results.push(result);
if (!shouldContinue(result, iteration)) {
break;
}
iteration++;
}
return results;
}
/**
* Repeat helper - Execute N times
*/
export async function repeat<T>(
execute: (iteration: number) => Promise<T>,
times: number
): Promise<T[]> {
const results: T[] = [];
for (let i = 0; i < times; i++) {
results.push(await execute(i));
}
return results;
}
Whitespace-only changes.
@@ -0,0 +1,315 @@
/**
* YAML Workflow Parser
* Parse YAML workflow definitions into executable workflows
*/
import { Workflow, WorkflowStep, WorkflowStepConfig } from './index';
export interface YAMLWorkflowDefinition {
name: string;
description?: string;
version?: string;
steps: YAMLStepDefinition[];
metadata?: Record<string, any>;
}
export interface YAMLStepDefinition {
name: string;
type: 'agent' | 'tool' | 'condition' | 'parallel' | 'loop';
agent?: string;
tool?: string;
input?: string | Record<string, any>;
output?: string;
condition?: string;
onError?: 'fail' | 'skip' | 'retry';
maxRetries?: number;
timeout?: number;
steps?: YAMLStepDefinition[]; // For parallel/loop
loopCondition?: string;
maxIterations?: number;
}
export interface ParsedWorkflow {
workflow: Workflow;
definition: YAMLWorkflowDefinition;
errors: string[];
}
/**
* Parse YAML string into workflow definition
*/
export function parseYAMLWorkflow(yamlContent: string): YAMLWorkflowDefinition {
// Simple YAML parser for workflow definitions
// For production, use js-yaml package
const lines = yamlContent.split('\n');
const result: YAMLWorkflowDefinition = {
name: '',
steps: []
};
let currentStep: Partial<YAMLStepDefinition> | null = null;
let indent = 0;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const currentIndent = line.search(/\S/);
// Parse key-value pairs
const colonIndex = trimmed.indexOf(':');
if (colonIndex === -1) continue;
const key = trimmed.substring(0, colonIndex).trim();
const value = trimmed.substring(colonIndex + 1).trim();
if (currentIndent === 0) {
// Top-level keys
if (key === 'name') result.name = value;
else if (key === 'description') result.description = value;
else if (key === 'version') result.version = value;
else if (key === 'steps') {
// Steps array starts
indent = currentIndent;
}
} else if (trimmed.startsWith('- name:')) {
// New step
if (currentStep && currentStep.name) {
result.steps.push(currentStep as YAMLStepDefinition);
}
currentStep = {
name: value,
type: 'agent'
};
} else if (currentStep) {
// Step properties
if (key === 'type') currentStep.type = value as any;
else if (key === 'agent') currentStep.agent = value;
else if (key === 'tool') currentStep.tool = value;
else if (key === 'input') currentStep.input = value;
else if (key === 'output') currentStep.output = value;
else if (key === 'condition') currentStep.condition = value;
else if (key === 'onError') currentStep.onError = value as any;
else if (key === 'maxRetries') currentStep.maxRetries = parseInt(value);
else if (key === 'timeout') currentStep.timeout = parseInt(value);
else if (key === 'loopCondition') currentStep.loopCondition = value;
else if (key === 'maxIterations') currentStep.maxIterations = parseInt(value);
}
}
// Add last step
if (currentStep && currentStep.name) {
result.steps.push(currentStep as YAMLStepDefinition);
}
return result;
}
/**
* Create executable workflow from YAML definition
*/
export function createWorkflowFromYAML(
definition: YAMLWorkflowDefinition,
agents: Record<string, any> = {},
tools: Record<string, any> = {}
): ParsedWorkflow {
const errors: string[] = [];
const workflow = new Workflow(definition.name);
for (const stepDef of definition.steps) {
try {
const stepConfig = createStepConfig(stepDef, agents, tools, errors);
if (stepConfig) {
workflow.addStep(stepConfig);
}
} catch (error: any) {
errors.push(`Error creating step ${stepDef.name}: ${error.message}`);
}
}
return { workflow, definition, errors };
}
function createStepConfig(
stepDef: YAMLStepDefinition,
agents: Record<string, any>,
tools: Record<string, any>,
errors: string[]
): WorkflowStepConfig | null {
const { name, type, agent, tool, onError, maxRetries, timeout, condition } = stepDef;
let execute: any;
switch (type) {
case 'agent':
if (!agent) {
errors.push(`Step ${name}: agent type requires 'agent' field`);
return null;
}
const agentInstance = agents[agent];
if (!agentInstance) {
errors.push(`Step ${name}: agent '${agent}' not found`);
return null;
}
execute = async (input: any) => {
if (typeof agentInstance.chat === 'function') {
return agentInstance.chat(typeof input === 'string' ? input : JSON.stringify(input));
}
return agentInstance(input);
};
break;
case 'tool':
if (!tool) {
errors.push(`Step ${name}: tool type requires 'tool' field`);
return null;
}
const toolInstance = tools[tool];
if (!toolInstance) {
errors.push(`Step ${name}: tool '${tool}' not found`);
return null;
}
execute = async (input: any) => {
if (typeof toolInstance.execute === 'function') {
return toolInstance.execute(input);
}
if (typeof toolInstance.run === 'function') {
return toolInstance.run(input);
}
return toolInstance(input);
};
break;
case 'condition':
execute = async (input: any, context: any) => {
// Evaluate condition and return input or skip
if (condition) {
// Simple condition evaluation
const result = evaluateCondition(condition, input, context);
return result ? input : null;
}
return input;
};
break;
default:
execute = async (input: any) => input;
}
return {
name,
execute,
onError,
maxRetries,
timeout,
condition: condition ? (context) => evaluateCondition(condition, null, context) : undefined
};
}
function evaluateCondition(condition: string, input: any, context: any): boolean {
// Simple condition evaluation
// Supports: "result.success", "input.length > 0", etc.
try {
// Create a safe evaluation context
const evalContext = {
input,
context,
result: context?.get?.('lastResult'),
...context?.metadata
};
// Simple expression evaluation
if (condition.includes('===')) {
const [left, right] = condition.split('===').map(s => s.trim());
return getNestedValue(evalContext, left) === parseValue(right);
}
if (condition.includes('!==')) {
const [left, right] = condition.split('!==').map(s => s.trim());
return getNestedValue(evalContext, left) !== parseValue(right);
}
if (condition.includes('>')) {
const [left, right] = condition.split('>').map(s => s.trim());
return getNestedValue(evalContext, left) > parseValue(right);
}
if (condition.includes('<')) {
const [left, right] = condition.split('<').map(s => s.trim());
return getNestedValue(evalContext, left) < parseValue(right);
}
// Boolean check
return !!getNestedValue(evalContext, condition);
} catch {
return false;
}
}
function getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
function parseValue(value: string): any {
value = value.trim();
if (value === 'true') return true;
if (value === 'false') return false;
if (value === 'null') return null;
if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
const num = Number(value);
if (!isNaN(num)) return num;
return value;
}
/**
* Load workflow from YAML file
*/
export async function loadWorkflowFromFile(
filePath: string,
agents: Record<string, any> = {},
tools: Record<string, any> = {}
): Promise<ParsedWorkflow> {
const fs = await import('fs/promises');
const content = await fs.readFile(filePath, 'utf-8');
const definition = parseYAMLWorkflow(content);
return createWorkflowFromYAML(definition, agents, tools);
}
/**
* Validate YAML workflow definition
*/
export function validateWorkflowDefinition(definition: YAMLWorkflowDefinition): string[] {
const errors: string[] = [];
if (!definition.name) {
errors.push('Workflow must have a name');
}
if (!definition.steps || definition.steps.length === 0) {
errors.push('Workflow must have at least one step');
}
const stepNames = new Set<string>();
for (const step of definition.steps) {
if (!step.name) {
errors.push('Each step must have a name');
} else if (stepNames.has(step.name)) {
errors.push(`Duplicate step name: ${step.name}`);
} else {
stepNames.add(step.name);
}
if (!step.type) {
errors.push(`Step ${step.name}: must have a type`);
}
if (step.type === 'agent' && !step.agent) {
errors.push(`Step ${step.name}: agent type requires 'agent' field`);
}
if (step.type === 'tool' && !step.tool) {
errors.push(`Step ${step.name}: tool type requires 'tool' field`);
}
}
return errors;
}
Loaded 100 of 173 files, more files were not shown because too many files have changed in this diff. Show more