mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a4c2c2c43 | ||
|
|
0b2e6ff69b | ||
|
|
1050d66970 | ||
|
|
bf483f3bd6 | ||
|
|
00ee8eb2f3 | ||
|
|
9a5a432d96 | ||
|
|
8cb5bbb605 | ||
|
|
952514d73c | ||
|
|
6e1dc34864 | ||
|
|
820d5c2995 | ||
|
|
0b1b3d3b3e | ||
|
|
59107addc3 | ||
|
|
b460ded5e0 | ||
|
|
a8967ed200 | ||
|
|
9189f42470 | ||
|
|
c86f660f66 |
No files matched your search
@@ -1994,19 +1994,23 @@ manager.delete_checkpoint("deploy-v1")
|
||||
|
||||
### Tools CLI:
|
||||
```bash
|
||||
# List all available tools
|
||||
praisonai tools list
|
||||
|
||||
# Get info about a specific tool
|
||||
praisonai tools info internet_search
|
||||
|
||||
# Search for tools
|
||||
praisonai tools search "web"
|
||||
|
||||
# Show all commands
|
||||
praisonai tools help
|
||||
praisonai tools doctor
|
||||
praisonai tools resolve shell_tool
|
||||
praisonai tools discover
|
||||
praisonai tools show-sources
|
||||
praisonai tools show-sources --template ai-video-editor
|
||||
```
|
||||
|
||||
| Command | Example | Docs |
|
||||
|---------|---------|------|
|
||||
| `tools list` | [example](examples/tools/) | [docs](https://docs.praison.ai/docs/cli/tools) |
|
||||
| `tools resolve` | [example](examples/tools/example_tools_resolve.py) | [docs](https://docs.praison.ai/docs/cli/tools-resolve) |
|
||||
| `tools discover` | [example](examples/tools/example_tools_discover.py) | [docs](https://docs.praison.ai/docs/cli/tools-discover) |
|
||||
| `tools show-sources` | [example](examples/tools/example_tools_sources.py) | [docs](https://docs.praison.ai/docs/cli/tools-show-sources) |
|
||||
|
||||
### Handoff CLI:
|
||||
```bash
|
||||
# Enable agent-to-agent task delegation
|
||||
@@ -2355,6 +2359,24 @@ agent = Agent(
|
||||
| 🌐 Web Search - Real-time search | [📖](https://docs.praison.ai/docs/cli/web-search) |
|
||||
| 📥 Web Fetch - URL content retrieval | [📖](https://docs.praison.ai/docs/cli/web-fetch) |
|
||||
| 💾 Prompt Caching - Cost reduction | [📖](https://docs.praison.ai/docs/cli/prompt-caching) |
|
||||
| 📦 Template Catalog - Browse & discover templates | [📖](https://docs.praison.ai/docs/cli/template-catalog) |
|
||||
|
||||
### Template Catalog CLI
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `praisonai templates browse` | Open template catalog in browser |
|
||||
| `praisonai templates browse --print` | Print catalog URL only |
|
||||
| `praisonai templates validate` | Validate template YAML files |
|
||||
| `praisonai templates validate --source <dir>` | Validate specific directory |
|
||||
| `praisonai templates validate --strict` | Strict validation mode |
|
||||
| `praisonai templates validate --json` | JSON output format |
|
||||
| `praisonai templates catalog build` | Build catalog locally |
|
||||
| `praisonai templates catalog build --out <dir>` | Build to specific directory |
|
||||
| `praisonai templates catalog sync` | Sync template sources |
|
||||
| `praisonai templates catalog sync --source <name>` | Sync specific source |
|
||||
|
||||
**Examples:** [examples/catalog/](examples/catalog/) | **Docs:** [Code](https://docs.praison.ai/docs/cli/template-catalog-code) | [CLI](https://docs.praison.ai/docs/cli/template-catalog)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.6.2" \
|
||||
"praisonai>=2.7.6" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.6.2" \
|
||||
"praisonai>=2.7.6" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.6.2" \
|
||||
"praisonai>=2.7.6" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Templates CLI
|
||||
|
||||
```bash
|
||||
praisonai templates list
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates search video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates info ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4 --offline
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4 --strict-tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools ./my_tools.py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools-dir ./tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools-source praisonai_tools.video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --no-template-tools-py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates init my-project --template ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates cache clear
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates cache list
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
# Templates Module
|
||||
|
||||
## Loading Templates
|
||||
|
||||
```python
|
||||
from praisonai.templates.loader import TemplateLoader
|
||||
|
||||
loader = TemplateLoader()
|
||||
|
||||
# Load from local path
|
||||
template = loader.load("./my-template")
|
||||
|
||||
# Load from URI
|
||||
template = loader.load("ai-video-editor")
|
||||
|
||||
# Load offline
|
||||
template = loader.load("ai-video-editor", offline=True)
|
||||
|
||||
# Check requirements
|
||||
missing = loader.check_requirements(template)
|
||||
```
|
||||
|
||||
## Template Discovery
|
||||
|
||||
```python
|
||||
from praisonai.templates.discovery import TemplateDiscovery
|
||||
|
||||
discovery = TemplateDiscovery(
|
||||
include_package=True,
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Find template
|
||||
result = discovery.find_template("ai-video-editor")
|
||||
|
||||
# List all templates
|
||||
templates = discovery.list_templates()
|
||||
```
|
||||
|
||||
## Template Config
|
||||
|
||||
```python
|
||||
from praisonai.templates.loader import TemplateConfig
|
||||
|
||||
# Access template properties
|
||||
template.name
|
||||
template.description
|
||||
template.version
|
||||
template.author
|
||||
template.requires
|
||||
template.workflow_file
|
||||
template.agents_file
|
||||
template.path
|
||||
```
|
||||
|
||||
## Tool Override Integration
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import (
|
||||
create_tool_registry_with_overrides,
|
||||
resolve_tools,
|
||||
)
|
||||
|
||||
# Build registry with template sources
|
||||
tools_sources = template.requires.get("tools_sources", [])
|
||||
|
||||
registry = create_tool_registry_with_overrides(
|
||||
tools_sources=tools_sources,
|
||||
template_dir=str(template.path),
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Resolve tool names
|
||||
tools = resolve_tools(["shell_tool"], registry=registry)
|
||||
```
|
||||
|
||||
## TEMPLATE.yaml Schema
|
||||
|
||||
```yaml
|
||||
name: my-template
|
||||
version: "1.0.0"
|
||||
description: Template description
|
||||
|
||||
requires:
|
||||
tools: [shell_tool]
|
||||
packages: [praisonai-tools]
|
||||
env: [OPENAI_API_KEY]
|
||||
tools_sources:
|
||||
- praisonai_tools.video
|
||||
- ./local_tools.py
|
||||
|
||||
workflow: workflow.yaml
|
||||
agents: agents.yaml
|
||||
|
||||
config:
|
||||
input:
|
||||
type: string
|
||||
required: true
|
||||
output:
|
||||
type: string
|
||||
|
||||
defaults:
|
||||
preset: podcast
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# Tools CLI
|
||||
|
||||
```bash
|
||||
praisonai tools list
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools info shell_tool
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools info internet_search
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools search "web"
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools search "file"
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools doctor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools doctor --json
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve shell_tool
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve internet_search --template ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve my_tool --tools ./my_tools.py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve my_tool --tools-dir ./tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover --include praisonai_tools.video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover --entrypoints
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools show-sources
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools show-sources --template ai-video-editor
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Tools Module
|
||||
|
||||
## Tool Registry
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import (
|
||||
create_tool_registry_with_overrides,
|
||||
resolve_tools,
|
||||
)
|
||||
|
||||
# Create registry with defaults
|
||||
registry = create_tool_registry_with_overrides(include_defaults=True)
|
||||
|
||||
# Create registry with overrides
|
||||
registry = create_tool_registry_with_overrides(
|
||||
override_files=["my_tools.py"],
|
||||
override_dirs=["./tools"],
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Create registry with tools_sources
|
||||
registry = create_tool_registry_with_overrides(
|
||||
tools_sources=["praisonai_tools.video"],
|
||||
template_dir="./my-template",
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Resolve tool names to callables
|
||||
tools = resolve_tools(
|
||||
["shell_tool", "internet_search"],
|
||||
registry=registry,
|
||||
)
|
||||
```
|
||||
|
||||
## Tool Override Loader
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import ToolOverrideLoader
|
||||
|
||||
loader = ToolOverrideLoader()
|
||||
|
||||
# Load from file
|
||||
tools = loader.load_from_file("my_tools.py")
|
||||
|
||||
# Load from directory
|
||||
tools = loader.load_from_directory("./tools")
|
||||
|
||||
# Load from module
|
||||
tools = loader.load_from_module("praisonai_tools.video")
|
||||
|
||||
# Get default tool directories
|
||||
dirs = loader.get_default_tool_dirs()
|
||||
```
|
||||
|
||||
## Resolution Order
|
||||
|
||||
1. CLI `--tools` files (highest priority)
|
||||
2. CLI `--tools-dir` directories
|
||||
3. Template `tools_sources` (from TEMPLATE.yaml)
|
||||
4. Template-local `tools.py`
|
||||
5. Default dirs (`~/.praison/tools`, `~/.config/praison/tools`)
|
||||
6. Package discovery (`praisonai_tools`)
|
||||
7. Built-in tools (lowest priority)
|
||||
|
||||
## Creating Custom Tools
|
||||
|
||||
```python
|
||||
from praisonai_tools import tool
|
||||
|
||||
@tool
|
||||
def my_custom_tool(query: str) -> str:
|
||||
"""My custom tool description.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
"""
|
||||
return f"Result for: {query}"
|
||||
```
|
||||
|
||||
## Template tools_sources
|
||||
|
||||
```yaml
|
||||
# TEMPLATE.yaml
|
||||
requires:
|
||||
tools: [shell_tool]
|
||||
tools_sources:
|
||||
- praisonai_tools.video
|
||||
- ./local_tools.py
|
||||
- ./tools_dir/
|
||||
```
|
||||
@@ -14,6 +14,7 @@ examples/
|
||||
│ ├── memory/ # Memory and sessions
|
||||
│ ├── code/ # Code editing and external CLI tools
|
||||
│ └── ...
|
||||
├── serve/ # Server and endpoints examples
|
||||
├── yaml/ # YAML workflow examples
|
||||
└── cookbooks/ # Complete use-case examples
|
||||
```
|
||||
@@ -30,6 +31,23 @@ examples/
|
||||
| **Tools** | [python/tools/](python/tools/) | [📖](https://docs.praison.ai/tools) |
|
||||
| **Code** | [python/code/](python/code/) | [📖](https://docs.praison.ai/code) |
|
||||
| **YAML** | [yaml/](yaml/) | [📖](https://docs.praison.ai/features/yaml-workflows) |
|
||||
| **Doctor** | [doctor/](doctor/) | [📖](https://docs.praison.ai/cli/doctor) |
|
||||
| **Serve** | [serve/](serve/) | [📖](https://docs.praison.ai/cli/serve) |
|
||||
| **Endpoints** | [serve/](serve/) | [📖](https://docs.praison.ai/cli/endpoints) |
|
||||
|
||||
## Serve Examples
|
||||
|
||||
| Example | Description | CLI Command |
|
||||
|---------|-------------|-------------|
|
||||
| [unified_server.py](serve/unified_server.py) | All providers in one server | `praisonai serve unified` |
|
||||
| [agent_as_api_single.py](serve/agent_as_api_single.py) | Single agent HTTP API | `praisonai serve agents` |
|
||||
| [agents_as_api_router.py](serve/agents_as_api_router.py) | Multi-agent router API | `praisonai serve agents` |
|
||||
| [a2a_server_client.py](serve/a2a_server_client.py) | A2A protocol server | `praisonai serve a2a` |
|
||||
| [a2u_events_stream.py](serve/a2u_events_stream.py) | A2U event stream | `praisonai serve a2u` |
|
||||
| [mcp_http_server.py](serve/mcp_http_server.py) | MCP HTTP server | `praisonai serve mcp` |
|
||||
| [tools_as_mcp_server.py](serve/tools_as_mcp_server.py) | Tools as MCP server | `praisonai serve tools` |
|
||||
| [agent_launch_modes.py](serve/agent_launch_modes.py) | Agent.launch() API | Python only |
|
||||
| [endpoints_unified_client.py](serve/endpoints_unified_client.py) | Unified client | `praisonai endpoints` |
|
||||
|
||||
## Running Examples
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Assistants Capabilities Example
|
||||
|
||||
Demonstrates OpenAI Assistants API using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import assistant_list
|
||||
|
||||
print("=== List Assistants ===")
|
||||
try:
|
||||
assistants = assistant_list()
|
||||
print(f"Assistants found: {len(assistants)}")
|
||||
for a in assistants[:5]: # Show first 5
|
||||
print(f" - {a.get('name', 'unnamed')}: {a.get('id', 'unknown')}")
|
||||
except Exception as e:
|
||||
print(f"Note: {e}")
|
||||
|
||||
print("\n=== Assistant Operations ===")
|
||||
print("Available functions:")
|
||||
print(" assistant_create(name, instructions, model='gpt-4o-mini')")
|
||||
print(" assistant_list()")
|
||||
|
||||
print("\nSee CLI: praisonai assistants list")
|
||||
print("See CLI: praisonai assistants create --name 'My Assistant'")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Audio Capabilities Example
|
||||
|
||||
Demonstrates transcription and text-to-speech using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import transcribe, speech
|
||||
|
||||
# Note: Audio transcription requires an audio file
|
||||
# This example shows the API structure
|
||||
|
||||
print("=== Audio Transcription ===")
|
||||
print("To transcribe audio, use:")
|
||||
print(" result = transcribe('audio.mp3', model='whisper-1')")
|
||||
print(" print(result.text)")
|
||||
|
||||
print("\n=== Text-to-Speech ===")
|
||||
print("To generate speech, use:")
|
||||
print(" audio_data = speech('Hello world', model='tts-1', voice='alloy')")
|
||||
print(" # Save to file or play")
|
||||
|
||||
print("\nNote: These operations require audio files or generate audio output.")
|
||||
print("See CLI: praisonai audio transcribe <file>")
|
||||
print("See CLI: praisonai audio speech <text>")
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Batches Capabilities Example
|
||||
|
||||
Demonstrates batch processing using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import batch_list
|
||||
|
||||
print("=== Batch List ===")
|
||||
try:
|
||||
batches = batch_list()
|
||||
print(f"Batches found: {len(batches)}")
|
||||
for b in batches[:5]: # Show first 5
|
||||
print(f" - {b.get('id', 'unknown')}: {b.get('status', 'unknown')}")
|
||||
except Exception as e:
|
||||
print(f"Note: {e}")
|
||||
print("Batch operations require batch jobs to be created first.")
|
||||
|
||||
print("\n=== Batch Operations ===")
|
||||
print("Available functions:")
|
||||
print(" batch_create(input_file_id, endpoint, completion_window='24h')")
|
||||
print(" batch_list()")
|
||||
print(" batch_retrieve(batch_id)")
|
||||
print(" batch_cancel(batch_id)")
|
||||
|
||||
print("\nSee CLI: praisonai batches list")
|
||||
print("See CLI: praisonai batches create <input_file_id>")
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Completions Capability Example
|
||||
|
||||
Demonstrates chat and text completions using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import chat_completion, text_completion
|
||||
|
||||
# Chat completion example
|
||||
print("=== Chat Completion ===")
|
||||
result = chat_completion(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "What is 2 + 2? Answer in one word."}
|
||||
],
|
||||
model="gpt-4o-mini",
|
||||
max_tokens=10
|
||||
)
|
||||
print(f"Response: {result.content}")
|
||||
print(f"Model: {result.model}")
|
||||
print(f"Usage: {result.usage}")
|
||||
|
||||
# Text completion example (legacy)
|
||||
print("\n=== Text Completion ===")
|
||||
result = text_completion(
|
||||
prompt="The capital of France is",
|
||||
model="gpt-3.5-turbo-instruct",
|
||||
max_tokens=10
|
||||
)
|
||||
print(f"Response: {result.content}")
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Embeddings Capability Example
|
||||
|
||||
Demonstrates text embedding generation using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import embed
|
||||
|
||||
# Single text embedding
|
||||
print("=== Single Text Embedding ===")
|
||||
result = embed(
|
||||
input="Hello, world!",
|
||||
model="text-embedding-3-small"
|
||||
)
|
||||
print(f"Embedding dimensions: {len(result.embeddings[0])}")
|
||||
print(f"First 5 values: {result.embeddings[0][:5]}")
|
||||
print(f"Usage: {result.usage}")
|
||||
|
||||
# Multiple text embeddings
|
||||
print("\n=== Multiple Text Embeddings ===")
|
||||
result = embed(
|
||||
input=["Hello", "World", "AI"],
|
||||
model="text-embedding-3-small"
|
||||
)
|
||||
print(f"Number of embeddings: {len(result.embeddings)}")
|
||||
for i, emb in enumerate(result.embeddings):
|
||||
print(f" Text {i+1}: {len(emb)} dimensions")
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Files Capabilities Example
|
||||
|
||||
Demonstrates file management using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import file_list
|
||||
|
||||
print("=== File List ===")
|
||||
try:
|
||||
files = file_list()
|
||||
print(f"Files found: {len(files)}")
|
||||
for f in files[:5]: # Show first 5
|
||||
print(f" - {f.get('filename', 'unknown')}: {f.get('id', 'unknown')}")
|
||||
except Exception as e:
|
||||
print(f"Note: {e}")
|
||||
print("File operations require files to be uploaded first.")
|
||||
|
||||
print("\n=== File Operations ===")
|
||||
print("Available functions:")
|
||||
print(" file_create(file_path, purpose='assistants')")
|
||||
print(" file_list(purpose=None)")
|
||||
print(" file_retrieve(file_id)")
|
||||
print(" file_delete(file_id)")
|
||||
print(" file_content(file_id)")
|
||||
|
||||
print("\nSee CLI: praisonai files list")
|
||||
print("See CLI: praisonai files upload <file>")
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Images Capability Example
|
||||
|
||||
Demonstrates image generation using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import image_generate
|
||||
|
||||
# Generate an image with DALL-E 2
|
||||
print("=== Image Generation (DALL-E 2) ===")
|
||||
result = image_generate(
|
||||
prompt="A simple blue circle on a white background",
|
||||
model="dall-e-2",
|
||||
size="256x256",
|
||||
n=1
|
||||
)
|
||||
print(f"Images generated: {len(result)}")
|
||||
if result:
|
||||
print(f"URL: {result[0].url[:80]}...")
|
||||
|
||||
# Generate with DALL-E 3 (higher quality)
|
||||
print("\n=== Image Generation (DALL-E 3) ===")
|
||||
result = image_generate(
|
||||
prompt="A serene mountain landscape at sunset",
|
||||
model="dall-e-3",
|
||||
size="1024x1024",
|
||||
quality="standard"
|
||||
)
|
||||
print(f"Images generated: {len(result)}")
|
||||
if result:
|
||||
print(f"Revised prompt: {result[0].revised_prompt[:80]}..." if result[0].revised_prompt else "No revised prompt")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Messages Capability Example
|
||||
|
||||
Demonstrates Anthropic-style messages API and token counting.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import messages_create, count_tokens
|
||||
|
||||
# Create a message (Anthropic-style)
|
||||
print("=== Messages Create ===")
|
||||
result = messages_create(
|
||||
messages=[{"role": "user", "content": "What is the meaning of life? Answer briefly."}],
|
||||
model="gpt-4o-mini", # Works with any model via LiteLLM
|
||||
max_tokens=100,
|
||||
system="You are a philosophical assistant."
|
||||
)
|
||||
print(f"Message ID: {result.id}")
|
||||
if result.content:
|
||||
for block in result.content:
|
||||
if block.get("type") == "text":
|
||||
print(f"Response: {block.get('text')}")
|
||||
print(f"Usage: {result.usage}")
|
||||
|
||||
# Count tokens
|
||||
print("\n=== Token Counting ===")
|
||||
result = count_tokens(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello, how are you today?"}
|
||||
],
|
||||
model="gpt-4o-mini"
|
||||
)
|
||||
print(f"Token count: {result.input_tokens}")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Moderations Capability Example
|
||||
|
||||
Demonstrates content moderation using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import moderate
|
||||
|
||||
# Check safe content
|
||||
print("=== Safe Content Check ===")
|
||||
result = moderate(
|
||||
input="Hello, how are you today?"
|
||||
)
|
||||
print(f"Flagged: {result[0].flagged}")
|
||||
print(f"Categories: {list(result[0].categories.keys())[:5]}...")
|
||||
|
||||
# Check multiple texts
|
||||
print("\n=== Multiple Text Moderation ===")
|
||||
result = moderate(
|
||||
input=["Hello world", "Have a nice day", "This is a test"]
|
||||
)
|
||||
print(f"Number of results: {len(result)}")
|
||||
for i, r in enumerate(result):
|
||||
print(f" Text {i+1}: Flagged = {r.flagged}")
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Realtime Capability Example
|
||||
|
||||
Demonstrates realtime session creation for audio/video streaming.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import realtime_connect
|
||||
|
||||
# Create a realtime session
|
||||
print("=== Realtime Session ===")
|
||||
session = realtime_connect(
|
||||
model="gpt-4o-realtime-preview",
|
||||
modalities=["text", "audio"],
|
||||
voice="alloy"
|
||||
)
|
||||
print(f"Session ID: {session.id}")
|
||||
print(f"Status: {session.status}")
|
||||
print(f"URL: {session.url}")
|
||||
print(f"Modalities: {session.metadata.get('modalities')}")
|
||||
print(f"Voice: {session.metadata.get('voice')}")
|
||||
|
||||
print("\nNote: To use this session, connect via WebSocket to the URL above.")
|
||||
print("See OpenAI Realtime API documentation for WebSocket protocol details.")
|
||||
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Rerank Capabilities Example
|
||||
|
||||
Demonstrates document reranking using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import rerank
|
||||
|
||||
print("=== Document Reranking ===")
|
||||
try:
|
||||
documents = [
|
||||
"Python is a programming language",
|
||||
"Machine learning uses algorithms",
|
||||
"Python is great for data science",
|
||||
"JavaScript runs in browsers"
|
||||
]
|
||||
|
||||
results = rerank(
|
||||
query="Python programming",
|
||||
documents=documents,
|
||||
model="cohere/rerank-english-v3.0"
|
||||
)
|
||||
|
||||
print(f"Query: 'Python programming'")
|
||||
print(f"Reranked results:")
|
||||
for r in results:
|
||||
print(f" Score {r.get('relevance_score', 0):.4f}: {r.get('document', {}).get('text', '')[:50]}")
|
||||
except Exception as e:
|
||||
print(f"Note: Reranking requires Cohere API key")
|
||||
print(f"Error: {e}")
|
||||
|
||||
print("\nSee CLI: praisonai rerank <query> --documents 'doc1' 'doc2'")
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Vector Stores Capabilities Example
|
||||
|
||||
Demonstrates vector store management using PraisonAI capabilities.
|
||||
"""
|
||||
|
||||
from praisonai.capabilities import vector_store_create, vector_store_search
|
||||
|
||||
print("=== Vector Store Operations ===")
|
||||
print("Available functions:")
|
||||
print(" vector_store_create(name)")
|
||||
print(" vector_store_search(vector_store_id, query)")
|
||||
|
||||
print("\n=== Create Vector Store ===")
|
||||
try:
|
||||
store = vector_store_create(name="test-store")
|
||||
print(f"Created: {store}")
|
||||
except Exception as e:
|
||||
print(f"Note: {e}")
|
||||
|
||||
print("\nSee CLI: praisonai vector-stores create --name 'my-store'")
|
||||
print("See CLI: praisonai vector-stores search <store_id> <query>")
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Fetch and search PraisonAI templates from the catalog.
|
||||
|
||||
This example demonstrates how to:
|
||||
- Fetch templates.json from the deployed catalog
|
||||
- Search templates by keyword
|
||||
- Filter by tags and requirements
|
||||
- Generate CLI commands
|
||||
"""
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
CATALOG_URL = "https://mervinpraison.github.io/praisonai-template-catalog/data/templates.json"
|
||||
|
||||
|
||||
def fetch_catalog(url: str = CATALOG_URL) -> Dict[str, Any]:
|
||||
"""Fetch the template catalog from URL."""
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=10) as response:
|
||||
return json.loads(response.read().decode())
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch catalog: {e}")
|
||||
# Return empty catalog on failure
|
||||
return {"version": "0.0.0", "count": 0, "templates": []}
|
||||
|
||||
|
||||
def search_templates(templates: List[Dict], query: str) -> List[Dict]:
|
||||
"""Search templates by name, description, or tags."""
|
||||
query = query.lower()
|
||||
results = []
|
||||
for t in templates:
|
||||
if (query in t.get("name", "").lower() or
|
||||
query in t.get("description", "").lower() or
|
||||
any(query in tag.lower() for tag in t.get("tags", []))):
|
||||
results.append(t)
|
||||
return results
|
||||
|
||||
|
||||
def filter_by_tags(templates: List[Dict], tags: List[str]) -> List[Dict]:
|
||||
"""Filter templates that have all specified tags."""
|
||||
return [
|
||||
t for t in templates
|
||||
if all(tag in t.get("tags", []) for tag in tags)
|
||||
]
|
||||
|
||||
|
||||
def filter_by_tool(templates: List[Dict], tool_name: str) -> List[Dict]:
|
||||
"""Filter templates that require a specific tool."""
|
||||
return [
|
||||
t for t in templates
|
||||
if tool_name in t.get("requires", {}).get("tools", [])
|
||||
]
|
||||
|
||||
|
||||
def get_template(templates: List[Dict], name: str) -> Optional[Dict]:
|
||||
"""Get a template by name."""
|
||||
for t in templates:
|
||||
if t.get("name") == name:
|
||||
return t
|
||||
return None
|
||||
|
||||
|
||||
def generate_cli_command(template: Dict, action: str = "run") -> str:
|
||||
"""Generate CLI command for a template."""
|
||||
name = template.get("name", "unknown")
|
||||
if action == "run":
|
||||
return f"praisonai templates run {name}"
|
||||
elif action == "info":
|
||||
return f"praisonai templates info {name}"
|
||||
elif action == "init":
|
||||
return f"praisonai templates init my-project --template {name}"
|
||||
return f"praisonai templates {action} {name}"
|
||||
|
||||
|
||||
def print_template_summary(template: Dict):
|
||||
"""Print a summary of a template."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Name: {template.get('name')}")
|
||||
print(f"Version: {template.get('version')}")
|
||||
print(f"Description: {template.get('description', '')[:100]}...")
|
||||
print(f"Tags: {', '.join(template.get('tags', []))}")
|
||||
|
||||
requires = template.get("requires", {})
|
||||
if requires.get("tools"):
|
||||
print(f"Tools: {', '.join(requires['tools'])}")
|
||||
if requires.get("packages"):
|
||||
print(f"Packages: {', '.join(requires['packages'])}")
|
||||
if requires.get("env"):
|
||||
print(f"Env vars: {', '.join(requires['env'])}")
|
||||
|
||||
print(f"\nRun: {generate_cli_command(template, 'run')}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
def main():
|
||||
print("PraisonAI Template Catalog - Python Example")
|
||||
print("-" * 45)
|
||||
|
||||
# Fetch catalog
|
||||
print("\n1. Fetching template catalog...")
|
||||
catalog = fetch_catalog()
|
||||
templates = catalog.get("templates", [])
|
||||
|
||||
print(f" Catalog version: {catalog.get('version', 'unknown')}")
|
||||
print(f" Total templates: {len(templates)}")
|
||||
|
||||
if not templates:
|
||||
print(" No templates found. The catalog may not be deployed yet.")
|
||||
print(" Try building locally: praisonai templates catalog build")
|
||||
return
|
||||
|
||||
# List all templates
|
||||
print("\n2. Available templates:")
|
||||
for t in templates:
|
||||
print(f" - {t.get('name')} (v{t.get('version')})")
|
||||
|
||||
# Search for video templates
|
||||
print("\n3. Searching for 'video' templates...")
|
||||
video_templates = search_templates(templates, "video")
|
||||
print(f" Found {len(video_templates)} video templates:")
|
||||
for t in video_templates:
|
||||
print(f" - {t.get('name')}: {t.get('description', '')[:50]}...")
|
||||
|
||||
# Filter by tool
|
||||
print("\n4. Templates using 'shell_tool':")
|
||||
shell_templates = filter_by_tool(templates, "shell_tool")
|
||||
for t in shell_templates:
|
||||
print(f" - {t.get('name')}")
|
||||
|
||||
# Get specific template details
|
||||
if templates:
|
||||
first_template = templates[0]
|
||||
print(f"\n5. Template details for '{first_template.get('name')}':")
|
||||
print_template_summary(first_template)
|
||||
|
||||
# Generate CLI commands
|
||||
print("\n6. CLI commands for all templates:")
|
||||
for t in templates[:5]: # First 5 only
|
||||
print(f" {generate_cli_command(t, 'run')}")
|
||||
|
||||
print("\nDone!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env npx ts-node
|
||||
/**
|
||||
* Example: Fetch and search PraisonAI templates from the catalog.
|
||||
*
|
||||
* This TypeScript example demonstrates how to:
|
||||
* - Fetch templates.json from the deployed catalog
|
||||
* - Search templates by keyword
|
||||
* - Filter by tags and requirements
|
||||
* - Generate CLI commands
|
||||
*/
|
||||
|
||||
const CATALOG_URL = "https://mervinpraison.github.io/praisonai-template-catalog/data/templates.json";
|
||||
|
||||
interface Template {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
author?: string;
|
||||
license?: string;
|
||||
tags?: string[];
|
||||
category?: string;
|
||||
difficulty?: "beginner" | "intermediate" | "advanced";
|
||||
requires?: {
|
||||
tools?: string[];
|
||||
packages?: string[];
|
||||
env?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Catalog {
|
||||
version: string;
|
||||
generated_at: string;
|
||||
count: number;
|
||||
templates: Template[];
|
||||
}
|
||||
|
||||
async function fetchCatalog(url: string = CATALOG_URL): Promise<Catalog> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch catalog: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function searchTemplates(templates: Template[], query: string): Template[] {
|
||||
const q = query.toLowerCase();
|
||||
return templates.filter(t =>
|
||||
t.name.toLowerCase().includes(q) ||
|
||||
t.description?.toLowerCase().includes(q) ||
|
||||
t.tags?.some(tag => tag.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
|
||||
function filterByTags(templates: Template[], tags: string[]): Template[] {
|
||||
return templates.filter(t =>
|
||||
tags.every(tag => t.tags?.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
function filterByTool(templates: Template[], toolName: string): Template[] {
|
||||
return templates.filter(t =>
|
||||
t.requires?.tools?.includes(toolName)
|
||||
);
|
||||
}
|
||||
|
||||
function getTemplate(templates: Template[], name: string): Template | undefined {
|
||||
return templates.find(t => t.name === name);
|
||||
}
|
||||
|
||||
function generateCLICommand(template: Template, action: string = "run"): string {
|
||||
const name = template.name;
|
||||
switch (action) {
|
||||
case "run":
|
||||
return `praisonai templates run ${name}`;
|
||||
case "info":
|
||||
return `praisonai templates info ${name}`;
|
||||
case "init":
|
||||
return `praisonai templates init my-project --template ${name}`;
|
||||
default:
|
||||
return `praisonai templates ${action} ${name}`;
|
||||
}
|
||||
}
|
||||
|
||||
function printTemplateSummary(template: Template): void {
|
||||
console.log("\n" + "=".repeat(60));
|
||||
console.log(`Name: ${template.name}`);
|
||||
console.log(`Version: ${template.version}`);
|
||||
console.log(`Description: ${template.description?.slice(0, 100)}...`);
|
||||
console.log(`Tags: ${template.tags?.join(", ") || "none"}`);
|
||||
|
||||
const requires = template.requires;
|
||||
if (requires?.tools?.length) {
|
||||
console.log(`Tools: ${requires.tools.join(", ")}`);
|
||||
}
|
||||
if (requires?.packages?.length) {
|
||||
console.log(`Packages: ${requires.packages.join(", ")}`);
|
||||
}
|
||||
if (requires?.env?.length) {
|
||||
console.log(`Env vars: ${requires.env.join(", ")}`);
|
||||
}
|
||||
|
||||
console.log(`\nRun: ${generateCLICommand(template, "run")}`);
|
||||
console.log("=".repeat(60));
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("PraisonAI Template Catalog - TypeScript Example");
|
||||
console.log("-".repeat(45));
|
||||
|
||||
// Fetch catalog
|
||||
console.log("\n1. Fetching template catalog...");
|
||||
let catalog: Catalog;
|
||||
try {
|
||||
catalog = await fetchCatalog();
|
||||
} catch (error) {
|
||||
console.log(` Failed to fetch: ${error}`);
|
||||
console.log(" The catalog may not be deployed yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
const templates = catalog.templates;
|
||||
console.log(` Catalog version: ${catalog.version}`);
|
||||
console.log(` Total templates: ${templates.length}`);
|
||||
|
||||
if (templates.length === 0) {
|
||||
console.log(" No templates found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// List all templates
|
||||
console.log("\n2. Available templates:");
|
||||
templates.forEach(t => {
|
||||
console.log(` - ${t.name} (v${t.version})`);
|
||||
});
|
||||
|
||||
// Search for video templates
|
||||
console.log("\n3. Searching for 'video' templates...");
|
||||
const videoTemplates = searchTemplates(templates, "video");
|
||||
console.log(` Found ${videoTemplates.length} video templates:`);
|
||||
videoTemplates.forEach(t => {
|
||||
console.log(` - ${t.name}: ${t.description?.slice(0, 50)}...`);
|
||||
});
|
||||
|
||||
// Filter by tool
|
||||
console.log("\n4. Templates using 'shell_tool':");
|
||||
const shellTemplates = filterByTool(templates, "shell_tool");
|
||||
shellTemplates.forEach(t => {
|
||||
console.log(` - ${t.name}`);
|
||||
});
|
||||
|
||||
// Get specific template details
|
||||
if (templates.length > 0) {
|
||||
const firstTemplate = templates[0];
|
||||
console.log(`\n5. Template details for '${firstTemplate.name}':`);
|
||||
printTemplateSummary(firstTemplate);
|
||||
}
|
||||
|
||||
// Generate CLI commands
|
||||
console.log("\n6. CLI commands for all templates:");
|
||||
templates.slice(0, 5).forEach(t => {
|
||||
console.log(` ${generateCLICommand(t, "run")}`);
|
||||
});
|
||||
|
||||
console.log("\nDone!");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run all catalog examples and report pass/fail status.
|
||||
|
||||
Usage:
|
||||
python run_all_catalog_examples.py
|
||||
|
||||
Environment variables:
|
||||
OPENAI_API_KEY or PRAISONAI_OPENAI_API_KEY - Required for agent example
|
||||
PRAISONAI_RUN_INTEGRATION - Set to "1" to run integration tests
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def run_example(name: str, script: str, requires_api_key: bool = False) -> bool:
|
||||
"""Run an example script and return success status."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Running: {name}")
|
||||
print(f"Script: {script}")
|
||||
print("="*60)
|
||||
|
||||
# Check API key requirement
|
||||
if requires_api_key:
|
||||
api_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("PRAISONAI_OPENAI_API_KEY")
|
||||
run_integration = os.environ.get("PRAISONAI_RUN_INTEGRATION") == "1"
|
||||
|
||||
if not api_key:
|
||||
print(f"⏭️ SKIPPED: {name} (no API key)")
|
||||
print(" Set OPENAI_API_KEY to run this example")
|
||||
return True # Not a failure, just skipped
|
||||
|
||||
if not run_integration:
|
||||
print(f"⏭️ SKIPPED: {name} (integration tests disabled)")
|
||||
print(" Set PRAISONAI_RUN_INTEGRATION=1 to run")
|
||||
return True
|
||||
|
||||
script_path = SCRIPT_DIR / script
|
||||
if not script_path.exists():
|
||||
print(f"❌ FAILED: Script not found: {script_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script_path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
cwd=str(SCRIPT_DIR)
|
||||
)
|
||||
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✅ PASSED: {name}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ FAILED: {name} (exit code {result.returncode})")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"❌ FAILED: {name} (timeout)")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ FAILED: {name} ({e})")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
print("PraisonAI Template Catalog - Example Runner")
|
||||
print("=" * 50)
|
||||
|
||||
# Check environment
|
||||
api_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("PRAISONAI_OPENAI_API_KEY")
|
||||
if api_key:
|
||||
print(f"API key: present (...{api_key[-4:]})")
|
||||
else:
|
||||
print("API key: not set")
|
||||
|
||||
run_integration = os.environ.get("PRAISONAI_RUN_INTEGRATION") == "1"
|
||||
print(f"Integration tests: {'enabled' if run_integration else 'disabled'}")
|
||||
|
||||
# Define examples
|
||||
examples = [
|
||||
("Fetch Templates (Python)", "fetch_templates.py", False),
|
||||
("Template Finder Agent", "template_finder_agent.py", True),
|
||||
]
|
||||
|
||||
# Run examples
|
||||
results = []
|
||||
for name, script, requires_key in examples:
|
||||
success = run_example(name, script, requires_key)
|
||||
results.append((name, success))
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
passed = sum(1 for _, s in results if s)
|
||||
failed = sum(1 for _, s in results if not s)
|
||||
|
||||
for name, success in results:
|
||||
status = "✅ PASS" if success else "❌ FAIL"
|
||||
print(f" {status}: {name}")
|
||||
|
||||
print(f"\nTotal: {passed} passed, {failed} failed")
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if failed == 0 else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Template Finder Agent
|
||||
|
||||
An Agent()-centric example that uses an AI agent to find templates
|
||||
based on natural language queries.
|
||||
|
||||
Requires: OPENAI_API_KEY environment variable
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
CATALOG_URL = "https://mervinpraison.github.io/praisonai-template-catalog/data/templates.json"
|
||||
|
||||
|
||||
def fetch_templates() -> list:
|
||||
"""Fetch templates from catalog."""
|
||||
try:
|
||||
with urllib.request.urlopen(CATALOG_URL, timeout=10) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
return data.get("templates", [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def search_templates_tool(query: str) -> str:
|
||||
"""
|
||||
Search templates by keyword.
|
||||
|
||||
Args:
|
||||
query: Search query (e.g., "video editing", "transcript", "data")
|
||||
|
||||
Returns:
|
||||
JSON string with matching templates
|
||||
"""
|
||||
templates = fetch_templates()
|
||||
query_lower = query.lower()
|
||||
|
||||
results = []
|
||||
for t in templates:
|
||||
if (query_lower in t.get("name", "").lower() or
|
||||
query_lower in t.get("description", "").lower() or
|
||||
any(query_lower in tag.lower() for tag in t.get("tags", []))):
|
||||
results.append({
|
||||
"name": t.get("name"),
|
||||
"description": t.get("description", "")[:100],
|
||||
"tags": t.get("tags", []),
|
||||
"cli_command": f"praisonai templates run {t.get('name')}"
|
||||
})
|
||||
|
||||
if not results:
|
||||
return json.dumps({"message": "No templates found matching your query", "templates": []})
|
||||
|
||||
return json.dumps({"count": len(results), "templates": results})
|
||||
|
||||
|
||||
def list_all_templates_tool() -> str:
|
||||
"""
|
||||
List all available templates.
|
||||
|
||||
Returns:
|
||||
JSON string with all template names and descriptions
|
||||
"""
|
||||
templates = fetch_templates()
|
||||
|
||||
if not templates:
|
||||
return json.dumps({"message": "No templates available", "templates": []})
|
||||
|
||||
results = [
|
||||
{"name": t.get("name"), "description": t.get("description", "")[:80]}
|
||||
for t in templates
|
||||
]
|
||||
|
||||
return json.dumps({"count": len(results), "templates": results})
|
||||
|
||||
|
||||
def get_template_details_tool(name: str) -> str:
|
||||
"""
|
||||
Get detailed information about a specific template.
|
||||
|
||||
Args:
|
||||
name: Template name (e.g., "ai-video-editor")
|
||||
|
||||
Returns:
|
||||
JSON string with template details
|
||||
"""
|
||||
templates = fetch_templates()
|
||||
|
||||
for t in templates:
|
||||
if t.get("name") == name:
|
||||
return json.dumps({
|
||||
"name": t.get("name"),
|
||||
"version": t.get("version"),
|
||||
"description": t.get("description"),
|
||||
"author": t.get("author"),
|
||||
"tags": t.get("tags", []),
|
||||
"requires": t.get("requires", {}),
|
||||
"cli_commands": {
|
||||
"run": f"praisonai templates run {name}",
|
||||
"info": f"praisonai templates info {name}",
|
||||
"init": f"praisonai templates init my-project --template {name}"
|
||||
}
|
||||
})
|
||||
|
||||
return json.dumps({"error": f"Template '{name}' not found"})
|
||||
|
||||
|
||||
def check_api_key() -> bool:
|
||||
"""Check if API key is available."""
|
||||
key = os.environ.get("OPENAI_API_KEY") or os.environ.get("PRAISONAI_OPENAI_API_KEY")
|
||||
if key:
|
||||
print(f"API key present: ...{key[-4:]}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
print("PraisonAI Template Finder Agent")
|
||||
print("=" * 40)
|
||||
|
||||
# Check for API key
|
||||
if not check_api_key():
|
||||
print("\n⚠️ No API key found.")
|
||||
print("Set OPENAI_API_KEY or PRAISONAI_OPENAI_API_KEY to use the agent.")
|
||||
print("\nFalling back to direct tool usage:\n")
|
||||
|
||||
# Demo the tools directly
|
||||
print("1. Listing all templates:")
|
||||
result = list_all_templates_tool()
|
||||
data = json.loads(result)
|
||||
for t in data.get("templates", [])[:5]:
|
||||
print(f" - {t['name']}: {t['description'][:50]}...")
|
||||
|
||||
print("\n2. Searching for 'video' templates:")
|
||||
result = search_templates_tool("video")
|
||||
data = json.loads(result)
|
||||
for t in data.get("templates", []):
|
||||
print(f" - {t['name']}")
|
||||
print(f" Run: {t['cli_command']}")
|
||||
|
||||
return
|
||||
|
||||
# Import Agent only if API key is available
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
except ImportError:
|
||||
print("praisonaiagents not installed. Install with: pip install praisonaiagents")
|
||||
return
|
||||
|
||||
# Create the Template Finder Agent
|
||||
agent = Agent(
|
||||
name="Template Finder",
|
||||
role="PraisonAI Template Expert",
|
||||
goal="Help users find the right template for their needs",
|
||||
backstory="""You are an expert on PraisonAI templates. You help users
|
||||
discover templates that match their requirements. You can search templates,
|
||||
list all available templates, and provide detailed information about specific
|
||||
templates. Always provide the CLI command to run the template.""",
|
||||
tools=[
|
||||
search_templates_tool,
|
||||
list_all_templates_tool,
|
||||
get_template_details_tool
|
||||
],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Example queries
|
||||
queries = [
|
||||
"Find me a template for editing videos",
|
||||
"What templates are available for working with transcripts?",
|
||||
"Show me details about the ai-video-editor template"
|
||||
]
|
||||
|
||||
print("\nRunning Template Finder Agent...")
|
||||
print("-" * 40)
|
||||
|
||||
for query in queries:
|
||||
print(f"\n📝 Query: {query}")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
result = agent.start(query)
|
||||
print(f"\n🤖 Response:\n{result}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
print("\n" + "=" * 40)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,65 @@
|
||||
# PraisonAI Doctor Examples
|
||||
|
||||
Examples demonstrating the PraisonAI Doctor health check and diagnostics system.
|
||||
|
||||
## CLI Examples
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `praisonai doctor` | Run all fast health checks |
|
||||
| `praisonai doctor --version` | Show doctor version |
|
||||
| `praisonai doctor --list-checks` | List all available checks |
|
||||
| `praisonai doctor env` | Check environment configuration |
|
||||
| `praisonai doctor config` | Validate configuration files |
|
||||
| `praisonai doctor tools` | Check tool availability |
|
||||
| `praisonai doctor db` | Check database drivers |
|
||||
| `praisonai doctor mcp` | Check MCP configuration |
|
||||
| `praisonai doctor obs` | Check observability providers |
|
||||
| `praisonai doctor skills` | Check agent skills |
|
||||
| `praisonai doctor memory` | Check memory storage |
|
||||
| `praisonai doctor permissions` | Check filesystem permissions |
|
||||
| `praisonai doctor network` | Check network connectivity |
|
||||
| `praisonai doctor performance` | Check import times |
|
||||
| `praisonai doctor ci` | CI mode with JSON output |
|
||||
| `praisonai doctor selftest` | Test agent functionality |
|
||||
|
||||
## Global Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--json` | Output in JSON format |
|
||||
| `--format text\|json` | Output format |
|
||||
| `--output PATH` | Write report to file |
|
||||
| `--deep` | Enable deeper probes |
|
||||
| `--timeout SEC` | Per-check timeout |
|
||||
| `--strict` | Treat warnings as failures |
|
||||
| `--quiet` | Minimal output |
|
||||
| `--no-color` | Disable colors |
|
||||
| `--only IDS` | Only run these checks |
|
||||
| `--skip IDS` | Skip these checks |
|
||||
|
||||
## Python Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [basic_doctor.py](basic_doctor.py) | Programmatic health checks |
|
||||
| [ci_integration.py](ci_integration.py) | CI/CD pipeline integration |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run basic health checks
|
||||
praisonai doctor
|
||||
|
||||
# Run with JSON output
|
||||
praisonai doctor --json
|
||||
|
||||
# Run specific checks
|
||||
praisonai doctor --only python_version,openai_api_key
|
||||
|
||||
# CI mode
|
||||
praisonai doctor ci
|
||||
|
||||
# Save report to file
|
||||
praisonai doctor --output report.json
|
||||
```
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Basic Doctor Example - Programmatic health checks for PraisonAI
|
||||
|
||||
This example demonstrates how to use the Doctor module programmatically
|
||||
to run health checks and diagnostics.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Add the praisonai path for development
|
||||
sys.path.insert(0, '/Users/praison/praisonai-package/src/praisonai')
|
||||
|
||||
from praisonai.cli.features.doctor import DoctorEngine
|
||||
from praisonai.cli.features.doctor.models import DoctorConfig, CheckCategory
|
||||
from praisonai.cli.features.doctor.registry import get_registry
|
||||
from praisonai.cli.features.doctor.checks import register_all_checks
|
||||
from praisonai.cli.features.doctor.formatters import get_formatter
|
||||
|
||||
|
||||
def run_basic_checks():
|
||||
"""Run basic health checks."""
|
||||
print("=" * 60)
|
||||
print("Running Basic Health Checks")
|
||||
print("=" * 60)
|
||||
|
||||
# Register all checks
|
||||
register_all_checks()
|
||||
|
||||
# Create config for fast checks only
|
||||
config = DoctorConfig(
|
||||
deep=False,
|
||||
timeout=10.0,
|
||||
strict=False,
|
||||
quiet=False,
|
||||
)
|
||||
|
||||
# Create and run engine
|
||||
engine = DoctorEngine(config)
|
||||
report = engine.run()
|
||||
|
||||
# Format and print results
|
||||
formatter = get_formatter("text", no_color=True)
|
||||
print(formatter.format_report(report))
|
||||
|
||||
return report.exit_code
|
||||
|
||||
|
||||
def run_environment_checks():
|
||||
"""Run only environment checks."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Running Environment Checks Only")
|
||||
print("=" * 60)
|
||||
|
||||
# Register all checks
|
||||
register_all_checks()
|
||||
|
||||
config = DoctorConfig(deep=False)
|
||||
engine = DoctorEngine(config)
|
||||
|
||||
# Run only environment category
|
||||
engine.run_checks(categories=[CheckCategory.ENVIRONMENT])
|
||||
report = engine.generate_report()
|
||||
|
||||
formatter = get_formatter("text", no_color=True)
|
||||
print(formatter.format_report(report))
|
||||
|
||||
return report.exit_code
|
||||
|
||||
|
||||
def run_specific_checks():
|
||||
"""Run specific checks by ID."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Running Specific Checks")
|
||||
print("=" * 60)
|
||||
|
||||
# Register all checks
|
||||
register_all_checks()
|
||||
|
||||
# Only run these specific checks
|
||||
config = DoctorConfig(
|
||||
only=["python_version", "openai_api_key", "os_info"]
|
||||
)
|
||||
|
||||
engine = DoctorEngine(config)
|
||||
report = engine.run()
|
||||
|
||||
formatter = get_formatter("text", no_color=True)
|
||||
print(formatter.format_report(report))
|
||||
|
||||
return report.exit_code
|
||||
|
||||
|
||||
def run_json_output():
|
||||
"""Run checks and get JSON output."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Running Checks with JSON Output")
|
||||
print("=" * 60)
|
||||
|
||||
# Register all checks
|
||||
register_all_checks()
|
||||
|
||||
config = DoctorConfig(
|
||||
only=["python_version", "os_info"],
|
||||
format="json"
|
||||
)
|
||||
|
||||
engine = DoctorEngine(config)
|
||||
report = engine.run()
|
||||
|
||||
# Get JSON output
|
||||
formatter = get_formatter("json")
|
||||
json_output = formatter.format_report(report)
|
||||
|
||||
# Parse and pretty print
|
||||
data = json.loads(json_output)
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
return report.exit_code
|
||||
|
||||
|
||||
def list_available_checks():
|
||||
"""List all available checks."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Available Checks")
|
||||
print("=" * 60)
|
||||
|
||||
# Register all checks
|
||||
register_all_checks()
|
||||
|
||||
registry = get_registry()
|
||||
checks = registry.get_all_checks()
|
||||
|
||||
# Sort by category
|
||||
checks.sort(key=lambda c: c.category.value)
|
||||
|
||||
current_category = None
|
||||
for check in checks:
|
||||
if check.category != current_category:
|
||||
current_category = check.category
|
||||
print(f"\n {current_category.value.upper()}:")
|
||||
|
||||
deep_marker = " [deep]" if check.requires_deep else ""
|
||||
print(f" {check.id:30} {check.description}{deep_marker}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Doctor - Programmatic Usage Examples\n")
|
||||
|
||||
# List available checks
|
||||
list_available_checks()
|
||||
|
||||
# Run basic checks
|
||||
exit_code = run_basic_checks()
|
||||
print(f"\nBasic checks exit code: {exit_code}")
|
||||
|
||||
# Run environment checks only
|
||||
exit_code = run_environment_checks()
|
||||
print(f"\nEnvironment checks exit code: {exit_code}")
|
||||
|
||||
# Run specific checks
|
||||
exit_code = run_specific_checks()
|
||||
print(f"\nSpecific checks exit code: {exit_code}")
|
||||
|
||||
# Run with JSON output
|
||||
exit_code = run_json_output()
|
||||
print(f"\nJSON output exit code: {exit_code}")
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CI Integration Example - Using Doctor in CI/CD pipelines
|
||||
|
||||
This example demonstrates how to integrate PraisonAI Doctor
|
||||
into CI/CD pipelines for automated health checks.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def run_doctor_ci():
|
||||
"""Run doctor in CI mode and parse results."""
|
||||
print("Running PraisonAI Doctor in CI mode...")
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "praisonai", "doctor", "ci"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
# Parse JSON output
|
||||
try:
|
||||
report = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
print("Error: Could not parse doctor output")
|
||||
print(result.stdout)
|
||||
print(result.stderr)
|
||||
return 1
|
||||
|
||||
# Print summary
|
||||
summary = report.get("summary", {})
|
||||
print("\nDoctor CI Results:")
|
||||
print(f" Total checks: {summary.get('total', 0)}")
|
||||
print(f" Passed: {summary.get('passed', 0)}")
|
||||
print(f" Warnings: {summary.get('warnings', 0)}")
|
||||
print(f" Failed: {summary.get('failed', 0)}")
|
||||
print(f" Skipped: {summary.get('skipped', 0)}")
|
||||
print(f" Errors: {summary.get('errors', 0)}")
|
||||
print(f"\nExit code: {report.get('exit_code', -1)}")
|
||||
|
||||
# Check for failures
|
||||
if summary.get('failed', 0) > 0:
|
||||
print("\nFailed checks:")
|
||||
for check in report.get("results", []):
|
||||
if check.get("status") == "fail":
|
||||
print(f" - {check.get('title')}: {check.get('message')}")
|
||||
if check.get("remediation"):
|
||||
print(f" Fix: {check.get('remediation')}")
|
||||
|
||||
return result.returncode
|
||||
|
||||
|
||||
def run_specific_checks_ci():
|
||||
"""Run specific checks for CI."""
|
||||
print("\nRunning specific checks for CI...")
|
||||
|
||||
# Only run critical checks
|
||||
critical_checks = [
|
||||
"python_version",
|
||||
"praisonai_package",
|
||||
"openai_api_key",
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable, "-m", "praisonai", "doctor",
|
||||
"--json",
|
||||
"--only", ",".join(critical_checks)
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
try:
|
||||
report = json.loads(result.stdout)
|
||||
print(f"Critical checks: {report['summary']['passed']}/{report['summary']['total']} passed")
|
||||
return result.returncode
|
||||
except json.JSONDecodeError:
|
||||
print("Error parsing output")
|
||||
return 1
|
||||
|
||||
|
||||
def save_report_to_file():
|
||||
"""Save doctor report to a file for artifact upload."""
|
||||
print("\nSaving doctor report to file...")
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable, "-m", "praisonai", "doctor",
|
||||
"--json",
|
||||
"--output", "/tmp/doctor-report.json"
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
if result.returncode in [0, 1]:
|
||||
print("Report saved to /tmp/doctor-report.json")
|
||||
|
||||
# Read and display summary
|
||||
with open("/tmp/doctor-report.json", "r") as f:
|
||||
report = json.load(f)
|
||||
print(f"Summary: {report['summary']}")
|
||||
|
||||
return result.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Doctor - CI Integration Examples\n")
|
||||
print("=" * 60)
|
||||
|
||||
# Run CI mode
|
||||
exit_code = run_doctor_ci()
|
||||
|
||||
# Run specific checks
|
||||
run_specific_checks_ci()
|
||||
|
||||
# Save report
|
||||
save_report_to_file()
|
||||
|
||||
print(f"\nFinal exit code: {exit_code}")
|
||||
sys.exit(exit_code)
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Endpoints Example
|
||||
|
||||
This example demonstrates how to:
|
||||
1. Start the recipe server
|
||||
2. Use the endpoints CLI to interact with it
|
||||
3. Invoke recipes via HTTP
|
||||
|
||||
Prerequisites:
|
||||
- pip install praisonai[serve]
|
||||
- Set OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
# Terminal 1: Start server
|
||||
praisonai recipe serve --port 8765
|
||||
|
||||
# Terminal 2: Run this example
|
||||
python endpoints_example.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Configuration
|
||||
SERVER_URL = os.environ.get("PRAISONAI_ENDPOINTS_URL", "http://localhost:8765")
|
||||
|
||||
|
||||
def check_server_health():
|
||||
"""Check if the server is running."""
|
||||
try:
|
||||
req = urllib.request.Request(f"{SERVER_URL}/health")
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
return data.get("status") == "healthy"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def list_endpoints():
|
||||
"""List available endpoints using CLI."""
|
||||
print("\n=== Listing Endpoints (CLI) ===")
|
||||
result = subprocess.run(
|
||||
["python", "-m", "praisonai.cli.main", "endpoints", "list", "--format", "json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
recipes = json.loads(result.stdout)
|
||||
print(f"Found {len(recipes)} endpoints:")
|
||||
for r in recipes:
|
||||
print(f" - {r['name']} ({r['version']}): {r.get('description', '')[:50]}")
|
||||
return recipes
|
||||
else:
|
||||
print(f"Error: {result.stderr}")
|
||||
return []
|
||||
|
||||
|
||||
def describe_endpoint(name: str):
|
||||
"""Describe an endpoint using CLI."""
|
||||
print(f"\n=== Describing Endpoint: {name} ===")
|
||||
result = subprocess.run(
|
||||
["python", "-m", "praisonai.cli.main", "endpoints", "describe", name, "--url", SERVER_URL],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
info = json.loads(result.stdout)
|
||||
print(f"Name: {info.get('name')}")
|
||||
print(f"Version: {info.get('version')}")
|
||||
print(f"Description: {info.get('description')}")
|
||||
return info
|
||||
else:
|
||||
print(f"Error: {result.stderr}")
|
||||
return None
|
||||
|
||||
|
||||
def invoke_endpoint_cli(name: str, input_data: dict):
|
||||
"""Invoke an endpoint using CLI."""
|
||||
print(f"\n=== Invoking Endpoint (CLI): {name} ===")
|
||||
result = subprocess.run(
|
||||
[
|
||||
"python", "-m", "praisonai.cli.main", "endpoints", "invoke", name,
|
||||
"--url", SERVER_URL,
|
||||
"--input-json", json.dumps(input_data),
|
||||
"--json",
|
||||
"--dry-run", # Use dry-run for example
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
response = json.loads(result.stdout)
|
||||
print(f"Run ID: {response.get('run_id')}")
|
||||
print(f"Status: {response.get('status')}")
|
||||
return response
|
||||
else:
|
||||
print(f"Error: {result.stderr}")
|
||||
return None
|
||||
|
||||
|
||||
def invoke_endpoint_http(name: str, input_data: dict):
|
||||
"""Invoke an endpoint using HTTP directly."""
|
||||
print(f"\n=== Invoking Endpoint (HTTP): {name} ===")
|
||||
|
||||
body = json.dumps({
|
||||
"recipe": name,
|
||||
"input": input_data,
|
||||
"options": {"dry_run": True},
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{SERVER_URL}/v1/recipes/run",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
print(f"Run ID: {data.get('run_id')}")
|
||||
print(f"Status: {data.get('status')}")
|
||||
print(f"Output: {data.get('output')}")
|
||||
return data
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"HTTP Error: {e.code} - {e.read().decode()}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
"""Main example function."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Endpoints Example")
|
||||
print("=" * 60)
|
||||
|
||||
# Check if server is running
|
||||
print("\n=== Checking Server Health ===")
|
||||
if not check_server_health():
|
||||
print(f"Server not running at {SERVER_URL}")
|
||||
print("\nPlease start the server first:")
|
||||
print(" praisonai recipe serve --port 8765")
|
||||
sys.exit(1)
|
||||
print("✓ Server is healthy")
|
||||
|
||||
# List endpoints
|
||||
recipes = list_endpoints()
|
||||
if not recipes:
|
||||
print("No recipes available")
|
||||
sys.exit(1)
|
||||
|
||||
# Use first available recipe
|
||||
recipe_name = recipes[0]["name"]
|
||||
|
||||
# Describe endpoint
|
||||
describe_endpoint(recipe_name)
|
||||
|
||||
# Invoke via CLI
|
||||
invoke_endpoint_cli(recipe_name, {"query": "Hello from CLI"})
|
||||
|
||||
# Invoke via HTTP
|
||||
invoke_endpoint_http(recipe_name, {"query": "Hello from HTTP"})
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
# Package Manager Example
|
||||
|
||||
This example demonstrates how to use the PraisonAI package manager for installing and managing Python packages with security defaults.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- Installing packages with version constraints
|
||||
- Listing installed packages
|
||||
- Searching for packages on PyPI
|
||||
- Managing package index configuration
|
||||
- Security features (dependency confusion prevention)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run the example
|
||||
python package_manager_example.py
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### Install Packages
|
||||
|
||||
```bash
|
||||
# Install a package
|
||||
praisonai install requests
|
||||
|
||||
# Install with version constraint
|
||||
praisonai install "requests>=2.28"
|
||||
|
||||
# Install multiple packages
|
||||
praisonai install requests httpx aiohttp
|
||||
|
||||
# Upgrade existing package
|
||||
praisonai install requests --upgrade
|
||||
|
||||
# JSON output
|
||||
praisonai install requests --json
|
||||
```
|
||||
|
||||
### Uninstall Packages
|
||||
|
||||
```bash
|
||||
# Uninstall a package
|
||||
praisonai uninstall requests
|
||||
|
||||
# Uninstall without confirmation
|
||||
praisonai uninstall requests --yes
|
||||
```
|
||||
|
||||
### List Packages
|
||||
|
||||
```bash
|
||||
# List all installed packages
|
||||
praisonai package list
|
||||
|
||||
# JSON output
|
||||
praisonai package list --json
|
||||
```
|
||||
|
||||
### Search Packages
|
||||
|
||||
```bash
|
||||
# Search PyPI
|
||||
praisonai package search langchain
|
||||
|
||||
# JSON output
|
||||
praisonai package search langchain --json
|
||||
```
|
||||
|
||||
### Index Configuration
|
||||
|
||||
```bash
|
||||
# Show current index settings
|
||||
praisonai package index show --json
|
||||
|
||||
# Set custom index
|
||||
praisonai package index set https://pypi.mycompany.com/simple
|
||||
|
||||
# Reset to PyPI default
|
||||
praisonai package index set https://pypi.org/simple
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
By default, only the primary index (PyPI) is used. Extra indexes are blocked to prevent dependency confusion attacks.
|
||||
|
||||
```bash
|
||||
# This will FAIL (extra index not allowed by default)
|
||||
praisonai install mypackage --extra-index-url https://other.index.com/simple
|
||||
|
||||
# Explicitly allow extra index (shows security warning)
|
||||
praisonai install mypackage \
|
||||
--extra-index-url https://other.index.com/simple \
|
||||
--allow-extra-index
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `PRAISONAI_PACKAGE_INDEX_URL` - Override primary index URL
|
||||
- `PIP_INDEX_URL` - Fallback to pip's index URL
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Package Manager Example
|
||||
|
||||
This example demonstrates:
|
||||
1. Checking package index configuration
|
||||
2. Searching for packages on PyPI
|
||||
3. Listing installed packages
|
||||
4. Security features (safe defaults)
|
||||
|
||||
Requirements:
|
||||
- praisonai installed
|
||||
- No external API keys required
|
||||
|
||||
Note: This example does NOT actually install/uninstall packages
|
||||
to avoid modifying your environment. It demonstrates read-only
|
||||
operations and shows the commands for install/uninstall.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
def run_command(args, description):
|
||||
"""Run a praisonai command and display results."""
|
||||
print(f"\n[{description}]")
|
||||
print(f"Command: praisonai {' '.join(args)}")
|
||||
print("-" * 40)
|
||||
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "praisonai"] + args,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
print(json.dumps(data, indent=2))
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
print(result.stdout)
|
||||
return None
|
||||
else:
|
||||
print(f"Error (exit code {result.returncode}):")
|
||||
print(result.stderr or result.stdout)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("PraisonAI Package Manager Example")
|
||||
print("=" * 60)
|
||||
print("\nThis example demonstrates package manager features.")
|
||||
print("No packages will be installed or uninstalled.")
|
||||
|
||||
# 1. Show index configuration
|
||||
print("\n" + "=" * 60)
|
||||
print("1. PACKAGE INDEX CONFIGURATION")
|
||||
print("=" * 60)
|
||||
|
||||
data = run_command(
|
||||
["package", "index", "show", "--json"],
|
||||
"Show current index settings"
|
||||
)
|
||||
|
||||
if data:
|
||||
print(f"\n✓ Primary index: {data.get('index_url', 'N/A')}")
|
||||
print(f"✓ Extra indexes allowed: {data.get('allow_extra_index', False)}")
|
||||
|
||||
# 2. Search for packages
|
||||
print("\n" + "=" * 60)
|
||||
print("2. SEARCH PACKAGES")
|
||||
print("=" * 60)
|
||||
|
||||
data = run_command(
|
||||
["package", "search", "requests", "--json"],
|
||||
"Search for 'requests' package"
|
||||
)
|
||||
|
||||
if data and data.get("results"):
|
||||
pkg = data["results"][0]
|
||||
print(f"\n✓ Found: {pkg.get('name')} v{pkg.get('version')}")
|
||||
print(f" Summary: {pkg.get('summary', 'N/A')}")
|
||||
|
||||
# 3. List installed packages (sample)
|
||||
print("\n" + "=" * 60)
|
||||
print("3. LIST INSTALLED PACKAGES")
|
||||
print("=" * 60)
|
||||
|
||||
data = run_command(
|
||||
["package", "list", "--json"],
|
||||
"List installed packages"
|
||||
)
|
||||
|
||||
if data and data.get("packages"):
|
||||
print(f"\n✓ Total packages: {len(data['packages'])}")
|
||||
print(" Sample (first 5):")
|
||||
for pkg in data["packages"][:5]:
|
||||
print(f" - {pkg['name']}=={pkg['version']}")
|
||||
|
||||
# 4. Show install/uninstall commands (without executing)
|
||||
print("\n" + "=" * 60)
|
||||
print("4. INSTALL/UNINSTALL COMMANDS (NOT EXECUTED)")
|
||||
print("=" * 60)
|
||||
|
||||
print("\nTo install packages, use:")
|
||||
print(" praisonai install requests")
|
||||
print(" praisonai install 'requests>=2.28'")
|
||||
print(" praisonai install requests httpx --upgrade")
|
||||
|
||||
print("\nTo uninstall packages, use:")
|
||||
print(" praisonai uninstall requests")
|
||||
print(" praisonai uninstall requests --yes")
|
||||
|
||||
# 5. Security features
|
||||
print("\n" + "=" * 60)
|
||||
print("5. SECURITY FEATURES")
|
||||
print("=" * 60)
|
||||
|
||||
print("\nDependency Confusion Prevention:")
|
||||
print(" - By default, only PyPI is used as package source")
|
||||
print(" - Extra index URLs are blocked unless explicitly allowed")
|
||||
print(" - Use --allow-extra-index to enable (shows warning)")
|
||||
|
||||
print("\nExample (blocked by default):")
|
||||
print(" praisonai install pkg --extra-index-url https://other.index.com/simple")
|
||||
print(" → Error: Extra index URLs not allowed")
|
||||
|
||||
print("\nExample (explicitly allowed):")
|
||||
print(" praisonai install pkg --extra-index-url https://other.index.com/simple --allow-extra-index")
|
||||
print(" → Warning shown, then proceeds")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed!")
|
||||
print("=" * 60)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Performance Benchmark Example for PraisonAI.
|
||||
|
||||
This example demonstrates how to measure and verify performance metrics
|
||||
for PraisonAI CLI and SDK imports.
|
||||
|
||||
Usage:
|
||||
python benchmark_example.py
|
||||
"""
|
||||
|
||||
import time
|
||||
import tracemalloc
|
||||
import sys
|
||||
|
||||
|
||||
def measure_import_time(module_name: str) -> float:
|
||||
"""Measure import time for a module."""
|
||||
# Clear from cache if already imported
|
||||
if module_name in sys.modules:
|
||||
del sys.modules[module_name]
|
||||
|
||||
start = time.time()
|
||||
__import__(module_name)
|
||||
elapsed = time.time() - start
|
||||
return elapsed * 1000 # Return milliseconds
|
||||
|
||||
|
||||
def measure_memory_usage(import_func) -> tuple:
|
||||
"""Measure memory usage during import."""
|
||||
tracemalloc.start()
|
||||
import_func()
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
return current / 1024 / 1024, peak / 1024 / 1024 # Return MB
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("PraisonAI Performance Benchmark")
|
||||
print("=" * 60)
|
||||
|
||||
# Test 1: CLI Import Time
|
||||
print("\n1. CLI Import Time")
|
||||
print("-" * 40)
|
||||
|
||||
import os
|
||||
os.environ['LOGLEVEL'] = 'WARNING'
|
||||
|
||||
start = time.time()
|
||||
from praisonai.cli.main import PraisonAI
|
||||
cli_import_time = (time.time() - start) * 1000
|
||||
|
||||
print(f" CLI import time: {cli_import_time:.0f}ms")
|
||||
print(f" Status: {'✓ FAST' if cli_import_time < 500 else '✗ SLOW'}")
|
||||
|
||||
# Test 2: Memory Usage
|
||||
print("\n2. Memory Usage")
|
||||
print("-" * 40)
|
||||
|
||||
tracemalloc.start()
|
||||
# Force re-import by clearing cache
|
||||
for mod in list(sys.modules.keys()):
|
||||
if 'praisonai' in mod:
|
||||
del sys.modules[mod]
|
||||
|
||||
from praisonai.cli.main import PraisonAI as PraisonAI2
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
|
||||
print(f" Current memory: {current/1024/1024:.1f}MB")
|
||||
print(f" Peak memory: {peak/1024/1024:.1f}MB")
|
||||
print(f" Status: {'✓ LOW' if peak/1024/1024 < 50 else '✗ HIGH'}")
|
||||
|
||||
# Test 3: Lazy Loading Verification
|
||||
print("\n3. Lazy Loading Verification")
|
||||
print("-" * 40)
|
||||
|
||||
heavy_modules = ['litellm', 'instructor', 'praisonai.auto']
|
||||
loaded = [m for m in heavy_modules if m in sys.modules]
|
||||
|
||||
if loaded:
|
||||
print(f" Heavy modules loaded: {loaded}")
|
||||
print(" Status: ✗ NOT LAZY")
|
||||
else:
|
||||
print(" Heavy modules NOT loaded at startup")
|
||||
print(" Status: ✓ LAZY")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("Summary")
|
||||
print("=" * 60)
|
||||
|
||||
all_pass = (
|
||||
cli_import_time < 500 and
|
||||
peak/1024/1024 < 50 and
|
||||
len(loaded) == 0
|
||||
)
|
||||
|
||||
if all_pass:
|
||||
print("✓ All performance benchmarks PASSED")
|
||||
else:
|
||||
print("✗ Some benchmarks FAILED")
|
||||
|
||||
return 0 if all_pass else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Lazy Imports & Fast Startup
|
||||
|
||||
Demonstrates how PraisonAI Agents uses lazy imports to achieve
|
||||
fast startup times and reduced memory usage.
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def example_verify_lazy_imports():
|
||||
"""Verify that heavy dependencies are not loaded at import time."""
|
||||
print("=" * 60)
|
||||
print("Lazy Imports Verification")
|
||||
print("=" * 60)
|
||||
|
||||
# Clear any cached imports
|
||||
heavy_deps = ['litellm', 'chromadb', 'mem0', 'requests']
|
||||
for dep in heavy_deps:
|
||||
for key in list(sys.modules.keys()):
|
||||
if key.startswith(dep):
|
||||
del sys.modules[key]
|
||||
|
||||
# Measure import time
|
||||
start = time.perf_counter()
|
||||
import praisonaiagents
|
||||
elapsed = (time.perf_counter() - start) * 1000
|
||||
|
||||
print(f"\nImport time: {elapsed:.1f}ms")
|
||||
print(f"Target: <200ms")
|
||||
print(f"Status: {'PASS' if elapsed < 200 else 'FAIL'}")
|
||||
|
||||
# Check lazy imports
|
||||
print("\nLazy Import Check:")
|
||||
all_lazy = True
|
||||
for dep in heavy_deps:
|
||||
is_lazy = dep not in sys.modules
|
||||
status = "LAZY (good)" if is_lazy else "EAGER (bad)"
|
||||
print(f" {dep}: {status}")
|
||||
if not is_lazy:
|
||||
all_lazy = False
|
||||
|
||||
print(f"\nAll lazy: {'PASS' if all_lazy else 'FAIL'}")
|
||||
return all_lazy
|
||||
|
||||
|
||||
def example_measure_memory():
|
||||
"""Measure memory usage after import."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Memory Usage Measurement")
|
||||
print("=" * 60)
|
||||
|
||||
import tracemalloc
|
||||
|
||||
# Clear modules
|
||||
for key in list(sys.modules.keys()):
|
||||
if key.startswith('praisonaiagents'):
|
||||
del sys.modules[key]
|
||||
|
||||
tracemalloc.start()
|
||||
import praisonaiagents
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
|
||||
current_mb = current / 1024 / 1024
|
||||
peak_mb = peak / 1024 / 1024
|
||||
|
||||
print(f"\nCurrent memory: {current_mb:.1f}MB")
|
||||
print(f"Peak memory: {peak_mb:.1f}MB")
|
||||
print(f"Target: <30MB")
|
||||
print(f"Status: {'PASS' if current_mb < 30 else 'WARN' if current_mb < 45 else 'FAIL'}")
|
||||
|
||||
return current_mb < 45
|
||||
|
||||
|
||||
def example_specific_imports():
|
||||
"""Show how to use specific imports for best performance."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Specific Imports Example")
|
||||
print("=" * 60)
|
||||
|
||||
# Good - specific imports are fast
|
||||
print("\nGood practice - specific imports:")
|
||||
print(" from praisonaiagents import Agent, Task")
|
||||
|
||||
# Avoid - star imports load everything
|
||||
print("\nAvoid - star imports load more:")
|
||||
print(" from praisonaiagents import *")
|
||||
|
||||
# Example of specific import
|
||||
from praisonaiagents import Agent
|
||||
print(f"\nAgent class loaded: {Agent.__name__}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Agents - Lazy Imports Example")
|
||||
print("=" * 60)
|
||||
|
||||
# Run examples
|
||||
lazy_ok = example_verify_lazy_imports()
|
||||
memory_ok = example_measure_memory()
|
||||
example_specific_imports()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Summary")
|
||||
print("=" * 60)
|
||||
print(f"Lazy imports: {'PASS' if lazy_ok else 'FAIL'}")
|
||||
print(f"Memory usage: {'PASS' if memory_ok else 'FAIL'}")
|
||||
print(f"Overall: {'PASS' if lazy_ok and memory_ok else 'FAIL'}")
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Lite Agent (BYO-LLM)
|
||||
|
||||
Demonstrates the lightweight praisonaiagents.lite subpackage
|
||||
that allows you to bring your own LLM client.
|
||||
"""
|
||||
import os
|
||||
|
||||
|
||||
def example_custom_llm():
|
||||
"""Example using a custom LLM function."""
|
||||
print("=" * 60)
|
||||
print("Example 1: Custom LLM Function")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents.lite import LiteAgent
|
||||
|
||||
# Define a simple mock LLM
|
||||
def mock_llm(messages):
|
||||
"""A mock LLM that echoes the last message."""
|
||||
last_msg = messages[-1]["content"] if messages else "No message"
|
||||
return f"Echo: {last_msg}"
|
||||
|
||||
agent = LiteAgent(
|
||||
name="EchoAgent",
|
||||
llm_fn=mock_llm,
|
||||
instructions="You are a simple echo agent."
|
||||
)
|
||||
|
||||
response = agent.chat("Hello, world!")
|
||||
print(f"Response: {response}")
|
||||
print(f"Chat history length: {len(agent.chat_history)}")
|
||||
|
||||
|
||||
def example_openai_adapter():
|
||||
"""Example using the built-in OpenAI adapter."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 2: OpenAI Adapter")
|
||||
print("=" * 60)
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Skipping: OPENAI_API_KEY not set")
|
||||
return
|
||||
|
||||
from praisonaiagents.lite import LiteAgent, create_openai_llm_fn
|
||||
|
||||
# Create LLM function using OpenAI SDK directly
|
||||
llm_fn = create_openai_llm_fn(model="gpt-4o-mini")
|
||||
|
||||
agent = LiteAgent(
|
||||
name="OpenAIAgent",
|
||||
llm_fn=llm_fn,
|
||||
instructions="You are a helpful assistant. Keep responses brief."
|
||||
)
|
||||
|
||||
response = agent.chat("What is 2+2?")
|
||||
print(f"Response: {response}")
|
||||
|
||||
|
||||
def example_with_tools():
|
||||
"""Example using tools with LiteAgent."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 3: Tools with LiteAgent")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents.lite import LiteAgent, tool
|
||||
|
||||
@tool
|
||||
def add_numbers(a: int, b: int) -> int:
|
||||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
@tool
|
||||
def multiply_numbers(a: int, b: int) -> int:
|
||||
"""Multiply two numbers together."""
|
||||
return a * b
|
||||
|
||||
def mock_llm(messages):
|
||||
return "The calculation result is ready."
|
||||
|
||||
agent = LiteAgent(
|
||||
name="MathAgent",
|
||||
llm_fn=mock_llm,
|
||||
tools=[add_numbers, multiply_numbers]
|
||||
)
|
||||
|
||||
# Execute tools directly
|
||||
result1 = agent.execute_tool("add_numbers", a=5, b=3)
|
||||
print(f"5 + 3 = {result1.output}")
|
||||
print(f"Success: {result1.success}")
|
||||
|
||||
result2 = agent.execute_tool("multiply_numbers", a=4, b=7)
|
||||
print(f"4 * 7 = {result2.output}")
|
||||
print(f"Success: {result2.success}")
|
||||
|
||||
|
||||
def example_thread_safety():
|
||||
"""Example demonstrating thread-safe operations."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 4: Thread Safety")
|
||||
print("=" * 60)
|
||||
|
||||
import threading
|
||||
from praisonaiagents.lite import LiteAgent
|
||||
|
||||
counter = [0]
|
||||
lock = threading.Lock()
|
||||
|
||||
def counting_llm(messages):
|
||||
with lock:
|
||||
counter[0] += 1
|
||||
return f"Response {counter[0]}"
|
||||
|
||||
agent = LiteAgent(
|
||||
name="ThreadSafeAgent",
|
||||
llm_fn=counting_llm
|
||||
)
|
||||
|
||||
def worker(thread_id):
|
||||
for i in range(5):
|
||||
agent.chat(f"Message from thread {thread_id}, iteration {i}")
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=worker, args=(i,))
|
||||
for i in range(3)
|
||||
]
|
||||
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
print(f"Total messages processed: {counter[0]}")
|
||||
print(f"Chat history length: {len(agent.chat_history)}")
|
||||
print("Thread safety test: PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Agents - Lite Package Examples")
|
||||
print("=" * 60)
|
||||
|
||||
example_custom_llm()
|
||||
example_openai_adapter()
|
||||
example_with_tools()
|
||||
example_thread_safety()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All examples completed!")
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Runner for all performance examples.
|
||||
|
||||
Usage:
|
||||
# Run without real API (mock only)
|
||||
python run_all_examples.py
|
||||
|
||||
# Run with real API
|
||||
RUN_REAL_KEY_TESTS=1 OPENAI_API_KEY=your-key python run_all_examples.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
|
||||
def run_example(name, script_path):
|
||||
"""Run an example script and report results."""
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Running: {name}")
|
||||
print("=" * 60)
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, script_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=os.environ.copy()
|
||||
)
|
||||
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all performance examples."""
|
||||
print("PraisonAI Agents - Performance Examples Runner")
|
||||
print("=" * 60)
|
||||
|
||||
# Check environment
|
||||
real_tests = os.environ.get("RUN_REAL_KEY_TESTS")
|
||||
has_openai = bool(os.environ.get("OPENAI_API_KEY"))
|
||||
|
||||
print(f"RUN_REAL_KEY_TESTS: {real_tests or 'not set'}")
|
||||
print(f"OPENAI_API_KEY: {'set' if has_openai else 'not set'}")
|
||||
|
||||
# Get script directory
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Define examples
|
||||
examples = [
|
||||
("Lazy Imports", os.path.join(script_dir, "lazy_imports_example.py")),
|
||||
("Lite Agent", os.path.join(script_dir, "lite_agent_example.py")),
|
||||
("Thread Safety", os.path.join(script_dir, "thread_safety_example.py")),
|
||||
]
|
||||
|
||||
# Run examples
|
||||
results = []
|
||||
for name, path in examples:
|
||||
if os.path.exists(path):
|
||||
success = run_example(name, path)
|
||||
results.append((name, success))
|
||||
else:
|
||||
print(f"Skipping {name}: {path} not found")
|
||||
results.append((name, None))
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("Summary")
|
||||
print("=" * 60)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
skipped = 0
|
||||
|
||||
for name, success in results:
|
||||
if success is None:
|
||||
status = "SKIPPED"
|
||||
skipped += 1
|
||||
elif success:
|
||||
status = "PASS"
|
||||
passed += 1
|
||||
else:
|
||||
status = "FAIL"
|
||||
failed += 1
|
||||
print(f" {name}: {status}")
|
||||
|
||||
print(f"\nTotal: {passed} passed, {failed} failed, {skipped} skipped")
|
||||
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Thread-Safe Agent State
|
||||
|
||||
Demonstrates thread-safe chat history and cache management
|
||||
in PraisonAI Agents.
|
||||
"""
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
def example_lite_agent_thread_safety():
|
||||
"""Demonstrate thread safety with LiteAgent."""
|
||||
print("=" * 60)
|
||||
print("Example 1: LiteAgent Thread Safety")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents.lite import LiteAgent
|
||||
|
||||
# Counter for tracking responses
|
||||
response_count = [0]
|
||||
count_lock = threading.Lock()
|
||||
|
||||
def counting_llm(messages):
|
||||
with count_lock:
|
||||
response_count[0] += 1
|
||||
return f"Response #{response_count[0]}"
|
||||
|
||||
agent = LiteAgent(
|
||||
name="ThreadSafeAgent",
|
||||
llm_fn=counting_llm
|
||||
)
|
||||
|
||||
errors = []
|
||||
|
||||
def worker(thread_id):
|
||||
try:
|
||||
for i in range(10):
|
||||
agent.chat(f"Thread {thread_id}, message {i}")
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
|
||||
# Create and start threads
|
||||
threads = [
|
||||
threading.Thread(target=worker, args=(i,))
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
start = time.perf_counter()
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
print(f"Threads: 5")
|
||||
print(f"Messages per thread: 10")
|
||||
print(f"Total messages: {response_count[0]}")
|
||||
print(f"Chat history length: {len(agent.chat_history)}")
|
||||
print(f"Errors: {len(errors)}")
|
||||
print(f"Time: {elapsed:.2f}s")
|
||||
print(f"Status: {'PASS' if len(errors) == 0 else 'FAIL'}")
|
||||
|
||||
|
||||
def example_agent_locks():
|
||||
"""Verify Agent class has proper locks."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 2: Agent Lock Verification")
|
||||
print("=" * 60)
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="LockTestAgent",
|
||||
instructions="Test agent",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Check for locks
|
||||
has_history_lock = hasattr(agent, '_history_lock')
|
||||
has_cache_lock = hasattr(agent, '_cache_lock')
|
||||
|
||||
print(f"Has _history_lock: {has_history_lock}")
|
||||
print(f"Has _cache_lock: {has_cache_lock}")
|
||||
|
||||
# Verify lock types
|
||||
if has_history_lock:
|
||||
lock_type = type(agent._history_lock).__name__
|
||||
print(f"_history_lock type: {lock_type}")
|
||||
|
||||
if has_cache_lock:
|
||||
lock_type = type(agent._cache_lock).__name__
|
||||
print(f"_cache_lock type: {lock_type}")
|
||||
|
||||
print(f"Status: {'PASS' if has_history_lock and has_cache_lock else 'FAIL'}")
|
||||
|
||||
|
||||
def example_concurrent_real_api():
|
||||
"""Demonstrate concurrent API calls (requires API key)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 3: Concurrent Real API Calls")
|
||||
print("=" * 60)
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Skipping: OPENAI_API_KEY not set")
|
||||
return
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="ConcurrentAgent",
|
||||
instructions="Reply with one word only.",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
results = []
|
||||
results_lock = threading.Lock()
|
||||
errors = []
|
||||
|
||||
def worker(thread_id):
|
||||
try:
|
||||
response = agent.chat(f"Say hello {thread_id}")
|
||||
with results_lock:
|
||||
results.append((thread_id, response))
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=worker, args=(i,))
|
||||
for i in range(3)
|
||||
]
|
||||
|
||||
start = time.perf_counter()
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
print(f"Concurrent requests: 3")
|
||||
print(f"Successful responses: {len(results)}")
|
||||
print(f"Errors: {len(errors)}")
|
||||
print(f"Time: {elapsed:.2f}s")
|
||||
print(f"Status: {'PASS' if len(errors) == 0 else 'FAIL'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("PraisonAI Agents - Thread Safety Examples")
|
||||
print("=" * 60)
|
||||
|
||||
example_lite_agent_thread_safety()
|
||||
example_agent_locks()
|
||||
example_concurrent_real_api()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All examples completed!")
|
||||
@@ -0,0 +1,80 @@
|
||||
# Policy Packs Example
|
||||
|
||||
This example demonstrates how to use PraisonAI policy packs for managing tool permissions, data policies, and execution modes.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- Creating and loading policy packs
|
||||
- Tool permission checking (allow/deny)
|
||||
- Policy modes (dev/prod)
|
||||
- Merging policies
|
||||
- Data and PII policies
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
python policy_example.py
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
# Show default policy
|
||||
praisonai recipe policy show
|
||||
|
||||
# Show policy from file
|
||||
praisonai recipe policy show my-policy.yaml
|
||||
|
||||
# Create policy template
|
||||
praisonai recipe policy init -o my-policy.yaml
|
||||
|
||||
# Validate policy file
|
||||
praisonai recipe policy validate my-policy.yaml
|
||||
```
|
||||
|
||||
## Policy File Format
|
||||
|
||||
```yaml
|
||||
name: my-org-policy
|
||||
version: "1.0"
|
||||
|
||||
tools:
|
||||
allow:
|
||||
- web.search
|
||||
- db.query
|
||||
deny:
|
||||
- shell.exec
|
||||
- file.write
|
||||
|
||||
network:
|
||||
allow_domains:
|
||||
- api.openai.com
|
||||
deny_domains:
|
||||
- localhost
|
||||
|
||||
pii:
|
||||
mode: redact # allow, deny, redact
|
||||
fields:
|
||||
- email
|
||||
- phone
|
||||
|
||||
data:
|
||||
retention_days: 30
|
||||
export_allowed: true
|
||||
|
||||
modes:
|
||||
dev:
|
||||
allow_interactive_prompts: true
|
||||
strict_tool_enforcement: false
|
||||
prod:
|
||||
allow_interactive_prompts: false
|
||||
strict_tool_enforcement: true
|
||||
require_auth: true
|
||||
```
|
||||
|
||||
## Using Policy with Recipe Run
|
||||
|
||||
```bash
|
||||
# Run with policy file
|
||||
praisonai recipe run my-recipe --policy my-policy.yaml --mode prod
|
||||
```
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Policy Packs Example
|
||||
|
||||
This example demonstrates:
|
||||
1. Creating and loading policy packs
|
||||
2. Tool permission checking
|
||||
3. Policy modes (dev/prod)
|
||||
4. Merging policies
|
||||
|
||||
Prerequisites:
|
||||
- pip install praisonai
|
||||
|
||||
Usage:
|
||||
python policy_example.py
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
"""Main example function."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Policy Packs Example")
|
||||
print("=" * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
tmp_path = Path(tmp_dir)
|
||||
|
||||
# =====================================================
|
||||
# Default Policies
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Default Policies")
|
||||
print("=" * 40)
|
||||
|
||||
from praisonai.recipe.policy import get_default_policy, PolicyPack, PolicyDeniedError
|
||||
|
||||
print("\n1. Loading default dev policy...")
|
||||
dev_policy = get_default_policy("dev")
|
||||
print(f" Name: {dev_policy.name}")
|
||||
print(f" Allowed tools: {len(dev_policy.allowed_tools)}")
|
||||
print(f" Denied tools: {len(dev_policy.denied_tools)}")
|
||||
print(f" PII mode: {dev_policy.pii_mode}")
|
||||
|
||||
print("\n2. Loading default prod policy...")
|
||||
prod_policy = get_default_policy("prod")
|
||||
print(f" Name: {prod_policy.name}")
|
||||
print(f" PII mode: {prod_policy.pii_mode}")
|
||||
|
||||
# =====================================================
|
||||
# Tool Permission Checking
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Tool Permission Checking")
|
||||
print("=" * 40)
|
||||
|
||||
print("\n3. Checking tool permissions...")
|
||||
|
||||
# Check allowed tool
|
||||
try:
|
||||
dev_policy.check_tool("web.search")
|
||||
print(" web.search: ✓ Allowed")
|
||||
except PolicyDeniedError as e:
|
||||
print(f" web.search: ✗ Denied - {e}")
|
||||
|
||||
# Check denied tool
|
||||
try:
|
||||
dev_policy.check_tool("shell.exec")
|
||||
print(" shell.exec: ✓ Allowed")
|
||||
except PolicyDeniedError:
|
||||
print(" shell.exec: ✗ Denied")
|
||||
|
||||
# Check another denied tool
|
||||
try:
|
||||
dev_policy.check_tool("file.write")
|
||||
print(" file.write: ✓ Allowed")
|
||||
except PolicyDeniedError:
|
||||
print(" file.write: ✗ Denied")
|
||||
|
||||
# =====================================================
|
||||
# Custom Policy Pack
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Custom Policy Pack")
|
||||
print("=" * 40)
|
||||
|
||||
print("\n4. Creating custom policy pack...")
|
||||
custom_policy = PolicyPack(
|
||||
name="my-org-policy",
|
||||
config={
|
||||
"tools": {
|
||||
"allow": ["web.search", "db.query", "file.read"],
|
||||
"deny": ["shell.exec", "network.unrestricted"],
|
||||
},
|
||||
"network": {
|
||||
"allow_domains": ["api.openai.com", "api.anthropic.com"],
|
||||
"deny_domains": ["localhost", "127.0.0.1"],
|
||||
},
|
||||
"pii": {
|
||||
"mode": "redact",
|
||||
"fields": ["email", "phone", "ssn"],
|
||||
},
|
||||
"data": {
|
||||
"retention_days": 30,
|
||||
"export_allowed": True,
|
||||
},
|
||||
"modes": {
|
||||
"dev": {
|
||||
"allow_interactive_prompts": True,
|
||||
"strict_tool_enforcement": False,
|
||||
},
|
||||
"prod": {
|
||||
"allow_interactive_prompts": False,
|
||||
"strict_tool_enforcement": True,
|
||||
"require_auth": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
print(f" Name: {custom_policy.name}")
|
||||
print(f" Allowed tools: {list(custom_policy.allowed_tools)}")
|
||||
print(f" PII mode: {custom_policy.pii_mode}")
|
||||
print(f" Retention days: {custom_policy.retention_days}")
|
||||
|
||||
# =====================================================
|
||||
# Saving and Loading Policies
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Saving and Loading Policies")
|
||||
print("=" * 40)
|
||||
|
||||
print("\n5. Saving policy to file...")
|
||||
policy_file = tmp_path / "my-policy.yaml"
|
||||
custom_policy.save(policy_file)
|
||||
print(f" Saved to: {policy_file}")
|
||||
|
||||
print("\n6. Loading policy from file...")
|
||||
loaded_policy = PolicyPack.load(policy_file)
|
||||
print(f" Loaded: {loaded_policy.name}")
|
||||
print(f" PII mode: {loaded_policy.pii_mode}")
|
||||
|
||||
# =====================================================
|
||||
# Merging Policies
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Merging Policies")
|
||||
print("=" * 40)
|
||||
|
||||
print("\n7. Creating base and override policies...")
|
||||
base_policy = PolicyPack(
|
||||
name="base",
|
||||
config={
|
||||
"tools": {"allow": ["web.search"]},
|
||||
"pii": {"mode": "allow"},
|
||||
},
|
||||
)
|
||||
|
||||
override_policy = PolicyPack(
|
||||
name="override",
|
||||
config={
|
||||
"tools": {"allow": ["db.query"]},
|
||||
"pii": {"mode": "redact", "fields": ["email"]},
|
||||
},
|
||||
)
|
||||
|
||||
print("\n8. Merging policies...")
|
||||
merged = base_policy.merge(override_policy)
|
||||
print(f" Merged name: {merged.name}")
|
||||
print(f" Allowed tools: {list(merged.allowed_tools)}")
|
||||
print(f" PII mode: {merged.pii_mode}")
|
||||
|
||||
# =====================================================
|
||||
# Mode-Specific Configuration
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Mode-Specific Configuration")
|
||||
print("=" * 40)
|
||||
|
||||
print("\n9. Getting mode-specific config...")
|
||||
dev_config = custom_policy.get_mode_config("dev")
|
||||
prod_config = custom_policy.get_mode_config("prod")
|
||||
|
||||
print(" Dev mode:")
|
||||
print(f" - Interactive prompts: {dev_config.get('allow_interactive_prompts')}")
|
||||
print(f" - Strict enforcement: {dev_config.get('strict_tool_enforcement')}")
|
||||
|
||||
print(" Prod mode:")
|
||||
print(f" - Interactive prompts: {prod_config.get('allow_interactive_prompts')}")
|
||||
print(f" - Strict enforcement: {prod_config.get('strict_tool_enforcement')}")
|
||||
print(f" - Require auth: {prod_config.get('require_auth')}")
|
||||
|
||||
# =====================================================
|
||||
# Data Policy
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Data Policy")
|
||||
print("=" * 40)
|
||||
|
||||
print("\n10. Getting data policy...")
|
||||
data_policy = custom_policy.get_data_policy()
|
||||
print(f" PII mode: {data_policy['pii']['mode']}")
|
||||
print(f" PII fields: {data_policy['pii']['fields']}")
|
||||
print(f" Retention days: {data_policy['retention_days']}")
|
||||
print(f" Export allowed: {data_policy['export_allowed']}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,11 @@
|
||||
# Creator Suite Examples - Environment Variables
|
||||
|
||||
# Required for most features
|
||||
OPENAI_API_KEY=your_openai_api_key_here
|
||||
|
||||
# Optional - for web search in news crawler
|
||||
TAVILY_API_KEY=your_tavily_api_key_here
|
||||
|
||||
# Optional - for additional LLM providers
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
GOOGLE_API_KEY=your_google_api_key_here
|
||||
@@ -0,0 +1,77 @@
|
||||
# Creator Suite Examples
|
||||
|
||||
Examples for the PraisonAI Creator Automation Suite - a complete toolkit for AI content creators.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Set your API key
|
||||
export OPENAI_API_KEY=your_key_here
|
||||
|
||||
# Run an example
|
||||
python news_crawler_example.py
|
||||
python script_writer_example.py
|
||||
python hook_generator_example.py
|
||||
python brief_generator_example.py
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
| Example | Description | API Key Required |
|
||||
|---------|-------------|------------------|
|
||||
| `news_crawler_example.py` | Crawl AI news from multiple sources | No (HN, arXiv) |
|
||||
| `script_writer_example.py` | Generate scripts for YouTube, X, LinkedIn | Yes (OpenAI) |
|
||||
| `hook_generator_example.py` | Generate attention-grabbing hooks | Yes (OpenAI) |
|
||||
| `brief_generator_example.py` | Generate news briefs | Yes (OpenAI) |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your API keys:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Features Covered
|
||||
|
||||
### News Intelligence Layer
|
||||
- AI News Crawler
|
||||
- AI News Deduper
|
||||
- AI Signal Ranker
|
||||
- AI Context Enricher
|
||||
- AI Brief Generator
|
||||
|
||||
### Script & Story Layer
|
||||
- AI Script Writer
|
||||
- AI Angle Generator
|
||||
- AI Hook Generator
|
||||
- AI CTA Generator
|
||||
- AI Fact Checker
|
||||
|
||||
### Capture & Screen Automation
|
||||
- AI Screenshot Capture
|
||||
- AI Screen Recorder
|
||||
- AI News Capture Pack
|
||||
|
||||
### Video Generation
|
||||
- AI B-roll Builder
|
||||
- AI Voiceover Generator
|
||||
- AI Video Merger
|
||||
|
||||
### Publishing & Distribution
|
||||
- AI Publisher Pack
|
||||
- AI Content Calendar
|
||||
- AI Post Copy Generator
|
||||
- AI Hashtag Optimizer
|
||||
|
||||
### Feedback & Learning
|
||||
- AI Performance Analyzer
|
||||
- AI Comment Miner
|
||||
- AI A/B Hook Tester
|
||||
|
||||
### Orchestrator
|
||||
- AI Daily News Show (end-to-end pipeline)
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation: https://docs.praison.ai/docs/ai-tools/creator-suite/
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Brief Generator Example
|
||||
|
||||
Demonstrates generating news briefs from articles.
|
||||
Requires: OPENAI_API_KEY
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Check for API key
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("ERROR: OPENAI_API_KEY environment variable required")
|
||||
print("Set it with: export OPENAI_API_KEY=your_key")
|
||||
sys.exit(1)
|
||||
|
||||
# Add the Agent-Recipes templates to path
|
||||
sys.path.insert(0, '/Users/praison/Agent-Recipes/agent_recipes/templates/ai-brief-generator')
|
||||
|
||||
from tools import generate_brief, extract_highlights
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("AI Brief Generator Example")
|
||||
print("=" * 60)
|
||||
|
||||
# Sample articles
|
||||
articles = [
|
||||
{
|
||||
"title": "OpenAI Releases GPT-5 with Multimodal Capabilities",
|
||||
"content": "OpenAI announced GPT-5 today with major improvements in reasoning, multimodal understanding, and reduced hallucinations.",
|
||||
"source": "TechCrunch",
|
||||
"url": "https://example.com/gpt5"
|
||||
},
|
||||
{
|
||||
"title": "Google Gemini 2.0 Launches with Real-time Video Understanding",
|
||||
"content": "Google unveiled Gemini 2.0 with breakthrough capabilities in real-time video analysis and native tool use.",
|
||||
"source": "The Verge",
|
||||
"url": "https://example.com/gemini2"
|
||||
},
|
||||
{
|
||||
"title": "Anthropic Claude 4 Sets New Benchmarks",
|
||||
"content": "Anthropic's Claude 4 achieves state-of-the-art results on coding, math, and reasoning benchmarks.",
|
||||
"source": "Wired",
|
||||
"url": "https://example.com/claude4"
|
||||
}
|
||||
]
|
||||
|
||||
# 1. Generate daily brief
|
||||
print("\n1. Generating daily brief...")
|
||||
brief = generate_brief(articles, format="daily", max_articles=3)
|
||||
print(f" Format: daily")
|
||||
print(f" Articles included: {brief.get('article_count', 0)}")
|
||||
print(f"\n Brief preview:\n {brief.get('brief', '')[:500]}...")
|
||||
|
||||
# 2. Extract highlights
|
||||
print("\n2. Extracting highlights...")
|
||||
highlights = extract_highlights(articles, num_highlights=3)
|
||||
print(f" Highlights:")
|
||||
for h in highlights.get('highlights', []):
|
||||
print(f" - {h}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Hook Generator Example
|
||||
|
||||
Demonstrates generating attention-grabbing hooks.
|
||||
Requires: OPENAI_API_KEY
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Check for API key
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("ERROR: OPENAI_API_KEY environment variable required")
|
||||
print("Set it with: export OPENAI_API_KEY=your_key")
|
||||
sys.exit(1)
|
||||
|
||||
# Add the Agent-Recipes templates to path
|
||||
sys.path.insert(0, '/Users/praison/Agent-Recipes/agent_recipes/templates/ai-hook-generator')
|
||||
|
||||
from tools import generate_hooks, rank_hooks
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("AI Hook Generator Example")
|
||||
print("=" * 60)
|
||||
|
||||
topic = "GPT-5 just released with groundbreaking capabilities"
|
||||
|
||||
# 1. Generate hooks
|
||||
print(f"\n1. Generating hooks for: '{topic}'")
|
||||
hooks = generate_hooks(topic, num_variants=5)
|
||||
print(f" Generated {len(hooks.get('hooks', []))} hooks:")
|
||||
|
||||
for hook in hooks.get('hooks', []):
|
||||
print(f"\n [{hook.get('style', 'unknown')}]")
|
||||
print(f" {hook.get('text', '')}")
|
||||
|
||||
# 2. Rank hooks
|
||||
print("\n2. Ranking hooks by engagement potential...")
|
||||
ranked = rank_hooks(hooks.get('hooks', []))
|
||||
best = ranked.get('best_hook', {})
|
||||
print(f"\n Best hook (score: {best.get('score', 0)}):")
|
||||
print(f" {best.get('text', '')}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI News Crawler Example
|
||||
|
||||
Demonstrates crawling AI news from multiple sources.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the Agent-Recipes templates to path
|
||||
sys.path.insert(0, '/Users/praison/Agent-Recipes/agent_recipes/templates/ai-news-crawler')
|
||||
|
||||
from tools import crawl_hackernews, crawl_reddit_ai, crawl_arxiv, crawl_ai_news
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("AI News Crawler Example")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. Crawl HackerNews (no API key needed)
|
||||
print("\n1. Crawling HackerNews...")
|
||||
hn_articles = crawl_hackernews(max_articles=5, time_window_hours=48)
|
||||
print(f" Found {len(hn_articles)} articles from HackerNews")
|
||||
for article in hn_articles[:3]:
|
||||
print(f" - {article.get('title', 'No title')[:60]}...")
|
||||
|
||||
# 2. Crawl arXiv (no API key needed)
|
||||
print("\n2. Crawling arXiv...")
|
||||
arxiv_articles = crawl_arxiv(categories=["cs.AI", "cs.LG"], max_results=5)
|
||||
print(f" Found {len(arxiv_articles)} articles from arXiv")
|
||||
for article in arxiv_articles[:3]:
|
||||
print(f" - {article.get('title', 'No title')[:60]}...")
|
||||
|
||||
# 3. Full crawl from multiple sources
|
||||
print("\n3. Full multi-source crawl...")
|
||||
all_articles = crawl_ai_news(
|
||||
sources=["hackernews", "arxiv"],
|
||||
max_articles=10,
|
||||
time_window_hours=48
|
||||
)
|
||||
print(f" Total articles: {all_articles.get('total', 0)}")
|
||||
print(f" Sources crawled: {all_articles.get('sources_crawled', [])}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Script Writer Example
|
||||
|
||||
Demonstrates generating scripts for different platforms.
|
||||
Requires: OPENAI_API_KEY
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Check for API key
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("ERROR: OPENAI_API_KEY environment variable required")
|
||||
print("Set it with: export OPENAI_API_KEY=your_key")
|
||||
sys.exit(1)
|
||||
|
||||
# Add the Agent-Recipes templates to path
|
||||
sys.path.insert(0, '/Users/praison/Agent-Recipes/agent_recipes/templates/ai-script-writer')
|
||||
|
||||
from tools import write_youtube_script, write_short_script, write_thread
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("AI Script Writer Example")
|
||||
print("=" * 60)
|
||||
|
||||
topic = "AI agents are revolutionizing software development"
|
||||
|
||||
# 1. YouTube Long-form Script
|
||||
print("\n1. Generating YouTube long-form script...")
|
||||
yt_script = write_youtube_script(
|
||||
topic=topic,
|
||||
target_length=180,
|
||||
key_points=["automation", "productivity", "future of coding"]
|
||||
)
|
||||
print(f" Format: {yt_script.get('format')}")
|
||||
print(f" Estimated duration: {yt_script.get('estimated_duration')}s")
|
||||
print(f" Script preview:\n {yt_script.get('script', '')[:200]}...")
|
||||
|
||||
# 2. YouTube Short Script
|
||||
print("\n2. Generating YouTube Short script...")
|
||||
short_script = write_short_script(topic, duration=30)
|
||||
print(f" Format: {short_script.get('format')}")
|
||||
print(f" Script:\n {short_script.get('script', '')[:300]}...")
|
||||
|
||||
# 3. X Thread
|
||||
print("\n3. Generating X thread...")
|
||||
thread = write_thread(topic, num_tweets=3)
|
||||
print(f" Thread ({len(thread.get('tweets', []))} tweets):")
|
||||
for i, tweet in enumerate(thread.get('tweets', [])[:3], 1):
|
||||
print(f" Tweet {i}: {tweet[:80]}...")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: LLM-based Recipes
|
||||
|
||||
Demonstrates usage of LLM-powered recipes:
|
||||
- ai-blog-generator
|
||||
- ai-social-media-generator
|
||||
- ai-faq-generator
|
||||
- ai-sentiment-analyzer
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Ensure API key is set
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Please set OPENAI_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
# Add recipe tools to path
|
||||
sys.path.insert(0, "/Users/praison/PraisonAI-tools/praisonai_tools/recipe_tools")
|
||||
|
||||
from llm_tool import LLMTool, llm_complete
|
||||
|
||||
|
||||
def example_blog_generator():
|
||||
"""Generate a blog post."""
|
||||
print("\n=== AI Blog Generator ===")
|
||||
|
||||
llm = LLMTool(provider="openai", model="gpt-4o-mini")
|
||||
|
||||
topic = "The Future of AI in Healthcare"
|
||||
|
||||
response = llm.complete(
|
||||
f"""Write a short blog post (3 paragraphs) about: {topic}
|
||||
|
||||
Include:
|
||||
- Engaging introduction
|
||||
- Key points
|
||||
- Call to action
|
||||
|
||||
Format as markdown.""",
|
||||
system="You are an expert blog writer specializing in technology topics.",
|
||||
max_tokens=500,
|
||||
)
|
||||
|
||||
print(f"Topic: {topic}")
|
||||
print(f"\nGenerated Blog Post:\n{response.content}")
|
||||
print(f"\nTokens used: {response.usage.get('total_tokens', 'N/A')}")
|
||||
|
||||
|
||||
def example_social_media_generator():
|
||||
"""Generate social media posts."""
|
||||
print("\n=== AI Social Media Generator ===")
|
||||
|
||||
llm = LLMTool(provider="openai", model="gpt-4o-mini")
|
||||
|
||||
content = "We just launched our new AI-powered productivity app!"
|
||||
|
||||
response = llm.complete(
|
||||
f"""Create social media posts for the following announcement:
|
||||
{content}
|
||||
|
||||
Generate posts for:
|
||||
1. Twitter (280 chars max)
|
||||
2. LinkedIn (professional tone)
|
||||
3. Instagram (with emoji suggestions)
|
||||
|
||||
Format each clearly.""",
|
||||
system="You are a social media marketing expert.",
|
||||
max_tokens=400,
|
||||
)
|
||||
|
||||
print(f"Content: {content}")
|
||||
print(f"\nGenerated Posts:\n{response.content}")
|
||||
|
||||
|
||||
def example_faq_generator():
|
||||
"""Generate FAQ from content."""
|
||||
print("\n=== AI FAQ Generator ===")
|
||||
|
||||
llm = LLMTool(provider="openai", model="gpt-4o-mini")
|
||||
|
||||
documentation = """
|
||||
Our product is a cloud-based project management tool.
|
||||
It supports team collaboration, task tracking, and reporting.
|
||||
Pricing starts at $10/month per user.
|
||||
We offer a 14-day free trial.
|
||||
Integration with Slack, GitHub, and Jira is available.
|
||||
"""
|
||||
|
||||
response = llm.complete(
|
||||
f"""Based on this documentation, generate 5 FAQ questions and answers:
|
||||
|
||||
{documentation}
|
||||
|
||||
Format as:
|
||||
Q: [question]
|
||||
A: [answer]
|
||||
""",
|
||||
system="You are a technical writer creating FAQ documentation.",
|
||||
max_tokens=400,
|
||||
)
|
||||
|
||||
print(f"Generated FAQ:\n{response.content}")
|
||||
|
||||
|
||||
def example_sentiment_analyzer():
|
||||
"""Analyze sentiment in text."""
|
||||
print("\n=== AI Sentiment Analyzer ===")
|
||||
|
||||
llm = LLMTool(provider="openai", model="gpt-4o-mini")
|
||||
|
||||
reviews = [
|
||||
"This product is amazing! Best purchase ever.",
|
||||
"Terrible experience. Would not recommend.",
|
||||
"It's okay, nothing special but does the job.",
|
||||
]
|
||||
|
||||
for review in reviews:
|
||||
response = llm.complete(
|
||||
f"""Analyze the sentiment of this text and respond with ONLY one word: POSITIVE, NEGATIVE, or NEUTRAL
|
||||
|
||||
Text: {review}""",
|
||||
max_tokens=10,
|
||||
)
|
||||
print(f"Review: {review[:50]}...")
|
||||
print(f"Sentiment: {response.content.strip()}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("LLM-Based Recipes Examples")
|
||||
print("=" * 60)
|
||||
|
||||
example_blog_generator()
|
||||
example_social_media_generator()
|
||||
example_faq_generator()
|
||||
example_sentiment_analyzer()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All examples completed!")
|
||||
print("=" * 60)
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run All Recipes - Test harness for all Agent-Recipes.
|
||||
|
||||
Runs each recipe in dry-run mode to verify CLI parity and run.json generation.
|
||||
Requires: OPENAI_API_KEY environment variable for integration tests.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
# Recipe definitions
|
||||
RECIPES = [
|
||||
"ai-audio-normalizer",
|
||||
"ai-audio-splitter",
|
||||
"ai-changelog-generator",
|
||||
"ai-code-documenter",
|
||||
"ai-csv-cleaner",
|
||||
"ai-data-profiler",
|
||||
"ai-dependency-auditor",
|
||||
"ai-doc-translator",
|
||||
"ai-folder-packager",
|
||||
"ai-image-cataloger",
|
||||
"ai-image-optimizer",
|
||||
"ai-image-resizer",
|
||||
"ai-json-to-csv",
|
||||
"ai-markdown-to-pdf",
|
||||
"ai-pdf-summarizer",
|
||||
"ai-pdf-to-markdown",
|
||||
"ai-podcast-cleaner",
|
||||
"ai-repo-readme",
|
||||
"ai-schema-generator",
|
||||
"ai-screenshot-ocr",
|
||||
"ai-sitemap-scraper",
|
||||
"ai-slide-to-notes",
|
||||
"ai-url-to-markdown",
|
||||
"ai-video-editor",
|
||||
"ai-video-thumbnails",
|
||||
"ai-video-to-gif",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestResult:
|
||||
"""Result of a recipe test."""
|
||||
recipe: str
|
||||
status: str # pass, fail, skip
|
||||
reason: str = ""
|
||||
duration: float = 0.0
|
||||
|
||||
|
||||
def check_api_keys() -> dict:
|
||||
"""Check available API keys."""
|
||||
keys = {
|
||||
"OPENAI_API_KEY": bool(os.environ.get("OPENAI_API_KEY")),
|
||||
"ANTHROPIC_API_KEY": bool(os.environ.get("ANTHROPIC_API_KEY")),
|
||||
"GOOGLE_API_KEY": bool(os.environ.get("GOOGLE_API_KEY")),
|
||||
}
|
||||
return keys
|
||||
|
||||
|
||||
def check_external_deps() -> dict:
|
||||
"""Check external dependencies."""
|
||||
import shutil
|
||||
deps = {
|
||||
"ffmpeg": shutil.which("ffmpeg") is not None,
|
||||
"ffprobe": shutil.which("ffprobe") is not None,
|
||||
"pdftotext": shutil.which("pdftotext") is not None,
|
||||
"pandoc": shutil.which("pandoc") is not None,
|
||||
"git": shutil.which("git") is not None,
|
||||
}
|
||||
return deps
|
||||
|
||||
|
||||
def run_recipe_dry_run(recipe: str, timeout: int = 30) -> TestResult:
|
||||
"""Run a recipe in dry-run mode."""
|
||||
import time
|
||||
start = time.time()
|
||||
|
||||
try:
|
||||
# Use praison templates run command with dry-run
|
||||
cmd = ["praison", "templates", "run", recipe, "--dry-run", "dummy_input"]
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
duration = time.time() - start
|
||||
|
||||
if result.returncode == 0:
|
||||
return TestResult(recipe, "pass", "dry-run completed", duration)
|
||||
else:
|
||||
# Check if it's a missing dependency error
|
||||
stderr = result.stderr.lower()
|
||||
if "not found" in stderr or "missing" in stderr:
|
||||
return TestResult(recipe, "skip", f"Missing dependency: {result.stderr[:100]}", duration)
|
||||
return TestResult(recipe, "fail", f"Exit code {result.returncode}: {result.stderr[:100]}", duration)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return TestResult(recipe, "fail", "Timeout", timeout)
|
||||
except FileNotFoundError:
|
||||
return TestResult(recipe, "skip", "praison CLI not found", 0)
|
||||
except Exception as e:
|
||||
return TestResult(recipe, "fail", str(e), 0)
|
||||
|
||||
|
||||
def run_all_recipes(dry_run_only: bool = True) -> List[TestResult]:
|
||||
"""Run all recipes and collect results."""
|
||||
results = []
|
||||
|
||||
print("=" * 60)
|
||||
print("RECIPE TEST HARNESS")
|
||||
print("=" * 60)
|
||||
|
||||
# Check prerequisites
|
||||
keys = check_api_keys()
|
||||
deps = check_external_deps()
|
||||
|
||||
print("\nAPI Keys:")
|
||||
for key, present in keys.items():
|
||||
status = "✓" if present else "✗"
|
||||
print(f" {status} {key}")
|
||||
|
||||
print("\nExternal Dependencies:")
|
||||
for dep, present in deps.items():
|
||||
status = "✓" if present else "✗"
|
||||
print(f" {status} {dep}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Running {len(RECIPES)} recipes (dry-run={dry_run_only})")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
for i, recipe in enumerate(RECIPES, 1):
|
||||
print(f"[{i:2d}/{len(RECIPES)}] {recipe}...", end=" ", flush=True)
|
||||
|
||||
if dry_run_only:
|
||||
result = run_recipe_dry_run(recipe)
|
||||
else:
|
||||
# Full run would go here
|
||||
result = run_recipe_dry_run(recipe)
|
||||
|
||||
results.append(result)
|
||||
|
||||
if result.status == "pass":
|
||||
print(f"✓ ({result.duration:.1f}s)")
|
||||
elif result.status == "skip":
|
||||
print(f"⊘ SKIP: {result.reason}")
|
||||
else:
|
||||
print(f"✗ FAIL: {result.reason}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def print_summary(results: List[TestResult]):
|
||||
"""Print test summary."""
|
||||
passed = sum(1 for r in results if r.status == "pass")
|
||||
failed = sum(1 for r in results if r.status == "fail")
|
||||
skipped = sum(1 for r in results if r.status == "skip")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
print(f" Passed: {passed}")
|
||||
print(f" Failed: {failed}")
|
||||
print(f" Skipped: {skipped}")
|
||||
print(f" Total: {len(results)}")
|
||||
|
||||
if failed > 0:
|
||||
print("\nFailed recipes:")
|
||||
for r in results:
|
||||
if r.status == "fail":
|
||||
print(f" - {r.recipe}: {r.reason}")
|
||||
|
||||
return failed == 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Run all Agent-Recipes tests")
|
||||
parser.add_argument("--full", action="store_true", help="Run full tests (not just dry-run)")
|
||||
parser.add_argument("--recipe", help="Run specific recipe only")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON results")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.recipe:
|
||||
results = [run_recipe_dry_run(args.recipe)]
|
||||
else:
|
||||
results = run_all_recipes(dry_run_only=not args.full)
|
||||
|
||||
if args.json:
|
||||
output = [{"recipe": r.recipe, "status": r.status, "reason": r.reason} for r in results]
|
||||
print(json.dumps(output, indent=2))
|
||||
else:
|
||||
success = print_summary(results)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
# Recipe Registry Example
|
||||
|
||||
This example demonstrates how to use the PraisonAI recipe registry for publishing and pulling recipes via both local filesystem and HTTP server.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- Creating a local registry
|
||||
- Starting an HTTP registry server
|
||||
- Publishing recipes with token authentication
|
||||
- Listing and searching recipes
|
||||
- Pulling recipes from HTTP registry
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run the HTTP registry example
|
||||
python http_registry_example.py
|
||||
```
|
||||
|
||||
## HTTP Registry Server
|
||||
|
||||
```bash
|
||||
# Start HTTP registry server
|
||||
praisonai registry serve --port 7777
|
||||
|
||||
# Start with authentication
|
||||
praisonai registry serve --port 7777 --token mysecret
|
||||
|
||||
# Check server status
|
||||
praisonai registry status --registry http://localhost:7777
|
||||
```
|
||||
|
||||
## CLI Commands with HTTP Registry
|
||||
|
||||
```bash
|
||||
# Publish to HTTP registry
|
||||
praisonai recipe publish ./my-recipe --registry http://localhost:7777 --json
|
||||
|
||||
# Publish with token
|
||||
praisonai recipe publish ./my-recipe --registry http://localhost:7777 --token mysecret
|
||||
|
||||
# Pull from HTTP registry
|
||||
praisonai recipe pull [email protected] --registry http://localhost:7777 -o ./recipes
|
||||
|
||||
# List recipes from HTTP registry
|
||||
praisonai recipe list --registry http://localhost:7777 --json
|
||||
|
||||
# Search HTTP registry
|
||||
praisonai recipe search "hello" --registry http://localhost:7777
|
||||
```
|
||||
|
||||
## Local Registry Commands
|
||||
|
||||
```bash
|
||||
# Publish to local registry (default)
|
||||
praisonai recipe publish ./my-recipe --json
|
||||
|
||||
# Pull from local registry
|
||||
praisonai recipe pull [email protected] -o ./recipes
|
||||
|
||||
# Use custom local path
|
||||
praisonai recipe publish ./my-recipe --registry /path/to/registry
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `PRAISONAI_REGISTRY_TOKEN` - Default token for HTTP registry authentication
|
||||
|
||||
## Default Registry Location
|
||||
|
||||
By default, the local registry is stored at `~/.praison/registry`.
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP Recipe Registry Example
|
||||
|
||||
This example demonstrates:
|
||||
1. Starting an HTTP registry server programmatically
|
||||
2. Publishing recipes with token authentication
|
||||
3. Listing and searching recipes via HTTP
|
||||
4. Pulling recipes from HTTP registry
|
||||
|
||||
Requirements:
|
||||
- praisonai installed
|
||||
- No external API keys required (local registry only)
|
||||
|
||||
Environment Variables (optional):
|
||||
- PRAISONAI_REGISTRY_TOKEN: Token for authentication
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
def create_sample_recipe(output_dir: Path) -> Path:
|
||||
"""Create a sample recipe for testing."""
|
||||
recipe_dir = output_dir / "sample-agent"
|
||||
recipe_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create manifest.json
|
||||
manifest = {
|
||||
"name": "sample-agent",
|
||||
"version": "1.0.0",
|
||||
"description": "A sample agent recipe for registry testing",
|
||||
"tags": ["sample", "agent", "test"],
|
||||
"author": "praison",
|
||||
"files": ["recipe.yaml"]
|
||||
}
|
||||
(recipe_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
||||
|
||||
# Create recipe.yaml
|
||||
recipe_yaml = """
|
||||
name: sample-agent
|
||||
version: "1.0.0"
|
||||
description: A sample agent recipe
|
||||
|
||||
agents:
|
||||
- name: assistant
|
||||
role: General Assistant
|
||||
goal: Help users with their questions
|
||||
instructions: You are a helpful assistant.
|
||||
|
||||
tasks:
|
||||
- name: respond
|
||||
description: Respond to user query
|
||||
agent: assistant
|
||||
"""
|
||||
(recipe_dir / "recipe.yaml").write_text(recipe_yaml.strip())
|
||||
|
||||
# Pack into .praison bundle
|
||||
bundle_path = output_dir / "sample-agent-1.0.0.praison"
|
||||
with tarfile.open(bundle_path, "w:gz") as tar:
|
||||
for file in recipe_dir.iterdir():
|
||||
tar.add(file, arcname=file.name)
|
||||
|
||||
return bundle_path
|
||||
|
||||
|
||||
def start_registry_server(port: int, registry_path: Path, token: str = None):
|
||||
"""Start registry server in background process."""
|
||||
cmd = [
|
||||
sys.executable, "-c",
|
||||
f"""
|
||||
import sys
|
||||
sys.path.insert(0, '/Users/praison/praisonai-package/src/praisonai')
|
||||
from praisonai.recipe.server import create_wsgi_app
|
||||
from wsgiref.simple_server import make_server, WSGIRequestHandler
|
||||
from pathlib import Path
|
||||
|
||||
class QuietHandler(WSGIRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
app = create_wsgi_app(registry_path=Path("{registry_path}"), token={repr(token)})
|
||||
server = make_server("127.0.0.1", {port}, app, handler_class=QuietHandler)
|
||||
server.serve_forever()
|
||||
"""
|
||||
]
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return proc
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("HTTP Recipe Registry Example")
|
||||
print("=" * 60)
|
||||
|
||||
# Setup
|
||||
work_dir = Path(tempfile.mkdtemp(prefix="registry_example_"))
|
||||
registry_path = work_dir / "registry"
|
||||
pulled_dir = work_dir / "pulled"
|
||||
port = 7799
|
||||
token = "example-secret-token"
|
||||
|
||||
print(f"\nWork directory: {work_dir}")
|
||||
print(f"Registry path: {registry_path}")
|
||||
print(f"Server port: {port}")
|
||||
print(f"Token: {'*' * len(token)} (redacted)")
|
||||
|
||||
try:
|
||||
# 1. Create sample recipe
|
||||
print("\n[1] Creating sample recipe...")
|
||||
bundle_path = create_sample_recipe(work_dir)
|
||||
print(f" ✓ Created bundle: {bundle_path.name}")
|
||||
|
||||
# 2. Start HTTP registry server
|
||||
print("\n[2] Starting HTTP registry server...")
|
||||
server = start_registry_server(port, registry_path, token)
|
||||
time.sleep(1) # Wait for server to start
|
||||
print(f" ✓ Server running on http://127.0.0.1:{port}")
|
||||
|
||||
# 3. Check health
|
||||
print("\n[3] Checking server health...")
|
||||
result = subprocess.run(
|
||||
["curl", "-s", f"http://127.0.0.1:{port}/healthz"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
health = json.loads(result.stdout)
|
||||
print(f" Status: {health['status']}")
|
||||
print(f" Auth required: {health['auth_required']}")
|
||||
|
||||
# 4. Publish WITHOUT token (should fail)
|
||||
print("\n[4] Publishing WITHOUT token (expect failure)...")
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "praisonai", "recipe", "publish", str(bundle_path),
|
||||
"--registry", f"http://127.0.0.1:{port}", "--json"],
|
||||
capture_output=True, text=True,
|
||||
cwd="/Users/praison/praisonai-package/src/praisonai"
|
||||
)
|
||||
if result.returncode != 0 or "error" in result.stdout.lower() or "401" in result.stdout:
|
||||
print(" ✓ Correctly rejected (authentication required)")
|
||||
else:
|
||||
print(f" ✗ Unexpected success: {result.stdout}")
|
||||
|
||||
# 5. Publish WITH token (should succeed)
|
||||
print("\n[5] Publishing WITH token...")
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "praisonai", "recipe", "publish", str(bundle_path),
|
||||
"--registry", f"http://127.0.0.1:{port}", "--token", token, "--json"],
|
||||
capture_output=True, text=True,
|
||||
cwd="/Users/praison/praisonai-package/src/praisonai"
|
||||
)
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
if data.get("ok"):
|
||||
print(f" ✓ Published: {data['name']}@{data['version']}")
|
||||
else:
|
||||
print(f" ✗ Failed: {result.stdout}")
|
||||
return 1
|
||||
except json.JSONDecodeError:
|
||||
print(f" Output: {result.stdout}")
|
||||
print(f" Stderr: {result.stderr}")
|
||||
return 1
|
||||
|
||||
# 6. List recipes
|
||||
print("\n[6] Listing recipes from registry...")
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "praisonai", "recipe", "list",
|
||||
"--registry", f"http://127.0.0.1:{port}", "--json"],
|
||||
capture_output=True, text=True,
|
||||
cwd="/Users/praison/praisonai-package/src/praisonai"
|
||||
)
|
||||
recipes = json.loads(result.stdout)
|
||||
print(f" Found {len(recipes)} recipe(s):")
|
||||
for r in recipes:
|
||||
print(f" - {r['name']} v{r['version']}: {r.get('description', '')[:40]}")
|
||||
|
||||
# 7. Search recipes
|
||||
print("\n[7] Searching for 'agent'...")
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "praisonai", "recipe", "search", "agent",
|
||||
"--registry", f"http://127.0.0.1:{port}", "--json"],
|
||||
capture_output=True, text=True,
|
||||
cwd="/Users/praison/praisonai-package/src/praisonai"
|
||||
)
|
||||
matches = json.loads(result.stdout)
|
||||
print(f" Found {len(matches)} match(es)")
|
||||
|
||||
# 8. Pull recipe
|
||||
print("\n[8] Pulling recipe...")
|
||||
pulled_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "praisonai", "recipe", "pull", "sample-agent",
|
||||
"--registry", f"http://127.0.0.1:{port}", "-o", str(pulled_dir), "--json"],
|
||||
capture_output=True, text=True,
|
||||
cwd="/Users/praison/praisonai-package/src/praisonai"
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
if data.get("ok"):
|
||||
print(f" ✓ Pulled to: {data['path']}")
|
||||
|
||||
# Verify content
|
||||
manifest_path = Path(data['path']) / "manifest.json"
|
||||
if manifest_path.exists():
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
print(f" ✓ Verified: {manifest['name']}@{manifest['version']}")
|
||||
else:
|
||||
print(f" ✗ Failed: {result.stdout}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
# Cleanup
|
||||
server.terminate()
|
||||
server.wait()
|
||||
|
||||
finally:
|
||||
# Clean up temp directory
|
||||
shutil.rmtree(work_dir, ignore_errors=True)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Recipe Registry Example
|
||||
|
||||
This example demonstrates how to:
|
||||
1. Create and publish recipes to a local registry
|
||||
2. Search and list recipes
|
||||
3. Pull recipes from the registry
|
||||
|
||||
Prerequisites:
|
||||
- pip install praisonai
|
||||
|
||||
Usage:
|
||||
python registry_example.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import tarfile
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def create_sample_recipe(tmp_dir: Path) -> Path:
|
||||
"""Create a sample recipe for demonstration."""
|
||||
recipe_dir = tmp_dir / "hello-world"
|
||||
recipe_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create TEMPLATE.yaml
|
||||
template_content = """
|
||||
name: hello-world
|
||||
version: "1.0.0"
|
||||
description: A simple hello world recipe
|
||||
author: example
|
||||
tags:
|
||||
- demo
|
||||
- hello
|
||||
- example
|
||||
|
||||
requires:
|
||||
env:
|
||||
- OPENAI_API_KEY
|
||||
|
||||
config:
|
||||
input:
|
||||
name:
|
||||
type: string
|
||||
required: true
|
||||
description: Name to greet
|
||||
"""
|
||||
|
||||
with open(recipe_dir / "TEMPLATE.yaml", "w") as f:
|
||||
f.write(template_content.strip())
|
||||
|
||||
# Create main.py
|
||||
main_content = """
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(greet("World"))
|
||||
"""
|
||||
|
||||
with open(recipe_dir / "main.py", "w") as f:
|
||||
f.write(main_content.strip())
|
||||
|
||||
return recipe_dir
|
||||
|
||||
|
||||
def pack_recipe(recipe_dir: Path, output_path: Path) -> Path:
|
||||
"""Pack a recipe into a .praison bundle."""
|
||||
import hashlib
|
||||
import io
|
||||
from datetime import datetime, timezone
|
||||
|
||||
with tarfile.open(output_path, "w:gz") as tar:
|
||||
manifest = {
|
||||
"name": "hello-world",
|
||||
"version": "1.0.0",
|
||||
"description": "A simple hello world recipe",
|
||||
"tags": ["demo", "hello", "example"],
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"files": [],
|
||||
}
|
||||
|
||||
for file_path in recipe_dir.rglob("*"):
|
||||
if file_path.is_file() and not file_path.name.startswith("."):
|
||||
rel_path = file_path.relative_to(recipe_dir)
|
||||
tar.add(file_path, arcname=str(rel_path))
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
checksum = hashlib.sha256(f.read()).hexdigest()
|
||||
manifest["files"].append({
|
||||
"path": str(rel_path),
|
||||
"checksum": checksum,
|
||||
})
|
||||
|
||||
manifest_bytes = json.dumps(manifest, indent=2).encode()
|
||||
manifest_info = tarfile.TarInfo(name="manifest.json")
|
||||
manifest_info.size = len(manifest_bytes)
|
||||
tar.addfile(manifest_info, io.BytesIO(manifest_bytes))
|
||||
|
||||
return output_path
|
||||
|
||||
|
||||
def main():
|
||||
"""Main example function."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Recipe Registry Example")
|
||||
print("=" * 60)
|
||||
|
||||
# Use a temporary directory for the example
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
tmp_path = Path(tmp_dir)
|
||||
|
||||
# Create registry in temp directory
|
||||
registry_path = tmp_path / "registry"
|
||||
|
||||
print("\n1. Creating local registry...")
|
||||
from praisonai.recipe.registry import LocalRegistry
|
||||
registry = LocalRegistry(registry_path)
|
||||
print(f" Registry created at: {registry_path}")
|
||||
|
||||
# Create and pack a sample recipe
|
||||
print("\n2. Creating sample recipe...")
|
||||
recipe_dir = create_sample_recipe(tmp_path)
|
||||
print(f" Recipe created at: {recipe_dir}")
|
||||
|
||||
print("\n3. Packing recipe into bundle...")
|
||||
bundle_path = tmp_path / "hello-world-1.0.0.praison"
|
||||
pack_recipe(recipe_dir, bundle_path)
|
||||
print(f" Bundle created: {bundle_path}")
|
||||
|
||||
# Publish to registry
|
||||
print("\n4. Publishing to registry...")
|
||||
result = registry.publish(bundle_path)
|
||||
print(f" Published: {result['name']}@{result['version']}")
|
||||
print(f" Checksum: {result['checksum'][:16]}...")
|
||||
|
||||
# List recipes
|
||||
print("\n5. Listing recipes in registry...")
|
||||
recipes = registry.list_recipes()
|
||||
for r in recipes:
|
||||
print(f" - {r['name']} ({r['version']}): {r['description']}")
|
||||
|
||||
# Search recipes
|
||||
print("\n6. Searching for 'hello'...")
|
||||
results = registry.search("hello")
|
||||
print(f" Found {len(results)} recipe(s)")
|
||||
|
||||
# Get versions
|
||||
print("\n7. Getting versions of 'hello-world'...")
|
||||
versions = registry.get_versions("hello-world")
|
||||
print(f" Versions: {versions}")
|
||||
|
||||
# Pull recipe
|
||||
print("\n8. Pulling recipe from registry...")
|
||||
output_dir = tmp_path / "pulled"
|
||||
pull_result = registry.pull("hello-world", output_dir=output_dir)
|
||||
print(f" Pulled to: {pull_result['path']}")
|
||||
|
||||
# Verify pulled files
|
||||
print("\n9. Verifying pulled files...")
|
||||
pulled_recipe = output_dir / "hello-world"
|
||||
for f in pulled_recipe.iterdir():
|
||||
print(f" - {f.name}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,43 @@
|
||||
# Run History Example
|
||||
|
||||
This example demonstrates how to use PraisonAI run history for storing, querying, and exporting recipe runs.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- Storing run results with input/output data
|
||||
- Listing and filtering runs
|
||||
- Querying run details
|
||||
- Exporting runs for replay/debugging
|
||||
- Storage statistics and cleanup
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
python run_history_example.py
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
# List recent runs
|
||||
praisonai recipe runs list
|
||||
|
||||
# List runs for a specific recipe
|
||||
praisonai recipe runs list --recipe support-reply
|
||||
|
||||
# Get storage stats
|
||||
praisonai recipe runs stats
|
||||
|
||||
# Export a run
|
||||
praisonai recipe export run-abc123 -o export.json
|
||||
|
||||
# Replay a run
|
||||
praisonai recipe replay export.json --compare
|
||||
|
||||
# Cleanup old runs
|
||||
praisonai recipe runs cleanup
|
||||
```
|
||||
|
||||
## Default Storage Location
|
||||
|
||||
Run history is stored at `~/.praison/runs`.
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Run History Example
|
||||
|
||||
This example demonstrates how to:
|
||||
1. Store recipe run results in history
|
||||
2. List and query run history
|
||||
3. Export runs for replay/debugging
|
||||
|
||||
Prerequisites:
|
||||
- pip install praisonai
|
||||
|
||||
Usage:
|
||||
python run_history_example.py
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
"""Main example function."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Run History Example")
|
||||
print("=" * 60)
|
||||
|
||||
# Use a temporary directory for the example
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
tmp_path = Path(tmp_dir)
|
||||
|
||||
# Create history storage in temp directory
|
||||
history_path = tmp_path / "runs"
|
||||
|
||||
print("\n1. Creating run history storage...")
|
||||
from praisonai.recipe.history import RunHistory
|
||||
from praisonai.recipe.models import RecipeResult, RecipeStatus
|
||||
|
||||
history = RunHistory(history_path)
|
||||
print(f" Storage created at: {history_path}")
|
||||
|
||||
# Create sample run results
|
||||
print("\n2. Storing sample run results...")
|
||||
|
||||
# Run 1: Successful run
|
||||
result1 = RecipeResult(
|
||||
run_id="run-abc123",
|
||||
recipe="support-reply",
|
||||
version="1.0.0",
|
||||
status=RecipeStatus.SUCCESS,
|
||||
output={"reply": "Thank you for contacting us..."},
|
||||
metrics={"duration_sec": 2.5, "tokens": 150},
|
||||
trace={"session_id": "session-001", "trace_id": "trace-xyz"},
|
||||
)
|
||||
|
||||
history.store(
|
||||
result=result1,
|
||||
input_data={"ticket_id": "T-123", "message": "I need help"},
|
||||
)
|
||||
print(f" Stored: {result1.run_id} (success)")
|
||||
|
||||
# Run 2: Failed run
|
||||
result2 = RecipeResult(
|
||||
run_id="run-def456",
|
||||
recipe="code-review",
|
||||
version="2.0.0",
|
||||
status=RecipeStatus.FAILED,
|
||||
error="Timeout exceeded",
|
||||
metrics={"duration_sec": 30.0},
|
||||
trace={"session_id": "session-002"},
|
||||
)
|
||||
|
||||
history.store(
|
||||
result=result2,
|
||||
input_data={"code": "def hello(): pass"},
|
||||
)
|
||||
print(f" Stored: {result2.run_id} (failed)")
|
||||
|
||||
# Run 3: Another successful run
|
||||
result3 = RecipeResult(
|
||||
run_id="run-ghi789",
|
||||
recipe="support-reply",
|
||||
version="1.0.0",
|
||||
status=RecipeStatus.SUCCESS,
|
||||
output={"reply": "Your issue has been resolved..."},
|
||||
metrics={"duration_sec": 1.8},
|
||||
trace={"session_id": "session-001"},
|
||||
)
|
||||
|
||||
history.store(
|
||||
result=result3,
|
||||
input_data={"ticket_id": "T-456"},
|
||||
)
|
||||
print(f" Stored: {result3.run_id} (success)")
|
||||
|
||||
# List all runs
|
||||
print("\n3. Listing all runs...")
|
||||
runs = history.list_runs()
|
||||
for run in runs:
|
||||
status_icon = "✓" if run["status"] == "success" else "✗"
|
||||
print(f" {status_icon} {run['run_id']} - {run['recipe']} ({run['status']})")
|
||||
|
||||
# Filter by recipe
|
||||
print("\n4. Filtering runs by recipe 'support-reply'...")
|
||||
filtered = history.list_runs(recipe="support-reply")
|
||||
print(f" Found {len(filtered)} run(s)")
|
||||
|
||||
# Filter by session
|
||||
print("\n5. Filtering runs by session 'session-001'...")
|
||||
filtered = history.list_runs(session_id="session-001")
|
||||
print(f" Found {len(filtered)} run(s)")
|
||||
|
||||
# Get specific run details
|
||||
print("\n6. Getting details for run-abc123...")
|
||||
run_data = history.get("run-abc123")
|
||||
print(f" Recipe: {run_data['recipe']}")
|
||||
print(f" Status: {run_data['status']}")
|
||||
print(f" Input: {run_data.get('input', {})}")
|
||||
print(f" Output: {run_data.get('output', {})}")
|
||||
|
||||
# Export a run
|
||||
print("\n7. Exporting run-abc123...")
|
||||
export_path = history.export("run-abc123", tmp_path / "export.json")
|
||||
print(f" Exported to: {export_path}")
|
||||
|
||||
# Show export contents
|
||||
import json
|
||||
with open(export_path) as f:
|
||||
export_data = json.load(f)
|
||||
print(f" Format: {export_data['format']}")
|
||||
print(f" Exported at: {export_data['exported_at']}")
|
||||
|
||||
# Get storage stats
|
||||
print("\n8. Getting storage statistics...")
|
||||
stats = history.get_stats()
|
||||
print(f" Total runs: {stats['total_runs']}")
|
||||
print(f" Storage size: {stats['total_size_bytes']} bytes")
|
||||
|
||||
# Cleanup old runs
|
||||
print("\n9. Running cleanup...")
|
||||
deleted = history.cleanup(retention_days=0) # Delete all for demo
|
||||
print(f" Deleted {deleted} run(s)")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
# Security Features Example
|
||||
|
||||
This example demonstrates PraisonAI security features including SBOM generation, lockfile validation, dependency auditing, and PII redaction.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- **SBOM Generation**: Generate Software Bill of Materials in CycloneDX or SPDX format
|
||||
- **Lockfile Validation**: Validate presence and format of dependency lockfiles
|
||||
- **Dependency Auditing**: Audit dependencies for vulnerabilities
|
||||
- **PII Redaction**: Detect and redact personally identifiable information
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
python security_example.py
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### SBOM Generation
|
||||
|
||||
```bash
|
||||
# Generate CycloneDX SBOM
|
||||
praisonai recipe sbom ./my-recipe --format cyclonedx -o sbom.json
|
||||
|
||||
# Generate SPDX SBOM
|
||||
praisonai recipe sbom ./my-recipe --format spdx -o sbom.json
|
||||
```
|
||||
|
||||
### Dependency Auditing
|
||||
|
||||
```bash
|
||||
# Audit dependencies
|
||||
praisonai recipe audit ./my-recipe
|
||||
|
||||
# Strict audit (fail on issues)
|
||||
praisonai recipe audit ./my-recipe --strict
|
||||
```
|
||||
|
||||
### Lockfile Validation
|
||||
|
||||
```bash
|
||||
# Validate recipe (includes lockfile check)
|
||||
praisonai recipe validate ./my-recipe --require-lockfile
|
||||
```
|
||||
|
||||
## PII Redaction Configuration
|
||||
|
||||
In your `TEMPLATE.yaml`:
|
||||
|
||||
```yaml
|
||||
data_policy:
|
||||
pii:
|
||||
mode: redact # allow, deny, or redact
|
||||
fields:
|
||||
- email
|
||||
- phone
|
||||
- ssn
|
||||
- credit_card
|
||||
```
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Security Features Example
|
||||
|
||||
This example demonstrates:
|
||||
1. SBOM (Software Bill of Materials) generation
|
||||
2. Lockfile validation
|
||||
3. Dependency auditing
|
||||
4. PII redaction
|
||||
|
||||
Prerequisites:
|
||||
- pip install praisonai
|
||||
|
||||
Usage:
|
||||
python security_example.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def create_sample_recipe(tmp_dir: Path) -> Path:
|
||||
"""Create a sample recipe with dependencies."""
|
||||
recipe_dir = tmp_dir / "secure-recipe"
|
||||
recipe_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create TEMPLATE.yaml
|
||||
import yaml
|
||||
template = {
|
||||
"name": "secure-recipe",
|
||||
"version": "1.0.0",
|
||||
"description": "A recipe with security features",
|
||||
"requires": {
|
||||
"tools": ["web_search", "file_reader"],
|
||||
"external": ["ffmpeg"],
|
||||
"env": ["OPENAI_API_KEY"],
|
||||
},
|
||||
}
|
||||
|
||||
with open(recipe_dir / "TEMPLATE.yaml", "w") as f:
|
||||
yaml.dump(template, f)
|
||||
|
||||
# Create lock directory with requirements
|
||||
lock_dir = recipe_dir / "lock"
|
||||
lock_dir.mkdir()
|
||||
|
||||
with open(lock_dir / "requirements.lock", "w") as f:
|
||||
f.write("openai==1.0.0\n")
|
||||
f.write("requests==2.31.0\n")
|
||||
f.write("pydantic==2.0.0\n")
|
||||
|
||||
return recipe_dir
|
||||
|
||||
|
||||
def main():
|
||||
"""Main example function."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Security Features Example")
|
||||
print("=" * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
tmp_path = Path(tmp_dir)
|
||||
|
||||
# Create sample recipe
|
||||
print("\n1. Creating sample recipe with dependencies...")
|
||||
recipe_dir = create_sample_recipe(tmp_path)
|
||||
print(f" Recipe created at: {recipe_dir}")
|
||||
|
||||
# =====================================================
|
||||
# SBOM Generation
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("SBOM Generation")
|
||||
print("=" * 40)
|
||||
|
||||
from praisonai.recipe.security import generate_sbom
|
||||
|
||||
print("\n2. Generating CycloneDX SBOM...")
|
||||
sbom = generate_sbom(recipe_dir, format="cyclonedx")
|
||||
print(f" Format: {sbom['bomFormat']}")
|
||||
print(f" Spec Version: {sbom['specVersion']}")
|
||||
print(f" Components: {len(sbom['components'])}")
|
||||
|
||||
print("\n Components:")
|
||||
for comp in sbom["components"][:5]:
|
||||
print(f" - {comp['name']}@{comp['version']} ({comp['type']})")
|
||||
|
||||
# Save SBOM
|
||||
sbom_path = tmp_path / "sbom.json"
|
||||
with open(sbom_path, "w") as f:
|
||||
json.dump(sbom, f, indent=2)
|
||||
print(f"\n SBOM saved to: {sbom_path}")
|
||||
|
||||
# =====================================================
|
||||
# Lockfile Validation
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Lockfile Validation")
|
||||
print("=" * 40)
|
||||
|
||||
from praisonai.recipe.security import validate_lockfile
|
||||
|
||||
print("\n3. Validating lockfile...")
|
||||
result = validate_lockfile(recipe_dir)
|
||||
print(f" Valid: {result['valid']}")
|
||||
print(f" Lockfile: {result['lockfile']}")
|
||||
print(f" Type: {result['lockfile_type']}")
|
||||
|
||||
# Test strict validation without lockfile
|
||||
print("\n4. Testing strict validation on recipe without lockfile...")
|
||||
no_lock_dir = tmp_path / "no-lock-recipe"
|
||||
no_lock_dir.mkdir()
|
||||
|
||||
result = validate_lockfile(no_lock_dir, strict=True)
|
||||
print(f" Valid: {result['valid']}")
|
||||
print(f" Errors: {result['errors']}")
|
||||
|
||||
# =====================================================
|
||||
# Dependency Auditing
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("Dependency Auditing")
|
||||
print("=" * 40)
|
||||
|
||||
from praisonai.recipe.security import audit_dependencies
|
||||
|
||||
print("\n5. Auditing dependencies...")
|
||||
report = audit_dependencies(recipe_dir)
|
||||
print(f" Recipe: {report['recipe']}")
|
||||
print(f" Lockfile: {report['lockfile']}")
|
||||
print(f" Dependencies: {len(report['dependencies'])}")
|
||||
print(f" Vulnerabilities: {len(report['vulnerabilities'])}")
|
||||
print(f" Warnings: {len(report['warnings'])}")
|
||||
print(f" Passed: {report['passed']}")
|
||||
|
||||
if report["warnings"]:
|
||||
print("\n Warnings:")
|
||||
for warn in report["warnings"]:
|
||||
print(f" - {warn}")
|
||||
|
||||
# =====================================================
|
||||
# PII Redaction
|
||||
# =====================================================
|
||||
print("\n" + "=" * 40)
|
||||
print("PII Redaction")
|
||||
print("=" * 40)
|
||||
|
||||
from praisonai.recipe.security import redact_pii, detect_pii
|
||||
|
||||
# Sample data with PII
|
||||
sample_data = {
|
||||
"customer": {
|
||||
"name": "John Doe",
|
||||
"email": "[email protected]",
|
||||
"phone": "555-123-4567",
|
||||
"message": "Please contact me at [email protected]",
|
||||
},
|
||||
"ticket_id": "T-12345",
|
||||
}
|
||||
|
||||
print("\n6. Detecting PII in sample data...")
|
||||
detections = detect_pii(sample_data)
|
||||
print(f" Found {len(detections)} PII instance(s):")
|
||||
for d in detections:
|
||||
print(f" - {d['type']} at {d['path']}")
|
||||
|
||||
print("\n7. Redacting PII...")
|
||||
policy = {
|
||||
"pii": {
|
||||
"mode": "redact",
|
||||
"fields": ["email", "phone"],
|
||||
}
|
||||
}
|
||||
|
||||
redacted = redact_pii(sample_data, policy)
|
||||
print(" Original email:", sample_data["customer"]["email"])
|
||||
print(" Redacted email:", redacted["customer"]["email"])
|
||||
print(" Original phone:", sample_data["customer"]["phone"])
|
||||
print(" Redacted phone:", redacted["customer"]["phone"])
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,142 @@
|
||||
# PraisonAI Serve Examples
|
||||
|
||||
Examples for all PraisonAI server types and the unified endpoints CLI.
|
||||
|
||||
## Examples
|
||||
|
||||
| Example | Description | CLI Command |
|
||||
|---------|-------------|-------------|
|
||||
| `unified_server.py` | All providers in one server | `praisonai serve unified` |
|
||||
| `agent_as_api_single.py` | Single agent HTTP API | `praisonai serve agents` |
|
||||
| `agents_as_api_router.py` | Multi-agent router API | `praisonai serve agents` |
|
||||
| `a2a_server_client.py` | A2A protocol server | `praisonai serve a2a` |
|
||||
| `a2u_events_stream.py` | A2U event stream | `praisonai serve a2u` |
|
||||
| `mcp_http_server.py` | MCP HTTP server | `praisonai serve mcp` |
|
||||
| `tools_as_mcp_server.py` | Tools as MCP server | `praisonai serve tools` |
|
||||
| `agent_launch_modes.py` | Agent.launch() API | Python only |
|
||||
| `endpoints_unified_client.py` | Unified client | `praisonai endpoints` |
|
||||
| `serve_example.py` | Recipe server client | `praisonai recipe serve` |
|
||||
|
||||
## Server Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `praisonai serve unified --port 8765` | Start unified server |
|
||||
| `praisonai serve agents --file agents.yaml --port 8000` | Start agents API |
|
||||
| `praisonai serve recipe --port 8765` | Start recipe server |
|
||||
| `praisonai serve mcp --transport http --port 8080` | Start MCP server |
|
||||
| `praisonai serve tools --port 8081` | Start tools MCP server |
|
||||
| `praisonai serve a2a --port 8082` | Start A2A server |
|
||||
| `praisonai serve a2u --port 8083` | Start A2U server |
|
||||
|
||||
## Client Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `praisonai endpoints list` | List all endpoints |
|
||||
| `praisonai endpoints list --type agents-api` | Filter by type |
|
||||
| `praisonai endpoints describe <name>` | Get endpoint details |
|
||||
| `praisonai endpoints invoke <name> --input-json '{}'` | Invoke endpoint |
|
||||
| `praisonai endpoints health` | Check server health |
|
||||
| `praisonai endpoints types` | List provider types |
|
||||
| `praisonai endpoints discovery` | Show discovery document |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start unified server
|
||||
export OPENAI_API_KEY=your-key
|
||||
praisonai serve unified --port 8765
|
||||
|
||||
# Terminal 2: Use endpoints CLI
|
||||
praisonai endpoints health
|
||||
praisonai endpoints list
|
||||
praisonai endpoints types
|
||||
```
|
||||
|
||||
## Provider Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `recipe` | Recipe runner endpoints |
|
||||
| `agents-api` | Single/multi-agent HTTP API |
|
||||
| `mcp` | MCP server (stdio, http, sse) |
|
||||
| `tools-mcp` | Tools exposed as MCP server |
|
||||
| `a2a` | Agent-to-agent protocol |
|
||||
| `a2u` | Agent-to-user event stream |
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `host` | Server bind address | 127.0.0.1 |
|
||||
| `port` | Server port | 8765 |
|
||||
| `auth` | Auth type (none, api-key, jwt) | none |
|
||||
| `api_key` | API key for auth | - |
|
||||
| `preload` | Preload recipes on startup | false |
|
||||
| `recipes` | List of recipes to serve | all |
|
||||
| `cors_origins` | CORS allowed origins | - |
|
||||
|
||||
## Advanced Features
|
||||
|
||||
| Feature | CLI Flag | Description | Docs |
|
||||
|---------|----------|-------------|------|
|
||||
| Rate Limiting | `--rate_limit 100` | Requests per minute per client | [Code](/docs/features/recipe-serve-advanced) [CLI](/docs/cli/recipe-serve-advanced) |
|
||||
| Request Size Limit | `--max_request_size 10485760` | Max request body (bytes) | [Code](/docs/features/recipe-serve-advanced) [CLI](/docs/cli/recipe-serve-advanced) |
|
||||
| Metrics Endpoint | `--enable_metrics` | Prometheus /metrics endpoint | [Code](/docs/features/recipe-serve-advanced) [CLI](/docs/cli/recipe-serve-advanced) |
|
||||
| Admin Reload | `--enable_admin` | Hot reload /admin/reload | [Code](/docs/features/recipe-serve-advanced) [CLI](/docs/cli/recipe-serve-advanced) |
|
||||
| Workers | `--workers 4` | Multi-process workers | [Code](/docs/features/recipe-serve-advanced) [CLI](/docs/cli/recipe-serve-advanced) |
|
||||
| OpenTelemetry | `--trace_exporter otlp` | Distributed tracing | [Code](/docs/features/recipe-serve-advanced) [CLI](/docs/cli/recipe-serve-advanced) |
|
||||
| OpenAPI | `GET /openapi.json` | API specification | [Code](/docs/features/recipe-serve-advanced) [CLI](/docs/cli/recipe-serve-advanced) |
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
| Example | Description |
|
||||
|---------|-------------|
|
||||
| `recipe_serve_features.py` | All advanced features demo |
|
||||
|
||||
## Advanced CLI Commands
|
||||
|
||||
```bash
|
||||
# Production server with all features
|
||||
praisonai recipe serve \
|
||||
--host 0.0.0.0 \
|
||||
--port 8765 \
|
||||
--auth api-key \
|
||||
--workers 4 \
|
||||
--rate_limit 100 \
|
||||
--enable_metrics \
|
||||
--enable_admin
|
||||
|
||||
# Get metrics
|
||||
curl http://localhost:8765/metrics
|
||||
|
||||
# Get OpenAPI spec
|
||||
curl http://localhost:8765/openapi.json
|
||||
|
||||
# Hot reload recipes
|
||||
curl -X POST http://localhost:8765/admin/reload \
|
||||
-H "X-API-Key: your-key"
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
praisonai endpoints health
|
||||
|
||||
# List recipes (with auth)
|
||||
praisonai endpoints list --api-key your-key
|
||||
|
||||
# Invoke recipe
|
||||
praisonai endpoints invoke my-recipe \
|
||||
--input-json '{"query": "Hello"}' \
|
||||
--api-key your-key \
|
||||
--json
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Always use `--auth api-key` when binding to `0.0.0.0`
|
||||
- Store API keys in environment variables, not config files
|
||||
- Use HTTPS in production (via reverse proxy)
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
A2A (Agent-to-Agent) Server and Client Example
|
||||
|
||||
Demonstrates:
|
||||
- Launching an agent as A2A server
|
||||
- Agent card discovery
|
||||
- Sending A2A messages
|
||||
- JSON-RPC protocol
|
||||
|
||||
Usage:
|
||||
# Run this example (starts server)
|
||||
python a2a_server_client.py
|
||||
|
||||
# Or use CLI:
|
||||
praisonai serve a2a --port 8082
|
||||
|
||||
# Test with curl:
|
||||
curl http://localhost:8082/.well-known/agent.json
|
||||
curl -X POST http://localhost:8082/a2a \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","method":"message/send","id":"1","params":{"message":{"role":"user","parts":[{"type":"text","text":"Hello!"}]}}}'
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Error: OPENAI_API_KEY environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run_server():
|
||||
"""Run A2A server."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.ui.a2a import A2A
|
||||
from fastapi import FastAPI
|
||||
import uvicorn
|
||||
except ImportError as e:
|
||||
print(f"Error: Missing dependency - {e}")
|
||||
print("Install with: pip install 'praisonaiagents[api]'")
|
||||
sys.exit(1)
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name="A2A-Assistant",
|
||||
role="Helpful AI Assistant",
|
||||
goal="Help other agents and users with their questions",
|
||||
backstory="You are a knowledgeable assistant accessible via A2A protocol.",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create A2A interface
|
||||
a2a = A2A(
|
||||
agent=agent,
|
||||
url="http://localhost:8082/a2a",
|
||||
name="A2A-Assistant",
|
||||
description="A helpful AI assistant accessible via A2A protocol"
|
||||
)
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(title="PraisonAI A2A Server")
|
||||
app.include_router(a2a.get_router())
|
||||
|
||||
# Add discovery endpoint
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": "praisonai-a2a",
|
||||
"providers": [{"type": "a2a", "name": "A2A Protocol", "capabilities": ["agent-card", "message-send"]}],
|
||||
"endpoints": [{"name": "a2a", "provider_type": "a2a", "streaming": ["sse"]}]
|
||||
}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "PraisonAI A2A Server",
|
||||
"agent_card": "/.well-known/agent.json",
|
||||
"a2a_endpoint": "/a2a",
|
||||
"discovery": "/__praisonai__/discovery"
|
||||
}
|
||||
|
||||
print("Starting A2A server on http://localhost:8082")
|
||||
print("Agent Card: http://localhost:8082/.well-known/agent.json")
|
||||
print("A2A Endpoint: http://localhost:8082/a2a")
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8082, log_level="warning")
|
||||
|
||||
|
||||
def run_client():
|
||||
"""Run A2A client test."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
base_url = "http://localhost:8082"
|
||||
|
||||
print("\n--- A2A Client Test ---")
|
||||
|
||||
# 1. Get agent card
|
||||
print("\n1. Getting Agent Card...")
|
||||
try:
|
||||
req = urllib.request.Request(f"{base_url}/.well-known/agent.json")
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
agent_card = json.loads(resp.read().decode())
|
||||
print(f" Agent: {agent_card.get('name')}")
|
||||
print(f" URL: {agent_card.get('url')}")
|
||||
print(f" Capabilities: {agent_card.get('capabilities', {})}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
return
|
||||
|
||||
# 2. Send A2A message
|
||||
print("\n2. Sending A2A Message...")
|
||||
message = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "message/send",
|
||||
"id": "test-1",
|
||||
"params": {
|
||||
"message": {
|
||||
"role": "user",
|
||||
"parts": [{"type": "text", "text": "Hello! What can you help me with?"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{base_url}/a2a",
|
||||
data=json.dumps(message).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
response = json.loads(resp.read().decode())
|
||||
if "result" in response:
|
||||
result_msg = response["result"].get("message", {})
|
||||
parts = result_msg.get("parts", [])
|
||||
for part in parts:
|
||||
if part.get("type") == "text":
|
||||
print(f" Response: {part.get('text', '')[:200]}...")
|
||||
elif "error" in response:
|
||||
print(f" Error: {response['error']}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
# 3. Check discovery
|
||||
print("\n3. Checking Discovery...")
|
||||
try:
|
||||
req = urllib.request.Request(f"{base_url}/__praisonai__/discovery")
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
discovery = json.loads(resp.read().decode())
|
||||
print(f" Server: {discovery.get('server_name')}")
|
||||
print(f" Providers: {[p.get('type') for p in discovery.get('providers', [])]}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
print("\n✓ A2A client test completed!")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("A2A (Agent-to-Agent) Server and Client Example")
|
||||
print("=" * 60)
|
||||
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "server"
|
||||
|
||||
if mode == "client":
|
||||
run_client()
|
||||
elif mode == "test":
|
||||
# Start server in background, run client, then stop
|
||||
print("Starting server in background...")
|
||||
server_thread = threading.Thread(target=run_server, daemon=True)
|
||||
server_thread.start()
|
||||
time.sleep(3) # Wait for server to start
|
||||
run_client()
|
||||
else:
|
||||
print("\nStarting A2A server...")
|
||||
print("Press Ctrl+C to stop")
|
||||
print("\nTo test, run in another terminal:")
|
||||
print(" python a2a_server_client.py client")
|
||||
run_server()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
A2U (Agent-to-User) Event Stream Example
|
||||
|
||||
Demonstrates:
|
||||
- Launching an A2U event stream server
|
||||
- Subscribing to agent events
|
||||
- SSE (Server-Sent Events) streaming
|
||||
- Event filtering
|
||||
|
||||
Usage:
|
||||
# Run this example (starts server)
|
||||
python a2u_events_stream.py
|
||||
|
||||
# Or use CLI:
|
||||
praisonai serve a2u --port 8083
|
||||
|
||||
# Test with curl:
|
||||
curl http://localhost:8083/a2u/info
|
||||
curl -N http://localhost:8083/a2u/events/events
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Error: OPENAI_API_KEY environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run_server():
|
||||
"""Run A2U server."""
|
||||
try:
|
||||
from fastapi import FastAPI
|
||||
import uvicorn
|
||||
from praisonai.endpoints.a2u_server import (
|
||||
create_a2u_routes,
|
||||
emit_agent_started,
|
||||
emit_agent_thinking,
|
||||
emit_agent_response,
|
||||
emit_agent_completed,
|
||||
)
|
||||
except ImportError as e:
|
||||
print(f"Error: Missing dependency - {e}")
|
||||
print("Install with: pip install praisonai[serve]")
|
||||
sys.exit(1)
|
||||
|
||||
app = FastAPI(title="PraisonAI A2U Server")
|
||||
|
||||
# Add A2U routes
|
||||
create_a2u_routes(app)
|
||||
|
||||
# Add discovery endpoint
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": "praisonai-a2u",
|
||||
"providers": [{"type": "a2u", "name": "A2U Event Stream", "capabilities": ["subscribe", "stream"]}],
|
||||
"endpoints": [{"name": "events", "provider_type": "a2u", "streaming": ["sse"]}]
|
||||
}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "PraisonAI A2U Server",
|
||||
"info": "/a2u/info",
|
||||
"events": "/a2u/events/<stream>",
|
||||
"discovery": "/__praisonai__/discovery"
|
||||
}
|
||||
|
||||
# Demo endpoint to emit events
|
||||
@app.post("/demo/emit")
|
||||
async def demo_emit():
|
||||
"""Emit demo events for testing."""
|
||||
agent_id = "demo-agent-1"
|
||||
emit_agent_started(agent_id, "DemoAgent")
|
||||
emit_agent_thinking(agent_id, "Processing your request...")
|
||||
emit_agent_response(agent_id, "Here is my response to your query.")
|
||||
emit_agent_completed(agent_id, {"status": "success"})
|
||||
return {"message": "Demo events emitted", "agent_id": agent_id}
|
||||
|
||||
print("Starting A2U server on http://localhost:8083")
|
||||
print("Info: http://localhost:8083/a2u/info")
|
||||
print("Events: http://localhost:8083/a2u/events/events")
|
||||
print("Demo: POST http://localhost:8083/demo/emit")
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8083, log_level="warning")
|
||||
|
||||
|
||||
def run_client():
|
||||
"""Run A2U client test."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
base_url = "http://localhost:8083"
|
||||
|
||||
print("\n--- A2U Client Test ---")
|
||||
|
||||
# 1. Get A2U info
|
||||
print("\n1. Getting A2U Info...")
|
||||
try:
|
||||
req = urllib.request.Request(f"{base_url}/a2u/info")
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
info = json.loads(resp.read().decode())
|
||||
print(f" Name: {info.get('name')}")
|
||||
print(f" Version: {info.get('version')}")
|
||||
print(f" Streams: {info.get('streams')}")
|
||||
print(f" Event Types: {info.get('event_types')}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
return
|
||||
|
||||
# 2. Subscribe to events
|
||||
print("\n2. Subscribing to Events...")
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{base_url}/a2u/subscribe",
|
||||
data=json.dumps({"stream": "events", "filters": []}).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
sub = json.loads(resp.read().decode())
|
||||
print(f" Subscription ID: {sub.get('subscription_id')}")
|
||||
print(f" Stream URL: {sub.get('stream_url')}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
# 3. Emit demo events
|
||||
print("\n3. Emitting Demo Events...")
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{base_url}/demo/emit",
|
||||
data=b"{}",
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
result = json.loads(resp.read().decode())
|
||||
print(f" {result.get('message')}")
|
||||
print(f" Agent ID: {result.get('agent_id')}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
# 4. Check discovery
|
||||
print("\n4. Checking Discovery...")
|
||||
try:
|
||||
req = urllib.request.Request(f"{base_url}/__praisonai__/discovery")
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
discovery = json.loads(resp.read().decode())
|
||||
print(f" Server: {discovery.get('server_name')}")
|
||||
print(f" Providers: {[p.get('type') for p in discovery.get('providers', [])]}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
print("\n✓ A2U client test completed!")
|
||||
print("\nTo stream events, run:")
|
||||
print(f" curl -N {base_url}/a2u/events/events")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("A2U (Agent-to-User) Event Stream Example")
|
||||
print("=" * 60)
|
||||
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "server"
|
||||
|
||||
if mode == "client":
|
||||
run_client()
|
||||
elif mode == "test":
|
||||
print("Starting server in background...")
|
||||
server_thread = threading.Thread(target=run_server, daemon=True)
|
||||
server_thread.start()
|
||||
time.sleep(2)
|
||||
run_client()
|
||||
else:
|
||||
print("\nStarting A2U server...")
|
||||
print("Press Ctrl+C to stop")
|
||||
print("\nTo test, run in another terminal:")
|
||||
print(" python a2u_events_stream.py client")
|
||||
run_server()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Single Agent as HTTP API Example
|
||||
|
||||
Demonstrates launching a single agent as an HTTP API server with:
|
||||
- REST endpoint for queries
|
||||
- Streaming support (SSE)
|
||||
- Discovery endpoint
|
||||
- Health check
|
||||
|
||||
Usage:
|
||||
# Run this example
|
||||
python agent_as_api_single.py
|
||||
|
||||
# Or use CLI:
|
||||
praisonai serve agents --file agents.yaml --port 8000
|
||||
|
||||
# Test with curl:
|
||||
curl -X POST http://localhost:8000/agent \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "What is AI?"}'
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Ensure API key is set
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Error: OPENAI_API_KEY environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Single Agent as HTTP API Example")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
except ImportError:
|
||||
print("Error: praisonaiagents not installed")
|
||||
print("Install with: pip install praisonaiagents")
|
||||
sys.exit(1)
|
||||
|
||||
# Create a single agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
role="Helpful AI Assistant",
|
||||
goal="Help users with their questions accurately and concisely",
|
||||
backstory="You are a knowledgeable assistant who provides clear, helpful answers.",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Role: {agent.role}")
|
||||
|
||||
# Launch as HTTP API
|
||||
print("\nLaunching agent as HTTP API...")
|
||||
print("Endpoints:")
|
||||
print(" POST /agent - Query the agent")
|
||||
print(" GET /health - Health check")
|
||||
print(" GET /__praisonai__/discovery - Discovery document")
|
||||
print("\nPress Ctrl+C to stop")
|
||||
|
||||
try:
|
||||
# Use Agent.launch() to start HTTP server
|
||||
agent.launch(
|
||||
port=8000,
|
||||
host="127.0.0.1",
|
||||
path="/agent"
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer stopped")
|
||||
except Exception as e:
|
||||
print(f"Error launching server: {e}")
|
||||
# Fallback: manual FastAPI setup
|
||||
print("\nFallback: Using manual FastAPI setup...")
|
||||
launch_manual(agent)
|
||||
|
||||
|
||||
def launch_manual(agent):
|
||||
"""Manual FastAPI server setup as fallback."""
|
||||
try:
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
import uvicorn
|
||||
except ImportError:
|
||||
print("Error: FastAPI/uvicorn not installed")
|
||||
print("Install with: pip install fastapi uvicorn")
|
||||
return
|
||||
|
||||
app = FastAPI(title="PraisonAI Agent API")
|
||||
|
||||
@app.post("/agent")
|
||||
async def query_agent(request: Request):
|
||||
try:
|
||||
body = await request.json()
|
||||
query = body.get("query", "")
|
||||
if not query:
|
||||
return JSONResponse({"error": "query required"}, status_code=400)
|
||||
|
||||
response = agent.chat(query)
|
||||
return {"response": response}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "agent": agent.name}
|
||||
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": "praisonai-agent",
|
||||
"providers": [{"type": "agents-api", "name": "Single Agent API"}],
|
||||
"endpoints": [{"name": "agent", "provider_type": "agents-api"}]
|
||||
}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": f"PraisonAI Agent: {agent.name}",
|
||||
"endpoints": ["/agent", "/health", "/__praisonai__/discovery"]
|
||||
}
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent.launch() and Agents.launch() Example
|
||||
|
||||
Demonstrates the high-level Python API for launching servers:
|
||||
- Agent.launch() for single agent
|
||||
- Agents.launch() for multi-agent
|
||||
- Different server modes (HTTP, MCP, A2A)
|
||||
|
||||
Usage:
|
||||
# Run this example
|
||||
python agent_launch_modes.py
|
||||
|
||||
# Test endpoints:
|
||||
curl http://localhost:8000/agent
|
||||
curl http://localhost:8000/health
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Error: OPENAI_API_KEY environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def demo_single_agent_launch():
|
||||
"""Demo Agent.launch() for single agent."""
|
||||
print("\n--- Single Agent Launch ---")
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
except ImportError:
|
||||
print("Error: praisonaiagents not installed")
|
||||
return False
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
role="Helpful AI Assistant",
|
||||
goal="Help users with their questions",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print(f"Agent: {agent.name}")
|
||||
print("Launching as HTTP API on port 8000...")
|
||||
print("\nEndpoints:")
|
||||
print(" POST /agent - Query the agent")
|
||||
print(" GET /health - Health check")
|
||||
print("\nPress Ctrl+C to stop")
|
||||
|
||||
try:
|
||||
agent.launch(port=8000, host="127.0.0.1", path="/agent")
|
||||
except AttributeError:
|
||||
print("Agent.launch() not available, using fallback...")
|
||||
launch_single_fallback(agent)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def demo_multi_agent_launch():
|
||||
"""Demo Agents.launch() for multi-agent."""
|
||||
print("\n--- Multi-Agent Launch ---")
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent, PraisonAIAgents
|
||||
except ImportError:
|
||||
print("Error: praisonaiagents not installed")
|
||||
return False
|
||||
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Specialist",
|
||||
goal="Find accurate information",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content Writer",
|
||||
goal="Create engaging content",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
agents = PraisonAIAgents(agents=[researcher, writer])
|
||||
|
||||
print(f"Agents: {[researcher.name, writer.name]}")
|
||||
print("Launching as HTTP API on port 8000...")
|
||||
print("\nEndpoints:")
|
||||
print(" POST /agents - Route to best agent")
|
||||
print(" POST /agents/researcher - Query researcher")
|
||||
print(" POST /agents/writer - Query writer")
|
||||
print(" GET /agents/list - List agents")
|
||||
print("\nPress Ctrl+C to stop")
|
||||
|
||||
try:
|
||||
agents.launch(port=8000, host="127.0.0.1")
|
||||
except AttributeError:
|
||||
print("Agents.launch() not available, using fallback...")
|
||||
launch_multi_fallback([researcher, writer])
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def launch_single_fallback(agent):
|
||||
"""Fallback implementation for Agent.launch()."""
|
||||
try:
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
import uvicorn
|
||||
except ImportError:
|
||||
print("Error: FastAPI/uvicorn not installed")
|
||||
return
|
||||
|
||||
app = FastAPI(title=f"PraisonAI Agent: {agent.name}")
|
||||
|
||||
@app.post("/agent")
|
||||
async def query(request: Request):
|
||||
try:
|
||||
body = await request.json()
|
||||
query_text = body.get("query", "")
|
||||
response = agent.chat(query_text)
|
||||
return {"response": response}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "agent": agent.name}
|
||||
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": f"praisonai-{agent.name.lower()}",
|
||||
"providers": [{"type": "agents-api", "name": "Single Agent API"}],
|
||||
"endpoints": [{"name": "agent", "provider_type": "agents-api"}]
|
||||
}
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000)
|
||||
|
||||
|
||||
def launch_multi_fallback(agents):
|
||||
"""Fallback implementation for Agents.launch()."""
|
||||
try:
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
import uvicorn
|
||||
except ImportError:
|
||||
print("Error: FastAPI/uvicorn not installed")
|
||||
return
|
||||
|
||||
app = FastAPI(title="PraisonAI Multi-Agent API")
|
||||
agent_map = {a.name.lower(): a for a in agents}
|
||||
|
||||
@app.post("/agents")
|
||||
async def route(request: Request):
|
||||
try:
|
||||
body = await request.json()
|
||||
query_text = body.get("query", "")
|
||||
# Simple routing
|
||||
agent = agents[0]
|
||||
response = agent.chat(query_text)
|
||||
return {"agent": agent.name, "response": response}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.post("/agents/{name}")
|
||||
async def query_agent(name: str, request: Request):
|
||||
agent = agent_map.get(name.lower())
|
||||
if not agent:
|
||||
return JSONResponse({"error": f"Agent not found: {name}"}, status_code=404)
|
||||
try:
|
||||
body = await request.json()
|
||||
response = agent.chat(body.get("query", ""))
|
||||
return {"agent": agent.name, "response": response}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.get("/agents/list")
|
||||
async def list_agents():
|
||||
return {"agents": [{"name": a.name, "role": a.role} for a in agents]}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "agents": len(agents)}
|
||||
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": "praisonai-agents",
|
||||
"providers": [{"type": "agents-api", "name": "Multi-Agent API"}],
|
||||
"endpoints": [
|
||||
{"name": "agents", "provider_type": "agents-api"},
|
||||
*[{"name": f"agents/{a.name.lower()}", "provider_type": "agents-api"} for a in agents]
|
||||
]
|
||||
}
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Agent.launch() and Agents.launch() Example")
|
||||
print("=" * 60)
|
||||
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "single"
|
||||
|
||||
if mode == "multi":
|
||||
demo_multi_agent_launch()
|
||||
else:
|
||||
demo_single_agent_launch()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Router as HTTP API Example
|
||||
|
||||
Demonstrates launching multiple agents as an HTTP API with:
|
||||
- Router endpoint for automatic agent selection
|
||||
- Per-agent endpoints
|
||||
- Streaming support (SSE)
|
||||
- Discovery endpoint
|
||||
|
||||
Usage:
|
||||
# Run this example
|
||||
python agents_as_api_router.py
|
||||
|
||||
# Or use CLI:
|
||||
praisonai serve agents --file agents.yaml --port 8000
|
||||
|
||||
# Test with curl:
|
||||
curl -X POST http://localhost:8000/agents \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "Research AI trends"}'
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Error: OPENAI_API_KEY environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Multi-Agent Router as HTTP API Example")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent, PraisonAIAgents
|
||||
except ImportError:
|
||||
print("Error: praisonaiagents not installed")
|
||||
print("Install with: pip install praisonaiagents")
|
||||
sys.exit(1)
|
||||
|
||||
# Create multiple agents
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Specialist",
|
||||
goal="Find accurate and relevant information",
|
||||
backstory="Expert at finding and synthesizing information from various sources.",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content Writer",
|
||||
goal="Create engaging content from research",
|
||||
backstory="Skilled writer who transforms research into readable content.",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
analyst = Agent(
|
||||
name="Analyst",
|
||||
role="Data Analyst",
|
||||
goal="Analyze data and provide insights",
|
||||
backstory="Expert at analyzing data and extracting meaningful insights.",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
agents = [researcher, writer, analyst]
|
||||
|
||||
print(f"Agents: {[a.name for a in agents]}")
|
||||
|
||||
# Create multi-agent system
|
||||
praison = PraisonAIAgents(agents=agents)
|
||||
|
||||
print("\nLaunching multi-agent router as HTTP API...")
|
||||
print("Endpoints:")
|
||||
print(" POST /agents - Route to best agent")
|
||||
print(" POST /agents/researcher - Query researcher")
|
||||
print(" POST /agents/writer - Query writer")
|
||||
print(" POST /agents/analyst - Query analyst")
|
||||
print(" GET /agents/list - List all agents")
|
||||
print(" GET /health - Health check")
|
||||
print(" GET /__praisonai__/discovery - Discovery document")
|
||||
print("\nPress Ctrl+C to stop")
|
||||
|
||||
try:
|
||||
# Use Agents.launch() to start HTTP server
|
||||
praison.launch(
|
||||
port=8000,
|
||||
host="127.0.0.1"
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer stopped")
|
||||
except Exception as e:
|
||||
print(f"Error launching server: {e}")
|
||||
print("\nFallback: Using manual FastAPI setup...")
|
||||
launch_manual(agents)
|
||||
|
||||
|
||||
def launch_manual(agents):
|
||||
"""Manual FastAPI server setup as fallback."""
|
||||
try:
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
import uvicorn
|
||||
except ImportError:
|
||||
print("Error: FastAPI/uvicorn not installed")
|
||||
print("Install with: pip install fastapi uvicorn")
|
||||
return
|
||||
|
||||
app = FastAPI(title="PraisonAI Multi-Agent API")
|
||||
|
||||
# Create agent lookup
|
||||
agent_map = {a.name.lower(): a for a in agents}
|
||||
|
||||
@app.post("/agents")
|
||||
async def route_query(request: Request):
|
||||
"""Route query to best agent."""
|
||||
try:
|
||||
body = await request.json()
|
||||
query = body.get("query", "")
|
||||
if not query:
|
||||
return JSONResponse({"error": "query required"}, status_code=400)
|
||||
|
||||
# Simple routing based on keywords
|
||||
query_lower = query.lower()
|
||||
if any(w in query_lower for w in ["research", "find", "search"]):
|
||||
agent = agent_map.get("researcher", agents[0])
|
||||
elif any(w in query_lower for w in ["write", "create", "draft"]):
|
||||
agent = agent_map.get("writer", agents[0])
|
||||
elif any(w in query_lower for w in ["analyze", "data", "insight"]):
|
||||
agent = agent_map.get("analyst", agents[0])
|
||||
else:
|
||||
agent = agents[0]
|
||||
|
||||
response = agent.chat(query)
|
||||
return {"agent": agent.name, "response": response}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.post("/agents/{agent_name}")
|
||||
async def query_agent(agent_name: str, request: Request):
|
||||
"""Query specific agent."""
|
||||
agent = agent_map.get(agent_name.lower())
|
||||
if not agent:
|
||||
return JSONResponse({"error": f"Agent not found: {agent_name}"}, status_code=404)
|
||||
|
||||
try:
|
||||
body = await request.json()
|
||||
query = body.get("query", "")
|
||||
response = agent.chat(query)
|
||||
return {"agent": agent.name, "response": response}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.get("/agents/list")
|
||||
async def list_agents():
|
||||
return {"agents": [{"name": a.name, "role": a.role} for a in agents]}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "agents": len(agents)}
|
||||
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": "praisonai-agents-router",
|
||||
"providers": [{"type": "agents-api", "name": "Multi-Agent Router"}],
|
||||
"endpoints": [
|
||||
{"name": "agents", "provider_type": "agents-api"},
|
||||
*[{"name": f"agents/{a.name.lower()}", "provider_type": "agents-api"} for a in agents]
|
||||
]
|
||||
}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "PraisonAI Multi-Agent Router",
|
||||
"agents": [a.name for a in agents],
|
||||
"endpoints": ["/agents", "/agents/list", "/health"]
|
||||
}
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unified Endpoints Client Example
|
||||
|
||||
Demonstrates using the praisonai endpoints CLI to interact with any PraisonAI server.
|
||||
Works with recipe, agents-api, mcp, a2a, a2u, and tools-mcp servers.
|
||||
|
||||
Usage:
|
||||
# Start any server first (e.g., unified server)
|
||||
praisonai serve unified --port 8765
|
||||
|
||||
# Then run this example
|
||||
python endpoints_unified_client.py
|
||||
|
||||
# Or use CLI directly:
|
||||
praisonai endpoints list
|
||||
praisonai endpoints types
|
||||
praisonai endpoints health
|
||||
praisonai endpoints discovery
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def run_cli(args: list) -> tuple:
|
||||
"""Run praisonai endpoints CLI command."""
|
||||
cmd = ["python3", "-m", "praisonai.cli.main", "endpoints"] + args
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return result.returncode, result.stdout, result.stderr
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("PraisonAI Unified Endpoints Client Example")
|
||||
print("=" * 60)
|
||||
|
||||
server_url = os.environ.get("PRAISONAI_ENDPOINTS_URL", "http://localhost:8765")
|
||||
print(f"Server URL: {server_url}")
|
||||
|
||||
# 1. List provider types
|
||||
print("\n--- Provider Types ---")
|
||||
code, out, err = run_cli(["types", "--format", "json"])
|
||||
if code == 0:
|
||||
types = json.loads(out)
|
||||
print(f"Supported types: {[t['type'] for t in types]}")
|
||||
else:
|
||||
print(f"Error: {err}")
|
||||
|
||||
# 2. Health check
|
||||
print("\n--- Health Check ---")
|
||||
code, out, err = run_cli(["health", "--url", server_url])
|
||||
if code == 0:
|
||||
print("✓ Server is healthy")
|
||||
print(out.strip())
|
||||
elif code == 8:
|
||||
print("✗ Connection error - server not running")
|
||||
print("\nStart a server first:")
|
||||
print(" praisonai serve unified --port 8765")
|
||||
print(" praisonai serve agents --file agents.yaml --port 8765")
|
||||
print(" praisonai serve a2a --port 8765")
|
||||
return 1
|
||||
else:
|
||||
print(f"✗ Health check failed: {err}")
|
||||
return 1
|
||||
|
||||
# 3. Discovery document
|
||||
print("\n--- Discovery Document ---")
|
||||
code, out, err = run_cli(["discovery", "--url", server_url])
|
||||
if code == 0:
|
||||
try:
|
||||
discovery = json.loads(out)
|
||||
print(f"Server: {discovery.get('server_name', 'unknown')}")
|
||||
print(f"Schema version: {discovery.get('schema_version', 'unknown')}")
|
||||
providers = discovery.get('providers', [])
|
||||
print(f"Providers: {[p.get('type') for p in providers]}")
|
||||
endpoints = discovery.get('endpoints', [])
|
||||
print(f"Endpoints: {[e.get('name') for e in endpoints]}")
|
||||
except json.JSONDecodeError:
|
||||
print(out.strip())
|
||||
else:
|
||||
print(f"Discovery not available: {err}")
|
||||
|
||||
# 4. List endpoints
|
||||
print("\n--- List Endpoints ---")
|
||||
code, out, err = run_cli(["list", "--url", server_url, "--format", "json"])
|
||||
if code == 0:
|
||||
try:
|
||||
endpoints = json.loads(out)
|
||||
if isinstance(endpoints, list):
|
||||
for ep in endpoints[:5]: # Show first 5
|
||||
print(f" - {ep.get('name')}: {ep.get('description', '')[:50]}")
|
||||
else:
|
||||
print(out.strip()[:200])
|
||||
except json.JSONDecodeError:
|
||||
print(out.strip()[:200])
|
||||
else:
|
||||
print(f"List failed: {err}")
|
||||
|
||||
# 5. Filter by type
|
||||
print("\n--- Filter by Provider Type ---")
|
||||
for ptype in ["agents-api", "a2a", "recipe"]:
|
||||
code, out, err = run_cli(["list", "--url", server_url, "--type", ptype])
|
||||
if code == 0 and out.strip():
|
||||
print(f" {ptype}: {len(out.strip().split(chr(10)))} endpoints")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP HTTP Server Example
|
||||
|
||||
Demonstrates launching an MCP server with HTTP transport:
|
||||
- HTTP-based MCP protocol
|
||||
- Tool registration
|
||||
- Discovery endpoint
|
||||
|
||||
Usage:
|
||||
# Run this example
|
||||
python mcp_http_server.py
|
||||
|
||||
# Or use CLI:
|
||||
praisonai serve mcp --transport http --port 8080
|
||||
|
||||
# Test with curl:
|
||||
curl http://localhost:8080/mcp/tools
|
||||
curl -X POST http://localhost:8080/mcp/tools/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tool": "search", "arguments": {"query": "AI news"}}'
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("MCP HTTP Server Example")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from fastapi import FastAPI
|
||||
import uvicorn
|
||||
except ImportError:
|
||||
print("Error: FastAPI/uvicorn not installed")
|
||||
print("Install with: pip install fastapi uvicorn")
|
||||
sys.exit(1)
|
||||
|
||||
# Define tools
|
||||
def search(query: str) -> str:
|
||||
"""Search the web for information."""
|
||||
return f"Search results for: {query}"
|
||||
|
||||
def calculate(expression: str) -> str:
|
||||
"""Calculate a math expression safely."""
|
||||
try:
|
||||
# Safe eval for basic math
|
||||
allowed = set("0123456789+-*/.() ")
|
||||
if all(c in allowed for c in expression):
|
||||
return str(eval(expression))
|
||||
return "Error: Invalid expression"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a city (mock)."""
|
||||
return f"Weather in {city}: Sunny, 72°F"
|
||||
|
||||
tools = {
|
||||
"search": {"func": search, "description": "Search the web for information"},
|
||||
"calculate": {"func": calculate, "description": "Calculate a math expression"},
|
||||
"get_weather": {"func": get_weather, "description": "Get weather for a city"},
|
||||
}
|
||||
|
||||
app = FastAPI(title="PraisonAI MCP Server")
|
||||
|
||||
@app.get("/mcp/tools")
|
||||
async def list_tools():
|
||||
"""List available MCP tools."""
|
||||
return {
|
||||
"tools": [
|
||||
{
|
||||
"name": name,
|
||||
"description": info["description"],
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
list(info["func"].__code__.co_varnames)[0]: {"type": "string"}
|
||||
},
|
||||
"required": [list(info["func"].__code__.co_varnames)[0]]
|
||||
}
|
||||
}
|
||||
for name, info in tools.items()
|
||||
]
|
||||
}
|
||||
|
||||
@app.post("/mcp/tools/call")
|
||||
async def call_tool(request: dict):
|
||||
"""Call an MCP tool."""
|
||||
tool_name = request.get("tool")
|
||||
arguments = request.get("arguments", {})
|
||||
|
||||
if tool_name not in tools:
|
||||
return {"error": f"Tool not found: {tool_name}"}
|
||||
|
||||
try:
|
||||
func = tools[tool_name]["func"]
|
||||
# Get first argument
|
||||
arg_name = list(func.__code__.co_varnames)[0]
|
||||
arg_value = arguments.get(arg_name, "")
|
||||
result = func(arg_value)
|
||||
return {"result": result, "tool": tool_name}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": "praisonai-mcp",
|
||||
"providers": [{"type": "mcp", "name": "MCP Server", "capabilities": ["list-tools", "call-tool"]}],
|
||||
"endpoints": [{"name": "mcp", "provider_type": "mcp"}]
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "tools": len(tools)}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "PraisonAI MCP Server (HTTP)",
|
||||
"tools": list(tools.keys()),
|
||||
"endpoints": ["/mcp/tools", "/mcp/tools/call", "/health"]
|
||||
}
|
||||
|
||||
print(f"Tools: {list(tools.keys())}")
|
||||
print("\nStarting MCP HTTP server on http://localhost:8080")
|
||||
print("List tools: GET http://localhost:8080/mcp/tools")
|
||||
print("Call tool: POST http://localhost:8080/mcp/tools/call")
|
||||
print("\nPress Ctrl+C to stop")
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8080)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Recipe Server Advanced Features Example
|
||||
|
||||
Demonstrates:
|
||||
1. Rate limiting
|
||||
2. Request size limits
|
||||
3. Metrics endpoint
|
||||
4. Admin reload endpoint
|
||||
5. OpenAPI specification
|
||||
6. Workers configuration
|
||||
7. OpenTelemetry tracing
|
||||
|
||||
Prerequisites:
|
||||
- pip install praisonai[serve]
|
||||
- Set OPENAI_API_KEY environment variable
|
||||
|
||||
Usage:
|
||||
# Run this example
|
||||
python recipe_serve_features.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Configuration
|
||||
SERVER_URL = os.environ.get("PRAISONAI_ENDPOINTS_URL", "http://localhost:8765")
|
||||
API_KEY = os.environ.get("PRAISONAI_API_KEY", "test-api-key")
|
||||
|
||||
|
||||
def make_request(method: str, path: str, data: dict = None, headers: dict = None) -> dict:
|
||||
"""Make HTTP request to server."""
|
||||
url = f"{SERVER_URL}{path}"
|
||||
default_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": API_KEY,
|
||||
}
|
||||
if headers:
|
||||
default_headers.update(headers)
|
||||
|
||||
body = json.dumps(data).encode() if data else None
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, data=body, headers=default_headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
return {
|
||||
"status": response.status,
|
||||
"data": json.loads(response.read().decode()),
|
||||
"headers": dict(response.headers)
|
||||
}
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode() if e.fp else ""
|
||||
try:
|
||||
error_data = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
error_data = {"raw": body}
|
||||
return {
|
||||
"status": e.code,
|
||||
"error": error_data,
|
||||
"headers": dict(e.headers) if hasattr(e, 'headers') else {}
|
||||
}
|
||||
except urllib.error.URLError as e:
|
||||
return {"status": 0, "error": str(e.reason)}
|
||||
|
||||
|
||||
def test_health():
|
||||
"""Test health endpoint."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: Health Check")
|
||||
print("=" * 60)
|
||||
|
||||
result = make_request("GET", "/health")
|
||||
|
||||
if result.get("status") == 200:
|
||||
data = result["data"]
|
||||
print("✓ Server healthy")
|
||||
print(f" Service: {data.get('service')}")
|
||||
print(f" Version: {data.get('version')}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Server unhealthy: {result.get('error')}")
|
||||
return False
|
||||
|
||||
|
||||
def test_openapi():
|
||||
"""Test OpenAPI specification endpoint."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: OpenAPI Specification")
|
||||
print("=" * 60)
|
||||
|
||||
result = make_request("GET", "/openapi.json")
|
||||
|
||||
if result.get("status") == 200:
|
||||
spec = result["data"]
|
||||
print("✓ OpenAPI spec available")
|
||||
print(f" Version: {spec.get('openapi')}")
|
||||
print(f" Title: {spec.get('info', {}).get('title')}")
|
||||
print(f" Paths: {list(spec.get('paths', {}).keys())[:5]}...")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ OpenAPI not available: {result.get('error')}")
|
||||
return False
|
||||
|
||||
|
||||
def test_metrics():
|
||||
"""Test metrics endpoint."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: Metrics Endpoint")
|
||||
print("=" * 60)
|
||||
|
||||
result = make_request("GET", "/metrics")
|
||||
|
||||
if result.get("status") == 200:
|
||||
# Metrics returns plain text, not JSON
|
||||
print("✓ Metrics endpoint available")
|
||||
print(" (Prometheus format)")
|
||||
return True
|
||||
elif result.get("status") == 404:
|
||||
print("⚠ Metrics endpoint not enabled")
|
||||
print(" Enable with: --enable_metrics")
|
||||
return True # Not a failure, just not enabled
|
||||
else:
|
||||
print(f"✗ Metrics error: {result.get('error')}")
|
||||
return False
|
||||
|
||||
|
||||
def test_admin_reload():
|
||||
"""Test admin reload endpoint."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: Admin Reload Endpoint")
|
||||
print("=" * 60)
|
||||
|
||||
result = make_request("POST", "/admin/reload")
|
||||
|
||||
if result.get("status") == 200:
|
||||
data = result["data"]
|
||||
print("✓ Admin reload successful")
|
||||
print(f" Status: {data.get('status')}")
|
||||
print(f" Timestamp: {data.get('timestamp')}")
|
||||
return True
|
||||
elif result.get("status") == 404:
|
||||
print("⚠ Admin endpoint not enabled")
|
||||
print(" Enable with: --enable_admin")
|
||||
return True # Not a failure, just not enabled
|
||||
elif result.get("status") == 401:
|
||||
print("⚠ Admin endpoint requires authentication")
|
||||
return True # Expected behavior
|
||||
else:
|
||||
print(f"✗ Admin reload error: {result.get('error')}")
|
||||
return False
|
||||
|
||||
|
||||
def test_rate_limiting():
|
||||
"""Test rate limiting."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: Rate Limiting")
|
||||
print("=" * 60)
|
||||
|
||||
# Make rapid requests
|
||||
rate_limited = False
|
||||
retry_after = None
|
||||
|
||||
for i in range(20):
|
||||
result = make_request("GET", "/v1/recipes")
|
||||
|
||||
if result.get("status") == 429:
|
||||
rate_limited = True
|
||||
retry_after = result.get("headers", {}).get("Retry-After")
|
||||
error = result.get("error", {})
|
||||
print(f"✓ Rate limited after {i} requests")
|
||||
print(f" Error code: {error.get('error', {}).get('code')}")
|
||||
print(f" Retry-After: {retry_after}")
|
||||
break
|
||||
|
||||
if not rate_limited:
|
||||
print("⚠ Rate limiting not triggered (may not be enabled)")
|
||||
print(" Enable with: --rate_limit 10")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_request_size_limit():
|
||||
"""Test request size limit."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: Request Size Limit")
|
||||
print("=" * 60)
|
||||
|
||||
# Create a large payload
|
||||
large_data = {
|
||||
"recipe": "test",
|
||||
"input": {"data": "x" * 100000} # ~100KB
|
||||
}
|
||||
|
||||
result = make_request("POST", "/v1/recipes/run", large_data)
|
||||
|
||||
if result.get("status") == 413:
|
||||
error = result.get("error", {})
|
||||
print("✓ Request size limit enforced")
|
||||
print(f" Error code: {error.get('error', {}).get('code')}")
|
||||
return True
|
||||
elif result.get("status") in [200, 400, 404]:
|
||||
print("⚠ Request size limit not triggered (limit may be higher)")
|
||||
print(" Set with: --max_request_size 1000")
|
||||
return True
|
||||
else:
|
||||
print(f" Response: {result.get('status')} - {result.get('error')}")
|
||||
return True
|
||||
|
||||
|
||||
def test_list_recipes():
|
||||
"""Test listing recipes."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: List Recipes")
|
||||
print("=" * 60)
|
||||
|
||||
result = make_request("GET", "/v1/recipes")
|
||||
|
||||
if result.get("status") == 200:
|
||||
recipes = result["data"].get("recipes", [])
|
||||
print(f"✓ Found {len(recipes)} recipes")
|
||||
for r in recipes[:3]:
|
||||
print(f" - {r.get('name')} ({r.get('version', 'unknown')})")
|
||||
if len(recipes) > 3:
|
||||
print(f" ... and {len(recipes) - 3} more")
|
||||
return True
|
||||
elif result.get("status") == 401:
|
||||
print("⚠ Authentication required")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Error: {result.get('error')}")
|
||||
return False
|
||||
|
||||
|
||||
def demonstrate_programmatic_usage():
|
||||
"""Demonstrate programmatic server configuration."""
|
||||
print("\n" + "=" * 60)
|
||||
print("DEMO: Programmatic Configuration")
|
||||
print("=" * 60)
|
||||
|
||||
print("""
|
||||
# Rate Limiter
|
||||
from praisonai.recipe.serve import create_rate_limiter
|
||||
limiter = create_rate_limiter(requests_per_minute=100)
|
||||
allowed, retry_after = limiter.check("client-id")
|
||||
|
||||
# Metrics Collector
|
||||
from praisonai.recipe.serve import MetricsCollector
|
||||
metrics = MetricsCollector()
|
||||
metrics.record_request("/health", "GET", 200, 0.01)
|
||||
print(metrics.get_prometheus_metrics())
|
||||
|
||||
# OpenAPI Spec
|
||||
from praisonai.recipe.serve import get_openapi_spec
|
||||
spec = get_openapi_spec({"enable_metrics": True, "enable_admin": True})
|
||||
print(spec["paths"].keys())
|
||||
|
||||
# Template Search Paths
|
||||
from praisonai.recipe.core import get_template_search_paths
|
||||
paths = get_template_search_paths()
|
||||
print(f"Searching {len(paths)} paths for recipes")
|
||||
|
||||
# Reload Registry
|
||||
from praisonai.recipe.core import reload_registry
|
||||
reload_registry()
|
||||
print("Registry reloaded")
|
||||
""")
|
||||
|
||||
# Actually run the demos
|
||||
try:
|
||||
from praisonai.recipe.serve import create_rate_limiter, MetricsCollector, get_openapi_spec
|
||||
from praisonai.recipe.core import get_template_search_paths, reload_registry
|
||||
|
||||
# Rate limiter
|
||||
limiter = create_rate_limiter(requests_per_minute=100)
|
||||
allowed, _ = limiter.check("demo-client")
|
||||
print(f"✓ Rate limiter: allowed={allowed}")
|
||||
|
||||
# Metrics
|
||||
metrics = MetricsCollector()
|
||||
metrics.record_request("/demo", "GET", 200, 0.01)
|
||||
prom = metrics.get_prometheus_metrics()
|
||||
print(f"✓ Metrics: {len(prom)} chars of Prometheus format")
|
||||
|
||||
# OpenAPI
|
||||
spec = get_openapi_spec({"enable_metrics": True})
|
||||
print(f"✓ OpenAPI: {len(spec['paths'])} paths defined")
|
||||
|
||||
# Search paths
|
||||
paths = get_template_search_paths()
|
||||
print(f"✓ Template paths: {len(paths)} directories")
|
||||
|
||||
# Reload
|
||||
reload_registry()
|
||||
print("✓ Registry reloaded")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"⚠ Import error: {e}")
|
||||
print(" Install with: pip install praisonai[serve]")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main example function."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Recipe Server Advanced Features")
|
||||
print("=" * 60)
|
||||
print(f"Server URL: {SERVER_URL}")
|
||||
print(f"API Key: {'*' * 8}..." if API_KEY else "API Key: Not set")
|
||||
|
||||
# Check if server is running
|
||||
if not test_health():
|
||||
print("\n" + "=" * 60)
|
||||
print("Server not running. Start it with:")
|
||||
print("=" * 60)
|
||||
print(f"""
|
||||
# Basic start
|
||||
praisonai recipe serve
|
||||
|
||||
# With all features enabled
|
||||
export PRAISONAI_API_KEY={API_KEY}
|
||||
praisonai recipe serve \\
|
||||
--auth api-key \\
|
||||
--rate_limit 100 \\
|
||||
--enable_metrics \\
|
||||
--enable_admin \\
|
||||
--workers 2
|
||||
""")
|
||||
# Still demonstrate programmatic usage
|
||||
demonstrate_programmatic_usage()
|
||||
return
|
||||
|
||||
# Run all tests
|
||||
test_openapi()
|
||||
test_metrics()
|
||||
test_admin_reload()
|
||||
test_rate_limiting()
|
||||
test_request_size_limit()
|
||||
test_list_recipes()
|
||||
|
||||
# Demonstrate programmatic usage
|
||||
demonstrate_programmatic_usage()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All tests completed!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
# PraisonAI Recipe Server Configuration
|
||||
# This file configures the recipe HTTP server
|
||||
|
||||
# Server binding
|
||||
host: 127.0.0.1
|
||||
port: 8765
|
||||
|
||||
# Authentication
|
||||
# Options: none, api-key, jwt
|
||||
auth: api-key
|
||||
|
||||
# API key for authentication (or use PRAISONAI_API_KEY env var)
|
||||
# api_key: your-secret-key-here
|
||||
|
||||
# Recipe filtering (optional)
|
||||
# Only serve these specific recipes
|
||||
# recipes:
|
||||
# - my-recipe
|
||||
# - another-recipe
|
||||
|
||||
# Performance
|
||||
# Preload recipes on startup for faster first request
|
||||
preload: true
|
||||
|
||||
# CORS configuration (for web clients)
|
||||
# cors_origins: "https://app.example.com,https://admin.example.com"
|
||||
|
||||
# Logging level
|
||||
# log_level: info
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PraisonAI Recipe Server Example
|
||||
|
||||
This example demonstrates how to:
|
||||
1. Start the recipe server with configuration
|
||||
2. Use API key authentication
|
||||
3. Make authenticated requests
|
||||
|
||||
Prerequisites:
|
||||
- pip install praisonai[serve]
|
||||
- Set OPENAI_API_KEY environment variable
|
||||
- Set PRAISONAI_API_KEY for authentication
|
||||
|
||||
Usage:
|
||||
# Terminal 1: Start server with config
|
||||
praisonai recipe serve --config serve.yaml
|
||||
|
||||
# Terminal 2: Run this example
|
||||
python serve_example.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Configuration
|
||||
SERVER_URL = os.environ.get("PRAISONAI_ENDPOINTS_URL", "http://localhost:8765")
|
||||
API_KEY = os.environ.get("PRAISONAI_API_KEY", "test-api-key")
|
||||
|
||||
|
||||
def make_request(method: str, path: str, data: dict = None) -> dict:
|
||||
"""Make authenticated HTTP request to server."""
|
||||
url = f"{SERVER_URL}{path}"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": API_KEY,
|
||||
}
|
||||
|
||||
body = json.dumps(data).encode() if data else None
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
return {"status": response.status, "data": json.loads(response.read().decode())}
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode() if e.fp else ""
|
||||
return {"status": e.code, "error": body}
|
||||
except urllib.error.URLError as e:
|
||||
return {"status": 0, "error": str(e.reason)}
|
||||
|
||||
|
||||
def check_health():
|
||||
"""Check server health."""
|
||||
print("\n=== Health Check ===")
|
||||
result = make_request("GET", "/health")
|
||||
|
||||
if result.get("status") == 200:
|
||||
data = result["data"]
|
||||
print("✓ Server healthy")
|
||||
print(f" Service: {data.get('service')}")
|
||||
print(f" Version: {data.get('version')}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Server unhealthy: {result.get('error')}")
|
||||
return False
|
||||
|
||||
|
||||
def list_recipes():
|
||||
"""List available recipes."""
|
||||
print("\n=== List Recipes ===")
|
||||
result = make_request("GET", "/v1/recipes")
|
||||
|
||||
if result.get("status") == 200:
|
||||
recipes = result["data"].get("recipes", [])
|
||||
print(f"Found {len(recipes)} recipes:")
|
||||
for r in recipes:
|
||||
print(f" - {r['name']} ({r['version']})")
|
||||
return recipes
|
||||
elif result.get("status") == 401:
|
||||
print("✗ Authentication failed - check API key")
|
||||
return []
|
||||
else:
|
||||
print(f"✗ Error: {result.get('error')}")
|
||||
return []
|
||||
|
||||
|
||||
def describe_recipe(name: str):
|
||||
"""Get recipe details."""
|
||||
print(f"\n=== Describe Recipe: {name} ===")
|
||||
result = make_request("GET", f"/v1/recipes/{name}")
|
||||
|
||||
if result.get("status") == 200:
|
||||
info = result["data"]
|
||||
print(f"Name: {info.get('name')}")
|
||||
print(f"Version: {info.get('version')}")
|
||||
print(f"Description: {info.get('description')}")
|
||||
return info
|
||||
else:
|
||||
print(f"✗ Error: {result.get('error')}")
|
||||
return None
|
||||
|
||||
|
||||
def invoke_recipe(name: str, input_data: dict, dry_run: bool = True):
|
||||
"""Invoke a recipe."""
|
||||
print(f"\n=== Invoke Recipe: {name} (dry_run={dry_run}) ===")
|
||||
|
||||
result = make_request("POST", "/v1/recipes/run", {
|
||||
"recipe": name,
|
||||
"input": input_data,
|
||||
"options": {"dry_run": dry_run}
|
||||
})
|
||||
|
||||
if result.get("status") == 200:
|
||||
data = result["data"]
|
||||
print("✓ Success")
|
||||
print(f" Run ID: {data.get('run_id')}")
|
||||
print(f" Status: {data.get('status')}")
|
||||
if data.get("output"):
|
||||
print(f" Output: {json.dumps(data.get('output'), indent=2)[:200]}...")
|
||||
return data
|
||||
elif result.get("status") == 401:
|
||||
print("✗ Authentication failed - check API key")
|
||||
return None
|
||||
else:
|
||||
print(f"✗ Error: {result.get('error')}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
"""Main example function."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Recipe Server Example")
|
||||
print("=" * 60)
|
||||
print(f"Server URL: {SERVER_URL}")
|
||||
print(f"API Key: {API_KEY[:8]}..." if API_KEY else "API Key: Not set")
|
||||
|
||||
# Check health
|
||||
if not check_health():
|
||||
print("\nServer not running. Start it with:")
|
||||
print(f" export PRAISONAI_API_KEY={API_KEY}")
|
||||
print(" praisonai recipe serve --config serve.yaml")
|
||||
sys.exit(1)
|
||||
|
||||
# List recipes
|
||||
recipes = list_recipes()
|
||||
if not recipes:
|
||||
print("\nNo recipes available or auth failed")
|
||||
sys.exit(1)
|
||||
|
||||
# Use first recipe
|
||||
recipe_name = recipes[0]["name"]
|
||||
|
||||
# Describe recipe
|
||||
describe_recipe(recipe_name)
|
||||
|
||||
# Invoke recipe (dry run)
|
||||
invoke_recipe(recipe_name, {"query": "Hello from serve example"}, dry_run=True)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Example completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tools as MCP Server Example
|
||||
|
||||
Demonstrates exposing Python tools as an MCP server:
|
||||
- Register custom functions as MCP tools
|
||||
- SSE transport for Claude Desktop/Cursor
|
||||
- Stdio transport for local integration
|
||||
|
||||
Usage:
|
||||
# Run this example (SSE mode)
|
||||
python tools_as_mcp_server.py
|
||||
|
||||
# Or use CLI:
|
||||
praisonai serve tools --port 8081
|
||||
|
||||
# Test with curl:
|
||||
curl http://localhost:8081/sse
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Tools as MCP Server Example")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from praisonaiagents.mcp import ToolsMCPServer
|
||||
except ImportError:
|
||||
print("Error: praisonaiagents[mcp] not installed")
|
||||
print("Install with: pip install 'praisonaiagents[mcp]'")
|
||||
print("\nFallback: Using manual implementation...")
|
||||
run_manual_server()
|
||||
return
|
||||
|
||||
# Define custom tools
|
||||
def search_web(query: str) -> str:
|
||||
"""Search the web for information."""
|
||||
return f"Search results for: {query}"
|
||||
|
||||
def analyze_text(text: str) -> str:
|
||||
"""Analyze text for sentiment and key points."""
|
||||
word_count = len(text.split())
|
||||
return f"Analysis: {word_count} words, sentiment: neutral"
|
||||
|
||||
def translate(text: str, target_language: str = "Spanish") -> str:
|
||||
"""Translate text to target language (mock)."""
|
||||
return f"[Translated to {target_language}]: {text}"
|
||||
|
||||
def summarize(text: str) -> str:
|
||||
"""Summarize long text."""
|
||||
words = text.split()
|
||||
if len(words) > 20:
|
||||
return " ".join(words[:20]) + "..."
|
||||
return text
|
||||
|
||||
# Create MCP server
|
||||
server = ToolsMCPServer(name="praisonai-tools")
|
||||
|
||||
# Register tools
|
||||
server.register_tool(search_web)
|
||||
server.register_tool(analyze_text)
|
||||
server.register_tool(translate)
|
||||
server.register_tool(summarize)
|
||||
|
||||
print(f"Registered tools: {[search_web.__name__, analyze_text.__name__, translate.__name__, summarize.__name__]}")
|
||||
print("\nStarting MCP server with SSE transport...")
|
||||
print("SSE endpoint: http://localhost:8081/sse")
|
||||
print("\nClaude Desktop config (claude_desktop_config.json):")
|
||||
print(''' {
|
||||
"mcpServers": {
|
||||
"praisonai-tools": {
|
||||
"url": "http://localhost:8081/sse"
|
||||
}
|
||||
}
|
||||
}''')
|
||||
print("\nPress Ctrl+C to stop")
|
||||
|
||||
server.run(transport="sse", host="0.0.0.0", port=8081)
|
||||
|
||||
|
||||
def run_manual_server():
|
||||
"""Manual MCP server implementation as fallback."""
|
||||
try:
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse
|
||||
import uvicorn
|
||||
import json
|
||||
import asyncio
|
||||
except ImportError:
|
||||
print("Error: FastAPI/uvicorn not installed")
|
||||
print("Install with: pip install fastapi uvicorn")
|
||||
sys.exit(1)
|
||||
|
||||
# Define tools
|
||||
tools = {
|
||||
"search_web": {
|
||||
"func": lambda query: f"Search results for: {query}",
|
||||
"description": "Search the web for information",
|
||||
"params": {"query": "string"}
|
||||
},
|
||||
"analyze_text": {
|
||||
"func": lambda text: f"Analysis: {len(text.split())} words",
|
||||
"description": "Analyze text for sentiment and key points",
|
||||
"params": {"text": "string"}
|
||||
},
|
||||
}
|
||||
|
||||
app = FastAPI(title="PraisonAI Tools MCP Server")
|
||||
|
||||
@app.get("/sse")
|
||||
async def sse_endpoint():
|
||||
"""SSE endpoint for MCP."""
|
||||
async def event_stream():
|
||||
# Send initial tools list
|
||||
tools_list = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"result": {
|
||||
"tools": [
|
||||
{
|
||||
"name": name,
|
||||
"description": info["description"],
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {k: {"type": v} for k, v in info["params"].items()},
|
||||
"required": list(info["params"].keys())
|
||||
}
|
||||
}
|
||||
for name, info in tools.items()
|
||||
]
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(tools_list)}\n\n"
|
||||
|
||||
# Keep connection alive
|
||||
while True:
|
||||
await asyncio.sleep(30)
|
||||
yield f"data: {json.dumps({'type': 'ping'})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
|
||||
)
|
||||
|
||||
@app.post("/messages/")
|
||||
async def handle_message(request: dict):
|
||||
"""Handle MCP messages."""
|
||||
method = request.get("method")
|
||||
params = request.get("params", {})
|
||||
req_id = request.get("id")
|
||||
|
||||
if method == "tools/list":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"result": {
|
||||
"tools": [
|
||||
{
|
||||
"name": name,
|
||||
"description": info["description"],
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {k: {"type": v} for k, v in info["params"].items()}
|
||||
}
|
||||
}
|
||||
for name, info in tools.items()
|
||||
]
|
||||
}
|
||||
}
|
||||
elif method == "tools/call":
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if tool_name not in tools:
|
||||
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Tool not found: {tool_name}"}}
|
||||
|
||||
try:
|
||||
func = tools[tool_name]["func"]
|
||||
arg = list(arguments.values())[0] if arguments else ""
|
||||
result = func(arg)
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"result": {"content": [{"type": "text", "text": result}]}
|
||||
}
|
||||
except Exception as e:
|
||||
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32000, "message": str(e)}}
|
||||
|
||||
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Unknown method: {method}"}}
|
||||
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": "praisonai-tools-mcp",
|
||||
"providers": [{"type": "tools-mcp", "name": "Tools MCP Server", "capabilities": ["list-tools", "call-tool"]}],
|
||||
"endpoints": [{"name": "tools", "provider_type": "tools-mcp"}]
|
||||
}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "PraisonAI Tools MCP Server",
|
||||
"tools": list(tools.keys()),
|
||||
"sse": "/sse",
|
||||
"messages": "/messages/"
|
||||
}
|
||||
|
||||
print(f"Tools: {list(tools.keys())}")
|
||||
print("\nStarting Tools MCP server on http://localhost:8081")
|
||||
print("SSE endpoint: http://localhost:8081/sse")
|
||||
print("\nPress Ctrl+C to stop")
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8081)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unified Server Example
|
||||
|
||||
Demonstrates launching a unified server with all provider types:
|
||||
- agents-api: Single/multi-agent HTTP API
|
||||
- recipe: Recipe runner endpoints
|
||||
- mcp: MCP server
|
||||
- a2a: Agent-to-agent protocol
|
||||
- a2u: Agent-to-user event stream
|
||||
|
||||
Usage:
|
||||
# Run this example
|
||||
python unified_server.py
|
||||
|
||||
# Or use CLI:
|
||||
praisonai serve unified --port 8765
|
||||
|
||||
# Test with curl:
|
||||
curl http://localhost:8765/__praisonai__/discovery
|
||||
curl http://localhost:8765/health
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Error: OPENAI_API_KEY environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Unified Server Example")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from fastapi import FastAPI
|
||||
import uvicorn
|
||||
from praisonaiagents import Agent
|
||||
except ImportError as e:
|
||||
print(f"Error: Missing dependency - {e}")
|
||||
print("Install with: pip install praisonaiagents fastapi uvicorn")
|
||||
sys.exit(1)
|
||||
|
||||
# Create agents
|
||||
assistant = Agent(
|
||||
name="Assistant",
|
||||
role="Helpful AI Assistant",
|
||||
goal="Help users with their questions",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Specialist",
|
||||
goal="Find accurate information",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
agents = [assistant, researcher]
|
||||
agent_map = {a.name.lower(): a for a in agents}
|
||||
|
||||
# Create unified app
|
||||
app = FastAPI(
|
||||
title="PraisonAI Unified Server",
|
||||
description="Unified server with all PraisonAI providers"
|
||||
)
|
||||
|
||||
# Discovery endpoint
|
||||
@app.get("/__praisonai__/discovery")
|
||||
async def discovery():
|
||||
return {
|
||||
"schema_version": "1.0.0",
|
||||
"server_name": "praisonai-unified",
|
||||
"providers": [
|
||||
{"type": "agents-api", "name": "Agents API", "capabilities": ["invoke", "health"]},
|
||||
{"type": "recipe", "name": "Recipe Runner", "capabilities": ["list", "describe", "invoke"]},
|
||||
{"type": "mcp", "name": "MCP Server", "capabilities": ["list-tools", "call-tool"]},
|
||||
{"type": "a2a", "name": "A2A Protocol", "capabilities": ["agent-card", "message-send"]},
|
||||
{"type": "a2u", "name": "A2U Event Stream", "capabilities": ["subscribe", "stream"]},
|
||||
],
|
||||
"endpoints": [
|
||||
{"name": "agents", "provider_type": "agents-api", "description": "Multi-agent router"},
|
||||
{"name": "agent", "provider_type": "agents-api", "description": "Single agent endpoint"},
|
||||
{"name": "mcp/tools", "provider_type": "mcp", "description": "MCP tools"},
|
||||
{"name": "a2a", "provider_type": "a2a", "description": "A2A protocol"},
|
||||
{"name": "a2u/events", "provider_type": "a2u", "description": "Event stream"},
|
||||
]
|
||||
}
|
||||
|
||||
# Health endpoint
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"providers": ["agents-api", "recipe", "mcp", "a2a", "a2u"],
|
||||
"agents": len(agents)
|
||||
}
|
||||
|
||||
# Agents API endpoints
|
||||
@app.post("/agents")
|
||||
async def route_agents(request: dict):
|
||||
query = request.get("query", "")
|
||||
agent = agents[0] # Simple routing
|
||||
try:
|
||||
response = agent.chat(query)
|
||||
return {"agent": agent.name, "response": response}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
@app.post("/agents/{name}")
|
||||
async def query_agent(name: str, request: dict):
|
||||
agent = agent_map.get(name.lower())
|
||||
if not agent:
|
||||
return {"error": f"Agent not found: {name}"}
|
||||
try:
|
||||
response = agent.chat(request.get("query", ""))
|
||||
return {"agent": agent.name, "response": response}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
@app.get("/agents/list")
|
||||
async def list_agents():
|
||||
return {"agents": [{"name": a.name, "role": a.role} for a in agents]}
|
||||
|
||||
# Single agent endpoint
|
||||
@app.post("/agent")
|
||||
async def single_agent(request: dict):
|
||||
query = request.get("query", "")
|
||||
try:
|
||||
response = assistant.chat(query)
|
||||
return {"response": response}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# MCP endpoints
|
||||
mcp_tools = {
|
||||
"search": {"description": "Search the web", "func": lambda q: f"Results for: {q}"},
|
||||
"calculate": {"description": "Calculate expression", "func": lambda e: str(eval(e)) if e.replace(" ", "").replace("+", "").replace("-", "").replace("*", "").replace("/", "").replace(".", "").isdigit() or True else "Error"},
|
||||
}
|
||||
|
||||
@app.get("/mcp/tools")
|
||||
async def list_mcp_tools():
|
||||
return {
|
||||
"tools": [
|
||||
{"name": name, "description": info["description"]}
|
||||
for name, info in mcp_tools.items()
|
||||
]
|
||||
}
|
||||
|
||||
@app.post("/mcp/tools/call")
|
||||
async def call_mcp_tool(request: dict):
|
||||
tool_name = request.get("tool")
|
||||
args = request.get("arguments", {})
|
||||
if tool_name not in mcp_tools:
|
||||
return {"error": f"Tool not found: {tool_name}"}
|
||||
try:
|
||||
result = mcp_tools[tool_name]["func"](list(args.values())[0] if args else "")
|
||||
return {"result": result}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# A2A endpoints
|
||||
@app.get("/.well-known/agent.json")
|
||||
async def agent_card():
|
||||
return {
|
||||
"name": "PraisonAI Unified",
|
||||
"description": "Unified PraisonAI server with multiple agents",
|
||||
"url": "http://localhost:8765/a2a",
|
||||
"version": "1.0.0",
|
||||
"capabilities": {"streaming": True}
|
||||
}
|
||||
|
||||
@app.post("/a2a")
|
||||
async def a2a_endpoint(request: dict):
|
||||
method = request.get("method")
|
||||
params = request.get("params", {})
|
||||
req_id = request.get("id")
|
||||
|
||||
if method == "message/send":
|
||||
message = params.get("message", {})
|
||||
parts = message.get("parts", [])
|
||||
text = "".join(p.get("text", "") for p in parts if p.get("type") == "text")
|
||||
|
||||
try:
|
||||
response = assistant.chat(text)
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"result": {
|
||||
"message": {
|
||||
"role": "agent",
|
||||
"parts": [{"type": "text", "text": response}]
|
||||
}
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32000, "message": str(e)}}
|
||||
|
||||
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Unknown method: {method}"}}
|
||||
|
||||
# A2U endpoints
|
||||
@app.get("/a2u/info")
|
||||
async def a2u_info():
|
||||
return {
|
||||
"name": "A2U Event Stream",
|
||||
"version": "1.0.0",
|
||||
"streams": ["events"],
|
||||
"event_types": ["agent.started", "agent.response", "agent.completed"]
|
||||
}
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "PraisonAI Unified Server",
|
||||
"providers": ["agents-api", "recipe", "mcp", "a2a", "a2u"],
|
||||
"discovery": "/__praisonai__/discovery",
|
||||
"health": "/health",
|
||||
"docs": "/docs"
|
||||
}
|
||||
|
||||
print(f"Agents: {[a.name for a in agents]}")
|
||||
print("\nStarting unified server on http://localhost:8765")
|
||||
print("\nEndpoints:")
|
||||
print(" GET /__praisonai__/discovery - Discovery document")
|
||||
print(" GET /health - Health check")
|
||||
print(" POST /agents - Multi-agent router")
|
||||
print(" POST /agent - Single agent")
|
||||
print(" GET /mcp/tools - List MCP tools")
|
||||
print(" GET /.well-known/agent.json - A2A agent card")
|
||||
print(" POST /a2a - A2A messages")
|
||||
print(" GET /a2u/info - A2U info")
|
||||
print("\nPress Ctrl+C to stop")
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8765)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Example: Using pandas as a custom tool with PraisonAI Agent
|
||||
|
||||
This example shows how to wrap pandas functions as tools
|
||||
that can be used with Agent().
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from io import StringIO
|
||||
|
||||
# Define pandas wrapper tools
|
||||
def analyze_csv_data(csv_data: str) -> str:
|
||||
"""Analyze CSV data using pandas DataFrame.
|
||||
|
||||
Args:
|
||||
csv_data: CSV-formatted string data
|
||||
|
||||
Returns:
|
||||
Analysis summary including shape, columns, and statistics
|
||||
"""
|
||||
df = pd.read_csv(StringIO(csv_data))
|
||||
summary = f"""DataFrame Analysis:
|
||||
- Shape: {df.shape[0]} rows, {df.shape[1]} columns
|
||||
- Columns: {list(df.columns)}
|
||||
- Data Types: {dict(df.dtypes)}
|
||||
- Summary Statistics:
|
||||
{df.describe().to_string()}"""
|
||||
return summary
|
||||
|
||||
|
||||
def filter_dataframe(csv_data: str, column: str, condition: str, value: str) -> str:
|
||||
"""Filter a DataFrame based on a condition.
|
||||
|
||||
Args:
|
||||
csv_data: CSV-formatted string data
|
||||
column: Column name to filter on
|
||||
condition: Condition type ('equals', 'greater', 'less', 'contains')
|
||||
value: Value to compare against
|
||||
|
||||
Returns:
|
||||
Filtered data as CSV string
|
||||
"""
|
||||
df = pd.read_csv(StringIO(csv_data))
|
||||
|
||||
if condition == 'equals':
|
||||
filtered = df[df[column] == value]
|
||||
elif condition == 'greater':
|
||||
filtered = df[df[column] > float(value)]
|
||||
elif condition == 'less':
|
||||
filtered = df[df[column] < float(value)]
|
||||
elif condition == 'contains':
|
||||
filtered = df[df[column].str.contains(value, na=False)]
|
||||
else:
|
||||
return f"Unknown condition: {condition}"
|
||||
|
||||
return filtered.to_csv(index=False)
|
||||
|
||||
|
||||
def calculate_statistics(csv_data: str, column: str) -> str:
|
||||
"""Calculate statistics for a specific column.
|
||||
|
||||
Args:
|
||||
csv_data: CSV-formatted string data
|
||||
column: Column name to analyze
|
||||
|
||||
Returns:
|
||||
Statistics summary for the column
|
||||
"""
|
||||
df = pd.read_csv(StringIO(csv_data))
|
||||
|
||||
if column not in df.columns:
|
||||
return f"Column '{column}' not found. Available: {list(df.columns)}"
|
||||
|
||||
col = df[column]
|
||||
|
||||
if pd.api.types.is_numeric_dtype(col):
|
||||
stats = f"""Statistics for '{column}':
|
||||
- Count: {col.count()}
|
||||
- Mean: {col.mean():.2f}
|
||||
- Std: {col.std():.2f}
|
||||
- Min: {col.min()}
|
||||
- Max: {col.max()}
|
||||
- Median: {col.median()}"""
|
||||
else:
|
||||
stats = f"""Statistics for '{column}':
|
||||
- Count: {col.count()}
|
||||
- Unique: {col.nunique()}
|
||||
- Top: {col.mode().iloc[0] if len(col.mode()) > 0 else 'N/A'}
|
||||
- Sample values: {list(col.head(3))}"""
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test data
|
||||
test_csv = """name,age,salary,department
|
||||
Alice,30,50000,Engineering
|
||||
Bob,25,45000,Marketing
|
||||
Charlie,35,60000,Engineering
|
||||
Diana,28,52000,Sales
|
||||
Eve,32,55000,Marketing"""
|
||||
|
||||
print("=== Test: analyze_csv_data ===")
|
||||
print(analyze_csv_data(test_csv))
|
||||
print()
|
||||
|
||||
print("=== Test: filter_dataframe ===")
|
||||
print(filter_dataframe(test_csv, "age", "greater", "28"))
|
||||
print()
|
||||
|
||||
print("=== Test: calculate_statistics ===")
|
||||
print(calculate_statistics(test_csv, "salary"))
|
||||
print()
|
||||
|
||||
# Example with Agent (requires praisonaiagents)
|
||||
print("=== Agent Usage Example ===")
|
||||
print("""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="data_analyst",
|
||||
role="Data Analyst",
|
||||
goal="Analyze data using pandas tools",
|
||||
tools=[analyze_csv_data, filter_dataframe, calculate_statistics]
|
||||
)
|
||||
|
||||
result = agent.start("Analyze this data and find the average salary")
|
||||
""")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Example: Discover tools from installed packages."""
|
||||
|
||||
# Discover from praisonaiagents built-in tools
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
print("Built-in tools from praisonaiagents:")
|
||||
for name in list(TOOL_MAPPINGS.keys())[:10]:
|
||||
print(f" - {name}")
|
||||
print(f" ... and {len(TOOL_MAPPINGS) - 10} more")
|
||||
except ImportError:
|
||||
print("praisonaiagents not installed")
|
||||
|
||||
# Discover from praisonai_tools
|
||||
try:
|
||||
import praisonai_tools
|
||||
print("\nTools from praisonai_tools:")
|
||||
|
||||
# Check for video module
|
||||
try:
|
||||
from praisonai_tools import video
|
||||
print(" - praisonai_tools.video (video editing)")
|
||||
except ImportError:
|
||||
pass
|
||||
except ImportError:
|
||||
print("praisonai_tools not installed")
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Example: Resolve tool names to callable tools."""
|
||||
from praisonai.templates.tool_override import (
|
||||
create_tool_registry_with_overrides,
|
||||
resolve_tools,
|
||||
)
|
||||
|
||||
# Create registry with defaults
|
||||
registry = create_tool_registry_with_overrides(include_defaults=True)
|
||||
|
||||
print(f"Registry contains {len(registry)} tools")
|
||||
|
||||
# Resolve tool names
|
||||
tool_names = ["shell_tool", "internet_search"]
|
||||
resolved = resolve_tools(tool_names, registry=registry)
|
||||
|
||||
print(f"\nResolved {len(resolved)} tools:")
|
||||
for tool in resolved:
|
||||
print(f" - {getattr(tool, '__name__', type(tool).__name__)}")
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Example: Configure tool sources for templates."""
|
||||
from praisonai.templates.tool_override import create_tool_registry_with_overrides
|
||||
|
||||
# Create registry with tools_sources
|
||||
registry = create_tool_registry_with_overrides(
|
||||
tools_sources=["praisonai_tools.video"],
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
print(f"Registry with tools_sources: {len(registry)} tools")
|
||||
|
||||
# List some tools
|
||||
for name in list(registry.keys())[:10]:
|
||||
print(f" - {name}")
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Import time benchmark for praisonaiagents.
|
||||
|
||||
This benchmark measures the import time of the praisonaiagents package
|
||||
and verifies that heavy dependencies are not loaded at import time.
|
||||
|
||||
Usage:
|
||||
python benchmarks/import_time.py
|
||||
|
||||
CI Gate:
|
||||
- PASS: import time < 200ms
|
||||
- WARN: import time 200-300ms
|
||||
- FAIL: import time > 300ms
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
import statistics
|
||||
|
||||
|
||||
def clear_modules():
|
||||
"""Clear all praisonai and litellm related modules from cache."""
|
||||
to_remove = [m for m in sys.modules.keys()
|
||||
if 'praison' in m or 'litellm' in m]
|
||||
for mod in to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
|
||||
def measure_import_time(runs: int = 5) -> dict:
|
||||
"""
|
||||
Measure import time over multiple runs.
|
||||
|
||||
Returns:
|
||||
dict with min, max, median, mean times in milliseconds
|
||||
"""
|
||||
times = []
|
||||
|
||||
for _ in range(runs):
|
||||
clear_modules()
|
||||
|
||||
start = time.perf_counter()
|
||||
import praisonaiagents # noqa: F401
|
||||
end = time.perf_counter()
|
||||
|
||||
times.append((end - start) * 1000)
|
||||
|
||||
# Clean up for next run
|
||||
if 'praisonaiagents' in sys.modules:
|
||||
del sys.modules['praisonaiagents']
|
||||
|
||||
times.sort()
|
||||
|
||||
return {
|
||||
'times': times,
|
||||
'min': min(times),
|
||||
'max': max(times),
|
||||
'median': statistics.median(times),
|
||||
'mean': statistics.mean(times),
|
||||
'stdev': statistics.stdev(times) if len(times) > 1 else 0,
|
||||
}
|
||||
|
||||
|
||||
def check_lazy_imports() -> dict:
|
||||
"""
|
||||
Check that heavy dependencies are NOT loaded after importing praisonaiagents.
|
||||
|
||||
Returns:
|
||||
dict with module names and whether they are loaded
|
||||
"""
|
||||
clear_modules()
|
||||
|
||||
import praisonaiagents # noqa: F401
|
||||
|
||||
heavy_modules = [
|
||||
'litellm',
|
||||
'chromadb',
|
||||
'mem0',
|
||||
'requests',
|
||||
]
|
||||
|
||||
results = {}
|
||||
for mod in heavy_modules:
|
||||
results[mod] = mod in sys.modules
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the benchmark and print results."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Agents Import Time Benchmark")
|
||||
print("=" * 60)
|
||||
|
||||
# Measure import time
|
||||
print("\n[1/2] Measuring import time (5 runs)...")
|
||||
results = measure_import_time(runs=5)
|
||||
|
||||
print(f"\nImport times (ms): {[round(t, 1) for t in results['times']]}")
|
||||
print(f" Min: {results['min']:.1f} ms")
|
||||
print(f" Max: {results['max']:.1f} ms")
|
||||
print(f" Median: {results['median']:.1f} ms")
|
||||
print(f" Mean: {results['mean']:.1f} ms")
|
||||
print(f" StdDev: {results['stdev']:.1f} ms")
|
||||
|
||||
# Check lazy imports
|
||||
print("\n[2/2] Checking lazy imports...")
|
||||
lazy_results = check_lazy_imports()
|
||||
|
||||
print("\nHeavy modules loaded at import time:")
|
||||
all_lazy = True
|
||||
for mod, loaded in lazy_results.items():
|
||||
status = "LOADED (BAD)" if loaded else "NOT LOADED (GOOD)"
|
||||
print(f" {mod}: {status}")
|
||||
if loaded:
|
||||
all_lazy = False
|
||||
|
||||
# Determine pass/fail
|
||||
print("\n" + "=" * 60)
|
||||
print("RESULTS")
|
||||
print("=" * 60)
|
||||
|
||||
median_time = results['median']
|
||||
|
||||
if median_time < 200:
|
||||
time_status = "PASS"
|
||||
time_msg = f"Import time {median_time:.0f}ms < 200ms target"
|
||||
elif median_time < 300:
|
||||
time_status = "WARN"
|
||||
time_msg = f"Import time {median_time:.0f}ms between 200-300ms"
|
||||
else:
|
||||
time_status = "FAIL"
|
||||
time_msg = f"Import time {median_time:.0f}ms > 300ms limit"
|
||||
|
||||
lazy_status = "PASS" if all_lazy else "FAIL"
|
||||
lazy_msg = "All heavy deps lazy loaded" if all_lazy else "Some heavy deps loaded eagerly"
|
||||
|
||||
print(f"\nImport Time: [{time_status}] {time_msg}")
|
||||
print(f"Lazy Imports: [{lazy_status}] {lazy_msg}")
|
||||
|
||||
overall = "PASS" if time_status != "FAIL" and lazy_status == "PASS" else "FAIL"
|
||||
print(f"\nOverall: [{overall}]")
|
||||
|
||||
# Exit code for CI
|
||||
return 0 if overall == "PASS" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Memory usage benchmark for praisonaiagents.
|
||||
|
||||
This benchmark measures the memory footprint of importing praisonaiagents
|
||||
and verifies it stays within acceptable limits.
|
||||
|
||||
Usage:
|
||||
python benchmarks/memory_usage.py
|
||||
|
||||
CI Gate:
|
||||
- PASS: memory < 30MB
|
||||
- WARN: memory 30-45MB
|
||||
- FAIL: memory > 45MB
|
||||
"""
|
||||
import sys
|
||||
import tracemalloc
|
||||
|
||||
|
||||
def clear_modules():
|
||||
"""Clear all praisonai and litellm related modules from cache."""
|
||||
to_remove = [m for m in sys.modules.keys()
|
||||
if 'praison' in m or 'litellm' in m]
|
||||
for mod in to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
|
||||
def measure_memory() -> dict:
|
||||
"""
|
||||
Measure memory usage after importing praisonaiagents.
|
||||
|
||||
Returns:
|
||||
dict with current and peak memory in MB
|
||||
"""
|
||||
clear_modules()
|
||||
|
||||
tracemalloc.start()
|
||||
|
||||
import praisonaiagents # noqa: F401
|
||||
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
|
||||
return {
|
||||
'current_bytes': current,
|
||||
'peak_bytes': peak,
|
||||
'current_mb': current / 1024 / 1024,
|
||||
'peak_mb': peak / 1024 / 1024,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the benchmark and print results."""
|
||||
print("=" * 60)
|
||||
print("PraisonAI Agents Memory Usage Benchmark")
|
||||
print("=" * 60)
|
||||
|
||||
print("\nMeasuring memory usage...")
|
||||
results = measure_memory()
|
||||
|
||||
print(f"\nMemory Usage:")
|
||||
print(f" Current: {results['current_mb']:.1f} MB")
|
||||
print(f" Peak: {results['peak_mb']:.1f} MB")
|
||||
|
||||
# Determine pass/fail based on peak memory
|
||||
peak_mb = results['peak_mb']
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("RESULTS")
|
||||
print("=" * 60)
|
||||
|
||||
if peak_mb < 30:
|
||||
status = "PASS"
|
||||
msg = f"Memory {peak_mb:.1f}MB < 30MB target"
|
||||
elif peak_mb < 45:
|
||||
status = "WARN"
|
||||
msg = f"Memory {peak_mb:.1f}MB between 30-45MB"
|
||||
else:
|
||||
status = "FAIL"
|
||||
msg = f"Memory {peak_mb:.1f}MB > 45MB limit"
|
||||
|
||||
print(f"\nMemory: [{status}] {msg}")
|
||||
|
||||
# Exit code for CI
|
||||
return 0 if status != "FAIL" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Using PraisonAI Agents Lite Package
|
||||
|
||||
The lite package provides a minimal agent framework without heavy dependencies
|
||||
like litellm. It's ideal for users who want to bring their own LLM client.
|
||||
|
||||
This example shows:
|
||||
1. Creating a LiteAgent with a custom LLM function
|
||||
2. Using the built-in OpenAI adapter
|
||||
3. Thread-safe chat history management
|
||||
"""
|
||||
import os
|
||||
|
||||
|
||||
def example_custom_llm():
|
||||
"""Example 1: Using a custom LLM function."""
|
||||
from praisonaiagents.lite import LiteAgent
|
||||
|
||||
# Define your own LLM function
|
||||
def my_simple_llm(messages):
|
||||
"""A mock LLM that echoes back the last message."""
|
||||
last_msg = messages[-1]["content"] if messages else "No message"
|
||||
return f"Echo: {last_msg}"
|
||||
|
||||
agent = LiteAgent(
|
||||
name="EchoAgent",
|
||||
llm_fn=my_simple_llm,
|
||||
instructions="You are a simple echo agent."
|
||||
)
|
||||
|
||||
response = agent.chat("Hello, world!")
|
||||
print(f"Response: {response}")
|
||||
print(f"Chat history length: {len(agent.chat_history)}")
|
||||
|
||||
|
||||
def example_openai_adapter():
|
||||
"""Example 2: Using the built-in OpenAI adapter."""
|
||||
from praisonaiagents.lite import LiteAgent, create_openai_llm_fn
|
||||
|
||||
# Check for API key
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Skipping OpenAI example - OPENAI_API_KEY not set")
|
||||
return
|
||||
|
||||
# Create LLM function using OpenAI SDK directly (no litellm)
|
||||
llm_fn = create_openai_llm_fn(model="gpt-4o-mini")
|
||||
|
||||
agent = LiteAgent(
|
||||
name="OpenAIAgent",
|
||||
llm_fn=llm_fn,
|
||||
instructions="You are a helpful assistant. Keep responses brief."
|
||||
)
|
||||
|
||||
response = agent.chat("What is 2+2?")
|
||||
print(f"OpenAI Response: {response}")
|
||||
|
||||
|
||||
def example_with_tools():
|
||||
"""Example 3: Using tools with LiteAgent."""
|
||||
from praisonaiagents.lite import LiteAgent, tool
|
||||
|
||||
@tool
|
||||
def add_numbers(a: int, b: int) -> int:
|
||||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
def mock_llm(messages):
|
||||
return "The sum is 8"
|
||||
|
||||
agent = LiteAgent(
|
||||
name="MathAgent",
|
||||
llm_fn=mock_llm,
|
||||
tools=[add_numbers]
|
||||
)
|
||||
|
||||
# Execute tool directly
|
||||
result = agent.execute_tool("add_numbers", a=5, b=3)
|
||||
print(f"Tool result: {result.output}")
|
||||
print(f"Tool success: {result.success}")
|
||||
|
||||
|
||||
def example_anthropic_adapter():
|
||||
"""Example 4: Using the built-in Anthropic adapter."""
|
||||
from praisonaiagents.lite import LiteAgent, create_anthropic_llm_fn
|
||||
|
||||
# Check for API key
|
||||
if not os.environ.get("ANTHROPIC_API_KEY"):
|
||||
print("Skipping Anthropic example - ANTHROPIC_API_KEY not set")
|
||||
return
|
||||
|
||||
# Create LLM function using Anthropic SDK directly
|
||||
llm_fn = create_anthropic_llm_fn(model="claude-3-5-sonnet-20241022")
|
||||
|
||||
agent = LiteAgent(
|
||||
name="ClaudeAgent",
|
||||
llm_fn=llm_fn,
|
||||
instructions="You are a helpful assistant. Keep responses brief."
|
||||
)
|
||||
|
||||
response = agent.chat("What is the capital of France?")
|
||||
print(f"Claude Response: {response}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("PraisonAI Agents Lite Examples")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n[1] Custom LLM Function:")
|
||||
example_custom_llm()
|
||||
|
||||
print("\n[2] OpenAI Adapter:")
|
||||
example_openai_adapter()
|
||||
|
||||
print("\n[3] Tools with LiteAgent:")
|
||||
example_with_tools()
|
||||
|
||||
print("\n[4] Anthropic Adapter:")
|
||||
example_anthropic_adapter()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Examples completed!")
|
||||
@@ -1,5 +1,8 @@
|
||||
"""
|
||||
Praison AI Agents - A package for hierarchical AI agent task execution
|
||||
|
||||
This module uses lazy loading to minimize import time and memory usage.
|
||||
Heavy dependencies like litellm are only loaded when actually needed.
|
||||
"""
|
||||
|
||||
# Apply warning patch BEFORE any imports to intercept warnings at the source
|
||||
@@ -11,51 +14,18 @@ from . import _logging
|
||||
# Configure root logger after logging is initialized
|
||||
_logging.configure_root_logger()
|
||||
|
||||
# Now import everything else
|
||||
from .agent.agent import Agent
|
||||
from .agent.image_agent import ImageAgent
|
||||
from .agent.context_agent import ContextAgent, create_context_agent
|
||||
from .agent.deep_research_agent import (
|
||||
DeepResearchAgent,
|
||||
DeepResearchResponse,
|
||||
Citation,
|
||||
ReasoningStep,
|
||||
WebSearchCall,
|
||||
CodeExecutionStep,
|
||||
MCPCall,
|
||||
FileSearchCall,
|
||||
Provider
|
||||
)
|
||||
from .agent.query_rewriter_agent import (
|
||||
QueryRewriterAgent,
|
||||
RewriteStrategy,
|
||||
RewriteResult
|
||||
)
|
||||
from .agent.prompt_expander_agent import (
|
||||
PromptExpanderAgent,
|
||||
ExpandStrategy,
|
||||
ExpandResult
|
||||
)
|
||||
from .agents.agents import PraisonAIAgents
|
||||
from .task.task import Task
|
||||
# Import configuration (lightweight, no heavy deps)
|
||||
from . import _config
|
||||
|
||||
# Lightweight imports that don't trigger heavy dependency chains
|
||||
from .tools.tools import Tools
|
||||
from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
|
||||
from .tools.decorator import tool, FunctionTool
|
||||
from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
|
||||
from .agents.autoagents import AutoAgents
|
||||
# Knowledge is lazy-loaded to avoid importing heavy deps (chromadb, mem0) at startup
|
||||
# MCP support (optional)
|
||||
try:
|
||||
from .mcp.mcp import MCP
|
||||
_mcp_available = True
|
||||
except ImportError:
|
||||
_mcp_available = False
|
||||
MCP = None
|
||||
from .session import Session
|
||||
# Memory is lazy-loaded to avoid importing chromadb at startup
|
||||
from .db import db
|
||||
from .obs import obs
|
||||
# Workflows - import from dedicated workflows module
|
||||
|
||||
# Workflows - lightweight module
|
||||
from .workflows import (
|
||||
Workflow, WorkflowStep, WorkflowContext, StepResult,
|
||||
Route, Parallel, Loop, Repeat,
|
||||
@@ -64,80 +34,18 @@ from .workflows import (
|
||||
)
|
||||
from .guardrails import GuardrailResult, LLMGuardrail
|
||||
|
||||
# Fast Context support (lazy loaded to avoid performance impact)
|
||||
def __getattr__(name):
|
||||
"""Lazy load heavy modules to avoid impacting package load time."""
|
||||
# Knowledge module (imports chromadb, mem0)
|
||||
if name == "Knowledge":
|
||||
from praisonaiagents.knowledge.knowledge import Knowledge
|
||||
return Knowledge
|
||||
elif name == "Chunking":
|
||||
from praisonaiagents.knowledge.chunking import Chunking
|
||||
return Chunking
|
||||
# FastContext support
|
||||
elif name == "FastContext":
|
||||
from praisonaiagents.context.fast import FastContext
|
||||
return FastContext
|
||||
elif name == "FastContextResult":
|
||||
from praisonaiagents.context.fast import FastContextResult
|
||||
return FastContextResult
|
||||
elif name == "FileMatch":
|
||||
from praisonaiagents.context.fast import FileMatch
|
||||
return FileMatch
|
||||
elif name == "LineRange":
|
||||
from praisonaiagents.context.fast import LineRange
|
||||
return LineRange
|
||||
# Agent Skills support (lazy loaded for zero performance impact)
|
||||
elif name == "SkillManager":
|
||||
from praisonaiagents.skills import SkillManager
|
||||
return SkillManager
|
||||
elif name == "SkillProperties":
|
||||
from praisonaiagents.skills import SkillProperties
|
||||
return SkillProperties
|
||||
elif name == "SkillMetadata":
|
||||
from praisonaiagents.skills import SkillMetadata
|
||||
return SkillMetadata
|
||||
elif name == "SkillLoader":
|
||||
from praisonaiagents.skills import SkillLoader
|
||||
return SkillLoader
|
||||
# Memory module (imports chromadb)
|
||||
elif name == "Memory":
|
||||
from praisonaiagents.memory.memory import Memory
|
||||
return Memory
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
# Planning mode support
|
||||
try:
|
||||
from .planning import (
|
||||
Plan,
|
||||
PlanStep,
|
||||
TodoList,
|
||||
TodoItem,
|
||||
PlanStorage,
|
||||
PlanningAgent,
|
||||
ApprovalCallback,
|
||||
READ_ONLY_TOOLS,
|
||||
RESTRICTED_TOOLS,
|
||||
)
|
||||
_planning_available = True
|
||||
except ImportError:
|
||||
_planning_available = False
|
||||
Plan = None
|
||||
PlanStep = None
|
||||
TodoList = None
|
||||
TodoItem = None
|
||||
PlanStorage = None
|
||||
PlanningAgent = None
|
||||
ApprovalCallback = None
|
||||
READ_ONLY_TOOLS = []
|
||||
RESTRICTED_TOOLS = []
|
||||
# Handoff - lightweight
|
||||
from .agent.handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
|
||||
# Flow display
|
||||
|
||||
# Flow display (optional)
|
||||
try:
|
||||
from .flow_display import FlowDisplay, track_workflow
|
||||
except ImportError:
|
||||
FlowDisplay = None
|
||||
track_workflow = None
|
||||
|
||||
# Main display utilities - these are used frequently so import directly
|
||||
# but they don't pull in litellm
|
||||
from .main import (
|
||||
TaskOutput,
|
||||
ReflectionOutput,
|
||||
@@ -154,98 +62,296 @@ from .main import (
|
||||
async_display_callbacks,
|
||||
)
|
||||
|
||||
# Telemetry support (lazy loaded)
|
||||
try:
|
||||
from .telemetry import (
|
||||
get_telemetry,
|
||||
enable_telemetry,
|
||||
disable_telemetry,
|
||||
enable_performance_mode,
|
||||
disable_performance_mode,
|
||||
cleanup_telemetry_resources,
|
||||
MinimalTelemetry,
|
||||
TelemetryCollector
|
||||
)
|
||||
_telemetry_available = True
|
||||
except ImportError:
|
||||
# Telemetry not available - provide stub functions
|
||||
_telemetry_available = False
|
||||
def get_telemetry():
|
||||
return None
|
||||
|
||||
def enable_telemetry(*args, **kwargs):
|
||||
import logging
|
||||
logging.warning(
|
||||
"Telemetry not available. Install with: pip install praisonaiagents[telemetry]"
|
||||
)
|
||||
return None
|
||||
|
||||
def disable_telemetry():
|
||||
pass
|
||||
|
||||
def enable_performance_mode():
|
||||
pass
|
||||
|
||||
def disable_performance_mode():
|
||||
pass
|
||||
|
||||
def cleanup_telemetry_resources():
|
||||
pass
|
||||
|
||||
MinimalTelemetry = None
|
||||
TelemetryCollector = None
|
||||
# Module-level caches for lazy-loaded classes
|
||||
_lazy_cache = {}
|
||||
|
||||
# Add Agents as an alias for PraisonAIAgents
|
||||
Agents = PraisonAIAgents
|
||||
# Flags for optional modules
|
||||
_mcp_available = None
|
||||
_planning_available = None
|
||||
_telemetry_available = None
|
||||
_agui_available = None
|
||||
_a2a_available = None
|
||||
|
||||
# Enable PostHog telemetry by default with actual event posting
|
||||
# PostHog events are posted by default unless explicitly disabled
|
||||
# Users can:
|
||||
# - Disable completely: PRAISONAI_DISABLE_TELEMETRY=true (or DO_NOT_TRACK=true)
|
||||
# - Enable performance mode: PRAISONAI_PERFORMANCE_MODE=true (minimal overhead, limited events)
|
||||
# - Enable full telemetry: PRAISONAI_FULL_TELEMETRY=true (detailed tracking)
|
||||
# - Legacy opt-in mode: PRAISONAI_AUTO_INSTRUMENT=true
|
||||
if _telemetry_available:
|
||||
|
||||
def __getattr__(name):
|
||||
"""
|
||||
Lazy load heavy modules to avoid impacting package load time.
|
||||
|
||||
This function is called when an attribute is not found in the module.
|
||||
It enables deferred loading of heavy dependencies like litellm.
|
||||
"""
|
||||
global _mcp_available, _planning_available, _telemetry_available
|
||||
global _agui_available, _a2a_available
|
||||
|
||||
# Return cached values if available
|
||||
if name in _lazy_cache:
|
||||
return _lazy_cache[name]
|
||||
|
||||
# Agent and related classes (triggers litellm import chain)
|
||||
if name == "Agent":
|
||||
from .agent.agent import Agent
|
||||
_lazy_cache[name] = Agent
|
||||
return Agent
|
||||
elif name == "ImageAgent":
|
||||
from .agent.image_agent import ImageAgent
|
||||
_lazy_cache[name] = ImageAgent
|
||||
return ImageAgent
|
||||
elif name == "ContextAgent":
|
||||
from .agent.context_agent import ContextAgent
|
||||
_lazy_cache[name] = ContextAgent
|
||||
return ContextAgent
|
||||
elif name == "create_context_agent":
|
||||
from .agent.context_agent import create_context_agent
|
||||
_lazy_cache[name] = create_context_agent
|
||||
return create_context_agent
|
||||
elif name == "DeepResearchAgent":
|
||||
from .agent.deep_research_agent import DeepResearchAgent
|
||||
_lazy_cache[name] = DeepResearchAgent
|
||||
return DeepResearchAgent
|
||||
elif name == "DeepResearchResponse":
|
||||
from .agent.deep_research_agent import DeepResearchResponse
|
||||
_lazy_cache[name] = DeepResearchResponse
|
||||
return DeepResearchResponse
|
||||
elif name == "Citation":
|
||||
from .agent.deep_research_agent import Citation
|
||||
_lazy_cache[name] = Citation
|
||||
return Citation
|
||||
elif name == "ReasoningStep":
|
||||
from .agent.deep_research_agent import ReasoningStep
|
||||
_lazy_cache[name] = ReasoningStep
|
||||
return ReasoningStep
|
||||
elif name == "WebSearchCall":
|
||||
from .agent.deep_research_agent import WebSearchCall
|
||||
_lazy_cache[name] = WebSearchCall
|
||||
return WebSearchCall
|
||||
elif name == "CodeExecutionStep":
|
||||
from .agent.deep_research_agent import CodeExecutionStep
|
||||
_lazy_cache[name] = CodeExecutionStep
|
||||
return CodeExecutionStep
|
||||
elif name == "MCPCall":
|
||||
from .agent.deep_research_agent import MCPCall
|
||||
_lazy_cache[name] = MCPCall
|
||||
return MCPCall
|
||||
elif name == "FileSearchCall":
|
||||
from .agent.deep_research_agent import FileSearchCall
|
||||
_lazy_cache[name] = FileSearchCall
|
||||
return FileSearchCall
|
||||
elif name == "Provider":
|
||||
from .agent.deep_research_agent import Provider
|
||||
_lazy_cache[name] = Provider
|
||||
return Provider
|
||||
elif name == "QueryRewriterAgent":
|
||||
from .agent.query_rewriter_agent import QueryRewriterAgent
|
||||
_lazy_cache[name] = QueryRewriterAgent
|
||||
return QueryRewriterAgent
|
||||
elif name == "RewriteStrategy":
|
||||
from .agent.query_rewriter_agent import RewriteStrategy
|
||||
_lazy_cache[name] = RewriteStrategy
|
||||
return RewriteStrategy
|
||||
elif name == "RewriteResult":
|
||||
from .agent.query_rewriter_agent import RewriteResult
|
||||
_lazy_cache[name] = RewriteResult
|
||||
return RewriteResult
|
||||
elif name == "PromptExpanderAgent":
|
||||
from .agent.prompt_expander_agent import PromptExpanderAgent
|
||||
_lazy_cache[name] = PromptExpanderAgent
|
||||
return PromptExpanderAgent
|
||||
elif name == "ExpandStrategy":
|
||||
from .agent.prompt_expander_agent import ExpandStrategy
|
||||
_lazy_cache[name] = ExpandStrategy
|
||||
return ExpandStrategy
|
||||
elif name == "ExpandResult":
|
||||
from .agent.prompt_expander_agent import ExpandResult
|
||||
_lazy_cache[name] = ExpandResult
|
||||
return ExpandResult
|
||||
|
||||
# PraisonAIAgents and Agents alias
|
||||
elif name == "PraisonAIAgents":
|
||||
from .agents.agents import PraisonAIAgents
|
||||
_lazy_cache[name] = PraisonAIAgents
|
||||
_lazy_cache["Agents"] = PraisonAIAgents # Also cache alias
|
||||
return PraisonAIAgents
|
||||
elif name == "Agents":
|
||||
from .agents.agents import PraisonAIAgents
|
||||
_lazy_cache["PraisonAIAgents"] = PraisonAIAgents
|
||||
_lazy_cache[name] = PraisonAIAgents
|
||||
return PraisonAIAgents
|
||||
|
||||
# Task
|
||||
elif name == "Task":
|
||||
from .task.task import Task
|
||||
_lazy_cache[name] = Task
|
||||
return Task
|
||||
|
||||
# AutoAgents
|
||||
elif name == "AutoAgents":
|
||||
from .agents.autoagents import AutoAgents
|
||||
_lazy_cache[name] = AutoAgents
|
||||
return AutoAgents
|
||||
|
||||
# Session (imports requests)
|
||||
elif name == "Session":
|
||||
from .session import Session
|
||||
_lazy_cache[name] = Session
|
||||
return Session
|
||||
|
||||
# MCP support (optional)
|
||||
elif name == "MCP":
|
||||
if _mcp_available is None:
|
||||
try:
|
||||
from .mcp.mcp import MCP as _MCP
|
||||
_lazy_cache[name] = _MCP
|
||||
return _MCP
|
||||
except ImportError:
|
||||
return None
|
||||
return _lazy_cache.get(name)
|
||||
|
||||
# Knowledge module (imports chromadb, mem0)
|
||||
elif name == "Knowledge":
|
||||
from praisonaiagents.knowledge.knowledge import Knowledge
|
||||
_lazy_cache[name] = Knowledge
|
||||
return Knowledge
|
||||
elif name == "Chunking":
|
||||
from praisonaiagents.knowledge.chunking import Chunking
|
||||
_lazy_cache[name] = Chunking
|
||||
return Chunking
|
||||
|
||||
# FastContext support
|
||||
elif name == "FastContext":
|
||||
from praisonaiagents.context.fast import FastContext
|
||||
_lazy_cache[name] = FastContext
|
||||
return FastContext
|
||||
elif name == "FastContextResult":
|
||||
from praisonaiagents.context.fast import FastContextResult
|
||||
_lazy_cache[name] = FastContextResult
|
||||
return FastContextResult
|
||||
elif name == "FileMatch":
|
||||
from praisonaiagents.context.fast import FileMatch
|
||||
_lazy_cache[name] = FileMatch
|
||||
return FileMatch
|
||||
elif name == "LineRange":
|
||||
from praisonaiagents.context.fast import LineRange
|
||||
_lazy_cache[name] = LineRange
|
||||
return LineRange
|
||||
|
||||
# Agent Skills support (lazy loaded for zero performance impact)
|
||||
elif name == "SkillManager":
|
||||
from praisonaiagents.skills import SkillManager
|
||||
_lazy_cache[name] = SkillManager
|
||||
return SkillManager
|
||||
elif name == "SkillProperties":
|
||||
from praisonaiagents.skills import SkillProperties
|
||||
_lazy_cache[name] = SkillProperties
|
||||
return SkillProperties
|
||||
elif name == "SkillMetadata":
|
||||
from praisonaiagents.skills import SkillMetadata
|
||||
_lazy_cache[name] = SkillMetadata
|
||||
return SkillMetadata
|
||||
elif name == "SkillLoader":
|
||||
from praisonaiagents.skills import SkillLoader
|
||||
_lazy_cache[name] = SkillLoader
|
||||
return SkillLoader
|
||||
|
||||
# Memory module (imports chromadb)
|
||||
elif name == "Memory":
|
||||
from praisonaiagents.memory.memory import Memory
|
||||
_lazy_cache[name] = Memory
|
||||
return Memory
|
||||
|
||||
# Planning mode support (lazy)
|
||||
elif name in ("Plan", "PlanStep", "TodoList", "TodoItem", "PlanStorage",
|
||||
"PlanningAgent", "ApprovalCallback", "READ_ONLY_TOOLS",
|
||||
"RESTRICTED_TOOLS"):
|
||||
try:
|
||||
from . import planning as _planning
|
||||
result = getattr(_planning, name)
|
||||
_lazy_cache[name] = result
|
||||
return result
|
||||
except ImportError:
|
||||
if name in ("READ_ONLY_TOOLS", "RESTRICTED_TOOLS"):
|
||||
return []
|
||||
return None
|
||||
|
||||
# Telemetry support (lazy loaded)
|
||||
elif name in ("get_telemetry", "enable_telemetry", "disable_telemetry",
|
||||
"enable_performance_mode", "disable_performance_mode",
|
||||
"cleanup_telemetry_resources", "MinimalTelemetry", "TelemetryCollector"):
|
||||
try:
|
||||
from . import telemetry as _telemetry
|
||||
result = getattr(_telemetry, name)
|
||||
_lazy_cache[name] = result
|
||||
return result
|
||||
except ImportError:
|
||||
# Provide stub functions
|
||||
if name == "get_telemetry":
|
||||
return lambda: None
|
||||
elif name == "enable_telemetry":
|
||||
import logging
|
||||
def _stub(*args, **kwargs):
|
||||
logging.warning(
|
||||
"Telemetry not available. Install with: pip install praisonaiagents[telemetry]"
|
||||
)
|
||||
return None
|
||||
return _stub
|
||||
elif name in ("disable_telemetry", "enable_performance_mode",
|
||||
"disable_performance_mode", "cleanup_telemetry_resources"):
|
||||
return lambda: None
|
||||
return None
|
||||
|
||||
# AG-UI support (optional, lazy loaded)
|
||||
elif name == "AGUI":
|
||||
try:
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
_lazy_cache[name] = AGUI
|
||||
return AGUI
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
# A2A support (optional, lazy loaded)
|
||||
elif name == "A2A":
|
||||
try:
|
||||
from praisonaiagents.ui.a2a import A2A
|
||||
_lazy_cache[name] = A2A
|
||||
return A2A
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
# Initialize telemetry only if explicitly enabled via config
|
||||
def _init_telemetry():
|
||||
"""Initialize telemetry if enabled via environment variable."""
|
||||
if not _config.TELEMETRY_ENABLED:
|
||||
return
|
||||
|
||||
try:
|
||||
import os
|
||||
from .telemetry import get_telemetry
|
||||
from .telemetry.integration import auto_instrument_all
|
||||
|
||||
# Check for explicit disable (respects DO_NOT_TRACK and other disable flags)
|
||||
telemetry_disabled = any([
|
||||
os.environ.get('PRAISONAI_TELEMETRY_DISABLED', '').lower() in ('true', '1', 'yes'),
|
||||
os.environ.get('PRAISONAI_DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'),
|
||||
os.environ.get('DO_NOT_TRACK', '').lower() in ('true', '1', 'yes'),
|
||||
])
|
||||
|
||||
# Check for performance mode (minimal overhead with limited events)
|
||||
performance_mode = os.environ.get('PRAISONAI_PERFORMANCE_MODE', '').lower() in ('true', '1', 'yes')
|
||||
|
||||
# Check for full telemetry mode (more detailed tracking)
|
||||
full_telemetry = os.environ.get('PRAISONAI_FULL_TELEMETRY', '').lower() in ('true', '1', 'yes')
|
||||
|
||||
# Legacy explicit auto-instrument option
|
||||
explicit_auto_instrument = os.environ.get('PRAISONAI_AUTO_INSTRUMENT', '').lower() in ('true', '1', 'yes')
|
||||
|
||||
# Enable PostHog by default unless explicitly disabled
|
||||
if not telemetry_disabled:
|
||||
_telemetry = get_telemetry()
|
||||
if _telemetry and _telemetry.enabled:
|
||||
from .telemetry.integration import auto_instrument_all
|
||||
|
||||
# Default: PostHog telemetry is enabled and events are posted
|
||||
# Performance mode can be explicitly enabled for minimal overhead
|
||||
use_performance_mode = performance_mode and not (full_telemetry or explicit_auto_instrument)
|
||||
auto_instrument_all(_telemetry, performance_mode=use_performance_mode)
|
||||
|
||||
# Track package import for basic usage analytics
|
||||
try:
|
||||
_telemetry.track_feature_usage("package_import")
|
||||
except Exception:
|
||||
pass
|
||||
_telemetry = get_telemetry()
|
||||
if _telemetry and _telemetry.enabled:
|
||||
use_performance_mode = _config.PERFORMANCE_MODE and not (
|
||||
_config.FULL_TELEMETRY or _config.AUTO_INSTRUMENT
|
||||
)
|
||||
auto_instrument_all(_telemetry, performance_mode=use_performance_mode)
|
||||
|
||||
# Track package import for basic usage analytics
|
||||
try:
|
||||
_telemetry.track_feature_usage("package_import")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
# Silently fail if there are any issues - never break user applications
|
||||
pass
|
||||
|
||||
|
||||
# Only initialize telemetry if explicitly enabled
|
||||
_init_telemetry()
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Core classes (lazy loaded)
|
||||
'Agent',
|
||||
'ImageAgent',
|
||||
'ContextAgent',
|
||||
@@ -314,11 +420,8 @@ __all__ = [
|
||||
'RewriteResult',
|
||||
'PromptExpanderAgent',
|
||||
'ExpandStrategy',
|
||||
'ExpandResult'
|
||||
]
|
||||
|
||||
# Add workflow exports
|
||||
__all__.extend([
|
||||
'ExpandResult',
|
||||
# Workflows
|
||||
'Workflow',
|
||||
'WorkflowStep',
|
||||
'WorkflowContext',
|
||||
@@ -331,48 +434,33 @@ __all__.extend([
|
||||
'parallel',
|
||||
'loop',
|
||||
'repeat',
|
||||
'Pipeline'
|
||||
])
|
||||
|
||||
# Add MCP to __all__ if available
|
||||
if _mcp_available:
|
||||
__all__.append('MCP')
|
||||
|
||||
# Add flow display if available
|
||||
if FlowDisplay is not None:
|
||||
__all__.extend(['FlowDisplay', 'track_workflow'])
|
||||
|
||||
# Add FastContext exports (lazy loaded)
|
||||
__all__.extend([
|
||||
'Pipeline',
|
||||
# MCP
|
||||
'MCP',
|
||||
# Flow display
|
||||
'FlowDisplay',
|
||||
'track_workflow',
|
||||
# FastContext
|
||||
'FastContext',
|
||||
'FastContextResult',
|
||||
'FileMatch',
|
||||
'LineRange'
|
||||
])
|
||||
|
||||
# Add Agent Skills exports (lazy loaded)
|
||||
__all__.extend([
|
||||
'LineRange',
|
||||
# Agent Skills
|
||||
'SkillManager',
|
||||
'SkillProperties',
|
||||
'SkillMetadata',
|
||||
'SkillLoader'
|
||||
])
|
||||
|
||||
# AG-UI support (optional, lazy loaded)
|
||||
try:
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
__all__.append('AGUI')
|
||||
_agui_available = True
|
||||
except ImportError:
|
||||
_agui_available = False
|
||||
AGUI = None
|
||||
|
||||
# A2A support (optional, lazy loaded)
|
||||
try:
|
||||
from praisonaiagents.ui.a2a import A2A
|
||||
__all__.append('A2A')
|
||||
_a2a_available = True
|
||||
except ImportError:
|
||||
_a2a_available = False
|
||||
A2A = None
|
||||
|
||||
'SkillLoader',
|
||||
# Planning
|
||||
'Plan',
|
||||
'PlanStep',
|
||||
'TodoList',
|
||||
'TodoItem',
|
||||
'PlanStorage',
|
||||
'PlanningAgent',
|
||||
'ApprovalCallback',
|
||||
'READ_ONLY_TOOLS',
|
||||
'RESTRICTED_TOOLS',
|
||||
# UI
|
||||
'AGUI',
|
||||
'A2A',
|
||||
]
|
||||
Whitespace-only changes.
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Configuration module for PraisonAI Agents.
|
||||
|
||||
Provides feature flags and configuration options that can be controlled
|
||||
via environment variables for performance tuning and behavior control.
|
||||
|
||||
Environment Variables:
|
||||
PRAISONAI_LAZY_IMPORTS: Enable lazy imports (default: true)
|
||||
PRAISONAI_TELEMETRY_ENABLED: Enable telemetry (default: false, opt-in)
|
||||
PRAISONAI_TELEMETRY_DISABLED: Disable telemetry (takes precedence)
|
||||
DO_NOT_TRACK: Standard opt-out flag (takes precedence)
|
||||
"""
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def _str_to_bool(value: Optional[str], default: bool = False) -> bool:
|
||||
"""Convert string environment variable to boolean."""
|
||||
if value is None:
|
||||
return default
|
||||
return value.lower() in ('true', '1', 'yes', 'on')
|
||||
|
||||
|
||||
# Feature Flags
|
||||
# -------------
|
||||
|
||||
# Lazy imports: When True (default), heavy dependencies like litellm are
|
||||
# only imported when actually needed, reducing import time significantly.
|
||||
LAZY_IMPORTS = _str_to_bool(
|
||||
os.environ.get('PRAISONAI_LAZY_IMPORTS'),
|
||||
default=True
|
||||
)
|
||||
|
||||
# Telemetry: Opt-in by default (False). Users must explicitly enable.
|
||||
# Precedence (highest to lowest):
|
||||
# 1. DO_NOT_TRACK=true -> disabled
|
||||
# 2. PRAISONAI_TELEMETRY_DISABLED=true -> disabled
|
||||
# 3. PRAISONAI_DISABLE_TELEMETRY=true -> disabled
|
||||
# 4. PRAISONAI_TELEMETRY_ENABLED=true -> enabled
|
||||
# 5. Default: disabled (opt-in model)
|
||||
def _get_telemetry_enabled() -> bool:
|
||||
"""Determine if telemetry should be enabled based on env vars."""
|
||||
# Check disable flags first (highest precedence)
|
||||
if _str_to_bool(os.environ.get('DO_NOT_TRACK')):
|
||||
return False
|
||||
if _str_to_bool(os.environ.get('PRAISONAI_TELEMETRY_DISABLED')):
|
||||
return False
|
||||
if _str_to_bool(os.environ.get('PRAISONAI_DISABLE_TELEMETRY')):
|
||||
return False
|
||||
|
||||
# Check explicit enable flag
|
||||
if _str_to_bool(os.environ.get('PRAISONAI_TELEMETRY_ENABLED')):
|
||||
return True
|
||||
|
||||
# Default: disabled (opt-in model)
|
||||
return False
|
||||
|
||||
|
||||
TELEMETRY_ENABLED = _get_telemetry_enabled()
|
||||
|
||||
# Performance mode: Minimal telemetry overhead when enabled
|
||||
PERFORMANCE_MODE = _str_to_bool(
|
||||
os.environ.get('PRAISONAI_PERFORMANCE_MODE'),
|
||||
default=False
|
||||
)
|
||||
|
||||
# Full telemetry mode: Detailed tracking when enabled
|
||||
FULL_TELEMETRY = _str_to_bool(
|
||||
os.environ.get('PRAISONAI_FULL_TELEMETRY'),
|
||||
default=False
|
||||
)
|
||||
|
||||
# Legacy auto-instrument flag
|
||||
AUTO_INSTRUMENT = _str_to_bool(
|
||||
os.environ.get('PRAISONAI_AUTO_INSTRUMENT'),
|
||||
default=False
|
||||
)
|
||||
|
||||
|
||||
def missing_dependency_error(
|
||||
package_name: str,
|
||||
extra_name: Optional[str] = None,
|
||||
feature_name: Optional[str] = None
|
||||
) -> ImportError:
|
||||
"""
|
||||
Create a standardized ImportError for missing optional dependencies.
|
||||
|
||||
Args:
|
||||
package_name: The name of the missing package (e.g., 'litellm')
|
||||
extra_name: The pip extra to install (e.g., 'llm' for [llm])
|
||||
feature_name: Human-readable feature name (e.g., 'LLM support')
|
||||
|
||||
Returns:
|
||||
ImportError with helpful install instructions
|
||||
"""
|
||||
if extra_name:
|
||||
install_cmd = f"pip install 'praisonaiagents[{extra_name}]'"
|
||||
else:
|
||||
install_cmd = f"pip install {package_name}"
|
||||
|
||||
feature_desc = f" for {feature_name}" if feature_name else ""
|
||||
|
||||
return ImportError(
|
||||
f"{package_name} is required{feature_desc} but not installed. "
|
||||
f"Please install with: {install_cmd}"
|
||||
)
|
||||
@@ -84,8 +84,18 @@ def _configure_loggers():
|
||||
# ========================================================================
|
||||
# LITELLM CONFIGURATION
|
||||
# ========================================================================
|
||||
def _configure_litellm():
|
||||
"""Configure litellm after it's imported."""
|
||||
_litellm_configured = False
|
||||
|
||||
def configure_litellm():
|
||||
"""Configure litellm after it's imported.
|
||||
|
||||
This function should be called lazily when litellm is actually used,
|
||||
not at module import time, to avoid importing litellm unnecessarily.
|
||||
"""
|
||||
global _litellm_configured
|
||||
if _litellm_configured:
|
||||
return
|
||||
|
||||
try:
|
||||
import litellm
|
||||
litellm.telemetry = False
|
||||
@@ -97,11 +107,17 @@ def _configure_litellm():
|
||||
|
||||
if hasattr(litellm, 'set_verbose'):
|
||||
litellm.set_verbose = False
|
||||
|
||||
_litellm_configured = True
|
||||
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
# Alias for backward compatibility
|
||||
_configure_litellm = configure_litellm
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# ROOT LOGGER CONFIGURATION
|
||||
# ========================================================================
|
||||
@@ -124,11 +140,16 @@ def configure_root_logger():
|
||||
# INITIALIZATION
|
||||
# ========================================================================
|
||||
def initialize_logging():
|
||||
"""Initialize all logging configuration."""
|
||||
"""Initialize all logging configuration.
|
||||
|
||||
Note: litellm configuration is NOT done here to avoid importing litellm
|
||||
at package import time. Call configure_litellm() when litellm is needed.
|
||||
"""
|
||||
_configure_environment()
|
||||
_configure_loggers()
|
||||
_configure_litellm()
|
||||
# NOTE: _configure_litellm() is NOT called here to avoid importing litellm
|
||||
# It will be called lazily when LLM class is instantiated
|
||||
|
||||
|
||||
# Auto-initialize on import
|
||||
# Auto-initialize on import (but NOT litellm)
|
||||
initialize_logging()
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import copy
|
||||
import logging
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import List, Optional, Any, Dict, Union, Literal, TYPE_CHECKING, Callable, Tuple, Generator
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
@@ -580,9 +581,9 @@ class Agent:
|
||||
self.embedder_config = embedder_config
|
||||
self.knowledge = knowledge
|
||||
self.use_system_prompt = use_system_prompt
|
||||
# NOTE: chat_history is not thread-safe. If concurrent access is needed,
|
||||
# consider using threading.Lock or other synchronization mechanisms
|
||||
# Thread-safe chat_history with lock for concurrent access
|
||||
self.chat_history = []
|
||||
self._history_lock = threading.Lock()
|
||||
self.markdown = markdown
|
||||
self.stream = stream
|
||||
self.metrics = metrics
|
||||
@@ -643,11 +644,11 @@ Your Goal: {self.goal}
|
||||
self._guardrail_fn = None
|
||||
self._setup_guardrail()
|
||||
|
||||
# Cache for system prompts and formatted tools
|
||||
# Note: In single-threaded usage (common case), these are safe
|
||||
# For multi-threaded usage, consider using threading.Lock
|
||||
# Cache for system prompts and formatted tools with thread-safe lock
|
||||
# RLock allows re-entrant access from the same thread
|
||||
self._system_prompt_cache = {}
|
||||
self._formatted_tools_cache = {}
|
||||
self._cache_lock = threading.RLock()
|
||||
# Limit cache size to prevent unbounded growth
|
||||
self._max_cache_size = 100
|
||||
|
||||
@@ -1745,7 +1746,11 @@ Your Goal: {self.goal}"""
|
||||
|
||||
# Special handling for MCP tools
|
||||
# Check if tools is an MCP instance with the requested function name
|
||||
from ..mcp.mcp import MCP
|
||||
MCP = None
|
||||
try:
|
||||
from ..mcp.mcp import MCP
|
||||
except ImportError:
|
||||
pass # MCP not available
|
||||
|
||||
# Helper function to execute MCP tool
|
||||
def _execute_mcp_tool(mcp_instance, func_name, args):
|
||||
@@ -1780,7 +1785,7 @@ Your Goal: {self.goal}"""
|
||||
return False, None
|
||||
|
||||
# Check if tools is a single MCP instance
|
||||
if isinstance(self.tools, MCP):
|
||||
if MCP is not None and isinstance(self.tools, MCP):
|
||||
logging.debug(f"Looking for MCP tool {function_name}")
|
||||
found, result = _execute_mcp_tool(self.tools, function_name, arguments)
|
||||
if found:
|
||||
@@ -1789,7 +1794,7 @@ Your Goal: {self.goal}"""
|
||||
# Check if tools is a list that may contain MCP instances
|
||||
if isinstance(self.tools, (list, tuple)):
|
||||
for tool in self.tools:
|
||||
if isinstance(tool, MCP):
|
||||
if MCP is not None and isinstance(tool, MCP):
|
||||
logging.debug(f"Looking for MCP tool {function_name} in MCP instance")
|
||||
found, result = _execute_mcp_tool(tool, function_name, arguments)
|
||||
if found:
|
||||
@@ -2211,8 +2216,12 @@ Your Goal: {self.goal}"""
|
||||
|
||||
# Convert MCP tool objects to OpenAI format if needed
|
||||
if tool_param is not None:
|
||||
from ..mcp.mcp import MCP
|
||||
if isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
MCP = None
|
||||
try:
|
||||
from ..mcp.mcp import MCP
|
||||
except ImportError:
|
||||
pass
|
||||
if MCP is not None and isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
# Single MCP instance
|
||||
logging.debug("Converting single MCP tool to OpenAI format")
|
||||
openai_tool = tool_param.to_openai_tool()
|
||||
@@ -2227,7 +2236,7 @@ Your Goal: {self.goal}"""
|
||||
# List that may contain MCP instances - convert each MCP to OpenAI format
|
||||
converted_tools = []
|
||||
for t in tool_param:
|
||||
if isinstance(t, MCP) and hasattr(t, 'to_openai_tool'):
|
||||
if MCP is not None and isinstance(t, MCP) and hasattr(t, 'to_openai_tool'):
|
||||
logging.debug("Converting MCP instance in list to OpenAI format")
|
||||
openai_tools = t.to_openai_tool()
|
||||
if isinstance(openai_tools, list):
|
||||
@@ -3162,8 +3171,12 @@ Write the complete compiled report:"""
|
||||
|
||||
# Convert MCP tools if needed
|
||||
if tool_param is not None:
|
||||
from ..mcp.mcp import MCP
|
||||
if isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
MCP = None
|
||||
try:
|
||||
from ..mcp.mcp import MCP
|
||||
except ImportError:
|
||||
pass
|
||||
if MCP is not None and isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
openai_tool = tool_param.to_openai_tool()
|
||||
if openai_tool:
|
||||
if isinstance(openai_tool, list):
|
||||
|
||||
@@ -5,13 +5,26 @@ This module uses only stdlib (typing, dataclasses) to define the interface.
|
||||
No external dependencies are imported here.
|
||||
|
||||
Implementations are provided by the wrapper layer (praisonai.db).
|
||||
|
||||
Schema Versioning:
|
||||
- SCHEMA_VERSION tracks the protocol version
|
||||
- Adapters should implement get_schema_version() to report their version
|
||||
- Adapters should implement migrate_schema() for upgrades
|
||||
- Version format: MAJOR.MINOR (e.g., "1.0", "1.1", "2.0")
|
||||
- MAJOR changes are breaking, MINOR changes are backward compatible
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
||||
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable, Tuple
|
||||
import time
|
||||
|
||||
|
||||
# Current schema version for the DbAdapter protocol
|
||||
# Increment MINOR for backward-compatible additions
|
||||
# Increment MAJOR for breaking changes
|
||||
SCHEMA_VERSION = "1.0"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DbToolCall:
|
||||
"""A tool call to be persisted."""
|
||||
@@ -365,6 +378,48 @@ class DbAdapter(Protocol):
|
||||
def close(self) -> None:
|
||||
"""Close the database connection."""
|
||||
...
|
||||
|
||||
# --- Schema Versioning (Optional) ---
|
||||
# These methods are optional for backward compatibility.
|
||||
# Adapters that don't implement them will work but won't support migrations.
|
||||
|
||||
def get_schema_version(self) -> str:
|
||||
"""
|
||||
Get the current schema version of this adapter.
|
||||
|
||||
Returns:
|
||||
Schema version string (e.g., "1.0")
|
||||
Default implementation returns "1.0"
|
||||
"""
|
||||
...
|
||||
|
||||
def migrate_schema(
|
||||
self,
|
||||
from_version: str,
|
||||
to_version: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Migrate the schema from one version to another.
|
||||
|
||||
Args:
|
||||
from_version: Current schema version
|
||||
to_version: Target schema version
|
||||
|
||||
Returns:
|
||||
True if migration succeeded, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def check_schema_compatibility(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
Check if the adapter's schema is compatible with the protocol.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_compatible, message)
|
||||
- is_compatible: True if schema is compatible
|
||||
- message: Description of compatibility status or required action
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
PraisonAI Agents Lite - Minimal agent framework without heavy dependencies.
|
||||
|
||||
This subpackage provides a lightweight version of PraisonAI Agents that:
|
||||
- Does NOT import litellm, chromadb, or other heavy dependencies at import time
|
||||
- Provides core Agent/Task/Tools abstractions
|
||||
- Allows users to bring their own LLM client (BYO-LLM)
|
||||
- Has minimal import time (<50ms) and memory footprint (<10MB)
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.lite import LiteAgent, LiteTask, Tools
|
||||
|
||||
# Create a lite agent with your own LLM function
|
||||
def my_llm(messages):
|
||||
# Your LLM implementation here
|
||||
return "response"
|
||||
|
||||
agent = LiteAgent(
|
||||
name="Assistant",
|
||||
llm_fn=my_llm,
|
||||
tools=[my_tool]
|
||||
)
|
||||
|
||||
response = agent.chat("Hello!")
|
||||
|
||||
For full functionality with litellm integration, use the main package:
|
||||
from praisonaiagents import Agent
|
||||
"""
|
||||
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from dataclasses import dataclass, field
|
||||
import threading
|
||||
|
||||
|
||||
@dataclass
|
||||
class LiteToolResult:
|
||||
"""Result from a tool execution."""
|
||||
output: Any
|
||||
success: bool = True
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LiteTask:
|
||||
"""
|
||||
Lightweight task definition without heavy dependencies.
|
||||
|
||||
Attributes:
|
||||
description: What the task should accomplish
|
||||
expected_output: Description of expected output format
|
||||
agent: Optional agent assigned to this task
|
||||
context: Additional context for the task
|
||||
result: Result after task execution
|
||||
"""
|
||||
description: str
|
||||
expected_output: str = ""
|
||||
agent: Optional['LiteAgent'] = None
|
||||
context: Dict[str, Any] = field(default_factory=dict)
|
||||
result: Optional[str] = None
|
||||
|
||||
def execute(self, input_text: Optional[str] = None) -> str:
|
||||
"""Execute the task using the assigned agent."""
|
||||
if self.agent is None:
|
||||
raise ValueError("No agent assigned to this task")
|
||||
|
||||
prompt = input_text or self.description
|
||||
if self.expected_output:
|
||||
prompt += f"\n\nExpected output format: {self.expected_output}"
|
||||
|
||||
self.result = self.agent.chat(prompt)
|
||||
return self.result
|
||||
|
||||
|
||||
class LiteAgent:
|
||||
"""
|
||||
Lightweight agent that works with any LLM function.
|
||||
|
||||
This agent does NOT import litellm or other heavy dependencies.
|
||||
Users provide their own LLM function for chat completions.
|
||||
|
||||
Args:
|
||||
name: Agent name
|
||||
llm_fn: Function that takes messages list and returns response string.
|
||||
Signature: (messages: List[Dict]) -> str
|
||||
instructions: System instructions for the agent
|
||||
tools: List of tool functions the agent can use
|
||||
verbose: Enable verbose output
|
||||
|
||||
Example:
|
||||
def my_openai_llm(messages):
|
||||
import openai
|
||||
client = openai.OpenAI()
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=messages
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
|
||||
agent = LiteAgent(
|
||||
name="Assistant",
|
||||
llm_fn=my_openai_llm,
|
||||
instructions="You are a helpful assistant."
|
||||
)
|
||||
|
||||
response = agent.chat("Hello!")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "LiteAgent",
|
||||
llm_fn: Optional[Callable[[List[Dict]], str]] = None,
|
||||
instructions: str = "You are a helpful assistant.",
|
||||
tools: Optional[List[Callable]] = None,
|
||||
verbose: bool = False,
|
||||
):
|
||||
self.name = name
|
||||
self.llm_fn = llm_fn
|
||||
self.instructions = instructions
|
||||
self.tools = tools or []
|
||||
self.verbose = verbose
|
||||
|
||||
# Thread-safe chat history
|
||||
self.chat_history: List[Dict[str, str]] = []
|
||||
self._history_lock = threading.Lock()
|
||||
|
||||
# Tool registry
|
||||
self._tool_map: Dict[str, Callable] = {}
|
||||
for tool_fn in self.tools:
|
||||
tool_name = getattr(tool_fn, '__name__', str(tool_fn))
|
||||
self._tool_map[tool_name] = tool_fn
|
||||
|
||||
def _build_messages(self, user_message: str) -> List[Dict[str, str]]:
|
||||
"""Build messages list for LLM call."""
|
||||
messages = [{"role": "system", "content": self.instructions}]
|
||||
|
||||
with self._history_lock:
|
||||
messages.extend(self.chat_history.copy())
|
||||
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
return messages
|
||||
|
||||
def _add_to_history(self, role: str, content: str):
|
||||
"""Thread-safe addition to chat history."""
|
||||
with self._history_lock:
|
||||
self.chat_history.append({"role": role, "content": content})
|
||||
|
||||
def chat(self, message: str) -> str:
|
||||
"""
|
||||
Send a message to the agent and get a response.
|
||||
|
||||
Args:
|
||||
message: User message
|
||||
|
||||
Returns:
|
||||
Agent's response string
|
||||
"""
|
||||
if self.llm_fn is None:
|
||||
raise ValueError(
|
||||
"No LLM function provided. Please provide an llm_fn when creating the agent.\n"
|
||||
"Example:\n"
|
||||
" def my_llm(messages):\n"
|
||||
" # Your LLM implementation\n"
|
||||
" return 'response'\n"
|
||||
" agent = LiteAgent(llm_fn=my_llm)"
|
||||
)
|
||||
|
||||
messages = self._build_messages(message)
|
||||
|
||||
if self.verbose:
|
||||
print(f"[{self.name}] Processing: {message[:50]}...")
|
||||
|
||||
response = self.llm_fn(messages)
|
||||
|
||||
# Add to history
|
||||
self._add_to_history("user", message)
|
||||
self._add_to_history("assistant", response)
|
||||
|
||||
if self.verbose:
|
||||
print(f"[{self.name}] Response: {response[:100]}...")
|
||||
|
||||
return response
|
||||
|
||||
def clear_history(self):
|
||||
"""Clear chat history."""
|
||||
with self._history_lock:
|
||||
self.chat_history.clear()
|
||||
|
||||
def execute_tool(self, tool_name: str, **kwargs) -> LiteToolResult:
|
||||
"""
|
||||
Execute a tool by name.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to execute
|
||||
**kwargs: Arguments to pass to the tool
|
||||
|
||||
Returns:
|
||||
LiteToolResult with output or error
|
||||
"""
|
||||
if tool_name not in self._tool_map:
|
||||
return LiteToolResult(
|
||||
output=None,
|
||||
success=False,
|
||||
error=f"Tool '{tool_name}' not found"
|
||||
)
|
||||
|
||||
try:
|
||||
tool_fn = self._tool_map[tool_name]
|
||||
result = tool_fn(**kwargs)
|
||||
return LiteToolResult(output=result, success=True)
|
||||
except Exception as e:
|
||||
return LiteToolResult(
|
||||
output=None,
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
def create_openai_llm_fn(
|
||||
model: str = "gpt-4o-mini",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Callable[[List[Dict]], str]:
|
||||
"""
|
||||
Create an LLM function using the OpenAI SDK.
|
||||
|
||||
This is a convenience function for users who want to use OpenAI
|
||||
without the full litellm dependency.
|
||||
|
||||
Args:
|
||||
model: OpenAI model name
|
||||
api_key: OpenAI API key (or use OPENAI_API_KEY env var)
|
||||
**kwargs: Additional arguments to pass to OpenAI
|
||||
|
||||
Returns:
|
||||
LLM function compatible with LiteAgent
|
||||
|
||||
Example:
|
||||
llm_fn = create_openai_llm_fn(model="gpt-4o-mini")
|
||||
agent = LiteAgent(llm_fn=llm_fn)
|
||||
"""
|
||||
def llm_fn(messages: List[Dict]) -> str:
|
||||
try:
|
||||
import openai
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"openai package is required for create_openai_llm_fn. "
|
||||
"Install with: pip install openai"
|
||||
)
|
||||
|
||||
client = openai.OpenAI(api_key=api_key)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
**kwargs
|
||||
)
|
||||
return response.choices[0].message.content or ""
|
||||
|
||||
return llm_fn
|
||||
|
||||
|
||||
def create_anthropic_llm_fn(
|
||||
model: str = "claude-3-5-sonnet-20241022",
|
||||
api_key: Optional[str] = None,
|
||||
max_tokens: int = 4096,
|
||||
**kwargs
|
||||
) -> Callable[[List[Dict]], str]:
|
||||
"""
|
||||
Create an LLM function using the Anthropic SDK.
|
||||
|
||||
Args:
|
||||
model: Anthropic model name
|
||||
api_key: Anthropic API key (or use ANTHROPIC_API_KEY env var)
|
||||
max_tokens: Maximum tokens in response
|
||||
**kwargs: Additional arguments to pass to Anthropic
|
||||
|
||||
Returns:
|
||||
LLM function compatible with LiteAgent
|
||||
"""
|
||||
def llm_fn(messages: List[Dict]) -> str:
|
||||
try:
|
||||
import anthropic
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"anthropic package is required for create_anthropic_llm_fn. "
|
||||
"Install with: pip install anthropic"
|
||||
)
|
||||
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
|
||||
# Extract system message
|
||||
system = ""
|
||||
chat_messages = []
|
||||
for msg in messages:
|
||||
if msg["role"] == "system":
|
||||
system = msg["content"]
|
||||
else:
|
||||
chat_messages.append(msg)
|
||||
|
||||
response = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
system=system,
|
||||
messages=chat_messages,
|
||||
**kwargs
|
||||
)
|
||||
return response.content[0].text
|
||||
|
||||
return llm_fn
|
||||
|
||||
|
||||
# Re-export Tools from main package (lightweight, no heavy deps)
|
||||
from ..tools.tools import Tools
|
||||
from ..tools.base import BaseTool, ToolResult
|
||||
from ..tools.decorator import tool
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LiteAgent",
|
||||
"LiteTask",
|
||||
"LiteToolResult",
|
||||
"Tools",
|
||||
"BaseTool",
|
||||
"ToolResult",
|
||||
"tool",
|
||||
"create_openai_llm_fn",
|
||||
"create_anthropic_llm_fn",
|
||||
]
|
||||
@@ -1,33 +1,102 @@
|
||||
"""
|
||||
LLM Module for PraisonAI Agents.
|
||||
|
||||
This module provides language model integrations with lazy loading
|
||||
to minimize import time when LLM functionality is not immediately needed.
|
||||
"""
|
||||
import os
|
||||
|
||||
# Ensure litellm telemetry is disabled before imports
|
||||
# Ensure litellm telemetry is disabled before any imports
|
||||
os.environ["LITELLM_TELEMETRY"] = "False"
|
||||
|
||||
# Import modules
|
||||
from .llm import LLM, LLMContextLengthExceededException
|
||||
from .openai_client import (
|
||||
OpenAIClient,
|
||||
get_openai_client,
|
||||
ChatCompletionMessage,
|
||||
Choice,
|
||||
CompletionTokensDetails,
|
||||
PromptTokensDetails,
|
||||
CompletionUsage,
|
||||
ChatCompletion,
|
||||
ToolCall,
|
||||
process_stream_chunks
|
||||
)
|
||||
from .model_capabilities import (
|
||||
supports_structured_outputs,
|
||||
supports_streaming_with_tools
|
||||
)
|
||||
from .model_router import (
|
||||
ModelRouter,
|
||||
ModelProfile,
|
||||
TaskComplexity,
|
||||
create_routing_agent
|
||||
)
|
||||
from .rate_limiter import RateLimiter
|
||||
# Module-level cache for lazy-loaded classes
|
||||
_lazy_cache = {}
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazy load LLM classes to avoid importing litellm at module load time."""
|
||||
if name in _lazy_cache:
|
||||
return _lazy_cache[name]
|
||||
|
||||
if name == "LLM":
|
||||
from .llm import LLM
|
||||
_lazy_cache[name] = LLM
|
||||
return LLM
|
||||
elif name == "LLMContextLengthExceededException":
|
||||
from .llm import LLMContextLengthExceededException
|
||||
_lazy_cache[name] = LLMContextLengthExceededException
|
||||
return LLMContextLengthExceededException
|
||||
elif name == "OpenAIClient":
|
||||
from .openai_client import OpenAIClient
|
||||
_lazy_cache[name] = OpenAIClient
|
||||
return OpenAIClient
|
||||
elif name == "get_openai_client":
|
||||
from .openai_client import get_openai_client
|
||||
_lazy_cache[name] = get_openai_client
|
||||
return get_openai_client
|
||||
elif name == "ChatCompletionMessage":
|
||||
from .openai_client import ChatCompletionMessage
|
||||
_lazy_cache[name] = ChatCompletionMessage
|
||||
return ChatCompletionMessage
|
||||
elif name == "Choice":
|
||||
from .openai_client import Choice
|
||||
_lazy_cache[name] = Choice
|
||||
return Choice
|
||||
elif name == "CompletionTokensDetails":
|
||||
from .openai_client import CompletionTokensDetails
|
||||
_lazy_cache[name] = CompletionTokensDetails
|
||||
return CompletionTokensDetails
|
||||
elif name == "PromptTokensDetails":
|
||||
from .openai_client import PromptTokensDetails
|
||||
_lazy_cache[name] = PromptTokensDetails
|
||||
return PromptTokensDetails
|
||||
elif name == "CompletionUsage":
|
||||
from .openai_client import CompletionUsage
|
||||
_lazy_cache[name] = CompletionUsage
|
||||
return CompletionUsage
|
||||
elif name == "ChatCompletion":
|
||||
from .openai_client import ChatCompletion
|
||||
_lazy_cache[name] = ChatCompletion
|
||||
return ChatCompletion
|
||||
elif name == "ToolCall":
|
||||
from .openai_client import ToolCall
|
||||
_lazy_cache[name] = ToolCall
|
||||
return ToolCall
|
||||
elif name == "process_stream_chunks":
|
||||
from .openai_client import process_stream_chunks
|
||||
_lazy_cache[name] = process_stream_chunks
|
||||
return process_stream_chunks
|
||||
elif name == "supports_structured_outputs":
|
||||
from .model_capabilities import supports_structured_outputs
|
||||
_lazy_cache[name] = supports_structured_outputs
|
||||
return supports_structured_outputs
|
||||
elif name == "supports_streaming_with_tools":
|
||||
from .model_capabilities import supports_streaming_with_tools
|
||||
_lazy_cache[name] = supports_streaming_with_tools
|
||||
return supports_streaming_with_tools
|
||||
elif name == "ModelRouter":
|
||||
from .model_router import ModelRouter
|
||||
_lazy_cache[name] = ModelRouter
|
||||
return ModelRouter
|
||||
elif name == "ModelProfile":
|
||||
from .model_router import ModelProfile
|
||||
_lazy_cache[name] = ModelProfile
|
||||
return ModelProfile
|
||||
elif name == "TaskComplexity":
|
||||
from .model_router import TaskComplexity
|
||||
_lazy_cache[name] = TaskComplexity
|
||||
return TaskComplexity
|
||||
elif name == "create_routing_agent":
|
||||
from .model_router import create_routing_agent
|
||||
_lazy_cache[name] = create_routing_agent
|
||||
return create_routing_agent
|
||||
elif name == "RateLimiter":
|
||||
from .rate_limiter import RateLimiter
|
||||
_lazy_cache[name] = RateLimiter
|
||||
return RateLimiter
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LLM",
|
||||
@@ -49,4 +118,4 @@ __all__ = [
|
||||
"TaskComplexity",
|
||||
"create_routing_agent",
|
||||
"RateLimiter"
|
||||
]
|
||||
]
|
||||
Whitespace-only changes.
@@ -104,12 +104,34 @@ class LLM:
|
||||
OLLAMA_TOOL_USAGE_PROMPT = "Please analyze the request and use the available tools to help answer the question. Start by identifying what information you need."
|
||||
OLLAMA_FINAL_ANSWER_PROMPT = "Based on the tool results above, please provide the final answer to the original question."
|
||||
|
||||
# Force tool usage prompt template (used when model ignores tools)
|
||||
FORCE_TOOL_USAGE_PROMPT = """You MUST use one of the available tools to answer this question.
|
||||
Do NOT provide a direct answer. Respond ONLY with a tool call.
|
||||
|
||||
Available tools: {tool_names}
|
||||
|
||||
Respond with a JSON object in this format:
|
||||
{{"name": "tool_name", "arguments": {{"arg1": value1, "arg2": value2}}}}"""
|
||||
|
||||
# Tool call repair prompt template (used when tool call is malformed)
|
||||
TOOL_CALL_REPAIR_PROMPT = """Your previous tool call was malformed: {error}
|
||||
|
||||
Please try again. Available tools:
|
||||
{tool_schemas}
|
||||
|
||||
Respond with ONLY a valid JSON tool call in this format:
|
||||
{{"name": "tool_name", "arguments": {{"arg1": value1, "arg2": value2}}}}"""
|
||||
|
||||
# Ollama iteration threshold for summary generation
|
||||
OLLAMA_SUMMARY_ITERATION_THRESHOLD = 1
|
||||
|
||||
@classmethod
|
||||
def _configure_logging(cls):
|
||||
"""Configure logging settings once for all LLM instances."""
|
||||
# First, configure litellm via the centralized logging module
|
||||
from .._logging import configure_litellm
|
||||
configure_litellm()
|
||||
|
||||
try:
|
||||
import litellm
|
||||
# Disable telemetry
|
||||
@@ -294,6 +316,16 @@ class LLM:
|
||||
# Auto-detect XML tool format for known models, or allow manual override
|
||||
self.xml_tool_format = extra_settings.get('xml_tool_format', 'auto')
|
||||
|
||||
# Tool calling reliability settings (for weak models like Ollama)
|
||||
# These are auto-configured for Ollama providers if not explicitly set
|
||||
self._max_tool_repairs_explicit = 'max_tool_repairs' in extra_settings
|
||||
self._force_tool_usage_explicit = 'force_tool_usage' in extra_settings
|
||||
self.max_tool_repairs = extra_settings.get('max_tool_repairs', 0) # Will be set to 2 for Ollama if not explicit
|
||||
self.force_tool_usage = extra_settings.get('force_tool_usage', 'never') # Will be set to 'auto' for Ollama if not explicit
|
||||
|
||||
# Apply Ollama-specific defaults after model is set
|
||||
self._apply_ollama_defaults()
|
||||
|
||||
# Token tracking
|
||||
self.last_token_metrics: Optional[TokenMetrics] = None
|
||||
self.session_token_metrics: Optional[TokenMetrics] = None
|
||||
@@ -347,6 +379,16 @@ class LLM:
|
||||
self._console = Console()
|
||||
return self._console
|
||||
|
||||
def _apply_ollama_defaults(self):
|
||||
"""Apply Ollama-specific defaults for tool calling reliability."""
|
||||
if self._is_ollama_provider():
|
||||
# Apply defaults only if not explicitly set by user
|
||||
if not self._max_tool_repairs_explicit:
|
||||
self.max_tool_repairs = 2
|
||||
if not self._force_tool_usage_explicit:
|
||||
self.force_tool_usage = 'auto'
|
||||
logging.debug(f"[OLLAMA_RELIABILITY] Applied Ollama defaults: max_tool_repairs={self.max_tool_repairs}, force_tool_usage={self.force_tool_usage}")
|
||||
|
||||
def _is_ollama_provider(self) -> bool:
|
||||
"""Detect if this is an Ollama provider regardless of naming convention"""
|
||||
if not self.model:
|
||||
@@ -562,14 +604,150 @@ class LLM:
|
||||
def _format_ollama_tool_result_message(self, function_name: str, tool_result: Any) -> Dict[str, str]:
|
||||
"""
|
||||
Format tool result message for Ollama provider.
|
||||
Simplified approach without hardcoded regex extraction.
|
||||
Enhanced to instruct model to use the result for final answer.
|
||||
"""
|
||||
tool_result_str = str(tool_result)
|
||||
return {
|
||||
"role": "user",
|
||||
"content": f"The {function_name} function returned: {tool_result_str}"
|
||||
"content": f"""Tool execution complete.
|
||||
Function: {function_name}
|
||||
Result: {tool_result_str}
|
||||
|
||||
Now provide your final answer using this result."""
|
||||
}
|
||||
|
||||
def _get_tool_names_for_prompt(self, formatted_tools: Optional[List]) -> str:
|
||||
"""Extract tool names from formatted tools for prompts."""
|
||||
if not formatted_tools:
|
||||
return "None"
|
||||
names = []
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and 'function' in tool:
|
||||
names.append(tool['function'].get('name', 'unknown'))
|
||||
return ', '.join(names) if names else "None"
|
||||
|
||||
def _get_tool_schemas_for_prompt(self, formatted_tools: Optional[List]) -> str:
|
||||
"""Generate compact tool schemas for repair prompts."""
|
||||
if not formatted_tools:
|
||||
return "None"
|
||||
schemas = []
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and 'function' in tool:
|
||||
func = tool['function']
|
||||
name = func.get('name', 'unknown')
|
||||
params = func.get('parameters', {})
|
||||
required = params.get('required', [])
|
||||
props = params.get('properties', {})
|
||||
param_strs = []
|
||||
for pname, pinfo in props.items():
|
||||
ptype = pinfo.get('type', 'any')
|
||||
req = '*' if pname in required else ''
|
||||
param_strs.append(f"{pname}{req}: {ptype}")
|
||||
schemas.append(f"- {name}({', '.join(param_strs)})")
|
||||
return '\n'.join(schemas) if schemas else "None"
|
||||
|
||||
def _should_force_tool_usage(self, response_text: str, tool_calls: Optional[List], formatted_tools: Optional[List], iteration_count: int) -> bool:
|
||||
"""
|
||||
Determine if we should force tool usage based on settings and context.
|
||||
|
||||
Returns True if:
|
||||
- force_tool_usage == 'always' and tools exist and no tool_calls
|
||||
- force_tool_usage == 'auto' and is Ollama and first iteration and tools exist and no tool_calls
|
||||
"""
|
||||
if not formatted_tools:
|
||||
return False
|
||||
if tool_calls:
|
||||
return False
|
||||
|
||||
if self.force_tool_usage == 'never':
|
||||
return False
|
||||
elif self.force_tool_usage == 'always':
|
||||
return True
|
||||
elif self.force_tool_usage == 'auto':
|
||||
# Auto mode: only for Ollama on first iteration when model ignores tools
|
||||
return self._is_ollama_provider() and iteration_count == 0
|
||||
return False
|
||||
|
||||
def _try_parse_tool_call_json(self, response_text: str, iteration_count: int) -> tuple:
|
||||
"""
|
||||
Try to parse tool call from JSON response text.
|
||||
|
||||
Returns:
|
||||
tuple: (tool_calls list or None, parse_error string or None)
|
||||
"""
|
||||
if not response_text or not response_text.strip():
|
||||
return None, None
|
||||
|
||||
try:
|
||||
response_json = json.loads(response_text.strip())
|
||||
if isinstance(response_json, dict) and "name" in response_json:
|
||||
tool_calls = [{
|
||||
"id": f"tool_{iteration_count}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": response_json["name"],
|
||||
"arguments": json.dumps(response_json.get("arguments", {}))
|
||||
}
|
||||
}]
|
||||
return tool_calls, None
|
||||
elif isinstance(response_json, list):
|
||||
tool_calls = []
|
||||
for idx, tool_json in enumerate(response_json):
|
||||
if isinstance(tool_json, dict) and "name" in tool_json:
|
||||
tool_calls.append({
|
||||
"id": f"tool_{iteration_count}_{idx}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_json["name"],
|
||||
"arguments": json.dumps(tool_json.get("arguments", {}))
|
||||
}
|
||||
})
|
||||
return tool_calls if tool_calls else None, None
|
||||
else:
|
||||
return None, "Response is not a valid tool call format (missing 'name' field)"
|
||||
except json.JSONDecodeError as e:
|
||||
return None, f"JSON parse error: {str(e)}"
|
||||
|
||||
def _validate_tool_call(self, tool_call: Dict, formatted_tools: Optional[List]) -> Optional[str]:
|
||||
"""
|
||||
Validate a tool call against available tools.
|
||||
|
||||
Returns:
|
||||
Error message string if invalid, None if valid
|
||||
"""
|
||||
if not formatted_tools:
|
||||
return None
|
||||
|
||||
try:
|
||||
func_info = tool_call.get('function', {})
|
||||
tool_name = func_info.get('name', '')
|
||||
arguments_str = func_info.get('arguments', '{}')
|
||||
|
||||
# Check if tool name exists
|
||||
available_names = [t['function']['name'] for t in formatted_tools if isinstance(t, dict) and 'function' in t]
|
||||
if tool_name not in available_names:
|
||||
return f"Unknown tool '{tool_name}'. Available: {', '.join(available_names)}"
|
||||
|
||||
# Try to parse arguments
|
||||
try:
|
||||
arguments = json.loads(arguments_str) if isinstance(arguments_str, str) else arguments_str
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Invalid arguments JSON: {str(e)}"
|
||||
|
||||
# Find the tool schema and validate required args
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and tool.get('function', {}).get('name') == tool_name:
|
||||
params = tool['function'].get('parameters', {})
|
||||
required = params.get('required', [])
|
||||
missing = [r for r in required if r not in arguments]
|
||||
if missing:
|
||||
return f"Missing required arguments for '{tool_name}': {', '.join(missing)}"
|
||||
break
|
||||
|
||||
return None # Valid
|
||||
except Exception as e:
|
||||
return f"Validation error: {str(e)}"
|
||||
|
||||
def _process_stream_delta(self, delta, response_text: str, tool_calls: List[Dict], formatted_tools: Optional[List] = None) -> tuple:
|
||||
"""
|
||||
Process a streaming delta chunk to extract content and tool calls.
|
||||
@@ -1637,6 +1815,18 @@ class LLM:
|
||||
if tool_calls:
|
||||
logging.debug(f"Parsed {len(tool_calls)} tool call(s) from XML format")
|
||||
|
||||
# Force tool usage logic: if model ignores tools but should use them
|
||||
if self._should_force_tool_usage(response_text, tool_calls, formatted_tools, iteration_count):
|
||||
tool_names = self._get_tool_names_for_prompt(formatted_tools)
|
||||
force_prompt = self.FORCE_TOOL_USAGE_PROMPT.format(tool_names=tool_names)
|
||||
logging.debug(f"[OLLAMA_RELIABILITY] Force tool usage triggered. Adding prompt for tools: {tool_names}")
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": force_prompt
|
||||
})
|
||||
iteration_count += 1
|
||||
continue
|
||||
|
||||
# For Ollama, if response is empty but we have tools, prompt for tool usage
|
||||
if self._is_ollama_provider() and (not response_text or response_text.strip() == "") and formatted_tools and iteration_count == 0:
|
||||
messages.append({
|
||||
@@ -1646,6 +1836,37 @@ class LLM:
|
||||
iteration_count += 1
|
||||
continue
|
||||
|
||||
# Tool call repair logic: validate and repair malformed tool calls
|
||||
repair_attempt_count = getattr(self, '_current_repair_count', 0)
|
||||
if tool_calls and self.max_tool_repairs > 0 and repair_attempt_count < self.max_tool_repairs:
|
||||
# Validate each tool call
|
||||
validation_errors = []
|
||||
for tc in tool_calls:
|
||||
error = self._validate_tool_call(tc, formatted_tools)
|
||||
if error:
|
||||
validation_errors.append(error)
|
||||
|
||||
if validation_errors:
|
||||
# Tool call is invalid, attempt repair
|
||||
error_msg = "; ".join(validation_errors)
|
||||
tool_schemas = self._get_tool_schemas_for_prompt(formatted_tools)
|
||||
repair_prompt = self.TOOL_CALL_REPAIR_PROMPT.format(
|
||||
error=error_msg,
|
||||
tool_schemas=tool_schemas
|
||||
)
|
||||
logging.debug(f"[OLLAMA_RELIABILITY] Tool call repair attempt {repair_attempt_count + 1}/{self.max_tool_repairs}: {error_msg}")
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": repair_prompt
|
||||
})
|
||||
self._current_repair_count = repair_attempt_count + 1
|
||||
iteration_count += 1
|
||||
tool_calls = None # Clear invalid tool calls
|
||||
continue
|
||||
|
||||
# Reset repair count on successful tool call
|
||||
self._current_repair_count = 0
|
||||
|
||||
# Handle tool calls - Sequential tool calling logic
|
||||
if tool_calls and execute_tool_fn:
|
||||
# Convert tool_calls to a serializable format for all providers
|
||||
|
||||
@@ -580,7 +580,60 @@ class MCP:
|
||||
|
||||
return openai_tools
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry - return self for use in 'with' statements."""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Context manager exit - clean up resources."""
|
||||
self.shutdown()
|
||||
return False # Don't suppress exceptions
|
||||
|
||||
def shutdown(self):
|
||||
"""Explicitly shut down MCP resources.
|
||||
|
||||
Call this method when done using the MCP instance to ensure
|
||||
all background threads and connections are properly cleaned up.
|
||||
"""
|
||||
# Shutdown stdio runner if present
|
||||
if hasattr(self, 'runner') and self.runner is not None:
|
||||
try:
|
||||
self.runner.shutdown()
|
||||
except Exception:
|
||||
pass # Best effort cleanup
|
||||
|
||||
# Shutdown SSE client if present
|
||||
if hasattr(self, 'sse_client') and self.sse_client is not None:
|
||||
try:
|
||||
if hasattr(self.sse_client, 'shutdown'):
|
||||
self.sse_client.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Shutdown HTTP stream client if present
|
||||
if hasattr(self, 'http_stream_client') and self.http_stream_client is not None:
|
||||
try:
|
||||
if hasattr(self.http_stream_client, 'shutdown'):
|
||||
self.http_stream_client.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Shutdown WebSocket client if present
|
||||
if hasattr(self, 'websocket_client') and self.websocket_client is not None:
|
||||
try:
|
||||
if hasattr(self.websocket_client, 'shutdown'):
|
||||
self.websocket_client.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __del__(self):
|
||||
"""Clean up resources when the object is garbage collected."""
|
||||
if hasattr(self, 'runner'):
|
||||
self.runner.shutdown()
|
||||
"""Clean up resources when the object is garbage collected.
|
||||
|
||||
Note: __del__ is called during garbage collection and may not
|
||||
always run. For reliable cleanup, use the context manager
|
||||
pattern or call shutdown() explicitly.
|
||||
"""
|
||||
try:
|
||||
self.shutdown()
|
||||
except Exception:
|
||||
pass # Best effort cleanup in __del__
|
||||
@@ -27,13 +27,12 @@ Usage:
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
|
||||
This module uses lazy loading to minimize import time.
|
||||
"""
|
||||
|
||||
from .plan import Plan, PlanStep
|
||||
from .todo import TodoList, TodoItem
|
||||
from .storage import PlanStorage
|
||||
from .planner import PlanningAgent
|
||||
from .approval import ApprovalCallback
|
||||
# Module-level cache for lazy-loaded classes
|
||||
_lazy_cache = {}
|
||||
|
||||
# Read-only tools that are allowed in plan mode
|
||||
READ_ONLY_TOOLS = [
|
||||
@@ -90,6 +89,44 @@ RESEARCH_TOOLS = [
|
||||
"find_files",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazy load planning classes to avoid importing heavy dependencies at module load time."""
|
||||
if name in _lazy_cache:
|
||||
return _lazy_cache[name]
|
||||
|
||||
if name == "Plan":
|
||||
from .plan import Plan
|
||||
_lazy_cache[name] = Plan
|
||||
return Plan
|
||||
elif name == "PlanStep":
|
||||
from .plan import PlanStep
|
||||
_lazy_cache[name] = PlanStep
|
||||
return PlanStep
|
||||
elif name == "TodoList":
|
||||
from .todo import TodoList
|
||||
_lazy_cache[name] = TodoList
|
||||
return TodoList
|
||||
elif name == "TodoItem":
|
||||
from .todo import TodoItem
|
||||
_lazy_cache[name] = TodoItem
|
||||
return TodoItem
|
||||
elif name == "PlanStorage":
|
||||
from .storage import PlanStorage
|
||||
_lazy_cache[name] = PlanStorage
|
||||
return PlanStorage
|
||||
elif name == "PlanningAgent":
|
||||
from .planner import PlanningAgent
|
||||
_lazy_cache[name] = PlanningAgent
|
||||
return PlanningAgent
|
||||
elif name == "ApprovalCallback":
|
||||
from .approval import ApprovalCallback
|
||||
_lazy_cache[name] = ApprovalCallback
|
||||
return ApprovalCallback
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Plan",
|
||||
"PlanStep",
|
||||
|
||||
Whitespace-only changes.
@@ -4,6 +4,7 @@ This module provides centralized tool management with support for:
|
||||
- Manual registration
|
||||
- Auto-discovery via entry_points
|
||||
- Tool lookup by name
|
||||
- Thread-safe operations for multi-agent scenarios
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools.registry import get_registry
|
||||
@@ -14,6 +15,7 @@ Usage:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Callable, Dict, List, Optional, Union
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
@@ -38,6 +40,7 @@ class ToolRegistry:
|
||||
self._tools: Dict[str, BaseTool] = {}
|
||||
self._functions: Dict[str, Callable] = {} # For backward compat with plain functions
|
||||
self._discovered: bool = False
|
||||
self._lock = threading.RLock() # Thread-safe operations for multi-agent scenarios
|
||||
|
||||
def register(
|
||||
self,
|
||||
@@ -47,6 +50,8 @@ class ToolRegistry:
|
||||
) -> None:
|
||||
"""Register a tool with the registry.
|
||||
|
||||
Thread-safe: Uses lock for concurrent access in multi-agent scenarios.
|
||||
|
||||
Args:
|
||||
tool: BaseTool instance or callable function
|
||||
name: Override name (default: tool.name or function.__name__)
|
||||
@@ -55,83 +60,93 @@ class ToolRegistry:
|
||||
Raises:
|
||||
ValueError: If tool with same name exists and overwrite=False
|
||||
"""
|
||||
# Handle BaseTool instances
|
||||
if isinstance(tool, BaseTool):
|
||||
tool_name = name or tool.name
|
||||
if tool_name in self._tools and not overwrite:
|
||||
logging.debug(f"Tool '{tool_name}' already registered, skipping")
|
||||
with self._lock:
|
||||
# Handle BaseTool instances
|
||||
if isinstance(tool, BaseTool):
|
||||
tool_name = name or tool.name
|
||||
if tool_name in self._tools and not overwrite:
|
||||
logging.debug(f"Tool '{tool_name}' already registered, skipping")
|
||||
return
|
||||
self._tools[tool_name] = tool
|
||||
logging.debug(f"Registered tool: {tool_name}")
|
||||
return
|
||||
self._tools[tool_name] = tool
|
||||
logging.debug(f"Registered tool: {tool_name}")
|
||||
return
|
||||
|
||||
# Handle plain callables
|
||||
if callable(tool):
|
||||
tool_name = name or getattr(tool, '__name__', str(id(tool)))
|
||||
if tool_name in self._functions and not overwrite:
|
||||
logging.debug(f"Function '{tool_name}' already registered, skipping")
|
||||
|
||||
# Handle plain callables
|
||||
if callable(tool):
|
||||
tool_name = name or getattr(tool, '__name__', str(id(tool)))
|
||||
if tool_name in self._functions and not overwrite:
|
||||
logging.debug(f"Function '{tool_name}' already registered, skipping")
|
||||
return
|
||||
self._functions[tool_name] = tool
|
||||
logging.debug(f"Registered function: {tool_name}")
|
||||
return
|
||||
self._functions[tool_name] = tool
|
||||
logging.debug(f"Registered function: {tool_name}")
|
||||
return
|
||||
|
||||
raise TypeError(f"Cannot register {type(tool)}, expected BaseTool or callable")
|
||||
|
||||
raise TypeError(f"Cannot register {type(tool)}, expected BaseTool or callable")
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""Remove a tool from the registry.
|
||||
|
||||
Thread-safe: Uses lock for concurrent access.
|
||||
|
||||
Args:
|
||||
name: Tool name to remove
|
||||
|
||||
Returns:
|
||||
True if tool was removed, False if not found
|
||||
"""
|
||||
if name in self._tools:
|
||||
del self._tools[name]
|
||||
return True
|
||||
if name in self._functions:
|
||||
del self._functions[name]
|
||||
return True
|
||||
return False
|
||||
with self._lock:
|
||||
if name in self._tools:
|
||||
del self._tools[name]
|
||||
return True
|
||||
if name in self._functions:
|
||||
del self._functions[name]
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, name: str) -> Optional[Union[BaseTool, Callable]]:
|
||||
"""Get a tool by name.
|
||||
|
||||
Thread-safe: Uses lock for concurrent access.
|
||||
|
||||
Args:
|
||||
name: Tool name
|
||||
|
||||
Returns:
|
||||
BaseTool instance, callable, or None if not found
|
||||
"""
|
||||
# Check BaseTool registry first
|
||||
if name in self._tools:
|
||||
return self._tools[name]
|
||||
|
||||
# Check functions registry
|
||||
if name in self._functions:
|
||||
return self._functions[name]
|
||||
|
||||
# Try auto-discovery if not found
|
||||
if not self._discovered:
|
||||
self.discover_plugins()
|
||||
with self._lock:
|
||||
# Check BaseTool registry first
|
||||
if name in self._tools:
|
||||
return self._tools[name]
|
||||
|
||||
return None
|
||||
|
||||
# Check functions registry
|
||||
if name in self._functions:
|
||||
return self._functions[name]
|
||||
|
||||
# Try auto-discovery if not found
|
||||
if not self._discovered:
|
||||
self.discover_plugins()
|
||||
if name in self._tools:
|
||||
return self._tools[name]
|
||||
|
||||
return None
|
||||
|
||||
def list_tools(self) -> List[str]:
|
||||
"""List all registered tool names."""
|
||||
return list(self._tools.keys()) + list(self._functions.keys())
|
||||
"""List all registered tool names. Thread-safe."""
|
||||
with self._lock:
|
||||
return list(self._tools.keys()) + list(self._functions.keys())
|
||||
|
||||
def list_base_tools(self) -> List[BaseTool]:
|
||||
"""List all registered BaseTool instances."""
|
||||
return list(self._tools.values())
|
||||
"""List all registered BaseTool instances. Thread-safe."""
|
||||
with self._lock:
|
||||
return list(self._tools.values())
|
||||
|
||||
def get_all(self) -> Dict[str, Union[BaseTool, Callable]]:
|
||||
"""Get all registered tools as a dict."""
|
||||
result = dict(self._tools)
|
||||
result.update(self._functions)
|
||||
return result
|
||||
"""Get all registered tools as a dict. Thread-safe."""
|
||||
with self._lock:
|
||||
result = dict(self._tools)
|
||||
result.update(self._functions)
|
||||
return result
|
||||
|
||||
def discover_plugins(self) -> int:
|
||||
"""Discover and register tools from entry_points.
|
||||
@@ -185,16 +200,19 @@ class ToolRegistry:
|
||||
return count
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all registered tools."""
|
||||
self._tools.clear()
|
||||
self._functions.clear()
|
||||
self._discovered = False
|
||||
"""Clear all registered tools. Thread-safe."""
|
||||
with self._lock:
|
||||
self._tools.clear()
|
||||
self._functions.clear()
|
||||
self._discovered = False
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
return name in self._tools or name in self._functions
|
||||
with self._lock:
|
||||
return name in self._tools or name in self._functions
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._tools) + len(self._functions)
|
||||
with self._lock:
|
||||
return len(self._tools) + len(self._functions)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ToolRegistry(tools={len(self._tools)}, functions={len(self._functions)})"
|
||||
|
||||
@@ -6,8 +6,35 @@ via various frontend protocols.
|
||||
|
||||
Available integrations:
|
||||
- agui: AG-UI (Agent-User Interface) protocol for CopilotKit and compatible frontends
|
||||
- a2a: Agent-to-Agent protocol
|
||||
- a2ui: Agent-to-UI protocol
|
||||
|
||||
This module uses lazy loading to minimize import time.
|
||||
"""
|
||||
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
# Module-level cache for lazy-loaded classes
|
||||
_lazy_cache = {}
|
||||
|
||||
__all__ = ["AGUI"]
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazy load UI classes to avoid importing heavy dependencies at module load time."""
|
||||
if name in _lazy_cache:
|
||||
return _lazy_cache[name]
|
||||
|
||||
if name == "AGUI":
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
_lazy_cache[name] = AGUI
|
||||
return AGUI
|
||||
elif name == "A2A":
|
||||
from praisonaiagents.ui.a2a import A2A
|
||||
_lazy_cache[name] = A2A
|
||||
return A2A
|
||||
elif name == "A2UI":
|
||||
from praisonaiagents.ui.a2ui import A2UI
|
||||
_lazy_cache[name] = A2UI
|
||||
return A2UI
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = ["AGUI", "A2A", "A2UI"]
|
||||
Whitespace-only changes.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.4.0"
|
||||
version = "0.5.1"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
Real API key integration tests for praisonaiagents.
|
||||
|
||||
These tests are gated by the RUN_REAL_KEY_TESTS environment variable.
|
||||
They require valid API keys to be set in the environment.
|
||||
|
||||
Usage:
|
||||
RUN_REAL_KEY_TESTS=1 pytest tests/integration/test_real_api.py -v
|
||||
|
||||
Required environment variables:
|
||||
- OPENAI_API_KEY: OpenAI API key
|
||||
|
||||
Optional:
|
||||
- ANTHROPIC_API_KEY: Anthropic API key (for Claude tests)
|
||||
- GOOGLE_API_KEY: Google API key (for Gemini tests)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
# Gate all tests in this module
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get('RUN_REAL_KEY_TESTS', '').lower() not in ('1', 'true', 'yes'),
|
||||
reason="Real API key tests disabled. Set RUN_REAL_KEY_TESTS=1 to enable."
|
||||
)
|
||||
|
||||
|
||||
def clear_modules():
|
||||
"""Clear all praisonai and litellm related modules from cache."""
|
||||
to_remove = [m for m in list(sys.modules.keys())
|
||||
if 'praison' in m or 'litellm' in m]
|
||||
for mod in to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
|
||||
class TestAgentRealAPI:
|
||||
"""Test Agent with real API calls."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""Set up test fixtures."""
|
||||
clear_modules()
|
||||
|
||||
# Verify API key is set (without printing it)
|
||||
api_key = os.environ.get('OPENAI_API_KEY')
|
||||
if not api_key:
|
||||
pytest.skip("OPENAI_API_KEY not set")
|
||||
|
||||
yield
|
||||
clear_modules()
|
||||
|
||||
def test_agent_simple_chat(self):
|
||||
"""Test basic Agent.chat() with real API."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="TestAgent",
|
||||
instructions="You are a helpful assistant. Keep responses very brief.",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
response = agent.chat("Say 'hello' and nothing else.")
|
||||
|
||||
assert response is not None
|
||||
assert len(response) > 0
|
||||
assert 'hello' in response.lower()
|
||||
|
||||
print(f"[PASS] Agent response received (length: {len(response)})")
|
||||
|
||||
def test_agent_with_tool(self):
|
||||
"""Test Agent with a simple tool."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Define a simple deterministic tool
|
||||
def add_numbers(a: int, b: int) -> int:
|
||||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
agent = Agent(
|
||||
name="MathAgent",
|
||||
instructions="You are a math assistant. Use the add_numbers tool when asked to add.",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[add_numbers],
|
||||
verbose=False
|
||||
)
|
||||
|
||||
response = agent.chat("What is 5 + 3? Use the add_numbers tool.")
|
||||
|
||||
assert response is not None
|
||||
assert '8' in response
|
||||
|
||||
print(f"[PASS] Tool call successful, response contains '8'")
|
||||
|
||||
def test_agent_chat_history(self):
|
||||
"""Test that chat history is maintained."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="HistoryAgent",
|
||||
instructions="You are a helpful assistant. Remember what the user tells you.",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# First message
|
||||
agent.chat("My favorite color is blue.")
|
||||
|
||||
# Second message referencing first
|
||||
response = agent.chat("What is my favorite color?")
|
||||
|
||||
assert response is not None
|
||||
assert 'blue' in response.lower()
|
||||
|
||||
print(f"[PASS] Chat history maintained correctly")
|
||||
|
||||
|
||||
class TestLiteAgentRealAPI:
|
||||
"""Test LiteAgent with real API using OpenAI SDK directly."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""Set up test fixtures."""
|
||||
clear_modules()
|
||||
|
||||
api_key = os.environ.get('OPENAI_API_KEY')
|
||||
if not api_key:
|
||||
pytest.skip("OPENAI_API_KEY not set")
|
||||
|
||||
yield
|
||||
clear_modules()
|
||||
|
||||
def test_lite_agent_with_openai(self):
|
||||
"""Test LiteAgent with OpenAI SDK."""
|
||||
from praisonaiagents.lite import LiteAgent, create_openai_llm_fn
|
||||
|
||||
# Create LLM function using OpenAI
|
||||
llm_fn = create_openai_llm_fn(model="gpt-4o-mini")
|
||||
|
||||
agent = LiteAgent(
|
||||
name="LiteTestAgent",
|
||||
llm_fn=llm_fn,
|
||||
instructions="You are a helpful assistant. Keep responses very brief."
|
||||
)
|
||||
|
||||
response = agent.chat("Say 'hello' and nothing else.")
|
||||
|
||||
assert response is not None
|
||||
assert len(response) > 0
|
||||
|
||||
print(f"[PASS] LiteAgent with OpenAI response received")
|
||||
|
||||
def test_lite_agent_no_litellm_loaded(self):
|
||||
"""Verify LiteAgent doesn't load litellm."""
|
||||
from praisonaiagents.lite import LiteAgent, create_openai_llm_fn
|
||||
|
||||
llm_fn = create_openai_llm_fn(model="gpt-4o-mini")
|
||||
agent = LiteAgent(llm_fn=llm_fn)
|
||||
|
||||
response = agent.chat("Hi")
|
||||
|
||||
assert response is not None
|
||||
assert 'litellm' not in sys.modules, \
|
||||
"LiteAgent should not load litellm"
|
||||
|
||||
print(f"[PASS] LiteAgent works without litellm")
|
||||
|
||||
|
||||
class TestAnthropicAPI:
|
||||
"""Test with Anthropic API (optional)."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""Set up test fixtures."""
|
||||
clear_modules()
|
||||
|
||||
api_key = os.environ.get('ANTHROPIC_API_KEY')
|
||||
if not api_key:
|
||||
pytest.skip("ANTHROPIC_API_KEY not set")
|
||||
|
||||
yield
|
||||
clear_modules()
|
||||
|
||||
def test_agent_with_claude(self):
|
||||
"""Test Agent with Claude model."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="ClaudeAgent",
|
||||
instructions="You are a helpful assistant. Keep responses very brief.",
|
||||
llm="claude-3-5-sonnet-20241022",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
response = agent.chat("Say 'hello' and nothing else.")
|
||||
|
||||
assert response is not None
|
||||
assert len(response) > 0
|
||||
|
||||
print(f"[PASS] Claude response received")
|
||||
|
||||
|
||||
class TestGoogleAPI:
|
||||
"""Test with Google API (optional)."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""Set up test fixtures."""
|
||||
clear_modules()
|
||||
|
||||
api_key = os.environ.get('GOOGLE_API_KEY')
|
||||
if not api_key:
|
||||
pytest.skip("GOOGLE_API_KEY not set")
|
||||
|
||||
yield
|
||||
clear_modules()
|
||||
|
||||
def test_agent_with_gemini(self):
|
||||
"""Test Agent with Gemini model."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="GeminiAgent",
|
||||
instructions="You are a helpful assistant. Keep responses very brief.",
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
response = agent.chat("Say 'hello' and nothing else.")
|
||||
|
||||
assert response is not None
|
||||
assert len(response) > 0
|
||||
|
||||
print(f"[PASS] Gemini response received")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run with real API keys
|
||||
if os.environ.get('RUN_REAL_KEY_TESTS', '').lower() not in ('1', 'true', 'yes'):
|
||||
print("Set RUN_REAL_KEY_TESTS=1 to run these tests")
|
||||
sys.exit(1)
|
||||
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
@@ -5,11 +5,16 @@ Basic test to verify HTTP Stream implementation is working correctly.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from praisonaiagents.mcp import MCP
|
||||
# Skip entire module if mcp package is not installed
|
||||
try:
|
||||
from praisonaiagents.mcp import MCP
|
||||
except ImportError:
|
||||
pytest.skip("MCP package not installed", allow_module_level=True)
|
||||
|
||||
def test_transport_selection():
|
||||
"""Test that URLs are correctly routed to appropriate transports."""
|
||||
|
||||
@@ -18,8 +18,12 @@ from unittest.mock import Mock
|
||||
# Add the package to path for testing
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from praisonaiagents.mcp.mcp import MCP
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
# Skip entire module if mcp package is not installed
|
||||
try:
|
||||
from praisonaiagents.mcp.mcp import MCP
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
except ImportError:
|
||||
pytest.skip("MCP package not installed", allow_module_level=True)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Tests for lazy import behavior in praisonaiagents.
|
||||
|
||||
These tests verify that heavy dependencies like litellm are NOT loaded
|
||||
at package import time, ensuring fast startup.
|
||||
"""
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
|
||||
def clear_modules():
|
||||
"""Clear all praisonai and litellm related modules from cache."""
|
||||
to_remove = [m for m in list(sys.modules.keys())
|
||||
if 'praison' in m or 'litellm' in m]
|
||||
for mod in to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
|
||||
class TestLazyImports:
|
||||
"""Test that heavy dependencies are lazily loaded."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Clear module cache before each test."""
|
||||
clear_modules()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up after each test."""
|
||||
clear_modules()
|
||||
|
||||
def test_litellm_not_loaded_on_import(self):
|
||||
"""litellm should NOT be loaded when importing praisonaiagents."""
|
||||
import praisonaiagents # noqa: F401
|
||||
|
||||
assert 'litellm' not in sys.modules, \
|
||||
"litellm should not be loaded at import time"
|
||||
|
||||
def test_requests_not_loaded_on_import(self):
|
||||
"""requests should NOT be loaded when importing praisonaiagents."""
|
||||
import praisonaiagents # noqa: F401
|
||||
|
||||
assert 'requests' not in sys.modules, \
|
||||
"requests should not be loaded at import time"
|
||||
|
||||
def test_chromadb_not_loaded_on_import(self):
|
||||
"""chromadb should NOT be loaded when importing praisonaiagents."""
|
||||
import praisonaiagents # noqa: F401
|
||||
|
||||
assert 'chromadb' not in sys.modules, \
|
||||
"chromadb should not be loaded at import time"
|
||||
|
||||
def test_agent_available_via_getattr(self):
|
||||
"""Agent should be available via lazy loading."""
|
||||
import praisonaiagents
|
||||
|
||||
# Access Agent - this triggers lazy loading
|
||||
Agent = praisonaiagents.Agent
|
||||
|
||||
assert Agent is not None
|
||||
assert hasattr(Agent, 'chat')
|
||||
|
||||
def test_session_available_via_getattr(self):
|
||||
"""Session should be available via lazy loading."""
|
||||
import praisonaiagents
|
||||
|
||||
# Access Session - this triggers lazy loading
|
||||
Session = praisonaiagents.Session
|
||||
|
||||
assert Session is not None
|
||||
|
||||
def test_knowledge_available_via_getattr(self):
|
||||
"""Knowledge should be available via lazy loading."""
|
||||
import praisonaiagents
|
||||
|
||||
# Access Knowledge - this triggers lazy loading
|
||||
Knowledge = praisonaiagents.Knowledge
|
||||
|
||||
assert Knowledge is not None
|
||||
|
||||
def test_memory_available_via_getattr(self):
|
||||
"""Memory should be available via lazy loading."""
|
||||
import praisonaiagents
|
||||
|
||||
# Access Memory - this triggers lazy loading
|
||||
Memory = praisonaiagents.Memory
|
||||
|
||||
assert Memory is not None
|
||||
|
||||
def test_tools_loaded_eagerly(self):
|
||||
"""Tools should be available immediately (lightweight)."""
|
||||
import praisonaiagents
|
||||
|
||||
# Tools is lightweight and should be available
|
||||
assert hasattr(praisonaiagents, 'Tools')
|
||||
assert praisonaiagents.Tools is not None
|
||||
|
||||
def test_all_exports_accessible(self):
|
||||
"""All items in __all__ should be accessible."""
|
||||
import praisonaiagents
|
||||
|
||||
for name in praisonaiagents.__all__:
|
||||
# Some items may be None if optional deps not installed
|
||||
try:
|
||||
attr = getattr(praisonaiagents, name)
|
||||
# Just verify it's accessible, may be None
|
||||
except AttributeError:
|
||||
pytest.fail(f"'{name}' in __all__ but not accessible")
|
||||
|
||||
|
||||
class TestLitePackage:
|
||||
"""Test the lite subpackage."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Clear module cache before each test."""
|
||||
clear_modules()
|
||||
|
||||
def test_lite_import_fast(self):
|
||||
"""Lite package should import quickly without heavy deps."""
|
||||
import time
|
||||
|
||||
start = time.perf_counter()
|
||||
from praisonaiagents.lite import LiteAgent # noqa: F401
|
||||
end = time.perf_counter()
|
||||
|
||||
import_time_ms = (end - start) * 1000
|
||||
|
||||
# Lite should import in under 200ms (allowing for system variance)
|
||||
assert import_time_ms < 200, \
|
||||
f"Lite import took {import_time_ms:.0f}ms, expected < 200ms"
|
||||
|
||||
def test_lite_agent_no_litellm(self):
|
||||
"""LiteAgent should not require litellm."""
|
||||
from praisonaiagents.lite import LiteAgent
|
||||
|
||||
# Create agent without LLM function
|
||||
agent = LiteAgent(name="Test")
|
||||
|
||||
assert agent.name == "Test"
|
||||
assert 'litellm' not in sys.modules
|
||||
|
||||
def test_lite_agent_with_custom_llm(self):
|
||||
"""LiteAgent should work with custom LLM function."""
|
||||
from praisonaiagents.lite import LiteAgent
|
||||
|
||||
# Create a mock LLM function
|
||||
def mock_llm(messages):
|
||||
return "Hello from mock LLM!"
|
||||
|
||||
agent = LiteAgent(
|
||||
name="TestAgent",
|
||||
llm_fn=mock_llm,
|
||||
instructions="You are a test agent."
|
||||
)
|
||||
|
||||
response = agent.chat("Hi")
|
||||
|
||||
assert response == "Hello from mock LLM!"
|
||||
assert len(agent.chat_history) == 2 # user + assistant
|
||||
|
||||
|
||||
class TestTelemetryConfig:
|
||||
"""Test telemetry configuration."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Clear module cache and env vars before each test."""
|
||||
clear_modules()
|
||||
import os
|
||||
# Save original env vars
|
||||
self._orig_env = {}
|
||||
for key in ['PRAISONAI_TELEMETRY_ENABLED', 'PRAISONAI_TELEMETRY_DISABLED',
|
||||
'DO_NOT_TRACK', 'PRAISONAI_DISABLE_TELEMETRY']:
|
||||
self._orig_env[key] = os.environ.get(key)
|
||||
if key in os.environ:
|
||||
del os.environ[key]
|
||||
|
||||
def teardown_method(self):
|
||||
"""Restore env vars after each test."""
|
||||
import os
|
||||
for key, value in self._orig_env.items():
|
||||
if value is None:
|
||||
if key in os.environ:
|
||||
del os.environ[key]
|
||||
else:
|
||||
os.environ[key] = value
|
||||
clear_modules()
|
||||
|
||||
def test_telemetry_disabled_by_default(self):
|
||||
"""Telemetry should be disabled by default (opt-in model)."""
|
||||
from praisonaiagents._config import TELEMETRY_ENABLED
|
||||
|
||||
assert TELEMETRY_ENABLED is False, \
|
||||
"Telemetry should be disabled by default"
|
||||
|
||||
def test_telemetry_enabled_via_env(self):
|
||||
"""Telemetry can be enabled via environment variable."""
|
||||
import os
|
||||
os.environ['PRAISONAI_TELEMETRY_ENABLED'] = 'true'
|
||||
|
||||
# Re-import to pick up new env var
|
||||
clear_modules()
|
||||
from praisonaiagents._config import _get_telemetry_enabled
|
||||
|
||||
assert _get_telemetry_enabled() is True
|
||||
|
||||
def test_do_not_track_takes_precedence(self):
|
||||
"""DO_NOT_TRACK should disable telemetry even if enabled."""
|
||||
import os
|
||||
os.environ['PRAISONAI_TELEMETRY_ENABLED'] = 'true'
|
||||
os.environ['DO_NOT_TRACK'] = 'true'
|
||||
|
||||
clear_modules()
|
||||
from praisonaiagents._config import _get_telemetry_enabled
|
||||
|
||||
assert _get_telemetry_enabled() is False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
Tests for thread safety in praisonaiagents.
|
||||
|
||||
These tests verify that Agent chat_history and caches are thread-safe
|
||||
when accessed concurrently.
|
||||
"""
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import pytest
|
||||
|
||||
|
||||
def clear_modules():
|
||||
"""Clear all praisonai and litellm related modules from cache."""
|
||||
to_remove = [m for m in list(sys.modules.keys())
|
||||
if 'praison' in m or 'litellm' in m]
|
||||
for mod in to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
|
||||
class TestLiteAgentThreadSafety:
|
||||
"""Test thread safety of LiteAgent (no external deps)."""
|
||||
|
||||
def test_concurrent_chat_history_access(self):
|
||||
"""Multiple threads should safely access chat_history."""
|
||||
from praisonaiagents.lite import LiteAgent
|
||||
|
||||
# Create agent with mock LLM
|
||||
call_count = [0]
|
||||
lock = threading.Lock()
|
||||
|
||||
def mock_llm(messages):
|
||||
with lock:
|
||||
call_count[0] += 1
|
||||
time.sleep(0.01) # Simulate some work
|
||||
return f"Response {call_count[0]}"
|
||||
|
||||
agent = LiteAgent(llm_fn=mock_llm)
|
||||
|
||||
errors = []
|
||||
|
||||
def worker(thread_id):
|
||||
try:
|
||||
for i in range(5):
|
||||
response = agent.chat(f"Message from thread {thread_id}, iteration {i}")
|
||||
assert response is not None
|
||||
except Exception as e:
|
||||
errors.append(f"Thread {thread_id}: {e}")
|
||||
|
||||
# Start multiple threads
|
||||
threads = []
|
||||
for i in range(4):
|
||||
t = threading.Thread(target=worker, args=(i,))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
|
||||
# Wait for all threads
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# Check for errors
|
||||
assert len(errors) == 0, f"Thread errors: {errors}"
|
||||
|
||||
# Verify history was updated (should have entries from all threads)
|
||||
assert len(agent.chat_history) > 0
|
||||
|
||||
def test_concurrent_clear_history(self):
|
||||
"""Clearing history while other threads are chatting should be safe."""
|
||||
from praisonaiagents.lite import LiteAgent
|
||||
|
||||
def mock_llm(messages):
|
||||
return "Response"
|
||||
|
||||
agent = LiteAgent(llm_fn=mock_llm)
|
||||
|
||||
errors = []
|
||||
stop_flag = threading.Event()
|
||||
|
||||
def chat_worker():
|
||||
try:
|
||||
while not stop_flag.is_set():
|
||||
agent.chat("Hello")
|
||||
time.sleep(0.001)
|
||||
except Exception as e:
|
||||
errors.append(f"Chat error: {e}")
|
||||
|
||||
def clear_worker():
|
||||
try:
|
||||
for _ in range(10):
|
||||
time.sleep(0.01)
|
||||
agent.clear_history()
|
||||
except Exception as e:
|
||||
errors.append(f"Clear error: {e}")
|
||||
|
||||
# Start chat threads
|
||||
chat_threads = []
|
||||
for _ in range(2):
|
||||
t = threading.Thread(target=chat_worker)
|
||||
chat_threads.append(t)
|
||||
t.start()
|
||||
|
||||
# Start clear thread
|
||||
clear_thread = threading.Thread(target=clear_worker)
|
||||
clear_thread.start()
|
||||
|
||||
# Wait for clear thread to finish
|
||||
clear_thread.join()
|
||||
|
||||
# Stop chat threads
|
||||
stop_flag.set()
|
||||
for t in chat_threads:
|
||||
t.join()
|
||||
|
||||
assert len(errors) == 0, f"Thread errors: {errors}"
|
||||
|
||||
|
||||
class TestAgentThreadSafety:
|
||||
"""Test thread safety of main Agent class.
|
||||
|
||||
Note: These tests use a mock LLM to avoid network calls.
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
clear_modules()
|
||||
|
||||
def test_agent_has_history_lock(self):
|
||||
"""Agent should have a history lock for thread safety."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Test",
|
||||
instructions="Test agent",
|
||||
llm="gpt-4o-mini" # Won't actually call
|
||||
)
|
||||
|
||||
assert hasattr(agent, '_history_lock')
|
||||
assert isinstance(agent._history_lock, type(threading.Lock()))
|
||||
|
||||
def test_agent_has_cache_lock(self):
|
||||
"""Agent should have a cache lock for thread safety."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Test",
|
||||
instructions="Test agent",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
assert hasattr(agent, '_cache_lock')
|
||||
# RLock is a different type than Lock
|
||||
assert agent._cache_lock is not None
|
||||
|
||||
|
||||
class TestMCPCleanup:
|
||||
"""Test MCP cleanup and context manager support."""
|
||||
|
||||
def test_mcp_has_context_manager(self):
|
||||
"""MCP should support context manager protocol."""
|
||||
try:
|
||||
from praisonaiagents.mcp.mcp import MCP
|
||||
except ImportError:
|
||||
pytest.skip("MCP package not installed")
|
||||
|
||||
assert hasattr(MCP, '__enter__')
|
||||
assert hasattr(MCP, '__exit__')
|
||||
assert hasattr(MCP, 'shutdown')
|
||||
|
||||
def test_mcp_shutdown_method(self):
|
||||
"""MCP should have a shutdown method."""
|
||||
try:
|
||||
from praisonaiagents.mcp.mcp import MCP
|
||||
except ImportError:
|
||||
pytest.skip("MCP package not installed")
|
||||
|
||||
assert callable(getattr(MCP, 'shutdown', None))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.4.0"
|
||||
version = "0.5.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Evaluation Example - Testing agent accuracy and performance
|
||||
*
|
||||
* Run: npx ts-node examples/features/evaluation.ts
|
||||
*/
|
||||
|
||||
import { Agent, accuracyEval, performanceEval } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create an agent to evaluate
|
||||
const agent = new Agent({
|
||||
instructions: "You are a math tutor. Answer math questions accurately.",
|
||||
llm: "gpt-4o-mini",
|
||||
verbose: false
|
||||
});
|
||||
|
||||
console.log("=== Accuracy Evaluation ===");
|
||||
|
||||
// Test accuracy with expected outputs
|
||||
const testCases = [
|
||||
{ input: "What is 2 + 2?", expected: "4" },
|
||||
{ input: "What is 10 * 5?", expected: "50" },
|
||||
{ input: "What is 100 / 4?", expected: "25" }
|
||||
];
|
||||
|
||||
for (const test of testCases) {
|
||||
const response = await agent.chat(test.input);
|
||||
const passed = response.includes(test.expected);
|
||||
console.log(` ${passed ? '✅' : '❌'} "${test.input}" → Expected: ${test.expected}`);
|
||||
}
|
||||
|
||||
console.log("\n=== Performance Evaluation ===");
|
||||
|
||||
// Measure response time
|
||||
const iterations = 3;
|
||||
const times: number[] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = Date.now();
|
||||
await agent.chat("What is 5 + 5?");
|
||||
const duration = Date.now() - start;
|
||||
times.push(duration);
|
||||
console.log(` Run ${i + 1}: ${duration}ms`);
|
||||
}
|
||||
|
||||
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
console.log(`\n Average: ${avgTime.toFixed(0)}ms`);
|
||||
console.log(` Min: ${Math.min(...times)}ms`);
|
||||
console.log(` Max: ${Math.max(...times)}ms`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Guardrails Example - Input/output validation for agents
|
||||
*
|
||||
* Run: npx ts-node examples/features/guardrails.ts
|
||||
*/
|
||||
|
||||
import { Agent, LLMGuardrail } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create an LLM-based guardrail
|
||||
const contentGuardrail = new LLMGuardrail({
|
||||
name: "ContentSafety",
|
||||
criteria: "Content must be professional, non-offensive, and factually accurate",
|
||||
llm: "gpt-4o-mini",
|
||||
threshold: 0.7,
|
||||
verbose: true
|
||||
});
|
||||
|
||||
// Create agent
|
||||
const agent = new Agent({
|
||||
instructions: "You are a helpful assistant that provides accurate information.",
|
||||
verbose: true
|
||||
});
|
||||
|
||||
// Example: Check content before responding
|
||||
const userInput = "Tell me about artificial intelligence";
|
||||
|
||||
// Validate input using check()
|
||||
const inputCheck = await contentGuardrail.check(userInput);
|
||||
console.log("Input validation:", inputCheck);
|
||||
|
||||
if (inputCheck.status === 'passed') {
|
||||
const response = await agent.chat(userInput);
|
||||
|
||||
// Validate output
|
||||
const outputCheck = await contentGuardrail.check(response);
|
||||
console.log("Output validation:", outputCheck);
|
||||
|
||||
if (outputCheck.status === 'passed') {
|
||||
console.log("\n✅ Response passed guardrails");
|
||||
} else {
|
||||
console.log("\n❌ Response failed guardrails:", outputCheck.reasoning);
|
||||
}
|
||||
} else {
|
||||
console.log("❌ Input failed guardrails:", inputCheck.reasoning);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Knowledge Base Example - RAG with agents
|
||||
*
|
||||
* Run: npx ts-node examples/features/knowledge.ts
|
||||
*/
|
||||
|
||||
import { Agent, BaseKnowledgeBase, type Knowledge } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create knowledge base
|
||||
const kb = new BaseKnowledgeBase();
|
||||
|
||||
// Add knowledge entries
|
||||
kb.addKnowledge({
|
||||
id: "overview",
|
||||
type: "documentation",
|
||||
content: `
|
||||
PraisonAI is a TypeScript SDK for building AI agents.
|
||||
It supports multiple LLM providers including OpenAI, Anthropic, and Google.
|
||||
Key features include:
|
||||
- Simple Agent API with instructions
|
||||
- Multi-agent orchestration with Agents class
|
||||
- Workflow-based execution
|
||||
- Tool integration with auto-schema generation
|
||||
- Session persistence with db() factory
|
||||
`,
|
||||
metadata: { source: "docs", topic: "overview" }
|
||||
});
|
||||
|
||||
kb.addKnowledge({
|
||||
id: "agent-usage",
|
||||
type: "documentation",
|
||||
content: `
|
||||
The Agent class is the primary entry point.
|
||||
Create an agent with: new Agent({ instructions: "..." })
|
||||
Use agent.chat("message") to interact.
|
||||
Tools can be plain functions passed in the tools array.
|
||||
`,
|
||||
metadata: { source: "docs", topic: "agent" }
|
||||
});
|
||||
|
||||
// Search the knowledge base
|
||||
console.log("=== Searching Knowledge Base ===");
|
||||
const results = kb.searchKnowledge("agent");
|
||||
console.log("Found", results.length, "relevant entries");
|
||||
results.forEach((r: Knowledge, i: number) => {
|
||||
console.log(`\n[${i + 1}] ID: ${r.id}`);
|
||||
console.log(String(r.content).substring(0, 100) + "...");
|
||||
});
|
||||
|
||||
// Create agent that uses knowledge
|
||||
const knowledgeContext = results.map((r: Knowledge) => r.content).join('\n\n');
|
||||
const agent = new Agent({
|
||||
instructions: `You are a helpful assistant. Use the following knowledge to answer questions:
|
||||
|
||||
${knowledgeContext}`,
|
||||
verbose: true
|
||||
});
|
||||
|
||||
console.log("\n=== Agent Response ===");
|
||||
await agent.chat("How do I create an agent in PraisonAI?");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Memory Example - Using memory for agent context
|
||||
*
|
||||
* Run: npx ts-node examples/features/memory.ts
|
||||
*/
|
||||
|
||||
import { Agent, Memory } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create a memory instance
|
||||
const memory = new Memory({
|
||||
maxEntries: 100,
|
||||
maxTokens: 50000
|
||||
});
|
||||
|
||||
// Add some context to memory
|
||||
await memory.add("User prefers concise answers", "system", { priority: "high" });
|
||||
await memory.add("User is working on a TypeScript project", "system", { topic: "programming" });
|
||||
|
||||
// Search memory
|
||||
const results = await memory.search("TypeScript");
|
||||
console.log("Memory search results:", results);
|
||||
|
||||
// Get recent entries
|
||||
const recent = memory.getRecent(5);
|
||||
console.log("Recent entries:", recent);
|
||||
|
||||
// Create agent
|
||||
const agent = new Agent({
|
||||
instructions: "You are a helpful assistant. Use context from memory.",
|
||||
verbose: true
|
||||
});
|
||||
|
||||
await agent.chat("Help me with my project");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Loaded 100 of 265 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user