mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 03:31:28 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ee8eb2f3 | ||
|
|
9a5a432d96 | ||
|
|
8cb5bbb605 | ||
|
|
952514d73c | ||
|
|
6e1dc34864 | ||
|
|
820d5c2995 | ||
|
|
0b1b3d3b3e | ||
|
|
59107addc3 | ||
|
|
b460ded5e0 | ||
|
|
a8967ed200 |
No files matched your search
@@ -2359,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.4" \
|
||||
"praisonai>=2.7.3" \
|
||||
"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.4" \
|
||||
"praisonai>=2.7.3" \
|
||||
"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.4" \
|
||||
"praisonai>=2.7.3" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ 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) |
|
||||
|
||||
## 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,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,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,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,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,48 @@
|
||||
# Recipe Registry Example
|
||||
|
||||
This example demonstrates how to use the PraisonAI recipe registry for publishing and pulling recipes.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- Creating a local registry
|
||||
- Packing recipes into bundles
|
||||
- Publishing recipes to the registry
|
||||
- Listing and searching recipes
|
||||
- Pulling recipes from the registry
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run the example
|
||||
python registry_example.py
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
# Publish a recipe
|
||||
praisonai recipe publish ./my-recipe --json
|
||||
|
||||
# Pull a recipe
|
||||
praisonai recipe pull [email protected] -o ./recipes
|
||||
|
||||
# List recipes in registry
|
||||
praisonai recipe list --json
|
||||
|
||||
# Search recipes
|
||||
praisonai recipe search "hello"
|
||||
```
|
||||
|
||||
## Using a Custom Registry Path
|
||||
|
||||
```bash
|
||||
# Publish to custom registry
|
||||
praisonai recipe publish ./my-recipe --registry /path/to/registry
|
||||
|
||||
# Pull from custom registry
|
||||
praisonai recipe pull [email protected] --registry /path/to/registry
|
||||
```
|
||||
|
||||
## Default Registry Location
|
||||
|
||||
By default, the registry is stored at `~/.praison/registry`.
|
||||
@@ -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,75 @@
|
||||
# Recipe Server Example
|
||||
|
||||
This example demonstrates how to configure and run the PraisonAI recipe server with authentication.
|
||||
|
||||
## Files
|
||||
|
||||
- `serve.yaml` - Server configuration file
|
||||
- `serve_example.py` - Python client example with authentication
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install praisonai[serve]
|
||||
```
|
||||
|
||||
### 2. Set Environment Variables
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-openai-key
|
||||
export PRAISONAI_API_KEY=your-secret-api-key
|
||||
```
|
||||
|
||||
### 3. Start the Server
|
||||
|
||||
```bash
|
||||
# Using config file
|
||||
praisonai recipe serve --config serve.yaml
|
||||
|
||||
# Or with CLI flags
|
||||
praisonai recipe serve --port 8765 --auth api-key
|
||||
```
|
||||
|
||||
### 4. Test with the Example
|
||||
|
||||
```bash
|
||||
python serve_example.py
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
See `serve.yaml` for all available 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 | - |
|
||||
|
||||
## 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,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,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,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
|
||||
|
||||
|
||||
@@ -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.
@@ -128,6 +128,10 @@ Respond with ONLY a valid JSON tool call in this format:
|
||||
@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
|
||||
|
||||
@@ -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.1"
|
||||
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.1"
|
||||
version = "0.5.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Test file for simplified TS SDK APIs
|
||||
* Run with: npx tsx examples/simplified-api-test.ts
|
||||
*/
|
||||
|
||||
import { Agent, db, Router, PlanningAgent, TaskAgent, AgentTelemetry } from '../src';
|
||||
|
||||
async function testSimpleAgent() {
|
||||
console.log('\n=== Test 1: Simple Agent ===');
|
||||
const agent = new Agent({
|
||||
instructions: 'You are a helpful assistant. Be brief.',
|
||||
verbose: false
|
||||
});
|
||||
|
||||
const response = await agent.chat('What is 2+2? Answer in one word.');
|
||||
console.log('Response:', response.slice(0, 100));
|
||||
console.log('✅ Simple Agent works');
|
||||
}
|
||||
|
||||
async function testAgentWithTools() {
|
||||
console.log('\n=== Test 2: Agent with Tools ===');
|
||||
|
||||
const getWeather = (city: string) => `Weather in ${city}: 22°C, sunny`;
|
||||
const calculate = (expr: string) => {
|
||||
try {
|
||||
return String(eval(expr));
|
||||
} catch {
|
||||
return 'Error';
|
||||
}
|
||||
};
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: 'You help with weather and calculations. Be brief.',
|
||||
tools: [getWeather, calculate],
|
||||
verbose: false
|
||||
});
|
||||
|
||||
const response = await agent.chat('What is 5 + 3?');
|
||||
console.log('Response:', response.slice(0, 100));
|
||||
console.log('✅ Agent with Tools works');
|
||||
}
|
||||
|
||||
async function testAgentWithPersistence() {
|
||||
console.log('\n=== Test 3: Agent with Persistence (Memory) ===');
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: 'You remember conversations. Be brief.',
|
||||
db: db('memory:'), // In-memory for testing
|
||||
sessionId: 'test-session-123',
|
||||
verbose: false
|
||||
});
|
||||
|
||||
await agent.chat('My name is Alice.');
|
||||
const response = await agent.chat('What is my name?');
|
||||
console.log('Response:', response.slice(0, 100));
|
||||
console.log('History length:', agent.getHistory().length);
|
||||
console.log('✅ Agent with Persistence works');
|
||||
}
|
||||
|
||||
async function testRouter() {
|
||||
console.log('\n=== Test 4: Simplified Router ===');
|
||||
|
||||
const mathAgent = new Agent({
|
||||
instructions: 'You are a math expert. Answer math questions briefly.',
|
||||
verbose: false
|
||||
});
|
||||
|
||||
const codeAgent = new Agent({
|
||||
instructions: 'You are a coding expert. Answer coding questions briefly.',
|
||||
verbose: false
|
||||
});
|
||||
|
||||
const router = new Router({
|
||||
math: { agent: mathAgent, keywords: ['calculate', 'math', 'sum', '+', '-', '*', '/'] },
|
||||
code: { agent: codeAgent, keywords: ['code', 'program', 'function', 'javascript'] }
|
||||
});
|
||||
|
||||
const response = await router.chat('Calculate 10 + 5');
|
||||
console.log('Response:', response.slice(0, 100));
|
||||
console.log('✅ Simplified Router works');
|
||||
}
|
||||
|
||||
async function testPlanningAgent() {
|
||||
console.log('\n=== Test 5: Planning Agent ===');
|
||||
|
||||
const agent = new PlanningAgent({
|
||||
verbose: false,
|
||||
maxSteps: 3 // Limit steps for testing
|
||||
});
|
||||
|
||||
const plan = await agent.createPlan('Make a cup of tea');
|
||||
console.log('Plan created with', plan.steps.length, 'steps');
|
||||
console.log('First step:', plan.steps[0]?.description || 'N/A');
|
||||
console.log('✅ Planning Agent works');
|
||||
}
|
||||
|
||||
async function testTaskAgent() {
|
||||
console.log('\n=== Test 6: Task Agent ===');
|
||||
|
||||
const agent = new TaskAgent({ verbose: false });
|
||||
|
||||
agent.addTask('Fix critical bug', 'high');
|
||||
agent.addTask('Write documentation', 'medium');
|
||||
agent.addTask('Review PR', 'low');
|
||||
|
||||
console.log('Pending tasks:', agent.getPendingTasks().length);
|
||||
|
||||
agent.completeTask('bug');
|
||||
console.log('After completing bug task:', agent.getPendingTasks().length);
|
||||
console.log('Progress:', agent.getProgress().percentage + '%');
|
||||
console.log('✅ Task Agent works');
|
||||
}
|
||||
|
||||
async function testAgentTelemetry() {
|
||||
console.log('\n=== Test 7: Agent Telemetry ===');
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: 'Be brief.',
|
||||
verbose: false
|
||||
});
|
||||
|
||||
const telemetry = new AgentTelemetry('TestAgent');
|
||||
|
||||
await telemetry.trackChat(async () => {
|
||||
return await agent.chat('Hello!');
|
||||
});
|
||||
|
||||
await telemetry.trackChat(async () => {
|
||||
return await agent.chat('How are you?');
|
||||
});
|
||||
|
||||
const stats = telemetry.getStats();
|
||||
console.log('Total chats:', stats.totalChats);
|
||||
console.log('Success rate:', telemetry.getSuccessRate() + '%');
|
||||
console.log('✅ Agent Telemetry works');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Testing Simplified TS SDK APIs\n');
|
||||
|
||||
try {
|
||||
await testSimpleAgent();
|
||||
await testAgentWithTools();
|
||||
await testAgentWithPersistence();
|
||||
await testRouter();
|
||||
await testPlanningAgent();
|
||||
await testTaskAgent();
|
||||
await testAgentTelemetry();
|
||||
|
||||
console.log('\n✅ All tests passed!');
|
||||
} catch (error) {
|
||||
console.error('\n❌ Test failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "praisonai",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.2",
|
||||
"description": "PraisonAI TypeScript AI Agents Framework - Node.js, npm, and Javascript AI Agents Framework",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* The primary exports are:
|
||||
* - Agent: Single agent with instructions, tools, and optional persistence
|
||||
* - Agents: Multi-agent orchestration (alias for PraisonAIAgents)
|
||||
* - Router: Simplified keyword/pattern-based routing
|
||||
* - Workflow: Step-based workflow execution (from workflows module)
|
||||
*/
|
||||
|
||||
@@ -11,6 +12,10 @@
|
||||
export { Agent, PraisonAIAgents, Agents } from './simple';
|
||||
export type { SimpleAgentConfig, PraisonAIAgentsConfig } from './simple';
|
||||
|
||||
// Router exports
|
||||
export { Router, RouterAgent, createRouter, routeConditions } from './router';
|
||||
export type { RouterConfig, RouteConfig, RouteContext, SimpleRouterConfig, SimpleRouteConfig } from './router';
|
||||
|
||||
// Task support (for advanced use cases)
|
||||
export { Task } from './types';
|
||||
export type { TaskConfig, AgentConfig as TaskAgentConfig } from './types';
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
/**
|
||||
* Router Agent - Route requests to specialized agents
|
||||
*
|
||||
* @example Simple usage (5 lines)
|
||||
* ```typescript
|
||||
* import { Agent, Router } from 'praisonai';
|
||||
*
|
||||
* const router = new Router({
|
||||
* math: { agent: new Agent({ instructions: 'Math expert' }), keywords: ['calculate', 'math'] },
|
||||
* code: { agent: new Agent({ instructions: 'Code expert' }), keywords: ['code', 'program'] }
|
||||
* });
|
||||
* await router.chat('Calculate 2+2'); // Routes to math agent
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { EnhancedAgent } from './enhanced';
|
||||
import { Agent } from './simple';
|
||||
|
||||
// Support both Agent and EnhancedAgent
|
||||
type AnyAgent = Agent | EnhancedAgent;
|
||||
|
||||
export interface RouteConfig {
|
||||
agent: EnhancedAgent;
|
||||
agent: AnyAgent;
|
||||
condition: (input: string, context?: RouteContext) => boolean | Promise<boolean>;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
/** Simplified route definition */
|
||||
export interface SimpleRouteConfig {
|
||||
agent: AnyAgent;
|
||||
keywords?: string[];
|
||||
pattern?: RegExp;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export interface RouteContext {
|
||||
history?: string[];
|
||||
metadata?: Record<string, any>;
|
||||
@@ -18,17 +41,20 @@ export interface RouteContext {
|
||||
export interface RouterConfig {
|
||||
name?: string;
|
||||
routes: RouteConfig[];
|
||||
defaultAgent?: EnhancedAgent;
|
||||
defaultAgent?: AnyAgent;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/** Simplified router config - just a map of route name to config */
|
||||
export type SimpleRouterConfig = Record<string, SimpleRouteConfig>;
|
||||
|
||||
/**
|
||||
* Router Agent - Routes requests to the most appropriate agent
|
||||
*/
|
||||
export class RouterAgent {
|
||||
readonly name: string;
|
||||
private routes: RouteConfig[];
|
||||
private defaultAgent?: EnhancedAgent;
|
||||
private defaultAgent?: AnyAgent;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: RouterConfig) {
|
||||
@@ -41,7 +67,7 @@ export class RouterAgent {
|
||||
/**
|
||||
* Route a request to the appropriate agent
|
||||
*/
|
||||
async route(input: string, context?: RouteContext): Promise<{ agent: EnhancedAgent; response: string } | null> {
|
||||
async route(input: string, context?: RouteContext): Promise<{ agent: AnyAgent; response: string } | null> {
|
||||
for (const route of this.routes) {
|
||||
const matches = await route.condition(input, context);
|
||||
if (matches) {
|
||||
@@ -49,7 +75,9 @@ export class RouterAgent {
|
||||
console.log(`[Router] Routing to: ${route.agent.name}`);
|
||||
}
|
||||
const response = await route.agent.chat(input);
|
||||
return { agent: route.agent, response: response.text };
|
||||
// Handle both Agent (returns string) and EnhancedAgent (returns ChatResult)
|
||||
const responseText = typeof response === 'string' ? response : response.text;
|
||||
return { agent: route.agent, response: responseText };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,12 +86,24 @@ export class RouterAgent {
|
||||
console.log(`[Router] Using default agent: ${this.defaultAgent.name}`);
|
||||
}
|
||||
const response = await this.defaultAgent.chat(input);
|
||||
return { agent: this.defaultAgent, response: response.text };
|
||||
const responseText = typeof response === 'string' ? response : response.text;
|
||||
return { agent: this.defaultAgent, response: responseText };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified chat method - routes and returns just the response
|
||||
*/
|
||||
async chat(input: string, context?: RouteContext): Promise<string> {
|
||||
const result = await this.route(input, context);
|
||||
if (!result) {
|
||||
throw new Error('No matching route found and no default agent configured');
|
||||
}
|
||||
return result.response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a route
|
||||
*/
|
||||
@@ -139,8 +179,89 @@ export const routeConditions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a router agent
|
||||
* Create a router agent (legacy API)
|
||||
*/
|
||||
export function createRouter(config: RouterConfig): RouterAgent {
|
||||
return new RouterAgent(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified Router class - uses keyword/pattern-based routing
|
||||
*
|
||||
* @example Simple usage (5 lines)
|
||||
* ```typescript
|
||||
* import { Agent, Router } from 'praisonai';
|
||||
*
|
||||
* const router = new Router({
|
||||
* math: { agent: new Agent({ instructions: 'Math expert' }), keywords: ['calculate', 'math'] },
|
||||
* code: { agent: new Agent({ instructions: 'Code expert' }), keywords: ['code', 'program'] }
|
||||
* });
|
||||
* await router.chat('Calculate 2+2'); // Routes to math agent
|
||||
* ```
|
||||
*/
|
||||
export class Router {
|
||||
private routerAgent: RouterAgent;
|
||||
private agentMap: Map<string, AnyAgent> = new Map();
|
||||
|
||||
constructor(config: SimpleRouterConfig, options?: { default?: string; verbose?: boolean }) {
|
||||
const routes: RouteConfig[] = [];
|
||||
let defaultAgent: AnyAgent | undefined;
|
||||
|
||||
for (const [name, routeConfig] of Object.entries(config)) {
|
||||
this.agentMap.set(name, routeConfig.agent);
|
||||
|
||||
// Build condition from keywords or pattern
|
||||
let condition: (input: string) => boolean;
|
||||
if (routeConfig.keywords) {
|
||||
condition = routeConditions.keywords(routeConfig.keywords);
|
||||
} else if (routeConfig.pattern) {
|
||||
condition = routeConditions.pattern(routeConfig.pattern);
|
||||
} else {
|
||||
condition = routeConditions.always();
|
||||
}
|
||||
|
||||
routes.push({
|
||||
agent: routeConfig.agent,
|
||||
condition,
|
||||
priority: routeConfig.priority
|
||||
});
|
||||
|
||||
// Set default agent
|
||||
if (options?.default === name) {
|
||||
defaultAgent = routeConfig.agent;
|
||||
}
|
||||
}
|
||||
|
||||
// If no default specified, use first agent
|
||||
if (!defaultAgent && routes.length > 0) {
|
||||
defaultAgent = routes[0].agent;
|
||||
}
|
||||
|
||||
this.routerAgent = new RouterAgent({
|
||||
routes,
|
||||
defaultAgent,
|
||||
verbose: options?.verbose
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Route and get response
|
||||
*/
|
||||
async chat(input: string): Promise<string> {
|
||||
return this.routerAgent.chat(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Route and get full result with agent info
|
||||
*/
|
||||
async route(input: string): Promise<{ agent: AnyAgent; response: string } | null> {
|
||||
return this.routerAgent.route(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent by name
|
||||
*/
|
||||
getAgent(name: string): AnyAgent | undefined {
|
||||
return this.agentMap.get(name);
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,22 @@ export interface SimpleAgentConfig {
|
||||
toolFunctions?: Record<string, Function>;
|
||||
/** Database adapter for persistence */
|
||||
db?: DbAdapter;
|
||||
/** Session ID for conversation persistence */
|
||||
/** Session ID for conversation persistence (auto-generated if not provided) */
|
||||
sessionId?: string;
|
||||
/** Run ID for tracing (auto-generated if not provided) */
|
||||
runId?: string;
|
||||
/** Max messages to restore from history (default: 100) */
|
||||
historyLimit?: number;
|
||||
/** Auto-restore conversation history from db (default: true) */
|
||||
autoRestore?: boolean;
|
||||
/** Auto-persist messages to db (default: true) */
|
||||
autoPersist?: boolean;
|
||||
/** Enable caching of responses */
|
||||
cache?: boolean;
|
||||
/** Cache TTL in seconds (default: 3600) */
|
||||
cacheTTL?: number;
|
||||
/** Enable telemetry tracking (default: false, opt-in) */
|
||||
telemetry?: boolean;
|
||||
|
||||
// Advanced mode (role/goal/backstory) - for compatibility
|
||||
/** Agent role (advanced mode) */
|
||||
@@ -96,6 +108,14 @@ export class Agent {
|
||||
private sessionId: string;
|
||||
private runId: string;
|
||||
private messages: Array<{ role: string; content: string | null; tool_calls?: any[]; tool_call_id?: string }> = [];
|
||||
private dbInitialized: boolean = false;
|
||||
private historyLimit: number;
|
||||
private autoRestore: boolean;
|
||||
private autoPersist: boolean;
|
||||
private cache: boolean;
|
||||
private cacheTTL: number;
|
||||
private responseCache: Map<string, { response: string; timestamp: number }> = new Map();
|
||||
private telemetryEnabled: boolean;
|
||||
|
||||
constructor(config: SimpleAgentConfig) {
|
||||
// Build instructions from either simple or advanced mode
|
||||
@@ -120,8 +140,14 @@ export class Agent {
|
||||
this.stream = config.stream ?? true;
|
||||
this.tools = config.tools;
|
||||
this.dbAdapter = config.db;
|
||||
this.sessionId = config.sessionId || randomUUID();
|
||||
this.sessionId = config.sessionId || this.generateSessionId();
|
||||
this.runId = config.runId || randomUUID();
|
||||
this.historyLimit = config.historyLimit ?? 100;
|
||||
this.autoRestore = config.autoRestore ?? true;
|
||||
this.autoPersist = config.autoPersist ?? true;
|
||||
this.cache = config.cache ?? false;
|
||||
this.cacheTTL = config.cacheTTL ?? 3600;
|
||||
this.telemetryEnabled = config.telemetry ?? false;
|
||||
this.llmService = new OpenAIService(this.llm);
|
||||
|
||||
// Configure logging
|
||||
@@ -180,6 +206,70 @@ export class Agent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a session ID based on current hour and agent name (like Python SDK)
|
||||
*/
|
||||
private generateSessionId(): string {
|
||||
const now = new Date();
|
||||
const hourStr = now.toISOString().slice(0, 13).replace(/[-T:]/g, '');
|
||||
const hash = this.name ? this.name.slice(0, 6) : 'agent';
|
||||
return `${hourStr}-${hash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize DB session - restore history on first chat (lazy)
|
||||
*/
|
||||
private async initDbSession(): Promise<void> {
|
||||
if (this.dbInitialized || !this.dbAdapter || !this.autoRestore) return;
|
||||
|
||||
try {
|
||||
// Restore previous messages from DB
|
||||
const history = await this.dbAdapter.getMessages(this.sessionId, this.historyLimit);
|
||||
if (history.length > 0) {
|
||||
this.messages = history.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}));
|
||||
Logger.debug(`Restored ${history.length} messages from session ${this.sessionId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.warn('Failed to initialize DB session:', error);
|
||||
}
|
||||
|
||||
this.dbInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached response if available and not expired
|
||||
*/
|
||||
private getCachedResponse(prompt: string): string | null {
|
||||
if (!this.cache) return null;
|
||||
|
||||
const cacheKey = `${this.sessionId}:${prompt}`;
|
||||
const cached = this.responseCache.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
const age = (Date.now() - cached.timestamp) / 1000;
|
||||
if (age < this.cacheTTL) {
|
||||
Logger.debug('Cache hit for prompt');
|
||||
return cached.response;
|
||||
}
|
||||
// Expired, remove it
|
||||
this.responseCache.delete(cacheKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache a response
|
||||
*/
|
||||
private cacheResponse(prompt: string, response: string): void {
|
||||
if (!this.cache) return;
|
||||
|
||||
const cacheKey = `${this.sessionId}:${prompt}`;
|
||||
this.responseCache.set(cacheKey, { response, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
private createSystemPrompt(): string {
|
||||
let prompt = this.instructions;
|
||||
if (this.markdown) {
|
||||
@@ -325,27 +415,32 @@ export class Agent {
|
||||
prompt = prompt.replace('{{previous}}', previousResult);
|
||||
}
|
||||
|
||||
// Initialize messages array
|
||||
// Initialize messages array with system prompt and conversation history
|
||||
const messages: Array<any> = [
|
||||
{ role: 'system', content: this.createSystemPrompt() },
|
||||
{ role: 'user', content: prompt }
|
||||
{ role: 'system', content: this.createSystemPrompt() }
|
||||
];
|
||||
|
||||
// Add conversation history (excluding the current prompt which will be added below)
|
||||
for (const msg of this.messages) {
|
||||
if (msg.role && msg.content) {
|
||||
messages.push({ role: msg.role, content: msg.content });
|
||||
}
|
||||
}
|
||||
|
||||
// Add current user prompt
|
||||
messages.push({ role: 'user', content: prompt });
|
||||
|
||||
let finalResponse = '';
|
||||
|
||||
if (this.stream && !this.tools) {
|
||||
// Use streaming without tools
|
||||
let fullResponse = '';
|
||||
await this.llmService.streamText(
|
||||
prompt,
|
||||
this.createSystemPrompt(),
|
||||
// Use streaming with full conversation history
|
||||
finalResponse = await this.llmService.streamChat(
|
||||
messages,
|
||||
0.7,
|
||||
(token: string) => {
|
||||
process.stdout.write(token);
|
||||
fullResponse += token;
|
||||
}
|
||||
);
|
||||
finalResponse = fullResponse;
|
||||
} else if (this.tools) {
|
||||
// Use tools (non-streaming for now to simplify implementation)
|
||||
let continueConversation = true;
|
||||
@@ -402,18 +497,36 @@ export class Agent {
|
||||
}
|
||||
|
||||
async chat(prompt: string, previousResult?: string): Promise<string> {
|
||||
// Persist user message if db is configured
|
||||
if (this.dbAdapter) {
|
||||
// Lazy init: restore history on first chat (like Python SDK)
|
||||
await this.initDbSession();
|
||||
|
||||
// Check cache first
|
||||
const cached = this.getCachedResponse(prompt);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Add user message to conversation history
|
||||
this.messages.push({ role: 'user', content: prompt });
|
||||
|
||||
// Persist user message if db is configured and autoPersist is enabled
|
||||
if (this.dbAdapter && this.autoPersist) {
|
||||
await this.persistMessage('user', prompt);
|
||||
}
|
||||
|
||||
const response = await this.start(prompt, previousResult);
|
||||
|
||||
// Persist assistant response if db is configured
|
||||
if (this.dbAdapter) {
|
||||
// Add assistant response to history
|
||||
this.messages.push({ role: 'assistant', content: response });
|
||||
|
||||
// Persist assistant response if db is configured and autoPersist is enabled
|
||||
if (this.dbAdapter && this.autoPersist) {
|
||||
await this.persistMessage('assistant', response);
|
||||
}
|
||||
|
||||
// Cache the response
|
||||
this.cacheResponse(prompt, response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -464,6 +577,35 @@ export class Agent {
|
||||
getInstructions(): string {
|
||||
return this.instructions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation history
|
||||
*/
|
||||
getHistory(): Array<{ role: string; content: string | null }> {
|
||||
return [...this.messages];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear conversation history (in memory and optionally in DB)
|
||||
*/
|
||||
async clearHistory(clearDb: boolean = true): Promise<void> {
|
||||
this.messages = [];
|
||||
if (clearDb && this.dbAdapter) {
|
||||
try {
|
||||
await this.dbAdapter.deleteMessages(this.sessionId);
|
||||
Logger.debug(`Cleared history for session ${this.sessionId}`);
|
||||
} catch (error) {
|
||||
Logger.warn('Failed to clear DB history:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear response cache
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.responseCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
// ============================================================================
|
||||
|
||||
// Agent - Single agent with instructions, tools, and optional persistence
|
||||
export { Agent, Agents, PraisonAIAgents } from './agent';
|
||||
export type { SimpleAgentConfig, PraisonAIAgentsConfig } from './agent';
|
||||
export { Agent, Agents, PraisonAIAgents, Router } from './agent';
|
||||
export type { SimpleAgentConfig, PraisonAIAgentsConfig, SimpleRouterConfig, SimpleRouteConfig } from './agent';
|
||||
|
||||
// Workflow - Step-based workflow execution
|
||||
export { Workflow, parallel, route, loop, repeat } from './workflows';
|
||||
@@ -124,8 +124,12 @@ export {
|
||||
type VectorStoreAdapter as AutoMemoryVectorStore, type KnowledgeBaseAdapter as AutoMemoryKnowledgeBase
|
||||
} from './memory/auto-memory';
|
||||
|
||||
// Export Telemetry
|
||||
export { TelemetryCollector, getTelemetry, enableTelemetry, disableTelemetry, cleanupTelemetry, type TelemetryEvent, type TelemetryConfig } from './telemetry';
|
||||
// Export Telemetry (Agent-focused)
|
||||
export {
|
||||
TelemetryCollector, AgentTelemetry,
|
||||
getTelemetry, enableTelemetry, disableTelemetry, cleanupTelemetry, createAgentTelemetry,
|
||||
type TelemetryEvent, type TelemetryConfig, type AgentStats
|
||||
} from './telemetry';
|
||||
|
||||
// Export AutoAgents
|
||||
export { AutoAgents, createAutoAgents, type AgentConfig, type TaskConfig, type TeamStructure, type AutoAgentsConfig } from './auto';
|
||||
@@ -146,8 +150,14 @@ export { PromptExpanderAgent, createPromptExpanderAgent, type PromptExpanderConf
|
||||
// Export LLMGuardrail
|
||||
export { LLMGuardrail, createLLMGuardrail, type LLMGuardrailConfig, type LLMGuardrailResult } from './guardrails/llm-guardrail';
|
||||
|
||||
// Export Planning
|
||||
export { Plan, PlanStep, TodoList, TodoItem, PlanStorage, createPlan, createTodoList, createPlanStorage, type PlanConfig, type PlanStepConfig, type TodoItemConfig, type PlanStatus, type TodoStatus } from './planning';
|
||||
// Export Planning (simplified API)
|
||||
export {
|
||||
Plan, PlanStep, TodoList, TodoItem, PlanStorage,
|
||||
PlanningAgent, TaskAgent,
|
||||
createPlan, createTodoList, createPlanStorage, createPlanningAgent, createTaskAgent,
|
||||
type PlanConfig, type PlanStepConfig, type TodoItemConfig, type PlanStatus, type TodoStatus,
|
||||
type PlanningAgentConfig, type PlanResult
|
||||
} from './planning';
|
||||
|
||||
// Export Cache
|
||||
export { BaseCache, MemoryCache, FileCache, createMemoryCache, createFileCache, type CacheConfig, type CacheEntry } from './cache';
|
||||
|
||||
@@ -322,6 +322,47 @@ export class OpenAIService {
|
||||
}
|
||||
}
|
||||
|
||||
async streamChat(
|
||||
messages: ChatMessage[],
|
||||
temperature: number = 0.7,
|
||||
onToken: (token: string) => void
|
||||
): Promise<string> {
|
||||
await Logger.debug('Starting chat stream with messages...', {
|
||||
model: this.model,
|
||||
messageCount: messages.length
|
||||
});
|
||||
|
||||
try {
|
||||
const openAIMessages = messages.map(convertToOpenAIMessage);
|
||||
|
||||
const stream = await this.getClient().then(client =>
|
||||
client.chat.completions.create({
|
||||
model: this.model,
|
||||
temperature,
|
||||
messages: openAIMessages,
|
||||
stream: true
|
||||
})
|
||||
);
|
||||
|
||||
let fullResponse = '';
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
if (delta?.content) {
|
||||
const token = delta.content;
|
||||
fullResponse += token;
|
||||
onToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
await Logger.debug('Chat stream completed');
|
||||
return fullResponse;
|
||||
} catch (error) {
|
||||
await Logger.error('Error in chat stream', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
temperature: number = 0.7,
|
||||
|
||||
@@ -305,3 +305,278 @@ export function createTodoList(name?: string): TodoList {
|
||||
export function createPlanStorage(): PlanStorage {
|
||||
return new PlanStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* PlanningAgent - Agent with built-in planning capabilities
|
||||
*
|
||||
* @example Simple usage (4 lines)
|
||||
* ```typescript
|
||||
* import { PlanningAgent } from 'praisonai';
|
||||
*
|
||||
* const agent = new PlanningAgent({ instructions: 'You break down tasks into steps' });
|
||||
* const result = await agent.planAndExecute('Build a web scraper');
|
||||
* console.log(result.plan); // The plan that was created
|
||||
* console.log(result.results); // Results from each step
|
||||
* ```
|
||||
*/
|
||||
export interface PlanningAgentConfig {
|
||||
instructions?: string;
|
||||
name?: string;
|
||||
llm?: string;
|
||||
verbose?: boolean;
|
||||
maxSteps?: number;
|
||||
}
|
||||
|
||||
export interface PlanResult {
|
||||
plan: Plan;
|
||||
results: string[];
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
// Import Agent dynamically to avoid circular dependency
|
||||
let AgentClass: any = null;
|
||||
|
||||
async function getAgentClass() {
|
||||
if (!AgentClass) {
|
||||
const { Agent } = await import('../agent/simple');
|
||||
AgentClass = Agent;
|
||||
}
|
||||
return AgentClass;
|
||||
}
|
||||
|
||||
export class PlanningAgent {
|
||||
private config: PlanningAgentConfig;
|
||||
private agent: any = null;
|
||||
private currentPlan: Plan | null = null;
|
||||
|
||||
constructor(config: PlanningAgentConfig = {}) {
|
||||
this.config = {
|
||||
instructions: config.instructions || 'You are a planning agent that breaks down complex tasks into steps.',
|
||||
name: config.name || 'PlanningAgent',
|
||||
llm: config.llm,
|
||||
verbose: config.verbose ?? true,
|
||||
maxSteps: config.maxSteps ?? 10
|
||||
};
|
||||
}
|
||||
|
||||
private async getAgent() {
|
||||
if (!this.agent) {
|
||||
const Agent = await getAgentClass();
|
||||
this.agent = new Agent({
|
||||
name: this.config.name,
|
||||
instructions: this.config.instructions,
|
||||
llm: this.config.llm,
|
||||
verbose: this.config.verbose
|
||||
});
|
||||
}
|
||||
return this.agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a plan for a task
|
||||
*/
|
||||
async createPlan(task: string): Promise<Plan> {
|
||||
const agent = await this.getAgent();
|
||||
|
||||
const planPrompt = `Create a step-by-step plan for: ${task}
|
||||
|
||||
Return ONLY a numbered list of steps, one per line. Example:
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Third step`;
|
||||
|
||||
const response = await agent.chat(planPrompt);
|
||||
|
||||
// Parse the response into steps
|
||||
const plan = new Plan({ name: task });
|
||||
const lines = response.split('\n').filter((l: string) => l.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
// Extract step description (remove numbering)
|
||||
const match = line.match(/^\d+[\.\)]\s*(.+)/);
|
||||
if (match) {
|
||||
plan.addStep(new PlanStep({ description: match[1].trim() }));
|
||||
}
|
||||
}
|
||||
|
||||
this.currentPlan = plan;
|
||||
return plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single step
|
||||
*/
|
||||
async executeStep(step: PlanStep): Promise<string> {
|
||||
const agent = await this.getAgent();
|
||||
step.start();
|
||||
|
||||
try {
|
||||
const response = await agent.chat(`Execute this step: ${step.description}`);
|
||||
step.complete();
|
||||
return response;
|
||||
} catch (error) {
|
||||
step.fail();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan and execute a task in one call
|
||||
*/
|
||||
async planAndExecute(task: string): Promise<PlanResult> {
|
||||
const plan = await this.createPlan(task);
|
||||
const results: string[] = [];
|
||||
let success = true;
|
||||
|
||||
plan.start();
|
||||
|
||||
for (const step of plan.steps.slice(0, this.config.maxSteps)) {
|
||||
try {
|
||||
if (this.config.verbose) {
|
||||
console.log(`[${plan.getProgress().percentage}%] Executing: ${step.description}`);
|
||||
}
|
||||
const result = await this.executeStep(step);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
success = false;
|
||||
results.push(`Error: ${error}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
plan.complete();
|
||||
}
|
||||
|
||||
return { plan, results, success };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current plan
|
||||
*/
|
||||
getPlan(): Plan | null {
|
||||
return this.currentPlan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple chat (without planning)
|
||||
*/
|
||||
async chat(message: string): Promise<string> {
|
||||
const agent = await this.getAgent();
|
||||
return agent.chat(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a planning agent
|
||||
*/
|
||||
export function createPlanningAgent(config?: PlanningAgentConfig): PlanningAgent {
|
||||
return new PlanningAgent(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* TaskAgent - Agent with built-in todo list management
|
||||
*
|
||||
* @example Simple usage (4 lines)
|
||||
* ```typescript
|
||||
* import { TaskAgent } from 'praisonai';
|
||||
*
|
||||
* const agent = new TaskAgent();
|
||||
* await agent.addTask('Fix critical bug', 'high');
|
||||
* await agent.addTask('Write docs', 'medium');
|
||||
* console.log(agent.getPendingTasks());
|
||||
* ```
|
||||
*/
|
||||
export class TaskAgent {
|
||||
private todos: TodoList;
|
||||
private agent: any = null;
|
||||
private config: { name?: string; llm?: string; verbose?: boolean };
|
||||
|
||||
constructor(config?: { name?: string; llm?: string; verbose?: boolean }) {
|
||||
this.todos = new TodoList('Agent Tasks');
|
||||
this.config = config || {};
|
||||
}
|
||||
|
||||
private async getAgent() {
|
||||
if (!this.agent) {
|
||||
const Agent = await getAgentClass();
|
||||
this.agent = new Agent({
|
||||
name: this.config.name || 'TaskAgent',
|
||||
instructions: 'You are a task management assistant.',
|
||||
llm: this.config.llm,
|
||||
verbose: this.config.verbose ?? false
|
||||
});
|
||||
}
|
||||
return this.agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task
|
||||
*/
|
||||
addTask(content: string, priority: 'low' | 'medium' | 'high' = 'medium'): TodoItem {
|
||||
const item = new TodoItem({ content, priority });
|
||||
this.todos.add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a task by content (partial match)
|
||||
*/
|
||||
completeTask(contentMatch: string): boolean {
|
||||
const item = this.todos.items.find(t =>
|
||||
t.content.toLowerCase().includes(contentMatch.toLowerCase()) && t.status !== 'completed'
|
||||
);
|
||||
if (item) {
|
||||
item.complete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending tasks
|
||||
*/
|
||||
getPendingTasks(): TodoItem[] {
|
||||
return this.todos.getPending();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tasks
|
||||
*/
|
||||
getAllTasks(): TodoItem[] {
|
||||
return [...this.todos.items];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress
|
||||
*/
|
||||
getProgress(): { completed: number; total: number; percentage: number } {
|
||||
return this.todos.getProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tasks
|
||||
*/
|
||||
clearTasks(): void {
|
||||
this.todos.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat with AI about tasks
|
||||
*/
|
||||
async chat(message: string): Promise<string> {
|
||||
const agent = await this.getAgent();
|
||||
const context = `Current tasks:\n${this.todos.items.map(t =>
|
||||
`- [${t.status}] ${t.content} (${t.priority})`
|
||||
).join('\n') || 'No tasks'}`;
|
||||
|
||||
return agent.chat(`${context}\n\nUser: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a task agent
|
||||
*/
|
||||
export function createTaskAgent(config?: { name?: string; llm?: string; verbose?: boolean }): TaskAgent {
|
||||
return new TaskAgent(config);
|
||||
}
|
||||
@@ -225,3 +225,154 @@ export function cleanupTelemetry(): void {
|
||||
globalTelemetry.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AgentTelemetry - Agent-focused telemetry wrapper
|
||||
*
|
||||
* @example Simple usage (3 lines)
|
||||
* ```typescript
|
||||
* import { Agent } from 'praisonai';
|
||||
*
|
||||
* // Enable telemetry on agent
|
||||
* const agent = new Agent({
|
||||
* instructions: 'You are helpful',
|
||||
* telemetry: true // Opt-in telemetry
|
||||
* });
|
||||
* await agent.chat('Hello!'); // Automatically tracked
|
||||
* ```
|
||||
*
|
||||
* @example Manual tracking
|
||||
* ```typescript
|
||||
* import { AgentTelemetry } from 'praisonai';
|
||||
*
|
||||
* const telemetry = new AgentTelemetry('MyAgent');
|
||||
* const result = await telemetry.trackChat(async () => {
|
||||
* return await agent.chat('Hello!');
|
||||
* });
|
||||
* console.log(telemetry.getStats());
|
||||
* ```
|
||||
*/
|
||||
export interface AgentStats {
|
||||
totalChats: number;
|
||||
successfulChats: number;
|
||||
failedChats: number;
|
||||
totalDuration: number;
|
||||
avgDuration: number;
|
||||
totalTokens: number;
|
||||
toolCalls: number;
|
||||
}
|
||||
|
||||
export class AgentTelemetry {
|
||||
private agentName: string;
|
||||
private collector: TelemetryCollector;
|
||||
private stats: AgentStats = {
|
||||
totalChats: 0,
|
||||
successfulChats: 0,
|
||||
failedChats: 0,
|
||||
totalDuration: 0,
|
||||
avgDuration: 0,
|
||||
totalTokens: 0,
|
||||
toolCalls: 0
|
||||
};
|
||||
|
||||
constructor(agentName: string, config?: TelemetryConfig) {
|
||||
this.agentName = agentName;
|
||||
this.collector = config ? new TelemetryCollector(config) : getTelemetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a chat execution
|
||||
*/
|
||||
async trackChat<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const startTime = Date.now();
|
||||
this.stats.totalChats++;
|
||||
|
||||
try {
|
||||
const result = await fn();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.stats.successfulChats++;
|
||||
this.stats.totalDuration += duration;
|
||||
this.stats.avgDuration = this.stats.totalDuration / this.stats.totalChats;
|
||||
|
||||
this.collector.trackAgentExecution(this.agentName, duration, true);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.stats.failedChats++;
|
||||
this.stats.totalDuration += duration;
|
||||
this.stats.avgDuration = this.stats.totalDuration / this.stats.totalChats;
|
||||
|
||||
this.collector.trackAgentExecution(this.agentName, duration, false);
|
||||
this.collector.trackError(String(error), { agent: this.agentName });
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a tool call
|
||||
*/
|
||||
trackToolCall(toolName: string, duration: number, success: boolean): void {
|
||||
this.stats.toolCalls++;
|
||||
this.collector.trackToolCall(toolName, duration, success);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track token usage
|
||||
*/
|
||||
trackTokens(tokens: number): void {
|
||||
this.stats.totalTokens += tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent statistics
|
||||
*/
|
||||
getStats(): AgentStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset statistics
|
||||
*/
|
||||
resetStats(): void {
|
||||
this.stats = {
|
||||
totalChats: 0,
|
||||
successfulChats: 0,
|
||||
failedChats: 0,
|
||||
totalDuration: 0,
|
||||
avgDuration: 0,
|
||||
totalTokens: 0,
|
||||
toolCalls: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get success rate
|
||||
*/
|
||||
getSuccessRate(): number {
|
||||
if (this.stats.totalChats === 0) return 0;
|
||||
return (this.stats.successfulChats / this.stats.totalChats) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print summary
|
||||
*/
|
||||
printSummary(): void {
|
||||
console.log(`\n📊 Agent Telemetry: ${this.agentName}`);
|
||||
console.log(` Total chats: ${this.stats.totalChats}`);
|
||||
console.log(` Success rate: ${this.getSuccessRate().toFixed(1)}%`);
|
||||
console.log(` Avg duration: ${this.stats.avgDuration.toFixed(0)}ms`);
|
||||
console.log(` Tool calls: ${this.stats.toolCalls}`);
|
||||
console.log(` Total tokens: ${this.stats.totalTokens}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent telemetry
|
||||
*/
|
||||
export function createAgentTelemetry(agentName: string, config?: TelemetryConfig): AgentTelemetry {
|
||||
return new AgentTelemetry(agentName, config);
|
||||
}
|
||||
@@ -2359,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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.6.4.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.6.4.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.7.3.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.7.3.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -18,6 +18,7 @@ __all__ = [
|
||||
'DeployConfig',
|
||||
'DeployType',
|
||||
'CloudProvider',
|
||||
'recipe',
|
||||
]
|
||||
|
||||
# Lazy loading for heavy imports
|
||||
@@ -38,6 +39,9 @@ def __getattr__(name):
|
||||
elif name == 'CloudProvider':
|
||||
from .deploy import CloudProvider
|
||||
return CloudProvider
|
||||
elif name == 'recipe':
|
||||
from .recipe import core as recipe_module
|
||||
return recipe_module
|
||||
|
||||
# Try praisonaiagents exports
|
||||
try:
|
||||
|
||||
@@ -59,7 +59,8 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO').upper(), format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
_loglevel = os.environ.get('LOGLEVEL', 'INFO').strip().upper() or 'INFO'
|
||||
logging.basicConfig(level=_loglevel, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# =============================================================================
|
||||
# Available Tools List (shared between generators) - Legacy for praisonai_tools
|
||||
|
||||
@@ -92,6 +92,10 @@ __all__ = [
|
||||
'OllamaHandler',
|
||||
# Capabilities (LiteLLM endpoint parity)
|
||||
'CapabilitiesHandler',
|
||||
# Performance benchmarking
|
||||
'PerformanceHandler',
|
||||
# Lite agent (BYO-LLM)
|
||||
'LiteHandler',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -215,4 +219,10 @@ def __getattr__(name):
|
||||
elif name == 'CapabilitiesHandler':
|
||||
from .capabilities import CapabilitiesHandler
|
||||
return CapabilitiesHandler
|
||||
elif name == 'PerformanceHandler':
|
||||
from .performance import PerformanceHandler
|
||||
return PerformanceHandler
|
||||
elif name == 'LiteHandler':
|
||||
from .lite import LiteHandler
|
||||
return LiteHandler
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Doctor CLI module for PraisonAI.
|
||||
|
||||
Provides comprehensive health checks, diagnostics, and validation for the PraisonAI ecosystem.
|
||||
|
||||
Commands:
|
||||
praisonai doctor - Run fast default checks
|
||||
praisonai doctor env - Environment and API key validation
|
||||
praisonai doctor config - Configuration file validation
|
||||
praisonai doctor tools - Tool availability checks
|
||||
praisonai doctor db - Database connectivity checks
|
||||
praisonai doctor mcp - MCP server validation
|
||||
praisonai doctor obs - Observability provider checks
|
||||
praisonai doctor skills - Agent skills validation
|
||||
praisonai doctor memory - Memory/session storage checks
|
||||
praisonai doctor permissions - Filesystem permission checks
|
||||
praisonai doctor network - Network connectivity checks
|
||||
praisonai doctor performance - Import time analysis
|
||||
praisonai doctor ci - CI-optimized checks
|
||||
praisonai doctor selftest - Minimal agent dry-run
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
__all__ = [
|
||||
"DoctorHandler",
|
||||
"DoctorEngine",
|
||||
"CheckResult",
|
||||
"CheckStatus",
|
||||
"DoctorReport",
|
||||
"CheckRegistry",
|
||||
"TextFormatter",
|
||||
"JsonFormatter",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load doctor components to minimize import overhead."""
|
||||
if name == "DoctorHandler":
|
||||
from .handler import DoctorHandler
|
||||
return DoctorHandler
|
||||
elif name == "DoctorEngine":
|
||||
from .engine import DoctorEngine
|
||||
return DoctorEngine
|
||||
elif name == "CheckResult":
|
||||
from .models import CheckResult
|
||||
return CheckResult
|
||||
elif name == "CheckStatus":
|
||||
from .models import CheckStatus
|
||||
return CheckStatus
|
||||
elif name == "DoctorReport":
|
||||
from .models import DoctorReport
|
||||
return DoctorReport
|
||||
elif name == "CheckRegistry":
|
||||
from .registry import CheckRegistry
|
||||
return CheckRegistry
|
||||
elif name == "TextFormatter":
|
||||
from .formatters import TextFormatter
|
||||
return TextFormatter
|
||||
elif name == "JsonFormatter":
|
||||
from .formatters import JsonFormatter
|
||||
return JsonFormatter
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Doctor checks package.
|
||||
|
||||
Contains all individual check implementations organized by category.
|
||||
"""
|
||||
|
||||
# Import all check modules to register them
|
||||
def register_all_checks():
|
||||
"""Register all doctor checks."""
|
||||
from . import env_checks
|
||||
from . import config_checks
|
||||
from . import tools_checks
|
||||
from . import db_checks
|
||||
from . import mcp_checks
|
||||
from . import obs_checks
|
||||
from . import skills_checks
|
||||
from . import memory_checks
|
||||
from . import permissions_checks
|
||||
from . import network_checks
|
||||
from . import performance_checks
|
||||
from . import selftest_checks
|
||||
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
Configuration checks for the Doctor CLI module.
|
||||
|
||||
Validates YAML configuration files like agents.yaml and workflow.yaml.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
def _find_config_file(config: DoctorConfig, default_names: list) -> str:
|
||||
"""Find a configuration file."""
|
||||
if config.config_file:
|
||||
return config.config_file
|
||||
|
||||
cwd = Path.cwd()
|
||||
for name in default_names:
|
||||
path = cwd / name
|
||||
if path.exists():
|
||||
return str(path)
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def _validate_yaml_syntax(file_path: str) -> tuple:
|
||||
"""Validate YAML syntax and return (valid, data, error)."""
|
||||
try:
|
||||
import yaml
|
||||
with open(file_path, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
return True, data, None
|
||||
except ImportError:
|
||||
return False, None, "PyYAML not installed"
|
||||
except yaml.YAMLError as e:
|
||||
return False, None, f"YAML syntax error: {e}"
|
||||
except Exception as e:
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="agents_yaml_exists",
|
||||
title="agents.yaml Exists",
|
||||
description="Check if agents.yaml configuration file exists",
|
||||
category=CheckCategory.CONFIG,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
)
|
||||
def check_agents_yaml_exists(config: DoctorConfig) -> CheckResult:
|
||||
"""Check if agents.yaml exists."""
|
||||
file_path = _find_config_file(config, ["agents.yaml", "agents.yml"])
|
||||
|
||||
if file_path and os.path.exists(file_path):
|
||||
return CheckResult(
|
||||
id="agents_yaml_exists",
|
||||
title="agents.yaml Exists",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Found: {file_path}",
|
||||
metadata={"path": file_path},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="agents_yaml_exists",
|
||||
title="agents.yaml Exists",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No agents.yaml found in current directory (optional)",
|
||||
details="agents.yaml is only required when running YAML-based workflows",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="agents_yaml_syntax",
|
||||
title="agents.yaml Syntax",
|
||||
description="Validate agents.yaml YAML syntax",
|
||||
category=CheckCategory.CONFIG,
|
||||
severity=CheckSeverity.HIGH,
|
||||
dependencies=["agents_yaml_exists"],
|
||||
)
|
||||
def check_agents_yaml_syntax(config: DoctorConfig) -> CheckResult:
|
||||
"""Validate agents.yaml YAML syntax."""
|
||||
file_path = _find_config_file(config, ["agents.yaml", "agents.yml"])
|
||||
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return CheckResult(
|
||||
id="agents_yaml_syntax",
|
||||
title="agents.yaml Syntax",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No agents.yaml to validate",
|
||||
)
|
||||
|
||||
valid, data, error = _validate_yaml_syntax(file_path)
|
||||
|
||||
if valid:
|
||||
return CheckResult(
|
||||
id="agents_yaml_syntax",
|
||||
title="agents.yaml Syntax",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.PASS,
|
||||
message="YAML syntax is valid",
|
||||
metadata={"path": file_path},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="agents_yaml_syntax",
|
||||
title="agents.yaml Syntax",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Invalid YAML: {error}",
|
||||
remediation="Fix the YAML syntax errors in agents.yaml",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="agents_yaml_schema",
|
||||
title="agents.yaml Schema",
|
||||
description="Validate agents.yaml schema structure",
|
||||
category=CheckCategory.CONFIG,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
dependencies=["agents_yaml_syntax"],
|
||||
)
|
||||
def check_agents_yaml_schema(config: DoctorConfig) -> CheckResult:
|
||||
"""Validate agents.yaml schema structure."""
|
||||
file_path = _find_config_file(config, ["agents.yaml", "agents.yml"])
|
||||
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return CheckResult(
|
||||
id="agents_yaml_schema",
|
||||
title="agents.yaml Schema",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No agents.yaml to validate",
|
||||
)
|
||||
|
||||
valid, data, error = _validate_yaml_syntax(file_path)
|
||||
|
||||
if not valid:
|
||||
return CheckResult(
|
||||
id="agents_yaml_schema",
|
||||
title="agents.yaml Schema",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Skipped due to syntax errors",
|
||||
)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return CheckResult(
|
||||
id="agents_yaml_schema",
|
||||
title="agents.yaml Schema",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.FAIL,
|
||||
message="agents.yaml must be a YAML mapping (dictionary)",
|
||||
remediation="Ensure agents.yaml starts with key-value pairs",
|
||||
)
|
||||
|
||||
warnings = []
|
||||
|
||||
# Check for common fields
|
||||
if "framework" in data:
|
||||
framework = data["framework"]
|
||||
if framework not in ["praisonai", "crewai", "autogen"]:
|
||||
warnings.append(f"Unknown framework: {framework}")
|
||||
|
||||
# Check for agents section
|
||||
agents = data.get("agents") or data.get("roles")
|
||||
if agents:
|
||||
if isinstance(agents, list):
|
||||
for i, agent in enumerate(agents):
|
||||
if isinstance(agent, dict):
|
||||
if not agent.get("name") and not agent.get("role"):
|
||||
warnings.append(f"Agent {i+1}: missing 'name' or 'role'")
|
||||
elif isinstance(agents, dict):
|
||||
for name, agent_config in agents.items():
|
||||
if isinstance(agent_config, dict):
|
||||
if not agent_config.get("role") and not agent_config.get("goal"):
|
||||
warnings.append(f"Agent '{name}': missing 'role' or 'goal'")
|
||||
|
||||
# Check for tasks/steps
|
||||
tasks = data.get("tasks") or data.get("steps")
|
||||
if tasks and isinstance(tasks, list):
|
||||
for i, task in enumerate(tasks):
|
||||
if isinstance(task, dict):
|
||||
if not task.get("description") and not task.get("action"):
|
||||
warnings.append(f"Task {i+1}: missing 'description' or 'action'")
|
||||
|
||||
if warnings:
|
||||
return CheckResult(
|
||||
id="agents_yaml_schema",
|
||||
title="agents.yaml Schema",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Schema valid with {len(warnings)} warning(s)",
|
||||
details="; ".join(warnings[:3]) + ("..." if len(warnings) > 3 else ""),
|
||||
metadata={"warnings": warnings},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="agents_yaml_schema",
|
||||
title="agents.yaml Schema",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.PASS,
|
||||
message="Schema structure is valid",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="workflow_yaml_exists",
|
||||
title="workflow.yaml Exists",
|
||||
description="Check if workflow.yaml configuration file exists",
|
||||
category=CheckCategory.CONFIG,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_workflow_yaml_exists(config: DoctorConfig) -> CheckResult:
|
||||
"""Check if workflow.yaml exists."""
|
||||
file_path = _find_config_file(config, ["workflow.yaml", "workflow.yml"])
|
||||
|
||||
if file_path and os.path.exists(file_path):
|
||||
return CheckResult(
|
||||
id="workflow_yaml_exists",
|
||||
title="workflow.yaml Exists",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Found: {file_path}",
|
||||
metadata={"path": file_path},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="workflow_yaml_exists",
|
||||
title="workflow.yaml Exists",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No workflow.yaml found (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="workflow_yaml_syntax",
|
||||
title="workflow.yaml Syntax",
|
||||
description="Validate workflow.yaml YAML syntax",
|
||||
category=CheckCategory.CONFIG,
|
||||
severity=CheckSeverity.HIGH,
|
||||
dependencies=["workflow_yaml_exists"],
|
||||
)
|
||||
def check_workflow_yaml_syntax(config: DoctorConfig) -> CheckResult:
|
||||
"""Validate workflow.yaml YAML syntax."""
|
||||
file_path = _find_config_file(config, ["workflow.yaml", "workflow.yml"])
|
||||
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return CheckResult(
|
||||
id="workflow_yaml_syntax",
|
||||
title="workflow.yaml Syntax",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No workflow.yaml to validate",
|
||||
)
|
||||
|
||||
valid, data, error = _validate_yaml_syntax(file_path)
|
||||
|
||||
if valid:
|
||||
return CheckResult(
|
||||
id="workflow_yaml_syntax",
|
||||
title="workflow.yaml Syntax",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.PASS,
|
||||
message="YAML syntax is valid",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="workflow_yaml_syntax",
|
||||
title="workflow.yaml Syntax",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Invalid YAML: {error}",
|
||||
remediation="Fix the YAML syntax errors in workflow.yaml",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="praison_config_dir",
|
||||
title=".praison Config Directory",
|
||||
description="Check .praison configuration directory",
|
||||
category=CheckCategory.CONFIG,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_praison_config_dir(config: DoctorConfig) -> CheckResult:
|
||||
"""Check .praison configuration directory."""
|
||||
home_dir = Path.home()
|
||||
cwd = Path.cwd()
|
||||
|
||||
locations = [
|
||||
cwd / ".praison",
|
||||
home_dir / ".praison",
|
||||
home_dir / ".config" / "praison",
|
||||
]
|
||||
|
||||
found = []
|
||||
for loc in locations:
|
||||
if loc.exists():
|
||||
found.append(str(loc))
|
||||
|
||||
if found:
|
||||
return CheckResult(
|
||||
id="praison_config_dir",
|
||||
title=".praison Config Directory",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Found {len(found)} config location(s)",
|
||||
details=", ".join(found),
|
||||
metadata={"locations": found},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="praison_config_dir",
|
||||
title=".praison Config Directory",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No .praison config directory found (will be created on first use)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="env_file",
|
||||
title=".env File",
|
||||
description="Check for .env file",
|
||||
category=CheckCategory.CONFIG,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_env_file(config: DoctorConfig) -> CheckResult:
|
||||
"""Check for .env file."""
|
||||
cwd = Path.cwd()
|
||||
env_files = [".env", ".env.local", ".env.development"]
|
||||
|
||||
found = []
|
||||
for name in env_files:
|
||||
path = cwd / name
|
||||
if path.exists():
|
||||
found.append(name)
|
||||
|
||||
if found:
|
||||
return CheckResult(
|
||||
id="env_file",
|
||||
title=".env File",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Found: {', '.join(found)}",
|
||||
metadata={"files": found},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="env_file",
|
||||
title=".env File",
|
||||
category=CheckCategory.CONFIG,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No .env file found (environment variables can be set directly)",
|
||||
)
|
||||
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
Database checks for the Doctor CLI module.
|
||||
|
||||
Validates database connectivity and configuration.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
def _parse_dsn(dsn: str) -> dict:
|
||||
"""Parse a database DSN into components."""
|
||||
result = {"type": "unknown", "host": "", "port": "", "database": ""}
|
||||
|
||||
if dsn.startswith("postgresql://") or dsn.startswith("postgres://"):
|
||||
result["type"] = "postgresql"
|
||||
elif dsn.startswith("sqlite://"):
|
||||
result["type"] = "sqlite"
|
||||
elif dsn.startswith("redis://"):
|
||||
result["type"] = "redis"
|
||||
elif dsn.startswith("mongodb://") or dsn.startswith("mongodb+srv://"):
|
||||
result["type"] = "mongodb"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@register_check(
|
||||
id="db_config",
|
||||
title="Database Configuration",
|
||||
description="Check database configuration from environment",
|
||||
category=CheckCategory.DATABASE,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_db_config(config: DoctorConfig) -> CheckResult:
|
||||
"""Check database configuration from environment."""
|
||||
dsn = config.dsn or os.environ.get("DATABASE_URL") or os.environ.get("PRAISONAI_DATABASE_URL")
|
||||
|
||||
if dsn:
|
||||
parsed = _parse_dsn(dsn)
|
||||
return CheckResult(
|
||||
id="db_config",
|
||||
title="Database Configuration",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Database configured: {parsed['type']}",
|
||||
metadata={"type": parsed["type"]},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="db_config",
|
||||
title="Database Configuration",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No database DSN configured (using file-based storage)",
|
||||
details="Set DATABASE_URL to enable database persistence",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="db_driver_postgresql",
|
||||
title="PostgreSQL Driver",
|
||||
description="Check PostgreSQL driver availability",
|
||||
category=CheckCategory.DATABASE,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_db_driver_postgresql(config: DoctorConfig) -> CheckResult:
|
||||
"""Check PostgreSQL driver availability."""
|
||||
try:
|
||||
import psycopg2
|
||||
version = getattr(psycopg2, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="db_driver_postgresql",
|
||||
title="PostgreSQL Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"psycopg2 {version} available",
|
||||
)
|
||||
except ImportError:
|
||||
try:
|
||||
import asyncpg
|
||||
version = getattr(asyncpg, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="db_driver_postgresql",
|
||||
title="PostgreSQL Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"asyncpg {version} available",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="db_driver_postgresql",
|
||||
title="PostgreSQL Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="PostgreSQL driver not installed (optional)",
|
||||
remediation="Install with: pip install psycopg2-binary or pip install asyncpg",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="db_driver_sqlite",
|
||||
title="SQLite Driver",
|
||||
description="Check SQLite driver availability",
|
||||
category=CheckCategory.DATABASE,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_db_driver_sqlite(config: DoctorConfig) -> CheckResult:
|
||||
"""Check SQLite driver availability."""
|
||||
try:
|
||||
import sqlite3
|
||||
version = sqlite3.sqlite_version
|
||||
return CheckResult(
|
||||
id="db_driver_sqlite",
|
||||
title="SQLite Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"SQLite {version} available (built-in)",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="db_driver_sqlite",
|
||||
title="SQLite Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.FAIL,
|
||||
message="SQLite not available",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="db_driver_redis",
|
||||
title="Redis Driver",
|
||||
description="Check Redis driver availability",
|
||||
category=CheckCategory.DATABASE,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_db_driver_redis(config: DoctorConfig) -> CheckResult:
|
||||
"""Check Redis driver availability."""
|
||||
try:
|
||||
import redis
|
||||
version = getattr(redis, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="db_driver_redis",
|
||||
title="Redis Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"redis-py {version} available",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="db_driver_redis",
|
||||
title="Redis Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Redis driver not installed (optional)",
|
||||
remediation="Install with: pip install redis",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="db_driver_mongodb",
|
||||
title="MongoDB Driver",
|
||||
description="Check MongoDB driver availability",
|
||||
category=CheckCategory.DATABASE,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_db_driver_mongodb(config: DoctorConfig) -> CheckResult:
|
||||
"""Check MongoDB driver availability."""
|
||||
try:
|
||||
import pymongo
|
||||
version = getattr(pymongo, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="db_driver_mongodb",
|
||||
title="MongoDB Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"pymongo {version} available",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="db_driver_mongodb",
|
||||
title="MongoDB Driver",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="MongoDB driver not installed (optional)",
|
||||
remediation="Install with: pip install pymongo",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
description="Test database connection",
|
||||
category=CheckCategory.DATABASE,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_db_connectivity(config: DoctorConfig) -> CheckResult:
|
||||
"""Test database connection."""
|
||||
dsn = config.dsn or os.environ.get("DATABASE_URL") or os.environ.get("PRAISONAI_DATABASE_URL")
|
||||
|
||||
if not dsn:
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No database DSN configured",
|
||||
)
|
||||
|
||||
parsed = _parse_dsn(dsn)
|
||||
|
||||
try:
|
||||
if parsed["type"] == "postgresql":
|
||||
try:
|
||||
import psycopg2
|
||||
conn = psycopg2.connect(dsn, connect_timeout=5)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT 1")
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message="PostgreSQL connection successful",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="PostgreSQL driver not installed",
|
||||
)
|
||||
|
||||
elif parsed["type"] == "sqlite":
|
||||
import sqlite3
|
||||
# Extract path from DSN
|
||||
db_path = dsn.replace("sqlite:///", "").replace("sqlite://", "")
|
||||
if db_path == ":memory:" or os.path.exists(db_path) or db_path == "":
|
||||
conn = sqlite3.connect(db_path if db_path else ":memory:")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT 1")
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message="SQLite connection successful",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"SQLite database file not found: {db_path}",
|
||||
details="File will be created on first use",
|
||||
)
|
||||
|
||||
elif parsed["type"] == "redis":
|
||||
try:
|
||||
import redis
|
||||
client = redis.from_url(dsn, socket_timeout=5)
|
||||
client.ping()
|
||||
client.close()
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message="Redis connection successful",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Redis driver not installed",
|
||||
)
|
||||
|
||||
elif parsed["type"] == "mongodb":
|
||||
try:
|
||||
import pymongo
|
||||
client = pymongo.MongoClient(dsn, serverSelectionTimeoutMS=5000)
|
||||
client.admin.command("ping")
|
||||
client.close()
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message="MongoDB connection successful",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="MongoDB driver not installed",
|
||||
)
|
||||
|
||||
else:
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message=f"Unknown database type: {parsed['type']}",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="db_connectivity",
|
||||
title="Database Connectivity",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Connection failed: {type(e).__name__}",
|
||||
details=str(e)[:200],
|
||||
remediation="Check database server is running and DSN is correct",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="db_chromadb",
|
||||
title="ChromaDB (Vector Store)",
|
||||
description="Check ChromaDB availability for RAG",
|
||||
category=CheckCategory.DATABASE,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_db_chromadb(config: DoctorConfig) -> CheckResult:
|
||||
"""Check ChromaDB availability for RAG."""
|
||||
try:
|
||||
import chromadb
|
||||
version = getattr(chromadb, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="db_chromadb",
|
||||
title="ChromaDB (Vector Store)",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"ChromaDB {version} available",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="db_chromadb",
|
||||
title="ChromaDB (Vector Store)",
|
||||
category=CheckCategory.DATABASE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="ChromaDB not installed (required for Knowledge/RAG features)",
|
||||
remediation="Install with: pip install chromadb",
|
||||
)
|
||||
@@ -0,0 +1,543 @@
|
||||
"""
|
||||
Environment checks for the Doctor CLI module.
|
||||
|
||||
Validates Python version, packages, API keys, and environment variables.
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
@register_check(
|
||||
id="python_version",
|
||||
title="Python Version",
|
||||
description="Check Python version is 3.9+",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.CRITICAL,
|
||||
)
|
||||
def check_python_version(config: DoctorConfig) -> CheckResult:
|
||||
"""Check Python version is 3.9+."""
|
||||
version = sys.version_info
|
||||
version_str = f"{version.major}.{version.minor}.{version.micro}"
|
||||
|
||||
if version.major >= 3 and version.minor >= 9:
|
||||
return CheckResult(
|
||||
id="python_version",
|
||||
title="Python Version",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Python {version_str} (>= 3.9 required)",
|
||||
metadata={"version": version_str, "executable": sys.executable},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="python_version",
|
||||
title="Python Version",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Python {version_str} found, but 3.9+ required",
|
||||
remediation="Upgrade Python to version 3.9 or higher",
|
||||
severity=CheckSeverity.CRITICAL,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="praisonai_package",
|
||||
title="PraisonAI Package",
|
||||
description="Check praisonai package is installed",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.CRITICAL,
|
||||
)
|
||||
def check_praisonai_package(config: DoctorConfig) -> CheckResult:
|
||||
"""Check praisonai package is installed."""
|
||||
try:
|
||||
from praisonai.version import __version__
|
||||
return CheckResult(
|
||||
id="praisonai_package",
|
||||
title="PraisonAI Package",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"praisonai {__version__} installed",
|
||||
metadata={"version": __version__},
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="praisonai_package",
|
||||
title="PraisonAI Package",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.FAIL,
|
||||
message="praisonai package not found",
|
||||
details=str(e),
|
||||
remediation="Install with: pip install praisonai",
|
||||
severity=CheckSeverity.CRITICAL,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="praisonaiagents_package",
|
||||
title="PraisonAI Agents Package",
|
||||
description="Check praisonaiagents package is installed",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.CRITICAL,
|
||||
)
|
||||
def check_praisonaiagents_package(config: DoctorConfig) -> CheckResult:
|
||||
"""Check praisonaiagents package is installed."""
|
||||
try:
|
||||
import praisonaiagents
|
||||
version = getattr(praisonaiagents, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="praisonaiagents_package",
|
||||
title="PraisonAI Agents Package",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"praisonaiagents {version} installed",
|
||||
metadata={"version": version},
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="praisonaiagents_package",
|
||||
title="PraisonAI Agents Package",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.FAIL,
|
||||
message="praisonaiagents package not found",
|
||||
details=str(e),
|
||||
remediation="Install with: pip install praisonaiagents",
|
||||
severity=CheckSeverity.CRITICAL,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="openai_api_key",
|
||||
title="OpenAI API Key",
|
||||
description="Check OPENAI_API_KEY is configured",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
def check_openai_api_key(config: DoctorConfig) -> CheckResult:
|
||||
"""Check OpenAI API key is configured."""
|
||||
key = os.environ.get("OPENAI_API_KEY", "")
|
||||
|
||||
if key:
|
||||
# Mask the key for display
|
||||
if config.show_keys and len(key) > 12:
|
||||
masked = f"{key[:4]}...{key[-4:]}"
|
||||
else:
|
||||
masked = "***configured***"
|
||||
|
||||
# Basic validation
|
||||
if key.startswith("sk-") or key.startswith("sk-proj-"):
|
||||
return CheckResult(
|
||||
id="openai_api_key",
|
||||
title="OpenAI API Key",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"OPENAI_API_KEY configured ({masked})",
|
||||
metadata={"masked_key": masked},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="openai_api_key",
|
||||
title="OpenAI API Key",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"OPENAI_API_KEY set but format unexpected ({masked})",
|
||||
remediation="Verify the API key format is correct",
|
||||
)
|
||||
else:
|
||||
# Check for alternative providers
|
||||
alternatives = []
|
||||
if os.environ.get("ANTHROPIC_API_KEY"):
|
||||
alternatives.append("ANTHROPIC_API_KEY")
|
||||
if os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY"):
|
||||
alternatives.append("GOOGLE/GEMINI_API_KEY")
|
||||
if os.environ.get("OLLAMA_HOST") or shutil.which("ollama"):
|
||||
alternatives.append("Ollama")
|
||||
|
||||
if alternatives:
|
||||
return CheckResult(
|
||||
id="openai_api_key",
|
||||
title="OpenAI API Key",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"OPENAI_API_KEY not set, but alternatives found: {', '.join(alternatives)}",
|
||||
details="OpenAI is the default provider. Other providers require explicit configuration.",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="openai_api_key",
|
||||
title="OpenAI API Key",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.FAIL,
|
||||
message="OPENAI_API_KEY not configured and no alternative providers found",
|
||||
remediation="Set OPENAI_API_KEY environment variable or configure an alternative provider",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="anthropic_api_key",
|
||||
title="Anthropic API Key",
|
||||
description="Check ANTHROPIC_API_KEY is configured",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_anthropic_api_key(config: DoctorConfig) -> CheckResult:
|
||||
"""Check Anthropic API key is configured."""
|
||||
key = os.environ.get("ANTHROPIC_API_KEY", "")
|
||||
|
||||
if key:
|
||||
if config.show_keys and len(key) > 12:
|
||||
masked = f"{key[:4]}...{key[-4:]}"
|
||||
else:
|
||||
masked = "***configured***"
|
||||
|
||||
return CheckResult(
|
||||
id="anthropic_api_key",
|
||||
title="Anthropic API Key",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"ANTHROPIC_API_KEY configured ({masked})",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="anthropic_api_key",
|
||||
title="Anthropic API Key",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.SKIP,
|
||||
message="ANTHROPIC_API_KEY not set (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="google_api_key",
|
||||
title="Google/Gemini API Key",
|
||||
description="Check GOOGLE_API_KEY or GEMINI_API_KEY is configured",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_google_api_key(config: DoctorConfig) -> CheckResult:
|
||||
"""Check Google/Gemini API key is configured."""
|
||||
key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY", "")
|
||||
|
||||
if key:
|
||||
if config.show_keys and len(key) > 12:
|
||||
masked = f"{key[:4]}...{key[-4:]}"
|
||||
else:
|
||||
masked = "***configured***"
|
||||
|
||||
return CheckResult(
|
||||
id="google_api_key",
|
||||
title="Google/Gemini API Key",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Google/Gemini API key configured ({masked})",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="google_api_key",
|
||||
title="Google/Gemini API Key",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.SKIP,
|
||||
message="GOOGLE_API_KEY/GEMINI_API_KEY not set (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="os_info",
|
||||
title="Operating System",
|
||||
description="Check operating system information",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_os_info(config: DoctorConfig) -> CheckResult:
|
||||
"""Check operating system information."""
|
||||
os_name = platform.system()
|
||||
os_version = platform.release()
|
||||
arch = platform.machine()
|
||||
|
||||
return CheckResult(
|
||||
id="os_info",
|
||||
title="Operating System",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"{os_name} {os_version} ({arch})",
|
||||
metadata={
|
||||
"os_name": os_name,
|
||||
"os_version": os_version,
|
||||
"architecture": arch,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="virtual_env",
|
||||
title="Virtual Environment",
|
||||
description="Check if running in a virtual environment",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_virtual_env(config: DoctorConfig) -> CheckResult:
|
||||
"""Check if running in a virtual environment."""
|
||||
venv = os.environ.get("VIRTUAL_ENV")
|
||||
conda_env = os.environ.get("CONDA_DEFAULT_ENV")
|
||||
|
||||
if venv:
|
||||
return CheckResult(
|
||||
id="virtual_env",
|
||||
title="Virtual Environment",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Running in venv: {os.path.basename(venv)}",
|
||||
metadata={"venv_path": venv},
|
||||
)
|
||||
elif conda_env:
|
||||
return CheckResult(
|
||||
id="virtual_env",
|
||||
title="Virtual Environment",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Running in conda env: {conda_env}",
|
||||
metadata={"conda_env": conda_env},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="virtual_env",
|
||||
title="Virtual Environment",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.WARN,
|
||||
message="Not running in a virtual environment",
|
||||
remediation="Consider using a virtual environment for isolation",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="git_available",
|
||||
title="Git",
|
||||
description="Check if git is available",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_git_available(config: DoctorConfig) -> CheckResult:
|
||||
"""Check if git is available."""
|
||||
git_path = shutil.which("git")
|
||||
|
||||
if git_path:
|
||||
import subprocess
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
version = result.stdout.strip() if result.returncode == 0 else "unknown"
|
||||
return CheckResult(
|
||||
id="git_available",
|
||||
title="Git",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Git available: {version}",
|
||||
metadata={"path": git_path},
|
||||
)
|
||||
except Exception:
|
||||
return CheckResult(
|
||||
id="git_available",
|
||||
title="Git",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Git available at {git_path}",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="git_available",
|
||||
title="Git",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.WARN,
|
||||
message="Git not found in PATH",
|
||||
remediation="Install git for version control features",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="docker_available",
|
||||
title="Docker",
|
||||
description="Check if Docker is available",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_docker_available(config: DoctorConfig) -> CheckResult:
|
||||
"""Check if Docker is available."""
|
||||
docker_path = shutil.which("docker")
|
||||
|
||||
if docker_path:
|
||||
import subprocess
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
version = result.stdout.strip()
|
||||
return CheckResult(
|
||||
id="docker_available",
|
||||
title="Docker",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Docker available: {version}",
|
||||
metadata={"path": docker_path},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return CheckResult(
|
||||
id="docker_available",
|
||||
title="Docker",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.WARN,
|
||||
message="Docker found but not responding",
|
||||
remediation="Ensure Docker daemon is running",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="docker_available",
|
||||
title="Docker",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Docker not found (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="npx_available",
|
||||
title="npx (Node.js)",
|
||||
description="Check if npx is available for MCP servers",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_npx_available(config: DoctorConfig) -> CheckResult:
|
||||
"""Check if npx is available for MCP servers."""
|
||||
npx_path = shutil.which("npx")
|
||||
|
||||
if npx_path:
|
||||
return CheckResult(
|
||||
id="npx_available",
|
||||
title="npx (Node.js)",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"npx available at {npx_path}",
|
||||
metadata={"path": npx_path},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="npx_available",
|
||||
title="npx (Node.js)",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.WARN,
|
||||
message="npx not found (required for some MCP servers)",
|
||||
remediation="Install Node.js to use MCP servers that require npx",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="optional_deps",
|
||||
title="Optional Dependencies",
|
||||
description="Check availability of optional dependencies",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.INFO,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_optional_deps(config: DoctorConfig) -> CheckResult:
|
||||
"""Check availability of optional dependencies."""
|
||||
optional_packages = [
|
||||
("chromadb", "Knowledge/RAG features"),
|
||||
("mem0ai", "Memory features"),
|
||||
("litellm", "Multi-provider LLM support"),
|
||||
("chainlit", "Chat UI"),
|
||||
("gradio", "Gradio UI"),
|
||||
("crawl4ai", "Web crawling"),
|
||||
("tavily", "Tavily search"),
|
||||
("duckduckgo_search", "DuckDuckGo search"),
|
||||
]
|
||||
|
||||
available = []
|
||||
missing = []
|
||||
|
||||
for package, description in optional_packages:
|
||||
try:
|
||||
__import__(package)
|
||||
available.append(package)
|
||||
except ImportError:
|
||||
missing.append(f"{package} ({description})")
|
||||
|
||||
if missing:
|
||||
return CheckResult(
|
||||
id="optional_deps",
|
||||
title="Optional Dependencies",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"{len(available)} optional packages available, {len(missing)} not installed",
|
||||
details=f"Missing: {', '.join(missing[:5])}{'...' if len(missing) > 5 else ''}",
|
||||
metadata={"available": available, "missing": missing},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="optional_deps",
|
||||
title="Optional Dependencies",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"All {len(available)} optional packages available",
|
||||
metadata={"available": available},
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="model_env_vars",
|
||||
title="Model Configuration",
|
||||
description="Check model-related environment variables",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_model_env_vars(config: DoctorConfig) -> CheckResult:
|
||||
"""Check model-related environment variables."""
|
||||
model_vars = {
|
||||
"MODEL_NAME": os.environ.get("MODEL_NAME"),
|
||||
"OPENAI_MODEL_NAME": os.environ.get("OPENAI_MODEL_NAME"),
|
||||
"OPENAI_BASE_URL": os.environ.get("OPENAI_BASE_URL"),
|
||||
"OPENAI_API_BASE": os.environ.get("OPENAI_API_BASE"),
|
||||
}
|
||||
|
||||
configured = {k: v for k, v in model_vars.items() if v}
|
||||
|
||||
if configured:
|
||||
details = ", ".join(f"{k}={v}" for k, v in configured.items())
|
||||
return CheckResult(
|
||||
id="model_env_vars",
|
||||
title="Model Configuration",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Model config: {details}",
|
||||
metadata=configured,
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="model_env_vars",
|
||||
title="Model Configuration",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.PASS,
|
||||
message="Using default model configuration (gpt-4o-mini)",
|
||||
metadata={"default_model": "gpt-4o-mini"},
|
||||
)
|
||||
@@ -0,0 +1,349 @@
|
||||
"""
|
||||
MCP (Model Context Protocol) checks for the Doctor CLI module.
|
||||
|
||||
Validates MCP server configurations and connectivity.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
def _find_mcp_config() -> dict:
|
||||
"""Find MCP configuration files."""
|
||||
locations = [
|
||||
Path.cwd() / ".praison" / "mcp.json",
|
||||
Path.cwd() / ".praison" / "mcp" / "config.json",
|
||||
Path.home() / ".praison" / "mcp.json",
|
||||
Path.home() / ".config" / "praison" / "mcp.json",
|
||||
]
|
||||
|
||||
for loc in locations:
|
||||
if loc.exists():
|
||||
try:
|
||||
import json
|
||||
with open(loc) as f:
|
||||
return {"path": str(loc), "config": json.load(f)}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
@register_check(
|
||||
id="mcp_config",
|
||||
title="MCP Configuration",
|
||||
description="Check MCP configuration file",
|
||||
category=CheckCategory.MCP,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_mcp_config(config: DoctorConfig) -> CheckResult:
|
||||
"""Check MCP configuration file."""
|
||||
mcp_config = _find_mcp_config()
|
||||
|
||||
if mcp_config:
|
||||
servers = mcp_config.get("config", {}).get("mcpServers", {})
|
||||
server_count = len(servers) if isinstance(servers, dict) else 0
|
||||
|
||||
return CheckResult(
|
||||
id="mcp_config",
|
||||
title="MCP Configuration",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"MCP config found with {server_count} server(s)",
|
||||
details=f"Path: {mcp_config['path']}",
|
||||
metadata={"path": mcp_config["path"], "server_count": server_count},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="mcp_config",
|
||||
title="MCP Configuration",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No MCP configuration found (optional)",
|
||||
details="Create .praison/mcp.json to configure MCP servers",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="mcp_npx",
|
||||
title="npx for MCP",
|
||||
description="Check npx availability for MCP servers",
|
||||
category=CheckCategory.MCP,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_mcp_npx(config: DoctorConfig) -> CheckResult:
|
||||
"""Check npx availability for MCP servers."""
|
||||
npx_path = shutil.which("npx")
|
||||
|
||||
if npx_path:
|
||||
return CheckResult(
|
||||
id="mcp_npx",
|
||||
title="npx for MCP",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"npx available at {npx_path}",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="mcp_npx",
|
||||
title="npx for MCP",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.WARN,
|
||||
message="npx not found (required for most MCP servers)",
|
||||
remediation="Install Node.js to use MCP servers",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="mcp_python",
|
||||
title="Python MCP Support",
|
||||
description="Check Python MCP package availability",
|
||||
category=CheckCategory.MCP,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_mcp_python(config: DoctorConfig) -> CheckResult:
|
||||
"""Check Python MCP package availability."""
|
||||
try:
|
||||
import mcp
|
||||
version = getattr(mcp, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="mcp_python",
|
||||
title="Python MCP Support",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"mcp package {version} available",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="mcp_python",
|
||||
title="Python MCP Support",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.SKIP,
|
||||
message="mcp package not installed (optional)",
|
||||
remediation="Install with: pip install mcp",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="mcp_servers_valid",
|
||||
title="MCP Server Configs Valid",
|
||||
description="Validate MCP server configurations",
|
||||
category=CheckCategory.MCP,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
dependencies=["mcp_config"],
|
||||
)
|
||||
def check_mcp_servers_valid(config: DoctorConfig) -> CheckResult:
|
||||
"""Validate MCP server configurations."""
|
||||
mcp_config = _find_mcp_config()
|
||||
|
||||
if not mcp_config:
|
||||
return CheckResult(
|
||||
id="mcp_servers_valid",
|
||||
title="MCP Server Configs Valid",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No MCP configuration to validate",
|
||||
)
|
||||
|
||||
servers = mcp_config.get("config", {}).get("mcpServers", {})
|
||||
if not servers:
|
||||
return CheckResult(
|
||||
id="mcp_servers_valid",
|
||||
title="MCP Server Configs Valid",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No MCP servers configured",
|
||||
)
|
||||
|
||||
valid = []
|
||||
invalid = []
|
||||
|
||||
for name, server_config in servers.items():
|
||||
if not isinstance(server_config, dict):
|
||||
invalid.append(f"{name}: invalid config format")
|
||||
continue
|
||||
|
||||
command = server_config.get("command")
|
||||
if not command:
|
||||
invalid.append(f"{name}: missing 'command'")
|
||||
continue
|
||||
|
||||
# Check if command executable exists
|
||||
cmd_parts = command.split() if isinstance(command, str) else [command]
|
||||
cmd_exe = cmd_parts[0] if cmd_parts else ""
|
||||
|
||||
if cmd_exe and shutil.which(cmd_exe):
|
||||
valid.append(name)
|
||||
else:
|
||||
invalid.append(f"{name}: command '{cmd_exe}' not found")
|
||||
|
||||
if invalid:
|
||||
return CheckResult(
|
||||
id="mcp_servers_valid",
|
||||
title="MCP Server Configs Valid",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"{len(valid)} valid, {len(invalid)} invalid server(s)",
|
||||
details="; ".join(invalid[:3]) + ("..." if len(invalid) > 3 else ""),
|
||||
metadata={"valid": valid, "invalid": invalid},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="mcp_servers_valid",
|
||||
title="MCP Server Configs Valid",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"All {len(valid)} MCP server config(s) valid",
|
||||
metadata={"valid": valid},
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="mcp_server_spawn",
|
||||
title="MCP Server Spawn Test",
|
||||
description="Test spawning MCP servers",
|
||||
category=CheckCategory.MCP,
|
||||
severity=CheckSeverity.LOW,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_mcp_server_spawn(config: DoctorConfig) -> CheckResult:
|
||||
"""Test spawning MCP servers (deep mode only)."""
|
||||
mcp_config = _find_mcp_config()
|
||||
|
||||
if not mcp_config:
|
||||
return CheckResult(
|
||||
id="mcp_server_spawn",
|
||||
title="MCP Server Spawn Test",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No MCP configuration to test",
|
||||
)
|
||||
|
||||
servers = mcp_config.get("config", {}).get("mcpServers", {})
|
||||
if not servers:
|
||||
return CheckResult(
|
||||
id="mcp_server_spawn",
|
||||
title="MCP Server Spawn Test",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No MCP servers to test",
|
||||
)
|
||||
|
||||
# Only test first server to avoid long delays
|
||||
test_server = config.name if config.name else list(servers.keys())[0]
|
||||
server_config = servers.get(test_server)
|
||||
|
||||
if not server_config:
|
||||
return CheckResult(
|
||||
id="mcp_server_spawn",
|
||||
title="MCP Server Spawn Test",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.SKIP,
|
||||
message=f"Server '{test_server}' not found",
|
||||
)
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
command = server_config.get("command", "")
|
||||
args = server_config.get("args", [])
|
||||
|
||||
if isinstance(command, str):
|
||||
cmd = command.split() + args
|
||||
else:
|
||||
cmd = [command] + args
|
||||
|
||||
# Try to start the server with a short timeout
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env={**os.environ, **server_config.get("env", {})},
|
||||
)
|
||||
|
||||
# Wait briefly to see if it starts
|
||||
import time
|
||||
time.sleep(1)
|
||||
|
||||
if proc.poll() is None:
|
||||
# Process is still running - success
|
||||
proc.terminate()
|
||||
proc.wait(timeout=2)
|
||||
return CheckResult(
|
||||
id="mcp_server_spawn",
|
||||
title="MCP Server Spawn Test",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Server '{test_server}' started successfully",
|
||||
)
|
||||
else:
|
||||
# Process exited
|
||||
stderr = proc.stderr.read().decode()[:200] if proc.stderr else ""
|
||||
return CheckResult(
|
||||
id="mcp_server_spawn",
|
||||
title="MCP Server Spawn Test",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Server '{test_server}' exited immediately",
|
||||
details=stderr if stderr else "No error output",
|
||||
)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
return CheckResult(
|
||||
id="mcp_server_spawn",
|
||||
title="MCP Server Spawn Test",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Command not found for '{test_server}'",
|
||||
details=str(e),
|
||||
remediation="Install the required MCP server package",
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="mcp_server_spawn",
|
||||
title="MCP Server Spawn Test",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Could not test server '{test_server}'",
|
||||
details=str(e)[:200],
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="mcp_praisonai_integration",
|
||||
title="PraisonAI MCP Integration",
|
||||
description="Check PraisonAI MCP integration",
|
||||
category=CheckCategory.MCP,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_mcp_praisonai_integration(config: DoctorConfig) -> CheckResult:
|
||||
"""Check PraisonAI MCP integration."""
|
||||
try:
|
||||
from praisonaiagents.mcp import MCP
|
||||
return CheckResult(
|
||||
id="mcp_praisonai_integration",
|
||||
title="PraisonAI MCP Integration",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.PASS,
|
||||
message="PraisonAI MCP integration available",
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="mcp_praisonai_integration",
|
||||
title="PraisonAI MCP Integration",
|
||||
category=CheckCategory.MCP,
|
||||
status=CheckStatus.SKIP,
|
||||
message="MCP integration not available",
|
||||
details=str(e),
|
||||
remediation="Install with: pip install praisonaiagents[mcp]",
|
||||
)
|
||||
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
Memory checks for the Doctor CLI module.
|
||||
|
||||
Validates memory/session storage and integrity.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
def _find_memory_dirs() -> list:
|
||||
"""Find memory storage directories."""
|
||||
locations = [
|
||||
Path.cwd() / ".praison" / "memory",
|
||||
Path.cwd() / ".praison" / "sessions",
|
||||
Path.home() / ".praison" / "memory",
|
||||
Path.home() / ".praison" / "sessions",
|
||||
Path.home() / ".config" / "praison" / "memory",
|
||||
]
|
||||
|
||||
found = []
|
||||
for loc in locations:
|
||||
if loc.exists() and loc.is_dir():
|
||||
found.append(str(loc))
|
||||
|
||||
return found
|
||||
|
||||
|
||||
@register_check(
|
||||
id="memory_dirs",
|
||||
title="Memory Directories",
|
||||
description="Check memory storage directories",
|
||||
category=CheckCategory.MEMORY,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_memory_dirs(config: DoctorConfig) -> CheckResult:
|
||||
"""Check memory storage directories."""
|
||||
dirs = _find_memory_dirs()
|
||||
|
||||
if dirs:
|
||||
return CheckResult(
|
||||
id="memory_dirs",
|
||||
title="Memory Directories",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Found {len(dirs)} memory directory(ies)",
|
||||
details=", ".join(dirs),
|
||||
metadata={"directories": dirs},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="memory_dirs",
|
||||
title="Memory Directories",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No memory directories found (will be created on first use)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="memory_files",
|
||||
title="Memory Files",
|
||||
description="Check memory file integrity",
|
||||
category=CheckCategory.MEMORY,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
dependencies=["memory_dirs"],
|
||||
)
|
||||
def check_memory_files(config: DoctorConfig) -> CheckResult:
|
||||
"""Check memory file integrity."""
|
||||
dirs = _find_memory_dirs()
|
||||
|
||||
if not dirs:
|
||||
return CheckResult(
|
||||
id="memory_files",
|
||||
title="Memory Files",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No memory directories to check",
|
||||
)
|
||||
|
||||
valid_files = 0
|
||||
corrupt_files = []
|
||||
total_size = 0
|
||||
|
||||
for dir_path in dirs:
|
||||
memory_dir = Path(dir_path)
|
||||
for file_path in memory_dir.rglob("*.json"):
|
||||
try:
|
||||
import json
|
||||
with open(file_path) as f:
|
||||
json.load(f)
|
||||
valid_files += 1
|
||||
total_size += file_path.stat().st_size
|
||||
except json.JSONDecodeError:
|
||||
corrupt_files.append(str(file_path))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if corrupt_files:
|
||||
return CheckResult(
|
||||
id="memory_files",
|
||||
title="Memory Files",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"{len(corrupt_files)} corrupt file(s) found",
|
||||
details="; ".join(corrupt_files[:3]) + ("..." if len(corrupt_files) > 3 else ""),
|
||||
remediation="Remove or repair corrupt memory files",
|
||||
metadata={"valid": valid_files, "corrupt": corrupt_files},
|
||||
)
|
||||
elif valid_files > 0:
|
||||
size_kb = total_size / 1024
|
||||
return CheckResult(
|
||||
id="memory_files",
|
||||
title="Memory Files",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"{valid_files} valid memory file(s) ({size_kb:.1f} KB)",
|
||||
metadata={"valid_count": valid_files, "total_size_bytes": total_size},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="memory_files",
|
||||
title="Memory Files",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No memory files found",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="memory_sessions",
|
||||
title="Session Storage",
|
||||
description="Check session storage",
|
||||
category=CheckCategory.MEMORY,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_memory_sessions(config: DoctorConfig) -> CheckResult:
|
||||
"""Check session storage."""
|
||||
session_dirs = [
|
||||
Path.cwd() / ".praison" / "sessions",
|
||||
Path.home() / ".praison" / "sessions",
|
||||
]
|
||||
|
||||
sessions = []
|
||||
for dir_path in session_dirs:
|
||||
if dir_path.exists():
|
||||
for item in dir_path.iterdir():
|
||||
if item.is_dir() or item.suffix == ".json":
|
||||
sessions.append(item.name)
|
||||
|
||||
if sessions:
|
||||
return CheckResult(
|
||||
id="memory_sessions",
|
||||
title="Session Storage",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Found {len(sessions)} saved session(s)",
|
||||
metadata={"sessions": sessions[:10]},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="memory_sessions",
|
||||
title="Session Storage",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No saved sessions found",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="memory_chromadb",
|
||||
title="ChromaDB Memory",
|
||||
description="Check ChromaDB vector memory",
|
||||
category=CheckCategory.MEMORY,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_memory_chromadb(config: DoctorConfig) -> CheckResult:
|
||||
"""Check ChromaDB vector memory."""
|
||||
chroma_dirs = [
|
||||
Path.cwd() / ".praison" / "chroma",
|
||||
Path.cwd() / "chroma_db",
|
||||
Path.home() / ".praison" / "chroma",
|
||||
]
|
||||
|
||||
found = None
|
||||
for dir_path in chroma_dirs:
|
||||
if dir_path.exists():
|
||||
found = dir_path
|
||||
break
|
||||
|
||||
if found:
|
||||
try:
|
||||
import chromadb
|
||||
# Try to open the database
|
||||
client = chromadb.PersistentClient(path=str(found))
|
||||
collections = client.list_collections()
|
||||
return CheckResult(
|
||||
id="memory_chromadb",
|
||||
title="ChromaDB Memory",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"ChromaDB found with {len(collections)} collection(s)",
|
||||
metadata={"path": str(found), "collections": len(collections)},
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="memory_chromadb",
|
||||
title="ChromaDB Memory",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.WARN,
|
||||
message="ChromaDB directory found but chromadb not installed",
|
||||
remediation="Install with: pip install chromadb",
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="memory_chromadb",
|
||||
title="ChromaDB Memory",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"ChromaDB found but cannot open: {type(e).__name__}",
|
||||
details=str(e)[:200],
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="memory_chromadb",
|
||||
title="ChromaDB Memory",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No ChromaDB storage found (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="memory_praisonai_module",
|
||||
title="PraisonAI Memory Module",
|
||||
description="Check PraisonAI memory module",
|
||||
category=CheckCategory.MEMORY,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_memory_praisonai_module(config: DoctorConfig) -> CheckResult:
|
||||
"""Check PraisonAI memory module."""
|
||||
try:
|
||||
from praisonaiagents.memory import FileMemory, Memory
|
||||
return CheckResult(
|
||||
id="memory_praisonai_module",
|
||||
title="PraisonAI Memory Module",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.PASS,
|
||||
message="Memory modules available (FileMemory, Memory)",
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="memory_praisonai_module",
|
||||
title="PraisonAI Memory Module",
|
||||
category=CheckCategory.MEMORY,
|
||||
status=CheckStatus.WARN,
|
||||
message="Memory module import failed",
|
||||
details=str(e),
|
||||
)
|
||||
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
Network checks for the Doctor CLI module.
|
||||
|
||||
Validates network connectivity and configuration.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
@register_check(
|
||||
id="network_dns",
|
||||
title="DNS Resolution",
|
||||
description="Check DNS resolution",
|
||||
category=CheckCategory.NETWORK,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_network_dns(config: DoctorConfig) -> CheckResult:
|
||||
"""Check DNS resolution."""
|
||||
test_hosts = [
|
||||
"api.openai.com",
|
||||
"api.anthropic.com",
|
||||
"google.com",
|
||||
]
|
||||
|
||||
resolved = []
|
||||
failed = []
|
||||
|
||||
for host in test_hosts:
|
||||
try:
|
||||
socket.gethostbyname(host)
|
||||
resolved.append(host)
|
||||
except socket.gaierror:
|
||||
failed.append(host)
|
||||
|
||||
if failed:
|
||||
return CheckResult(
|
||||
id="network_dns",
|
||||
title="DNS Resolution",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"DNS resolution failed for {len(failed)} host(s)",
|
||||
details=f"Failed: {', '.join(failed)}",
|
||||
remediation="Check DNS configuration and network connectivity",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="network_dns",
|
||||
title="DNS Resolution",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"DNS resolution working ({len(resolved)} hosts tested)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="network_https",
|
||||
title="HTTPS Connectivity",
|
||||
description="Check HTTPS connectivity to key endpoints",
|
||||
category=CheckCategory.NETWORK,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_network_https(config: DoctorConfig) -> CheckResult:
|
||||
"""Check HTTPS connectivity to key endpoints."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
endpoints = [
|
||||
("https://api.openai.com/v1/models", "OpenAI"),
|
||||
("https://api.anthropic.com/v1/messages", "Anthropic"),
|
||||
]
|
||||
|
||||
reachable = []
|
||||
unreachable = []
|
||||
|
||||
for url, name in endpoints:
|
||||
try:
|
||||
req = urllib.request.Request(url, method="HEAD")
|
||||
req.add_header("User-Agent", "PraisonAI-Doctor/1.0")
|
||||
urllib.request.urlopen(req, timeout=5)
|
||||
reachable.append(name)
|
||||
except urllib.error.HTTPError as e:
|
||||
# HTTP errors (401, 403, etc.) mean we reached the server
|
||||
if e.code in (401, 403, 404, 405):
|
||||
reachable.append(name)
|
||||
else:
|
||||
unreachable.append(f"{name} ({e.code})")
|
||||
except Exception as e:
|
||||
unreachable.append(f"{name} ({type(e).__name__})")
|
||||
|
||||
if unreachable:
|
||||
return CheckResult(
|
||||
id="network_https",
|
||||
title="HTTPS Connectivity",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Cannot reach {len(unreachable)} endpoint(s)",
|
||||
details=f"Unreachable: {', '.join(unreachable)}",
|
||||
remediation="Check network connectivity and firewall settings",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="network_https",
|
||||
title="HTTPS Connectivity",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"HTTPS connectivity verified ({len(reachable)} endpoints)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="network_proxy",
|
||||
title="Proxy Configuration",
|
||||
description="Check proxy environment variables",
|
||||
category=CheckCategory.NETWORK,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_network_proxy(config: DoctorConfig) -> CheckResult:
|
||||
"""Check proxy environment variables."""
|
||||
proxy_vars = {
|
||||
"HTTP_PROXY": os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy"),
|
||||
"HTTPS_PROXY": os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy"),
|
||||
"NO_PROXY": os.environ.get("NO_PROXY") or os.environ.get("no_proxy"),
|
||||
}
|
||||
|
||||
configured = {k: v for k, v in proxy_vars.items() if v}
|
||||
|
||||
if configured:
|
||||
# Mask proxy URLs that might contain credentials
|
||||
masked = {}
|
||||
for k, v in configured.items():
|
||||
if "@" in v:
|
||||
# Contains credentials, mask them
|
||||
parts = v.split("@")
|
||||
masked[k] = f"***@{parts[-1]}"
|
||||
else:
|
||||
masked[k] = v
|
||||
|
||||
return CheckResult(
|
||||
id="network_proxy",
|
||||
title="Proxy Configuration",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Proxy configured: {', '.join(configured.keys())}",
|
||||
details="; ".join(f"{k}={v}" for k, v in masked.items()),
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="network_proxy",
|
||||
title="Proxy Configuration",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No proxy configured (direct connection)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="network_ssl",
|
||||
title="SSL Configuration",
|
||||
description="Check SSL/TLS configuration",
|
||||
category=CheckCategory.NETWORK,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_network_ssl(config: DoctorConfig) -> CheckResult:
|
||||
"""Check SSL/TLS configuration."""
|
||||
ssl_vars = {
|
||||
"SSL_CERT_FILE": os.environ.get("SSL_CERT_FILE"),
|
||||
"SSL_CERT_DIR": os.environ.get("SSL_CERT_DIR"),
|
||||
"REQUESTS_CA_BUNDLE": os.environ.get("REQUESTS_CA_BUNDLE"),
|
||||
"CURL_CA_BUNDLE": os.environ.get("CURL_CA_BUNDLE"),
|
||||
}
|
||||
|
||||
configured = {k: v for k, v in ssl_vars.items() if v}
|
||||
|
||||
# Check for SSL verification disable (security concern)
|
||||
ssl_verify = os.environ.get("PYTHONHTTPSVERIFY", "1")
|
||||
requests_verify = os.environ.get("REQUESTS_SSL_VERIFY", "1")
|
||||
|
||||
warnings = []
|
||||
if ssl_verify == "0":
|
||||
warnings.append("PYTHONHTTPSVERIFY=0 (SSL verification disabled)")
|
||||
if requests_verify.lower() in ("0", "false"):
|
||||
warnings.append("REQUESTS_SSL_VERIFY disabled")
|
||||
|
||||
if warnings:
|
||||
return CheckResult(
|
||||
id="network_ssl",
|
||||
title="SSL Configuration",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.WARN,
|
||||
message="SSL verification disabled",
|
||||
details="; ".join(warnings),
|
||||
remediation="Enable SSL verification for security",
|
||||
)
|
||||
elif configured:
|
||||
return CheckResult(
|
||||
id="network_ssl",
|
||||
title="SSL Configuration",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Custom SSL config: {', '.join(configured.keys())}",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="network_ssl",
|
||||
title="SSL Configuration",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.PASS,
|
||||
message="Using default SSL configuration",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="network_openai_base_url",
|
||||
title="OpenAI Base URL",
|
||||
description="Check OpenAI base URL configuration",
|
||||
category=CheckCategory.NETWORK,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_network_openai_base_url(config: DoctorConfig) -> CheckResult:
|
||||
"""Check OpenAI base URL configuration."""
|
||||
base_url = os.environ.get("OPENAI_BASE_URL") or os.environ.get("OPENAI_API_BASE")
|
||||
|
||||
if base_url:
|
||||
return CheckResult(
|
||||
id="network_openai_base_url",
|
||||
title="OpenAI Base URL",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Custom OpenAI base URL: {base_url}",
|
||||
details="Using custom endpoint (e.g., Azure, local proxy)",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="network_openai_base_url",
|
||||
title="OpenAI Base URL",
|
||||
category=CheckCategory.NETWORK,
|
||||
status=CheckStatus.PASS,
|
||||
message="Using default OpenAI endpoint",
|
||||
)
|
||||
@@ -0,0 +1,328 @@
|
||||
"""
|
||||
Observability checks for the Doctor CLI module.
|
||||
|
||||
Validates observability provider configurations.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
# Observability providers and their required env vars
|
||||
OBS_PROVIDERS = {
|
||||
"langfuse": {
|
||||
"env_vars": ["LANGFUSE_PUBLIC_KEY", "LANGFUSE_SECRET_KEY"],
|
||||
"optional_vars": ["LANGFUSE_HOST"],
|
||||
"package": "langfuse",
|
||||
},
|
||||
"langsmith": {
|
||||
"env_vars": ["LANGSMITH_API_KEY"],
|
||||
"optional_vars": ["LANGSMITH_PROJECT"],
|
||||
"package": "langsmith",
|
||||
},
|
||||
"agentops": {
|
||||
"env_vars": ["AGENTOPS_API_KEY"],
|
||||
"optional_vars": [],
|
||||
"package": "agentops",
|
||||
},
|
||||
"arize": {
|
||||
"env_vars": ["ARIZE_API_KEY"],
|
||||
"optional_vars": ["ARIZE_SPACE_KEY"],
|
||||
"package": "arize",
|
||||
},
|
||||
"datadog": {
|
||||
"env_vars": ["DATADOG_API_KEY"],
|
||||
"optional_vars": ["DD_SITE"],
|
||||
"package": "ddtrace",
|
||||
},
|
||||
"mlflow": {
|
||||
"env_vars": [],
|
||||
"optional_vars": ["MLFLOW_TRACKING_URI"],
|
||||
"package": "mlflow",
|
||||
},
|
||||
"wandb": {
|
||||
"env_vars": ["WANDB_API_KEY"],
|
||||
"optional_vars": ["WANDB_PROJECT"],
|
||||
"package": "wandb",
|
||||
},
|
||||
"braintrust": {
|
||||
"env_vars": ["BRAINTRUST_API_KEY"],
|
||||
"optional_vars": [],
|
||||
"package": "braintrust",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@register_check(
|
||||
id="obs_config",
|
||||
title="Observability Configuration",
|
||||
description="Check observability provider configuration",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_obs_config(config: DoctorConfig) -> CheckResult:
|
||||
"""Check observability provider configuration."""
|
||||
configured = []
|
||||
|
||||
for provider, info in OBS_PROVIDERS.items():
|
||||
env_vars = info["env_vars"]
|
||||
if env_vars:
|
||||
if all(os.environ.get(var) for var in env_vars):
|
||||
configured.append(provider)
|
||||
else:
|
||||
# Check optional vars for providers without required vars
|
||||
if any(os.environ.get(var) for var in info.get("optional_vars", [])):
|
||||
configured.append(provider)
|
||||
|
||||
if configured:
|
||||
return CheckResult(
|
||||
id="obs_config",
|
||||
title="Observability Configuration",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Configured provider(s): {', '.join(configured)}",
|
||||
metadata={"providers": configured},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="obs_config",
|
||||
title="Observability Configuration",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No observability providers configured (optional)",
|
||||
details="Set provider API keys to enable observability",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="obs_langfuse",
|
||||
title="Langfuse",
|
||||
description="Check Langfuse observability provider",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_obs_langfuse(config: DoctorConfig) -> CheckResult:
|
||||
"""Check Langfuse observability provider."""
|
||||
public_key = os.environ.get("LANGFUSE_PUBLIC_KEY")
|
||||
secret_key = os.environ.get("LANGFUSE_SECRET_KEY")
|
||||
|
||||
if public_key and secret_key:
|
||||
try:
|
||||
import langfuse
|
||||
version = getattr(langfuse, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="obs_langfuse",
|
||||
title="Langfuse",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Langfuse configured (SDK {version})",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="obs_langfuse",
|
||||
title="Langfuse",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.WARN,
|
||||
message="Langfuse keys set but SDK not installed",
|
||||
remediation="Install with: pip install langfuse",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="obs_langfuse",
|
||||
title="Langfuse",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Langfuse not configured (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="obs_langsmith",
|
||||
title="LangSmith",
|
||||
description="Check LangSmith observability provider",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_obs_langsmith(config: DoctorConfig) -> CheckResult:
|
||||
"""Check LangSmith observability provider."""
|
||||
api_key = os.environ.get("LANGSMITH_API_KEY")
|
||||
|
||||
if api_key:
|
||||
try:
|
||||
import langsmith
|
||||
version = getattr(langsmith, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="obs_langsmith",
|
||||
title="LangSmith",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"LangSmith configured (SDK {version})",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="obs_langsmith",
|
||||
title="LangSmith",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.WARN,
|
||||
message="LangSmith key set but SDK not installed",
|
||||
remediation="Install with: pip install langsmith",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="obs_langsmith",
|
||||
title="LangSmith",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="LangSmith not configured (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="obs_agentops",
|
||||
title="AgentOps",
|
||||
description="Check AgentOps observability provider",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_obs_agentops(config: DoctorConfig) -> CheckResult:
|
||||
"""Check AgentOps observability provider."""
|
||||
api_key = os.environ.get("AGENTOPS_API_KEY")
|
||||
|
||||
if api_key:
|
||||
try:
|
||||
import agentops
|
||||
version = getattr(agentops, "__version__", "unknown")
|
||||
return CheckResult(
|
||||
id="obs_agentops",
|
||||
title="AgentOps",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"AgentOps configured (SDK {version})",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="obs_agentops",
|
||||
title="AgentOps",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.WARN,
|
||||
message="AgentOps key set but SDK not installed",
|
||||
remediation="Install with: pip install agentops",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="obs_agentops",
|
||||
title="AgentOps",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="AgentOps not configured (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="obs_praisonai_telemetry",
|
||||
title="PraisonAI Telemetry",
|
||||
description="Check PraisonAI built-in telemetry",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_obs_praisonai_telemetry(config: DoctorConfig) -> CheckResult:
|
||||
"""Check PraisonAI built-in telemetry."""
|
||||
telemetry_enabled = os.environ.get("PRAISONAI_TELEMETRY_ENABLED", "").lower() in ("true", "1", "yes")
|
||||
|
||||
try:
|
||||
from praisonaiagents.telemetry import get_telemetry
|
||||
|
||||
if telemetry_enabled:
|
||||
return CheckResult(
|
||||
id="obs_praisonai_telemetry",
|
||||
title="PraisonAI Telemetry",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.PASS,
|
||||
message="PraisonAI telemetry enabled",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="obs_praisonai_telemetry",
|
||||
title="PraisonAI Telemetry",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="PraisonAI telemetry available but not enabled",
|
||||
details="Set PRAISONAI_TELEMETRY_ENABLED=true to enable",
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="obs_praisonai_telemetry",
|
||||
title="PraisonAI Telemetry",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="PraisonAI telemetry module not available",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="obs_connectivity",
|
||||
title="Observability Connectivity",
|
||||
description="Test connectivity to observability providers",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
severity=CheckSeverity.LOW,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_obs_connectivity(config: DoctorConfig) -> CheckResult:
|
||||
"""Test connectivity to observability providers (deep mode)."""
|
||||
# Only test if a specific provider is requested or if we have configured providers
|
||||
provider = config.provider
|
||||
|
||||
if provider == "langfuse":
|
||||
public_key = os.environ.get("LANGFUSE_PUBLIC_KEY")
|
||||
secret_key = os.environ.get("LANGFUSE_SECRET_KEY")
|
||||
|
||||
if not (public_key and secret_key):
|
||||
return CheckResult(
|
||||
id="obs_connectivity",
|
||||
title="Observability Connectivity",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Langfuse not configured",
|
||||
)
|
||||
|
||||
try:
|
||||
from langfuse import Langfuse
|
||||
client = Langfuse(
|
||||
public_key=public_key,
|
||||
secret_key=secret_key,
|
||||
)
|
||||
# Try to authenticate
|
||||
client.auth_check()
|
||||
return CheckResult(
|
||||
id="obs_connectivity",
|
||||
title="Observability Connectivity",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.PASS,
|
||||
message="Langfuse connectivity verified",
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="obs_connectivity",
|
||||
title="Observability Connectivity",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Langfuse connection failed: {type(e).__name__}",
|
||||
details=str(e)[:200],
|
||||
)
|
||||
|
||||
# Default: skip if no specific provider requested
|
||||
return CheckResult(
|
||||
id="obs_connectivity",
|
||||
title="Observability Connectivity",
|
||||
category=CheckCategory.OBSERVABILITY,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Use --provider to test specific provider connectivity",
|
||||
)
|
||||
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
Performance checks for the Doctor CLI module.
|
||||
|
||||
Measures import times and identifies slow imports.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
def _measure_import_time(module_name: str) -> tuple:
|
||||
"""Measure import time for a module."""
|
||||
# Remove from cache if present
|
||||
modules_before = set(sys.modules.keys())
|
||||
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
__import__(module_name)
|
||||
elapsed = (time.perf_counter() - start) * 1000 # ms
|
||||
return True, elapsed, None
|
||||
except ImportError as e:
|
||||
elapsed = (time.perf_counter() - start) * 1000
|
||||
return False, elapsed, str(e)
|
||||
finally:
|
||||
# Clean up imported modules (optional, for accurate re-measurement)
|
||||
pass
|
||||
|
||||
|
||||
@register_check(
|
||||
id="performance_praisonai_import",
|
||||
title="PraisonAI Import Time",
|
||||
description="Measure praisonai package import time",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_performance_praisonai_import(config: DoctorConfig) -> CheckResult:
|
||||
"""Measure praisonai package import time."""
|
||||
budget_ms = config.budget_ms or 2000 # 2 second default budget
|
||||
|
||||
# praisonai is already imported, measure a fresh import of a submodule
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
# Import version module (lightweight)
|
||||
from praisonai import version
|
||||
elapsed = (time.perf_counter() - start) * 1000
|
||||
|
||||
if elapsed > budget_ms:
|
||||
return CheckResult(
|
||||
id="performance_praisonai_import",
|
||||
title="PraisonAI Import Time",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Import time {elapsed:.0f}ms exceeds budget {budget_ms}ms",
|
||||
remediation="Check for slow dependencies or disk I/O issues",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="performance_praisonai_import",
|
||||
title="PraisonAI Import Time",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Import time: {elapsed:.0f}ms (budget: {budget_ms}ms)",
|
||||
metadata={"import_time_ms": elapsed, "budget_ms": budget_ms},
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="performance_praisonai_import",
|
||||
title="PraisonAI Import Time",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Import failed: {e}",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="performance_praisonaiagents_import",
|
||||
title="PraisonAI Agents Import Time",
|
||||
description="Measure praisonaiagents package import time",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_performance_praisonaiagents_import(config: DoctorConfig) -> CheckResult:
|
||||
"""Measure praisonaiagents package import time."""
|
||||
budget_ms = config.budget_ms or 3000 # 3 second default budget
|
||||
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
import praisonaiagents
|
||||
elapsed = (time.perf_counter() - start) * 1000
|
||||
|
||||
if elapsed > budget_ms:
|
||||
return CheckResult(
|
||||
id="performance_praisonaiagents_import",
|
||||
title="PraisonAI Agents Import Time",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Import time {elapsed:.0f}ms exceeds budget {budget_ms}ms",
|
||||
remediation="Check for slow dependencies",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="performance_praisonaiagents_import",
|
||||
title="PraisonAI Agents Import Time",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Import time: {elapsed:.0f}ms (budget: {budget_ms}ms)",
|
||||
metadata={"import_time_ms": elapsed, "budget_ms": budget_ms},
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="performance_praisonaiagents_import",
|
||||
title="PraisonAI Agents Import Time",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Import failed: {e}",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="performance_slow_imports",
|
||||
title="Slow Import Detection",
|
||||
description="Detect slow-loading optional dependencies",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
severity=CheckSeverity.INFO,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_performance_slow_imports(config: DoctorConfig) -> CheckResult:
|
||||
"""Detect slow-loading optional dependencies."""
|
||||
modules_to_check = [
|
||||
"litellm",
|
||||
"chromadb",
|
||||
"langchain",
|
||||
"torch",
|
||||
"transformers",
|
||||
"pandas",
|
||||
"numpy",
|
||||
]
|
||||
|
||||
top_n = config.top_n or 5
|
||||
budget_ms = config.budget_ms or 1000
|
||||
|
||||
import_times = []
|
||||
|
||||
for module in modules_to_check:
|
||||
if module in sys.modules:
|
||||
# Already imported, skip
|
||||
continue
|
||||
|
||||
success, elapsed, error = _measure_import_time(module)
|
||||
if success:
|
||||
import_times.append((module, elapsed))
|
||||
|
||||
# Sort by time, descending
|
||||
import_times.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
slow_imports = [(m, t) for m, t in import_times if t > budget_ms]
|
||||
top_imports = import_times[:top_n]
|
||||
|
||||
if slow_imports:
|
||||
slow_str = ", ".join(f"{m}({t:.0f}ms)" for m, t in slow_imports[:3])
|
||||
return CheckResult(
|
||||
id="performance_slow_imports",
|
||||
title="Slow Import Detection",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"{len(slow_imports)} slow import(s) detected",
|
||||
details=f"Slow: {slow_str}",
|
||||
metadata={"slow_imports": slow_imports, "top_imports": top_imports},
|
||||
)
|
||||
elif top_imports:
|
||||
top_str = ", ".join(f"{m}({t:.0f}ms)" for m, t in top_imports[:3])
|
||||
return CheckResult(
|
||||
id="performance_slow_imports",
|
||||
title="Slow Import Detection",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"No slow imports (top: {top_str})",
|
||||
metadata={"top_imports": top_imports},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="performance_slow_imports",
|
||||
title="Slow Import Detection",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No optional modules to measure",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="performance_loaded_modules",
|
||||
title="Loaded Modules Count",
|
||||
description="Count currently loaded Python modules",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_performance_loaded_modules(config: DoctorConfig) -> CheckResult:
|
||||
"""Count currently loaded Python modules."""
|
||||
module_count = len(sys.modules)
|
||||
|
||||
# Categorize modules
|
||||
stdlib_count = 0
|
||||
third_party_count = 0
|
||||
praison_count = 0
|
||||
|
||||
for name in sys.modules:
|
||||
if name.startswith("praisonai"):
|
||||
praison_count += 1
|
||||
elif "site-packages" in str(getattr(sys.modules[name], "__file__", "")):
|
||||
third_party_count += 1
|
||||
else:
|
||||
stdlib_count += 1
|
||||
|
||||
return CheckResult(
|
||||
id="performance_loaded_modules",
|
||||
title="Loaded Modules Count",
|
||||
category=CheckCategory.PERFORMANCE,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"{module_count} modules loaded (praison: {praison_count})",
|
||||
metadata={
|
||||
"total": module_count,
|
||||
"praison": praison_count,
|
||||
"third_party": third_party_count,
|
||||
"stdlib": stdlib_count,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Permissions checks for the Doctor CLI module.
|
||||
|
||||
Validates filesystem permissions for PraisonAI directories.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
def _check_dir_writable(path: Path) -> tuple:
|
||||
"""Check if a directory is writable."""
|
||||
try:
|
||||
if not path.exists():
|
||||
return False, "does not exist"
|
||||
|
||||
if not path.is_dir():
|
||||
return False, "not a directory"
|
||||
|
||||
# Try to create a temp file
|
||||
test_file = path / ".praison_write_test"
|
||||
try:
|
||||
test_file.write_text("test")
|
||||
test_file.unlink()
|
||||
return True, "writable"
|
||||
except PermissionError:
|
||||
return False, "permission denied"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="permissions_home_praison",
|
||||
title="~/.praison Directory",
|
||||
description="Check ~/.praison directory permissions",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
)
|
||||
def check_permissions_home_praison(config: DoctorConfig) -> CheckResult:
|
||||
"""Check ~/.praison directory permissions."""
|
||||
praison_dir = Path.home() / ".praison"
|
||||
|
||||
if not praison_dir.exists():
|
||||
# Try to create it
|
||||
try:
|
||||
praison_dir.mkdir(parents=True, exist_ok=True)
|
||||
return CheckResult(
|
||||
id="permissions_home_praison",
|
||||
title="~/.praison Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.PASS,
|
||||
message="~/.praison created successfully",
|
||||
metadata={"path": str(praison_dir)},
|
||||
)
|
||||
except PermissionError:
|
||||
return CheckResult(
|
||||
id="permissions_home_praison",
|
||||
title="~/.praison Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.FAIL,
|
||||
message="Cannot create ~/.praison directory",
|
||||
remediation="Check home directory permissions",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
writable, reason = _check_dir_writable(praison_dir)
|
||||
|
||||
if writable:
|
||||
return CheckResult(
|
||||
id="permissions_home_praison",
|
||||
title="~/.praison Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"~/.praison is writable",
|
||||
metadata={"path": str(praison_dir)},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="permissions_home_praison",
|
||||
title="~/.praison Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"~/.praison is not writable: {reason}",
|
||||
remediation="Fix permissions: chmod 755 ~/.praison",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="permissions_project_praison",
|
||||
title=".praison Directory (Project)",
|
||||
description="Check project .praison directory permissions",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_permissions_project_praison(config: DoctorConfig) -> CheckResult:
|
||||
"""Check project .praison directory permissions."""
|
||||
praison_dir = Path.cwd() / ".praison"
|
||||
|
||||
if not praison_dir.exists():
|
||||
return CheckResult(
|
||||
id="permissions_project_praison",
|
||||
title=".praison Directory (Project)",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No .praison directory in current project",
|
||||
)
|
||||
|
||||
writable, reason = _check_dir_writable(praison_dir)
|
||||
|
||||
if writable:
|
||||
return CheckResult(
|
||||
id="permissions_project_praison",
|
||||
title=".praison Directory (Project)",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.PASS,
|
||||
message=".praison is writable",
|
||||
metadata={"path": str(praison_dir)},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="permissions_project_praison",
|
||||
title=".praison Directory (Project)",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.WARN,
|
||||
message=f".praison is not writable: {reason}",
|
||||
remediation="Fix permissions or use ~/.praison instead",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="permissions_temp_dir",
|
||||
title="Temp Directory",
|
||||
description="Check temp directory permissions",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
)
|
||||
def check_permissions_temp_dir(config: DoctorConfig) -> CheckResult:
|
||||
"""Check temp directory permissions."""
|
||||
temp_dir = Path(tempfile.gettempdir())
|
||||
|
||||
try:
|
||||
# Try to create a temp file
|
||||
with tempfile.NamedTemporaryFile(delete=True) as f:
|
||||
f.write(b"test")
|
||||
|
||||
return CheckResult(
|
||||
id="permissions_temp_dir",
|
||||
title="Temp Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Temp directory writable: {temp_dir}",
|
||||
metadata={"path": str(temp_dir)},
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="permissions_temp_dir",
|
||||
title="Temp Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Temp directory not writable: {e}",
|
||||
remediation="Check TMPDIR environment variable and permissions",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="permissions_cwd",
|
||||
title="Current Working Directory",
|
||||
description="Check current directory permissions",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_permissions_cwd(config: DoctorConfig) -> CheckResult:
|
||||
"""Check current directory permissions."""
|
||||
cwd = Path.cwd()
|
||||
|
||||
writable, reason = _check_dir_writable(cwd)
|
||||
|
||||
if writable:
|
||||
return CheckResult(
|
||||
id="permissions_cwd",
|
||||
title="Current Working Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Current directory writable: {cwd}",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="permissions_cwd",
|
||||
title="Current Working Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Current directory not writable: {reason}",
|
||||
details="Some features may not work without write access",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="permissions_config_dir",
|
||||
title="Config Directory",
|
||||
description="Check ~/.config/praison directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_permissions_config_dir(config: DoctorConfig) -> CheckResult:
|
||||
"""Check ~/.config/praison directory."""
|
||||
config_dir = Path.home() / ".config" / "praison"
|
||||
|
||||
if not config_dir.exists():
|
||||
# Check if we can create it
|
||||
try:
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
return CheckResult(
|
||||
id="permissions_config_dir",
|
||||
title="Config Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.PASS,
|
||||
message="~/.config/praison created successfully",
|
||||
)
|
||||
except Exception:
|
||||
return CheckResult(
|
||||
id="permissions_config_dir",
|
||||
title="Config Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="~/.config/praison does not exist (will use ~/.praison)",
|
||||
)
|
||||
|
||||
writable, reason = _check_dir_writable(config_dir)
|
||||
|
||||
if writable:
|
||||
return CheckResult(
|
||||
id="permissions_config_dir",
|
||||
title="Config Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.PASS,
|
||||
message="~/.config/praison is writable",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="permissions_config_dir",
|
||||
title="Config Directory",
|
||||
category=CheckCategory.PERMISSIONS,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"~/.config/praison not writable: {reason}",
|
||||
)
|
||||
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
Selftest checks for the Doctor CLI module.
|
||||
|
||||
Performs minimal agent dry-run to validate the system.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
@register_check(
|
||||
id="selftest_agent_import",
|
||||
title="Agent Import",
|
||||
description="Check Agent class can be imported",
|
||||
category=CheckCategory.SELFTEST,
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
def check_selftest_agent_import(config: DoctorConfig) -> CheckResult:
|
||||
"""Check Agent class can be imported."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
return CheckResult(
|
||||
id="selftest_agent_import",
|
||||
title="Agent Import",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.PASS,
|
||||
message="Agent class imported successfully",
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="selftest_agent_import",
|
||||
title="Agent Import",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Cannot import Agent: {e}",
|
||||
remediation="Ensure praisonaiagents is properly installed",
|
||||
severity=CheckSeverity.CRITICAL,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="selftest_agent_create",
|
||||
title="Agent Creation",
|
||||
description="Check Agent can be instantiated",
|
||||
category=CheckCategory.SELFTEST,
|
||||
severity=CheckSeverity.HIGH,
|
||||
dependencies=["selftest_agent_import"],
|
||||
)
|
||||
def check_selftest_agent_create(config: DoctorConfig) -> CheckResult:
|
||||
"""Check Agent can be instantiated."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Create a minimal agent without making API calls
|
||||
agent = Agent(
|
||||
name="DoctorTestAgent",
|
||||
instructions="You are a test agent.",
|
||||
llm="gpt-4o-mini", # Default model
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
id="selftest_agent_create",
|
||||
title="Agent Creation",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.PASS,
|
||||
message="Agent instantiated successfully",
|
||||
metadata={"agent_name": agent.name},
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="selftest_agent_create",
|
||||
title="Agent Creation",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Agent creation failed: {type(e).__name__}",
|
||||
details=str(e)[:200],
|
||||
remediation="Check configuration and dependencies",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="selftest_llm_config",
|
||||
title="LLM Configuration",
|
||||
description="Check LLM configuration is valid",
|
||||
category=CheckCategory.SELFTEST,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
)
|
||||
def check_selftest_llm_config(config: DoctorConfig) -> CheckResult:
|
||||
"""Check LLM configuration is valid."""
|
||||
# Check for API key
|
||||
openai_key = os.environ.get("OPENAI_API_KEY")
|
||||
anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||
google_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
||||
ollama_host = os.environ.get("OLLAMA_HOST")
|
||||
|
||||
providers = []
|
||||
if openai_key:
|
||||
providers.append("OpenAI")
|
||||
if anthropic_key:
|
||||
providers.append("Anthropic")
|
||||
if google_key:
|
||||
providers.append("Google")
|
||||
if ollama_host:
|
||||
providers.append("Ollama")
|
||||
|
||||
if providers:
|
||||
return CheckResult(
|
||||
id="selftest_llm_config",
|
||||
title="LLM Configuration",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"LLM provider(s) configured: {', '.join(providers)}",
|
||||
metadata={"providers": providers},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="selftest_llm_config",
|
||||
title="LLM Configuration",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.FAIL,
|
||||
message="No LLM provider configured",
|
||||
remediation="Set OPENAI_API_KEY or another provider's API key",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="selftest_mock_chat",
|
||||
title="Mock Chat Test",
|
||||
description="Test agent chat with mock response",
|
||||
category=CheckCategory.SELFTEST,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_selftest_mock_chat(config: DoctorConfig) -> CheckResult:
|
||||
"""Test agent chat with mock response (no API call)."""
|
||||
if not config.mock and not config.live:
|
||||
# Default to mock in deep mode
|
||||
config.mock = True
|
||||
|
||||
if config.live:
|
||||
return CheckResult(
|
||||
id="selftest_mock_chat",
|
||||
title="Mock Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Skipped (--live mode uses live API)",
|
||||
)
|
||||
|
||||
try:
|
||||
# Test basic agent functionality without making API calls
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="MockTestAgent",
|
||||
instructions="You are a test agent. Always respond with 'OK'.",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# Verify agent is properly configured
|
||||
if hasattr(agent, 'name') and hasattr(agent, 'instructions'):
|
||||
return CheckResult(
|
||||
id="selftest_mock_chat",
|
||||
title="Mock Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.PASS,
|
||||
message="Agent mock test passed",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="selftest_mock_chat",
|
||||
title="Mock Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.FAIL,
|
||||
message="Agent missing expected attributes",
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="selftest_mock_chat",
|
||||
title="Mock Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Mock test failed: {type(e).__name__}",
|
||||
details=str(e)[:200],
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="selftest_live_chat",
|
||||
title="Live Chat Test",
|
||||
description="Test agent chat with live API call",
|
||||
category=CheckCategory.SELFTEST,
|
||||
severity=CheckSeverity.LOW,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_selftest_live_chat(config: DoctorConfig) -> CheckResult:
|
||||
"""Test agent chat with live API call."""
|
||||
if not config.live:
|
||||
return CheckResult(
|
||||
id="selftest_live_chat",
|
||||
title="Live Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Skipped (use --live to enable)",
|
||||
)
|
||||
|
||||
# Check for API key first
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
return CheckResult(
|
||||
id="selftest_live_chat",
|
||||
title="Live Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Skipped (OPENAI_API_KEY not set)",
|
||||
)
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
model = config.model or "gpt-4o-mini"
|
||||
|
||||
agent = Agent(
|
||||
name="LiveTestAgent",
|
||||
instructions="You are a test agent. Respond with exactly: 'Doctor test OK'",
|
||||
llm=model,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# Make a simple API call
|
||||
response = agent.chat("Say hello")
|
||||
|
||||
if response:
|
||||
return CheckResult(
|
||||
id="selftest_live_chat",
|
||||
title="Live Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Live API call successful (model: {model})",
|
||||
metadata={"model": model, "response_length": len(str(response))},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="selftest_live_chat",
|
||||
title="Live Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.FAIL,
|
||||
message="Live API call returned empty response",
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="selftest_live_chat",
|
||||
title="Live Chat Test",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Live API call failed: {type(e).__name__}",
|
||||
details=str(e)[:200],
|
||||
remediation="Check API key and network connectivity",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="selftest_tools_wiring",
|
||||
title="Tools Wiring",
|
||||
description="Check tools can be wired to agent",
|
||||
category=CheckCategory.SELFTEST,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_selftest_tools_wiring(config: DoctorConfig) -> CheckResult:
|
||||
"""Check tools can be wired to agent."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Define a simple test tool
|
||||
def test_tool(x: str) -> str:
|
||||
"""A test tool that echoes input."""
|
||||
return f"Echo: {x}"
|
||||
|
||||
agent = Agent(
|
||||
name="ToolTestAgent",
|
||||
instructions="You are a test agent with tools.",
|
||||
tools=[test_tool],
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# Check tools are registered
|
||||
if hasattr(agent, 'tools') or hasattr(agent, '_tools'):
|
||||
return CheckResult(
|
||||
id="selftest_tools_wiring",
|
||||
title="Tools Wiring",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.PASS,
|
||||
message="Tools wired to agent successfully",
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="selftest_tools_wiring",
|
||||
title="Tools Wiring",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.WARN,
|
||||
message="Tools wiring could not be verified",
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
id="selftest_tools_wiring",
|
||||
title="Tools Wiring",
|
||||
category=CheckCategory.SELFTEST,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Tools wiring failed: {type(e).__name__}",
|
||||
details=str(e)[:200],
|
||||
)
|
||||
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Skills checks for the Doctor CLI module.
|
||||
|
||||
Validates Agent Skills configurations and manifests.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
def _find_skills_dirs() -> list:
|
||||
"""Find skills directories."""
|
||||
locations = [
|
||||
Path.cwd() / ".praison" / "skills",
|
||||
Path.cwd() / ".claude" / "skills",
|
||||
Path.home() / ".praison" / "skills",
|
||||
Path.home() / ".config" / "praison" / "skills",
|
||||
]
|
||||
|
||||
found = []
|
||||
for loc in locations:
|
||||
if loc.exists() and loc.is_dir():
|
||||
found.append(str(loc))
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def _validate_skill_dir(skill_path: Path) -> dict:
|
||||
"""Validate a single skill directory."""
|
||||
result = {"valid": False, "errors": [], "warnings": []}
|
||||
|
||||
skill_md = skill_path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
result["errors"].append("Missing SKILL.md")
|
||||
return result
|
||||
|
||||
try:
|
||||
content = skill_md.read_text()
|
||||
|
||||
# Check for YAML frontmatter
|
||||
if not content.startswith("---"):
|
||||
result["errors"].append("SKILL.md missing YAML frontmatter")
|
||||
return result
|
||||
|
||||
# Parse frontmatter
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
result["errors"].append("Invalid YAML frontmatter format")
|
||||
return result
|
||||
|
||||
try:
|
||||
import yaml
|
||||
frontmatter = yaml.safe_load(parts[1])
|
||||
|
||||
# Check required fields
|
||||
if not frontmatter.get("name"):
|
||||
result["errors"].append("Missing 'name' in frontmatter")
|
||||
if not frontmatter.get("description"):
|
||||
result["errors"].append("Missing 'description' in frontmatter")
|
||||
|
||||
# Check name format
|
||||
name = frontmatter.get("name", "")
|
||||
if name and not name.replace("-", "").replace("_", "").isalnum():
|
||||
result["warnings"].append(f"Name '{name}' should be alphanumeric with hyphens")
|
||||
|
||||
if not result["errors"]:
|
||||
result["valid"] = True
|
||||
result["name"] = name
|
||||
result["description"] = frontmatter.get("description", "")[:100]
|
||||
|
||||
except Exception as e:
|
||||
result["errors"].append(f"YAML parse error: {e}")
|
||||
|
||||
except Exception as e:
|
||||
result["errors"].append(f"Cannot read SKILL.md: {e}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@register_check(
|
||||
id="skills_dirs",
|
||||
title="Skills Directories",
|
||||
description="Check for skills directories",
|
||||
category=CheckCategory.SKILLS,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_skills_dirs(config: DoctorConfig) -> CheckResult:
|
||||
"""Check for skills directories."""
|
||||
dirs = _find_skills_dirs()
|
||||
|
||||
if config.path:
|
||||
# Check specific path
|
||||
path = Path(config.path)
|
||||
if path.exists():
|
||||
dirs = [str(path)]
|
||||
else:
|
||||
return CheckResult(
|
||||
id="skills_dirs",
|
||||
title="Skills Directories",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Specified path not found: {config.path}",
|
||||
)
|
||||
|
||||
if dirs:
|
||||
return CheckResult(
|
||||
id="skills_dirs",
|
||||
title="Skills Directories",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Found {len(dirs)} skills directory(ies)",
|
||||
details=", ".join(dirs),
|
||||
metadata={"directories": dirs},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="skills_dirs",
|
||||
title="Skills Directories",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No skills directories found (optional)",
|
||||
details="Create .praison/skills/ to add agent skills",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="skills_valid",
|
||||
title="Skills Validation",
|
||||
description="Validate installed skills",
|
||||
category=CheckCategory.SKILLS,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
dependencies=["skills_dirs"],
|
||||
)
|
||||
def check_skills_valid(config: DoctorConfig) -> CheckResult:
|
||||
"""Validate installed skills."""
|
||||
dirs = _find_skills_dirs()
|
||||
|
||||
if config.path:
|
||||
path = Path(config.path)
|
||||
if path.exists():
|
||||
dirs = [str(path)]
|
||||
|
||||
if not dirs:
|
||||
return CheckResult(
|
||||
id="skills_valid",
|
||||
title="Skills Validation",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No skills to validate",
|
||||
)
|
||||
|
||||
valid_skills = []
|
||||
invalid_skills = []
|
||||
|
||||
for dir_path in dirs:
|
||||
skills_dir = Path(dir_path)
|
||||
for skill_path in skills_dir.iterdir():
|
||||
if skill_path.is_dir() and not skill_path.name.startswith("."):
|
||||
result = _validate_skill_dir(skill_path)
|
||||
if result["valid"]:
|
||||
valid_skills.append(result.get("name", skill_path.name))
|
||||
else:
|
||||
invalid_skills.append(f"{skill_path.name}: {'; '.join(result['errors'])}")
|
||||
|
||||
total = len(valid_skills) + len(invalid_skills)
|
||||
|
||||
if total == 0:
|
||||
return CheckResult(
|
||||
id="skills_valid",
|
||||
title="Skills Validation",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No skills found in directories",
|
||||
)
|
||||
|
||||
if invalid_skills:
|
||||
return CheckResult(
|
||||
id="skills_valid",
|
||||
title="Skills Validation",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.WARN,
|
||||
message=f"{len(valid_skills)}/{total} skills valid",
|
||||
details="; ".join(invalid_skills[:3]) + ("..." if len(invalid_skills) > 3 else ""),
|
||||
metadata={"valid": valid_skills, "invalid": invalid_skills},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="skills_valid",
|
||||
title="Skills Validation",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"All {total} skill(s) valid",
|
||||
metadata={"valid": valid_skills},
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="skills_praisonai_integration",
|
||||
title="PraisonAI Skills Integration",
|
||||
description="Check PraisonAI skills module",
|
||||
category=CheckCategory.SKILLS,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_skills_praisonai_integration(config: DoctorConfig) -> CheckResult:
|
||||
"""Check PraisonAI skills module."""
|
||||
try:
|
||||
from praisonaiagents.skills import SkillManager
|
||||
return CheckResult(
|
||||
id="skills_praisonai_integration",
|
||||
title="PraisonAI Skills Integration",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="PraisonAI skills module available",
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="skills_praisonai_integration",
|
||||
title="PraisonAI Skills Integration",
|
||||
category=CheckCategory.SKILLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Skills module not available",
|
||||
details=str(e),
|
||||
)
|
||||
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
Tools checks for the Doctor CLI module.
|
||||
|
||||
Validates tool availability, dependencies, and API keys.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ..models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
)
|
||||
from ..registry import register_check
|
||||
|
||||
|
||||
# Tool categories and their requirements
|
||||
TOOL_CATEGORIES = {
|
||||
"web_search": {
|
||||
"tools": ["internet_search", "duckduckgo", "tavily_search", "exa_search", "searxng_search"],
|
||||
"description": "Web search tools",
|
||||
},
|
||||
"file_ops": {
|
||||
"tools": ["read_file", "write_file", "list_files", "file_tools"],
|
||||
"description": "File operation tools",
|
||||
},
|
||||
"data": {
|
||||
"tools": ["csv_tools", "json_tools", "excel_tools", "xml_tools", "yaml_tools", "pandas_tools"],
|
||||
"description": "Data processing tools",
|
||||
},
|
||||
"database": {
|
||||
"tools": ["duckdb_tools", "mongodb_tools"],
|
||||
"description": "Database tools",
|
||||
},
|
||||
"code": {
|
||||
"tools": ["python_tools", "shell_tools", "execute_code"],
|
||||
"description": "Code execution tools",
|
||||
},
|
||||
"web_crawl": {
|
||||
"tools": ["crawl4ai", "spider_tools", "newspaper_tools"],
|
||||
"description": "Web crawling tools",
|
||||
},
|
||||
"knowledge": {
|
||||
"tools": ["wiki_search", "arxiv_tools", "wikipedia_tools"],
|
||||
"description": "Knowledge/research tools",
|
||||
},
|
||||
"finance": {
|
||||
"tools": ["yfinance", "get_stock_price", "get_stock_info"],
|
||||
"description": "Financial data tools",
|
||||
},
|
||||
}
|
||||
|
||||
# Tools that require API keys
|
||||
TOOLS_REQUIRING_API_KEYS = {
|
||||
"tavily_search": "TAVILY_API_KEY",
|
||||
"tavily": "TAVILY_API_KEY",
|
||||
"exa_search": "EXA_API_KEY",
|
||||
"exa": "EXA_API_KEY",
|
||||
"ydc_search": "YDC_API_KEY",
|
||||
"ydc": "YDC_API_KEY",
|
||||
}
|
||||
|
||||
# Tools that require optional dependencies
|
||||
TOOLS_REQUIRING_DEPS = {
|
||||
"crawl4ai": "crawl4ai",
|
||||
"tavily_search": "tavily",
|
||||
"tavily": "tavily",
|
||||
"pandas_tools": "pandas",
|
||||
"excel_tools": "openpyxl",
|
||||
"yfinance": "yfinance",
|
||||
"duckdb_tools": "duckdb",
|
||||
"mongodb_tools": "pymongo",
|
||||
"newspaper_tools": "newspaper3k",
|
||||
"arxiv_tools": "arxiv",
|
||||
}
|
||||
|
||||
|
||||
@register_check(
|
||||
id="tools_registry",
|
||||
title="Tools Registry",
|
||||
description="Check tool registry is accessible",
|
||||
category=CheckCategory.TOOLS,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
)
|
||||
def check_tools_registry(config: DoctorConfig) -> CheckResult:
|
||||
"""Check tool registry is accessible."""
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
tool_count = len(TOOL_MAPPINGS)
|
||||
|
||||
return CheckResult(
|
||||
id="tools_registry",
|
||||
title="Tools Registry",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Tool registry accessible with {tool_count} tools",
|
||||
metadata={"tool_count": tool_count},
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="tools_registry",
|
||||
title="Tools Registry",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.FAIL,
|
||||
message="Cannot access tool registry",
|
||||
details=str(e),
|
||||
remediation="Ensure praisonaiagents is properly installed",
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="tools_web_search",
|
||||
title="Web Search Tools",
|
||||
description="Check web search tool availability",
|
||||
category=CheckCategory.TOOLS,
|
||||
severity=CheckSeverity.MEDIUM,
|
||||
)
|
||||
def check_tools_web_search(config: DoctorConfig) -> CheckResult:
|
||||
"""Check web search tool availability."""
|
||||
available = []
|
||||
missing = []
|
||||
|
||||
# Check DuckDuckGo (no API key required)
|
||||
try:
|
||||
from praisonaiagents.tools import internet_search
|
||||
available.append("internet_search (DuckDuckGo)")
|
||||
except ImportError:
|
||||
missing.append("internet_search")
|
||||
|
||||
# Check Tavily
|
||||
if os.environ.get("TAVILY_API_KEY"):
|
||||
try:
|
||||
from praisonaiagents.tools import tavily_search
|
||||
available.append("tavily_search")
|
||||
except ImportError:
|
||||
missing.append("tavily_search (missing tavily package)")
|
||||
else:
|
||||
missing.append("tavily_search (TAVILY_API_KEY not set)")
|
||||
|
||||
# Check Exa
|
||||
if os.environ.get("EXA_API_KEY"):
|
||||
try:
|
||||
from praisonaiagents.tools import exa_search
|
||||
available.append("exa_search")
|
||||
except ImportError:
|
||||
missing.append("exa_search (missing exa package)")
|
||||
else:
|
||||
missing.append("exa_search (EXA_API_KEY not set)")
|
||||
|
||||
if available:
|
||||
return CheckResult(
|
||||
id="tools_web_search",
|
||||
title="Web Search Tools",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"{len(available)} web search tool(s) available",
|
||||
details=", ".join(available),
|
||||
metadata={"available": available, "missing": missing},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="tools_web_search",
|
||||
title="Web Search Tools",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message="No web search tools available",
|
||||
details=", ".join(missing),
|
||||
remediation="Install duckduckgo-search or set TAVILY_API_KEY",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="tools_file_ops",
|
||||
title="File Operation Tools",
|
||||
description="Check file operation tool availability",
|
||||
category=CheckCategory.TOOLS,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_tools_file_ops(config: DoctorConfig) -> CheckResult:
|
||||
"""Check file operation tool availability."""
|
||||
try:
|
||||
from praisonaiagents.tools import read_file, write_file, list_files
|
||||
return CheckResult(
|
||||
id="tools_file_ops",
|
||||
title="File Operation Tools",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message="File operation tools available (read_file, write_file, list_files)",
|
||||
)
|
||||
except ImportError as e:
|
||||
return CheckResult(
|
||||
id="tools_file_ops",
|
||||
title="File Operation Tools",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.WARN,
|
||||
message="Some file operation tools not available",
|
||||
details=str(e),
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="tools_code_execution",
|
||||
title="Code Execution Tools",
|
||||
description="Check code execution tool availability",
|
||||
category=CheckCategory.TOOLS,
|
||||
severity=CheckSeverity.LOW,
|
||||
)
|
||||
def check_tools_code_execution(config: DoctorConfig) -> CheckResult:
|
||||
"""Check code execution tool availability."""
|
||||
available = []
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import execute_code
|
||||
available.append("execute_code")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import execute_command
|
||||
available.append("execute_command")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if available:
|
||||
return CheckResult(
|
||||
id="tools_code_execution",
|
||||
title="Code Execution Tools",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Code execution tools available: {', '.join(available)}",
|
||||
metadata={"available": available},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="tools_code_execution",
|
||||
title="Code Execution Tools",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Code execution tools not loaded (optional)",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="tools_api_keys",
|
||||
title="Tool API Keys",
|
||||
description="Check API keys for tools that require them",
|
||||
category=CheckCategory.TOOLS,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_tools_api_keys(config: DoctorConfig) -> CheckResult:
|
||||
"""Check API keys for tools that require them."""
|
||||
configured = []
|
||||
missing = []
|
||||
|
||||
api_keys = {
|
||||
"TAVILY_API_KEY": "Tavily search",
|
||||
"EXA_API_KEY": "Exa search",
|
||||
"YDC_API_KEY": "You.com search",
|
||||
"SPIDER_API_KEY": "Spider web crawling",
|
||||
}
|
||||
|
||||
for key, description in api_keys.items():
|
||||
if os.environ.get(key):
|
||||
configured.append(description)
|
||||
else:
|
||||
missing.append(f"{description} ({key})")
|
||||
|
||||
if configured:
|
||||
return CheckResult(
|
||||
id="tools_api_keys",
|
||||
title="Tool API Keys",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"{len(configured)} tool API key(s) configured",
|
||||
details=f"Configured: {', '.join(configured)}",
|
||||
metadata={"configured": configured, "missing": missing},
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
id="tools_api_keys",
|
||||
title="Tool API Keys",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="No optional tool API keys configured",
|
||||
details="Tools like Tavily, Exa require API keys for enhanced functionality",
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="tools_optional_deps",
|
||||
title="Tool Optional Dependencies",
|
||||
description="Check optional dependencies for tools",
|
||||
category=CheckCategory.TOOLS,
|
||||
severity=CheckSeverity.INFO,
|
||||
requires_deep=True,
|
||||
)
|
||||
def check_tools_optional_deps(config: DoctorConfig) -> CheckResult:
|
||||
"""Check optional dependencies for tools."""
|
||||
available = []
|
||||
missing = []
|
||||
|
||||
deps_to_check = [
|
||||
("crawl4ai", "Crawl4AI web crawling"),
|
||||
("tavily", "Tavily search"),
|
||||
("pandas", "Pandas data tools"),
|
||||
("openpyxl", "Excel tools"),
|
||||
("yfinance", "Yahoo Finance tools"),
|
||||
("duckdb", "DuckDB tools"),
|
||||
("pymongo", "MongoDB tools"),
|
||||
("arxiv", "arXiv tools"),
|
||||
]
|
||||
|
||||
for package, description in deps_to_check:
|
||||
try:
|
||||
__import__(package)
|
||||
available.append(description)
|
||||
except ImportError:
|
||||
missing.append(f"{description} ({package})")
|
||||
|
||||
total = len(deps_to_check)
|
||||
|
||||
return CheckResult(
|
||||
id="tools_optional_deps",
|
||||
title="Tool Optional Dependencies",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"{len(available)}/{total} optional tool dependencies installed",
|
||||
details=f"Missing: {', '.join(missing[:3])}{'...' if len(missing) > 3 else ''}" if missing else "All installed",
|
||||
metadata={"available": available, "missing": missing},
|
||||
)
|
||||
|
||||
|
||||
@register_check(
|
||||
id="tools_summary",
|
||||
title="Tools Summary",
|
||||
description="Summary of tool availability by category",
|
||||
category=CheckCategory.TOOLS,
|
||||
severity=CheckSeverity.INFO,
|
||||
)
|
||||
def check_tools_summary(config: DoctorConfig) -> CheckResult:
|
||||
"""Summary of tool availability by category."""
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
|
||||
category_counts = {}
|
||||
for cat_name, cat_info in TOOL_CATEGORIES.items():
|
||||
available = sum(1 for t in cat_info["tools"] if t in TOOL_MAPPINGS)
|
||||
total = len(cat_info["tools"])
|
||||
category_counts[cat_name] = f"{available}/{total}"
|
||||
|
||||
summary = ", ".join(f"{k}: {v}" for k, v in category_counts.items())
|
||||
|
||||
return CheckResult(
|
||||
id="tools_summary",
|
||||
title="Tools Summary",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Tool categories: {summary}",
|
||||
metadata={"categories": category_counts},
|
||||
)
|
||||
except ImportError:
|
||||
return CheckResult(
|
||||
id="tools_summary",
|
||||
title="Tools Summary",
|
||||
category=CheckCategory.TOOLS,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Cannot generate tools summary",
|
||||
)
|
||||
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Doctor engine for executing health checks.
|
||||
|
||||
Provides the core execution logic for running doctor checks.
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import traceback
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
|
||||
from datetime import datetime, timezone
|
||||
from typing import Callable, List, Optional
|
||||
import time
|
||||
|
||||
from .models import (
|
||||
CheckCategory,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CheckSeverity,
|
||||
DoctorConfig,
|
||||
DoctorReport,
|
||||
EnvironmentSummary,
|
||||
)
|
||||
from .registry import CheckRegistry, get_registry
|
||||
|
||||
|
||||
class DoctorEngine:
|
||||
"""
|
||||
Engine for executing doctor checks.
|
||||
|
||||
Handles check execution, timeout management, and report generation.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[DoctorConfig] = None):
|
||||
"""
|
||||
Initialize the doctor engine.
|
||||
|
||||
Args:
|
||||
config: Doctor configuration
|
||||
"""
|
||||
self.config = config or DoctorConfig()
|
||||
self.registry = get_registry()
|
||||
self._results: List[CheckResult] = []
|
||||
|
||||
def get_environment_summary(self) -> EnvironmentSummary:
|
||||
"""Gather environment information."""
|
||||
# Get package versions lazily
|
||||
praisonai_version = "unknown"
|
||||
praisonaiagents_version = "unknown"
|
||||
|
||||
try:
|
||||
from praisonai.version import __version__ as pai_version
|
||||
praisonai_version = pai_version
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import praisonaiagents
|
||||
praisonaiagents_version = getattr(praisonaiagents, "__version__", "unknown")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return EnvironmentSummary(
|
||||
python_version=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
python_executable=sys.executable,
|
||||
os_name=platform.system(),
|
||||
os_version=platform.release(),
|
||||
architecture=platform.machine(),
|
||||
praisonai_version=praisonai_version,
|
||||
praisonaiagents_version=praisonaiagents_version,
|
||||
working_directory=os.getcwd(),
|
||||
virtual_env=os.environ.get("VIRTUAL_ENV"),
|
||||
)
|
||||
|
||||
def run_check(
|
||||
self,
|
||||
check_id: str,
|
||||
implementation: Callable,
|
||||
timeout: Optional[float] = None,
|
||||
) -> CheckResult:
|
||||
"""
|
||||
Run a single check with timeout handling.
|
||||
|
||||
Args:
|
||||
check_id: Check identifier
|
||||
implementation: Check function
|
||||
timeout: Timeout in seconds
|
||||
|
||||
Returns:
|
||||
Check result
|
||||
"""
|
||||
timeout = timeout or self.config.timeout
|
||||
definition = self.registry.get_check(check_id)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Run with timeout using ThreadPoolExecutor
|
||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||
future = executor.submit(implementation, self.config)
|
||||
try:
|
||||
result = future.result(timeout=timeout)
|
||||
result.duration_ms = (time.time() - start_time) * 1000
|
||||
return result
|
||||
except FuturesTimeoutError:
|
||||
return CheckResult(
|
||||
id=check_id,
|
||||
title=definition.title if definition else check_id,
|
||||
category=definition.category if definition else CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.ERROR,
|
||||
message=f"Check timed out after {timeout}s",
|
||||
remediation="Increase timeout with --timeout or check for hanging operations",
|
||||
duration_ms=(time.time() - start_time) * 1000,
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
except Exception as e:
|
||||
# Capture exception details but redact potential secrets
|
||||
tb = traceback.format_exc()
|
||||
return CheckResult(
|
||||
id=check_id,
|
||||
title=definition.title if definition else check_id,
|
||||
category=definition.category if definition else CheckCategory.ENVIRONMENT,
|
||||
status=CheckStatus.ERROR,
|
||||
message=f"Check failed with error: {type(e).__name__}: {str(e)[:100]}",
|
||||
details=tb[:500] if not self.config.quiet else None,
|
||||
remediation="Check the error details and fix the underlying issue",
|
||||
duration_ms=(time.time() - start_time) * 1000,
|
||||
severity=CheckSeverity.HIGH,
|
||||
)
|
||||
|
||||
def run_checks(
|
||||
self,
|
||||
check_ids: Optional[List[str]] = None,
|
||||
categories: Optional[List[CheckCategory]] = None,
|
||||
) -> List[CheckResult]:
|
||||
"""
|
||||
Run multiple checks.
|
||||
|
||||
Args:
|
||||
check_ids: Specific check IDs to run (None = all)
|
||||
categories: Filter by categories
|
||||
|
||||
Returns:
|
||||
List of check results
|
||||
"""
|
||||
# Get filtered checks
|
||||
checks = self.registry.filter_checks(
|
||||
only=check_ids or self.config.only or None,
|
||||
skip=self.config.skip or None,
|
||||
categories=categories,
|
||||
deep_mode=self.config.deep,
|
||||
)
|
||||
|
||||
# Resolve dependencies and get ordered list
|
||||
ordered_ids = self.registry.resolve_dependencies([c.id for c in checks])
|
||||
|
||||
results = []
|
||||
failed_deps = set()
|
||||
|
||||
for check_id in ordered_ids:
|
||||
definition = self.registry.get_check(check_id)
|
||||
implementation = self.registry.get_implementation(check_id)
|
||||
|
||||
if not definition or not implementation:
|
||||
continue
|
||||
|
||||
# Check if dependencies failed
|
||||
if any(dep in failed_deps for dep in definition.dependencies):
|
||||
results.append(CheckResult(
|
||||
id=check_id,
|
||||
title=definition.title,
|
||||
category=definition.category,
|
||||
status=CheckStatus.SKIP,
|
||||
message="Skipped due to failed dependency",
|
||||
severity=definition.severity,
|
||||
))
|
||||
continue
|
||||
|
||||
# Run the check
|
||||
result = self.run_check(check_id, implementation)
|
||||
results.append(result)
|
||||
|
||||
# Track failures for dependency resolution
|
||||
if result.status in (CheckStatus.FAIL, CheckStatus.ERROR):
|
||||
failed_deps.add(check_id)
|
||||
|
||||
# Fail fast if configured
|
||||
if self.config.fail_fast:
|
||||
break
|
||||
|
||||
self._results = results
|
||||
return results
|
||||
|
||||
def generate_report(self, results: Optional[List[CheckResult]] = None) -> DoctorReport:
|
||||
"""
|
||||
Generate a complete doctor report.
|
||||
|
||||
Args:
|
||||
results: Check results (uses stored results if None)
|
||||
|
||||
Returns:
|
||||
Complete doctor report
|
||||
"""
|
||||
results = results or self._results
|
||||
|
||||
report = DoctorReport(
|
||||
version="1.0.0",
|
||||
timestamp=datetime.now(timezone.utc).isoformat(),
|
||||
environment=self.get_environment_summary(),
|
||||
results=results,
|
||||
mode="deep" if self.config.deep else "fast",
|
||||
filters={
|
||||
"only": self.config.only,
|
||||
"skip": self.config.skip,
|
||||
},
|
||||
)
|
||||
|
||||
report.calculate_summary()
|
||||
report.exit_code = report.calculate_exit_code(strict=self.config.strict)
|
||||
|
||||
return report
|
||||
|
||||
def run(
|
||||
self,
|
||||
check_ids: Optional[List[str]] = None,
|
||||
categories: Optional[List[CheckCategory]] = None,
|
||||
) -> DoctorReport:
|
||||
"""
|
||||
Run checks and generate report.
|
||||
|
||||
Args:
|
||||
check_ids: Specific check IDs to run
|
||||
categories: Filter by categories
|
||||
|
||||
Returns:
|
||||
Complete doctor report
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
results = self.run_checks(check_ids, categories)
|
||||
report = self.generate_report(results)
|
||||
|
||||
report.duration_ms = (time.time() - start_time) * 1000
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def run_doctor(
|
||||
config: Optional[DoctorConfig] = None,
|
||||
check_ids: Optional[List[str]] = None,
|
||||
categories: Optional[List[CheckCategory]] = None,
|
||||
) -> DoctorReport:
|
||||
"""
|
||||
Convenience function to run doctor checks.
|
||||
|
||||
Args:
|
||||
config: Doctor configuration
|
||||
check_ids: Specific check IDs to run
|
||||
categories: Filter by categories
|
||||
|
||||
Returns:
|
||||
Complete doctor report
|
||||
"""
|
||||
engine = DoctorEngine(config)
|
||||
return engine.run(check_ids, categories)
|
||||
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
Output formatters for the Doctor CLI module.
|
||||
|
||||
Provides text and JSON formatting with secret redaction.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional, TextIO
|
||||
from .models import CheckResult, CheckStatus, DoctorReport, ReportSummary
|
||||
|
||||
|
||||
# Patterns for detecting secrets
|
||||
SECRET_PATTERNS = [
|
||||
re.compile(r'(sk-[a-zA-Z0-9]{20,})', re.IGNORECASE), # OpenAI
|
||||
re.compile(r'(sk-ant-[a-zA-Z0-9-]{20,})', re.IGNORECASE), # Anthropic
|
||||
re.compile(r'(AIza[a-zA-Z0-9_-]{35})', re.IGNORECASE), # Google
|
||||
re.compile(r'(tvly-[a-zA-Z0-9]{20,})', re.IGNORECASE), # Tavily
|
||||
re.compile(r'(xai-[a-zA-Z0-9]{20,})', re.IGNORECASE), # xAI
|
||||
re.compile(r'([a-zA-Z0-9_-]*api[_-]?key[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
|
||||
re.compile(r'([a-zA-Z0-9_-]*token[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
|
||||
re.compile(r'([a-zA-Z0-9_-]*secret[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
|
||||
re.compile(r'(password\s*=\s*["\']?[^\s"\']{8,})', re.IGNORECASE),
|
||||
]
|
||||
|
||||
# Known API key environment variable names
|
||||
API_KEY_ENV_VARS = [
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GOOGLE_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"TAVILY_API_KEY",
|
||||
"EXA_API_KEY",
|
||||
"COHERE_API_KEY",
|
||||
"HUGGINGFACE_API_KEY",
|
||||
"HF_TOKEN",
|
||||
"LANGFUSE_PUBLIC_KEY",
|
||||
"LANGFUSE_SECRET_KEY",
|
||||
"LANGSMITH_API_KEY",
|
||||
"AGENTOPS_API_KEY",
|
||||
"WANDB_API_KEY",
|
||||
"DATADOG_API_KEY",
|
||||
"BRAINTRUST_API_KEY",
|
||||
"YDC_API_KEY",
|
||||
]
|
||||
|
||||
|
||||
def redact_secrets(text: str, show_prefix_suffix: bool = False) -> str:
|
||||
"""
|
||||
Redact secrets from text.
|
||||
|
||||
Args:
|
||||
text: Text to redact
|
||||
show_prefix_suffix: If True, show first 4 and last 4 chars
|
||||
|
||||
Returns:
|
||||
Text with secrets redacted
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
result = text
|
||||
|
||||
for pattern in SECRET_PATTERNS:
|
||||
def replace_match(match):
|
||||
secret = match.group(1)
|
||||
if show_prefix_suffix and len(secret) > 12:
|
||||
return f"{secret[:4]}...{secret[-4:]}"
|
||||
return "***REDACTED***"
|
||||
result = pattern.sub(replace_match, result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def redact_dict(data: Dict[str, Any], show_prefix_suffix: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Recursively redact secrets from a dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary to redact
|
||||
show_prefix_suffix: If True, show first 4 and last 4 chars
|
||||
|
||||
Returns:
|
||||
Dictionary with secrets redacted
|
||||
"""
|
||||
result = {}
|
||||
for key, value in data.items():
|
||||
if isinstance(value, str):
|
||||
# Check if key suggests it's a secret
|
||||
key_lower = key.lower()
|
||||
if any(s in key_lower for s in ["key", "token", "secret", "password", "credential"]):
|
||||
if show_prefix_suffix and len(value) > 12:
|
||||
result[key] = f"{value[:4]}...{value[-4:]}"
|
||||
else:
|
||||
result[key] = "***REDACTED***"
|
||||
else:
|
||||
result[key] = redact_secrets(value, show_prefix_suffix)
|
||||
elif isinstance(value, dict):
|
||||
result[key] = redact_dict(value, show_prefix_suffix)
|
||||
elif isinstance(value, list):
|
||||
result[key] = [
|
||||
redact_dict(v, show_prefix_suffix) if isinstance(v, dict)
|
||||
else redact_secrets(v, show_prefix_suffix) if isinstance(v, str)
|
||||
else v
|
||||
for v in value
|
||||
]
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
class BaseFormatter:
|
||||
"""Base class for output formatters."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
no_color: bool = False,
|
||||
quiet: bool = False,
|
||||
redact: bool = True,
|
||||
show_prefix_suffix: bool = False,
|
||||
):
|
||||
self.no_color = no_color or not sys.stdout.isatty()
|
||||
self.quiet = quiet
|
||||
self.redact = redact
|
||||
self.show_prefix_suffix = show_prefix_suffix
|
||||
|
||||
def format_report(self, report: DoctorReport) -> str:
|
||||
"""Format a complete report."""
|
||||
raise NotImplementedError
|
||||
|
||||
def format_result(self, result: CheckResult) -> str:
|
||||
"""Format a single check result."""
|
||||
raise NotImplementedError
|
||||
|
||||
def write(self, report: DoctorReport, output: Optional[TextIO] = None) -> None:
|
||||
"""Write formatted report to output."""
|
||||
output = output or sys.stdout
|
||||
output.write(self.format_report(report))
|
||||
if not self.format_report(report).endswith("\n"):
|
||||
output.write("\n")
|
||||
|
||||
|
||||
class TextFormatter(BaseFormatter):
|
||||
"""Text formatter with optional color support."""
|
||||
|
||||
# ANSI color codes
|
||||
COLORS = {
|
||||
"reset": "\033[0m",
|
||||
"bold": "\033[1m",
|
||||
"dim": "\033[2m",
|
||||
"red": "\033[31m",
|
||||
"green": "\033[32m",
|
||||
"yellow": "\033[33m",
|
||||
"blue": "\033[34m",
|
||||
"cyan": "\033[36m",
|
||||
"white": "\033[37m",
|
||||
}
|
||||
|
||||
STATUS_COLORS = {
|
||||
CheckStatus.PASS: "green",
|
||||
CheckStatus.WARN: "yellow",
|
||||
CheckStatus.FAIL: "red",
|
||||
CheckStatus.SKIP: "dim",
|
||||
CheckStatus.ERROR: "red",
|
||||
}
|
||||
|
||||
STATUS_SYMBOLS = {
|
||||
CheckStatus.PASS: "✓",
|
||||
CheckStatus.WARN: "⚠",
|
||||
CheckStatus.FAIL: "✗",
|
||||
CheckStatus.SKIP: "○",
|
||||
CheckStatus.ERROR: "✗",
|
||||
}
|
||||
|
||||
def _color(self, text: str, color: str) -> str:
|
||||
"""Apply color to text if colors are enabled."""
|
||||
if self.no_color:
|
||||
return text
|
||||
return f"{self.COLORS.get(color, '')}{text}{self.COLORS['reset']}"
|
||||
|
||||
def _status_symbol(self, status: CheckStatus) -> str:
|
||||
"""Get colored status symbol."""
|
||||
symbol = self.STATUS_SYMBOLS.get(status, "?")
|
||||
color = self.STATUS_COLORS.get(status, "white")
|
||||
return self._color(symbol, color)
|
||||
|
||||
def format_result(self, result: CheckResult) -> str:
|
||||
"""Format a single check result."""
|
||||
symbol = self._status_symbol(result.status)
|
||||
message = result.message
|
||||
if self.redact:
|
||||
message = redact_secrets(message, self.show_prefix_suffix)
|
||||
|
||||
line = f"{symbol} {result.title}: {message}"
|
||||
|
||||
if result.details and not self.quiet:
|
||||
details = result.details
|
||||
if self.redact:
|
||||
details = redact_secrets(details, self.show_prefix_suffix)
|
||||
line += f"\n {self._color(details, 'dim')}"
|
||||
|
||||
if result.remediation and result.status in (CheckStatus.FAIL, CheckStatus.ERROR):
|
||||
line += f"\n {self._color('Fix:', 'yellow')} {result.remediation}"
|
||||
|
||||
return line
|
||||
|
||||
def format_summary(self, summary: ReportSummary, strict: bool = False) -> str:
|
||||
"""Format the summary section."""
|
||||
parts = []
|
||||
|
||||
if summary.passed > 0:
|
||||
parts.append(self._color(f"{summary.passed} passed", "green"))
|
||||
if summary.warnings > 0:
|
||||
color = "red" if strict else "yellow"
|
||||
parts.append(self._color(f"{summary.warnings} warnings", color))
|
||||
if summary.failed > 0:
|
||||
parts.append(self._color(f"{summary.failed} failed", "red"))
|
||||
if summary.skipped > 0:
|
||||
parts.append(self._color(f"{summary.skipped} skipped", "dim"))
|
||||
if summary.errors > 0:
|
||||
parts.append(self._color(f"{summary.errors} errors", "red"))
|
||||
|
||||
return f"{summary.total} checks: " + ", ".join(parts)
|
||||
|
||||
def format_report(self, report: DoctorReport) -> str:
|
||||
"""Format a complete report."""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
if not self.quiet:
|
||||
header = f"PraisonAI Doctor v{report.version}"
|
||||
lines.append(self._color(header, "bold"))
|
||||
lines.append("━" * 70)
|
||||
|
||||
# Results
|
||||
for result in report.results:
|
||||
lines.append(self.format_result(result))
|
||||
|
||||
# Summary
|
||||
if not self.quiet:
|
||||
lines.append("━" * 70)
|
||||
lines.append(self.format_summary(report.summary))
|
||||
|
||||
# Duration
|
||||
if not self.quiet and report.duration_ms > 0:
|
||||
lines.append(self._color(f"Completed in {report.duration_ms:.0f}ms", "dim"))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class JsonFormatter(BaseFormatter):
|
||||
"""JSON formatter with deterministic output."""
|
||||
|
||||
def format_result(self, result: CheckResult) -> str:
|
||||
"""Format a single check result as JSON."""
|
||||
data = result.to_dict()
|
||||
if self.redact:
|
||||
data = redact_dict(data, self.show_prefix_suffix)
|
||||
return json.dumps(data, indent=2, sort_keys=True)
|
||||
|
||||
def format_report(self, report: DoctorReport) -> str:
|
||||
"""Format a complete report as JSON."""
|
||||
data = report.to_dict()
|
||||
if self.redact:
|
||||
data = redact_dict(data, self.show_prefix_suffix)
|
||||
|
||||
# Ensure deterministic ordering
|
||||
return json.dumps(data, indent=2, sort_keys=True)
|
||||
|
||||
def write(self, report: DoctorReport, output: Optional[TextIO] = None) -> None:
|
||||
"""Write formatted report to output."""
|
||||
output = output or sys.stdout
|
||||
output.write(self.format_report(report))
|
||||
output.write("\n")
|
||||
|
||||
|
||||
def get_formatter(
|
||||
format_type: str = "text",
|
||||
no_color: bool = False,
|
||||
quiet: bool = False,
|
||||
redact: bool = True,
|
||||
show_prefix_suffix: bool = False,
|
||||
) -> BaseFormatter:
|
||||
"""
|
||||
Get a formatter instance.
|
||||
|
||||
Args:
|
||||
format_type: "text" or "json"
|
||||
no_color: Disable ANSI colors
|
||||
quiet: Minimal output
|
||||
redact: Redact secrets
|
||||
show_prefix_suffix: Show partial secrets
|
||||
|
||||
Returns:
|
||||
Formatter instance
|
||||
"""
|
||||
if format_type == "json":
|
||||
return JsonFormatter(
|
||||
no_color=True, # JSON never has colors
|
||||
quiet=quiet,
|
||||
redact=redact,
|
||||
show_prefix_suffix=show_prefix_suffix,
|
||||
)
|
||||
return TextFormatter(
|
||||
no_color=no_color,
|
||||
quiet=quiet,
|
||||
redact=redact,
|
||||
show_prefix_suffix=show_prefix_suffix,
|
||||
)
|
||||
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
Doctor CLI handler for PraisonAI.
|
||||
|
||||
Provides the main DoctorHandler class that integrates with the CLI.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
from ..base import CommandHandler
|
||||
from .models import CheckCategory, DoctorConfig, DoctorReport
|
||||
from .engine import DoctorEngine
|
||||
from .registry import get_registry
|
||||
from .formatters import get_formatter
|
||||
|
||||
|
||||
class DoctorHandler(CommandHandler):
|
||||
"""
|
||||
Handler for the 'praisonai doctor' command.
|
||||
|
||||
Provides comprehensive health checks and diagnostics.
|
||||
"""
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
return "doctor"
|
||||
|
||||
def get_actions(self) -> List[str]:
|
||||
return [
|
||||
"env",
|
||||
"config",
|
||||
"tools",
|
||||
"db",
|
||||
"mcp",
|
||||
"obs",
|
||||
"skills",
|
||||
"memory",
|
||||
"permissions",
|
||||
"network",
|
||||
"performance",
|
||||
"ci",
|
||||
"selftest",
|
||||
]
|
||||
|
||||
def _register_checks(self) -> None:
|
||||
"""Register all doctor checks."""
|
||||
from .checks import register_all_checks
|
||||
register_all_checks()
|
||||
|
||||
def _parse_args(self, args: List[str]) -> argparse.Namespace:
|
||||
"""Parse doctor command arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="praisonai doctor",
|
||||
description="PraisonAI health checks and diagnostics",
|
||||
)
|
||||
|
||||
# Subcommand
|
||||
parser.add_argument(
|
||||
"subcommand",
|
||||
nargs="?",
|
||||
choices=self.get_actions() + [None],
|
||||
help="Subcommand to run (default: run all fast checks)",
|
||||
)
|
||||
|
||||
# Global flags
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output in JSON format",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["text", "json"],
|
||||
default="text",
|
||||
help="Output format (default: text)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o",
|
||||
type=str,
|
||||
help="Write report to file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deep",
|
||||
action="store_true",
|
||||
help="Enable deeper probes (DB connects, network checks)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=10.0,
|
||||
help="Per-check timeout in seconds (default: 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Treat warnings as failures",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quiet", "-q",
|
||||
action="store_true",
|
||||
help="Minimal output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-color",
|
||||
action="store_true",
|
||||
help="Disable ANSI colors",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--only",
|
||||
type=str,
|
||||
help="Only run these check IDs (comma-separated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip",
|
||||
type=str,
|
||||
help="Skip these check IDs (comma-separated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-checks",
|
||||
action="store_true",
|
||||
help="List available check IDs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="store_true",
|
||||
help="Show doctor module version",
|
||||
)
|
||||
|
||||
# Subcommand-specific flags
|
||||
parser.add_argument(
|
||||
"--show-keys",
|
||||
action="store_true",
|
||||
help="Show masked API key values (env subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--require",
|
||||
type=str,
|
||||
help="Require these env vars (comma-separated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file", "-f",
|
||||
type=str,
|
||||
help="Config file to validate (config subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--schema",
|
||||
action="store_true",
|
||||
help="Print expected schema (config subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dsn",
|
||||
type=str,
|
||||
help="Database DSN (db subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--provider",
|
||||
type=str,
|
||||
help="Provider name (db/obs subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--read-only",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Read-only mode for DB checks (default: true)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
type=str,
|
||||
help="Name filter (mcp/tools subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--category",
|
||||
type=str,
|
||||
help="Category filter (tools subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
dest="all_checks",
|
||||
help="Show all items (tools subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--missing-only",
|
||||
action="store_true",
|
||||
help="Show only missing items (tools subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-tools",
|
||||
action="store_true",
|
||||
help="List MCP tools (mcp subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
type=str,
|
||||
help="Path to check (skills subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-installed",
|
||||
action="store_true",
|
||||
help="Check all installed skills",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--budget-ms",
|
||||
type=int,
|
||||
help="Import time budget in ms (performance subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top",
|
||||
type=int,
|
||||
default=10,
|
||||
dest="top_n",
|
||||
help="Number of top items to show (default: 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fail-fast",
|
||||
action="store_true",
|
||||
help="Stop on first failure (ci subcommand)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mock",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Use mock mode (selftest, default: true)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live",
|
||||
action="store_true",
|
||||
help="Use live API calls (selftest)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
help="Model to use for selftest",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save-report",
|
||||
action="store_true",
|
||||
help="Save selftest report",
|
||||
)
|
||||
|
||||
return parser.parse_args(args)
|
||||
|
||||
def _build_config(self, args: argparse.Namespace) -> DoctorConfig:
|
||||
"""Build DoctorConfig from parsed arguments."""
|
||||
return DoctorConfig(
|
||||
deep=args.deep,
|
||||
timeout=args.timeout if args.deep else min(args.timeout, 10.0),
|
||||
strict=args.strict,
|
||||
quiet=args.quiet,
|
||||
no_color=args.no_color,
|
||||
format="json" if args.json else args.format,
|
||||
output_path=args.output,
|
||||
only=args.only.split(",") if args.only else [],
|
||||
skip=args.skip.split(",") if args.skip else [],
|
||||
show_keys=args.show_keys,
|
||||
require_keys=args.require.split(",") if args.require else [],
|
||||
config_file=args.file,
|
||||
dsn=args.dsn,
|
||||
provider=args.provider,
|
||||
read_only=args.read_only,
|
||||
mock=args.mock and not args.live,
|
||||
live=args.live,
|
||||
model=args.model,
|
||||
budget_ms=args.budget_ms,
|
||||
top_n=args.top_n,
|
||||
fail_fast=args.fail_fast,
|
||||
list_tools=args.list_tools,
|
||||
all_checks=args.all_checks,
|
||||
missing_only=args.missing_only,
|
||||
name=args.name,
|
||||
category=args.category,
|
||||
path=args.path,
|
||||
save_report=args.save_report,
|
||||
)
|
||||
|
||||
def _get_categories_for_subcommand(self, subcommand: Optional[str]) -> Optional[List[CheckCategory]]:
|
||||
"""Get check categories for a subcommand."""
|
||||
category_map = {
|
||||
"env": [CheckCategory.ENVIRONMENT],
|
||||
"config": [CheckCategory.CONFIG],
|
||||
"tools": [CheckCategory.TOOLS],
|
||||
"db": [CheckCategory.DATABASE],
|
||||
"mcp": [CheckCategory.MCP],
|
||||
"obs": [CheckCategory.OBSERVABILITY],
|
||||
"skills": [CheckCategory.SKILLS],
|
||||
"memory": [CheckCategory.MEMORY],
|
||||
"permissions": [CheckCategory.PERMISSIONS],
|
||||
"network": [CheckCategory.NETWORK],
|
||||
"performance": [CheckCategory.PERFORMANCE],
|
||||
"selftest": [CheckCategory.SELFTEST],
|
||||
"ci": None, # CI runs all checks
|
||||
}
|
||||
return category_map.get(subcommand)
|
||||
|
||||
def execute(self, action: str, action_args: List[str], **kwargs) -> int:
|
||||
"""
|
||||
Execute the doctor command.
|
||||
|
||||
Args:
|
||||
action: The subcommand or empty string
|
||||
action_args: Additional arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0=pass, 1=fail, 2=error)
|
||||
"""
|
||||
# Combine action and action_args for parsing
|
||||
all_args = [action] + action_args if action and action != "help" else action_args
|
||||
|
||||
try:
|
||||
args = self._parse_args(all_args)
|
||||
except SystemExit:
|
||||
return 0 # Help was shown
|
||||
|
||||
# Handle special flags
|
||||
if args.version:
|
||||
from . import __version__
|
||||
print(f"PraisonAI Doctor v{__version__}")
|
||||
return 0
|
||||
|
||||
# Register all checks
|
||||
self._register_checks()
|
||||
|
||||
if args.list_checks:
|
||||
registry = get_registry()
|
||||
print(registry.list_checks_text())
|
||||
return 0
|
||||
|
||||
# Build config
|
||||
config = self._build_config(args)
|
||||
|
||||
# Handle CI mode
|
||||
if args.subcommand == "ci":
|
||||
config.format = "json"
|
||||
config.no_color = True
|
||||
config.quiet = True
|
||||
|
||||
# Get categories for subcommand
|
||||
categories = self._get_categories_for_subcommand(args.subcommand)
|
||||
|
||||
# Run doctor
|
||||
engine = DoctorEngine(config)
|
||||
report = engine.run(categories=categories)
|
||||
|
||||
# Format output
|
||||
formatter = get_formatter(
|
||||
format_type=config.format,
|
||||
no_color=config.no_color,
|
||||
quiet=config.quiet,
|
||||
show_prefix_suffix=config.show_keys,
|
||||
)
|
||||
|
||||
# Write output
|
||||
if config.output_path:
|
||||
with open(config.output_path, "w") as f:
|
||||
formatter.write(report, f)
|
||||
if not config.quiet:
|
||||
print(f"Report written to: {config.output_path}")
|
||||
else:
|
||||
formatter.write(report, sys.stdout)
|
||||
|
||||
return report.exit_code
|
||||
|
||||
def run(self, args: List[str]) -> int:
|
||||
"""
|
||||
Run the doctor command from CLI.
|
||||
|
||||
Args:
|
||||
args: Command line arguments after 'doctor'
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
if not args:
|
||||
return self.execute("", [])
|
||||
|
||||
action = args[0] if args else ""
|
||||
action_args = args[1:] if len(args) > 1 else []
|
||||
|
||||
return self.execute(action, action_args)
|
||||
|
||||
|
||||
def run_doctor(args: Optional[List[str]] = None) -> int:
|
||||
"""
|
||||
Convenience function to run doctor from CLI.
|
||||
|
||||
Args:
|
||||
args: Command line arguments (default: sys.argv[1:])
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
handler = DoctorHandler()
|
||||
return handler.run(args)
|
||||
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
Data models for the Doctor CLI module.
|
||||
|
||||
Defines the core data structures for check results, reports, and configuration.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
import time
|
||||
|
||||
|
||||
class CheckStatus(Enum):
|
||||
"""Status of a doctor check."""
|
||||
PASS = "pass"
|
||||
WARN = "warn"
|
||||
FAIL = "fail"
|
||||
SKIP = "skip"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
class CheckCategory(Enum):
|
||||
"""Category of a doctor check."""
|
||||
ENVIRONMENT = "environment"
|
||||
CONFIG = "config"
|
||||
TOOLS = "tools"
|
||||
DATABASE = "database"
|
||||
MCP = "mcp"
|
||||
OBSERVABILITY = "observability"
|
||||
SKILLS = "skills"
|
||||
MEMORY = "memory"
|
||||
PERMISSIONS = "permissions"
|
||||
NETWORK = "network"
|
||||
PERFORMANCE = "performance"
|
||||
SELFTEST = "selftest"
|
||||
|
||||
|
||||
class CheckSeverity(Enum):
|
||||
"""Severity level of a check."""
|
||||
CRITICAL = "critical"
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
INFO = "info"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckResult:
|
||||
"""Result of a single doctor check."""
|
||||
id: str
|
||||
title: str
|
||||
category: CheckCategory
|
||||
status: CheckStatus
|
||||
message: str
|
||||
details: Optional[str] = None
|
||||
remediation: Optional[str] = None
|
||||
duration_ms: float = 0.0
|
||||
severity: CheckSeverity = CheckSeverity.MEDIUM
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"title": self.title,
|
||||
"category": self.category.value,
|
||||
"status": self.status.value,
|
||||
"message": self.message,
|
||||
"details": self.details,
|
||||
"remediation": self.remediation,
|
||||
"duration_ms": round(self.duration_ms, 2),
|
||||
"severity": self.severity.value,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
@property
|
||||
def passed(self) -> bool:
|
||||
"""Check if this result is considered passing."""
|
||||
return self.status in (CheckStatus.PASS, CheckStatus.SKIP)
|
||||
|
||||
@property
|
||||
def is_warning(self) -> bool:
|
||||
"""Check if this result is a warning."""
|
||||
return self.status == CheckStatus.WARN
|
||||
|
||||
@property
|
||||
def is_failure(self) -> bool:
|
||||
"""Check if this result is a failure."""
|
||||
return self.status in (CheckStatus.FAIL, CheckStatus.ERROR)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckDefinition:
|
||||
"""Definition of a doctor check."""
|
||||
id: str
|
||||
title: str
|
||||
description: str
|
||||
category: CheckCategory
|
||||
severity: CheckSeverity = CheckSeverity.MEDIUM
|
||||
requires_deep: bool = False
|
||||
dependencies: List[str] = field(default_factory=list)
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"title": self.title,
|
||||
"description": self.description,
|
||||
"category": self.category.value,
|
||||
"severity": self.severity.value,
|
||||
"requires_deep": self.requires_deep,
|
||||
"dependencies": self.dependencies,
|
||||
"tags": self.tags,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvironmentSummary:
|
||||
"""Summary of the runtime environment."""
|
||||
python_version: str = ""
|
||||
python_executable: str = ""
|
||||
os_name: str = ""
|
||||
os_version: str = ""
|
||||
architecture: str = ""
|
||||
praisonai_version: str = ""
|
||||
praisonaiagents_version: str = ""
|
||||
working_directory: str = ""
|
||||
virtual_env: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"python_version": self.python_version,
|
||||
"python_executable": self.python_executable,
|
||||
"os_name": self.os_name,
|
||||
"os_version": self.os_version,
|
||||
"architecture": self.architecture,
|
||||
"praisonai_version": self.praisonai_version,
|
||||
"praisonaiagents_version": self.praisonaiagents_version,
|
||||
"working_directory": self.working_directory,
|
||||
"virtual_env": self.virtual_env,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReportSummary:
|
||||
"""Summary statistics for a doctor report."""
|
||||
total: int = 0
|
||||
passed: int = 0
|
||||
warnings: int = 0
|
||||
failed: int = 0
|
||||
skipped: int = 0
|
||||
errors: int = 0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"total": self.total,
|
||||
"passed": self.passed,
|
||||
"warnings": self.warnings,
|
||||
"failed": self.failed,
|
||||
"skipped": self.skipped,
|
||||
"errors": self.errors,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DoctorReport:
|
||||
"""Complete doctor report."""
|
||||
version: str = "1.0.0"
|
||||
timestamp: str = ""
|
||||
duration_ms: float = 0.0
|
||||
environment: EnvironmentSummary = field(default_factory=EnvironmentSummary)
|
||||
results: List[CheckResult] = field(default_factory=list)
|
||||
summary: ReportSummary = field(default_factory=ReportSummary)
|
||||
exit_code: int = 0
|
||||
mode: str = "fast"
|
||||
filters: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.timestamp:
|
||||
from datetime import datetime, timezone
|
||||
self.timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def calculate_summary(self) -> None:
|
||||
"""Calculate summary statistics from results."""
|
||||
self.summary = ReportSummary(
|
||||
total=len(self.results),
|
||||
passed=sum(1 for r in self.results if r.status == CheckStatus.PASS),
|
||||
warnings=sum(1 for r in self.results if r.status == CheckStatus.WARN),
|
||||
failed=sum(1 for r in self.results if r.status == CheckStatus.FAIL),
|
||||
skipped=sum(1 for r in self.results if r.status == CheckStatus.SKIP),
|
||||
errors=sum(1 for r in self.results if r.status == CheckStatus.ERROR),
|
||||
)
|
||||
|
||||
def calculate_exit_code(self, strict: bool = False) -> int:
|
||||
"""
|
||||
Calculate exit code based on results.
|
||||
|
||||
Args:
|
||||
strict: If True, treat warnings as failures
|
||||
|
||||
Returns:
|
||||
0: All passed (warnings allowed unless strict)
|
||||
1: Failures present
|
||||
2: Internal errors
|
||||
"""
|
||||
if self.summary.errors > 0:
|
||||
return 2
|
||||
if self.summary.failed > 0:
|
||||
return 1
|
||||
if strict and self.summary.warnings > 0:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
return {
|
||||
"version": self.version,
|
||||
"timestamp": self.timestamp,
|
||||
"duration_ms": round(self.duration_ms, 2),
|
||||
"environment": self.environment.to_dict(),
|
||||
"results": [r.to_dict() for r in self.results],
|
||||
"summary": self.summary.to_dict(),
|
||||
"exit_code": self.exit_code,
|
||||
"mode": self.mode,
|
||||
"filters": self.filters,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DoctorConfig:
|
||||
"""Configuration for doctor execution."""
|
||||
deep: bool = False
|
||||
timeout: float = 10.0
|
||||
strict: bool = False
|
||||
quiet: bool = False
|
||||
no_color: bool = False
|
||||
format: str = "text"
|
||||
output_path: Optional[str] = None
|
||||
only: List[str] = field(default_factory=list)
|
||||
skip: List[str] = field(default_factory=list)
|
||||
|
||||
# Subcommand-specific options
|
||||
show_keys: bool = False
|
||||
require_keys: List[str] = field(default_factory=list)
|
||||
config_file: Optional[str] = None
|
||||
dsn: Optional[str] = None
|
||||
provider: Optional[str] = None
|
||||
read_only: bool = True
|
||||
mock: bool = True
|
||||
live: bool = False
|
||||
model: Optional[str] = None
|
||||
budget_ms: Optional[int] = None
|
||||
top_n: int = 10
|
||||
fail_fast: bool = False
|
||||
list_tools: bool = False
|
||||
all_checks: bool = False
|
||||
missing_only: bool = False
|
||||
name: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
path: Optional[str] = None
|
||||
save_report: bool = False
|
||||
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Check registry for the Doctor CLI module.
|
||||
|
||||
Manages registration and discovery of doctor checks.
|
||||
"""
|
||||
|
||||
from typing import Callable, Dict, List, Optional, Set
|
||||
from .models import CheckDefinition, CheckCategory, CheckSeverity, CheckResult
|
||||
|
||||
|
||||
class CheckRegistry:
|
||||
"""
|
||||
Registry for doctor checks.
|
||||
|
||||
Manages check definitions and their implementations.
|
||||
"""
|
||||
|
||||
_instance: Optional["CheckRegistry"] = None
|
||||
|
||||
def __new__(cls) -> "CheckRegistry":
|
||||
"""Singleton pattern for global registry."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._checks: Dict[str, CheckDefinition] = {}
|
||||
cls._instance._implementations: Dict[str, Callable] = {}
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "CheckRegistry":
|
||||
"""Get the singleton instance."""
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def reset(cls) -> None:
|
||||
"""Reset the registry (for testing)."""
|
||||
cls._instance = None
|
||||
|
||||
def register(
|
||||
self,
|
||||
id: str,
|
||||
title: str,
|
||||
description: str,
|
||||
category: CheckCategory,
|
||||
implementation: Callable,
|
||||
severity: CheckSeverity = CheckSeverity.MEDIUM,
|
||||
requires_deep: bool = False,
|
||||
dependencies: Optional[List[str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Register a doctor check.
|
||||
|
||||
Args:
|
||||
id: Unique identifier (snake_case)
|
||||
title: Human-readable title
|
||||
description: Short description of what the check does
|
||||
category: Check category
|
||||
implementation: Callable that performs the check
|
||||
severity: Severity level
|
||||
requires_deep: Whether this check requires --deep mode
|
||||
dependencies: List of check IDs this check depends on
|
||||
tags: Optional tags for filtering
|
||||
"""
|
||||
definition = CheckDefinition(
|
||||
id=id,
|
||||
title=title,
|
||||
description=description,
|
||||
category=category,
|
||||
severity=severity,
|
||||
requires_deep=requires_deep,
|
||||
dependencies=dependencies or [],
|
||||
tags=tags or [],
|
||||
)
|
||||
self._checks[id] = definition
|
||||
self._implementations[id] = implementation
|
||||
|
||||
def get_check(self, id: str) -> Optional[CheckDefinition]:
|
||||
"""Get a check definition by ID."""
|
||||
return self._checks.get(id)
|
||||
|
||||
def get_implementation(self, id: str) -> Optional[Callable]:
|
||||
"""Get a check implementation by ID."""
|
||||
return self._implementations.get(id)
|
||||
|
||||
def get_all_checks(self) -> List[CheckDefinition]:
|
||||
"""Get all registered checks."""
|
||||
return list(self._checks.values())
|
||||
|
||||
def get_check_ids(self) -> List[str]:
|
||||
"""Get all registered check IDs."""
|
||||
return list(self._checks.keys())
|
||||
|
||||
def get_checks_by_category(self, category: CheckCategory) -> List[CheckDefinition]:
|
||||
"""Get all checks in a category."""
|
||||
return [c for c in self._checks.values() if c.category == category]
|
||||
|
||||
def get_checks_by_tag(self, tag: str) -> List[CheckDefinition]:
|
||||
"""Get all checks with a specific tag."""
|
||||
return [c for c in self._checks.values() if tag in c.tags]
|
||||
|
||||
def filter_checks(
|
||||
self,
|
||||
only: Optional[List[str]] = None,
|
||||
skip: Optional[List[str]] = None,
|
||||
categories: Optional[List[CheckCategory]] = None,
|
||||
deep_mode: bool = False,
|
||||
) -> List[CheckDefinition]:
|
||||
"""
|
||||
Filter checks based on criteria.
|
||||
|
||||
Args:
|
||||
only: Only include these check IDs
|
||||
skip: Skip these check IDs
|
||||
categories: Only include checks in these categories
|
||||
deep_mode: Include checks that require deep mode
|
||||
|
||||
Returns:
|
||||
Filtered list of check definitions
|
||||
"""
|
||||
checks = list(self._checks.values())
|
||||
|
||||
# Filter by only
|
||||
if only:
|
||||
only_set = set(only)
|
||||
checks = [c for c in checks if c.id in only_set]
|
||||
|
||||
# Filter by skip
|
||||
if skip:
|
||||
skip_set = set(skip)
|
||||
checks = [c for c in checks if c.id not in skip_set]
|
||||
|
||||
# Filter by categories
|
||||
if categories:
|
||||
checks = [c for c in checks if c.category in categories]
|
||||
|
||||
# Filter by deep mode
|
||||
if not deep_mode:
|
||||
checks = [c for c in checks if not c.requires_deep]
|
||||
|
||||
return checks
|
||||
|
||||
def resolve_dependencies(self, check_ids: List[str]) -> List[str]:
|
||||
"""
|
||||
Resolve check dependencies and return ordered list.
|
||||
|
||||
Args:
|
||||
check_ids: List of check IDs to resolve
|
||||
|
||||
Returns:
|
||||
Ordered list of check IDs with dependencies first
|
||||
"""
|
||||
resolved: List[str] = []
|
||||
seen: Set[str] = set()
|
||||
|
||||
def resolve(id: str) -> None:
|
||||
if id in seen:
|
||||
return
|
||||
seen.add(id)
|
||||
|
||||
check = self._checks.get(id)
|
||||
if check:
|
||||
for dep in check.dependencies:
|
||||
resolve(dep)
|
||||
resolved.append(id)
|
||||
|
||||
for id in check_ids:
|
||||
resolve(id)
|
||||
|
||||
return resolved
|
||||
|
||||
def list_checks_text(self) -> str:
|
||||
"""Generate text listing of all checks."""
|
||||
lines = ["Available Doctor Checks:", ""]
|
||||
|
||||
# Group by category
|
||||
by_category: Dict[CheckCategory, List[CheckDefinition]] = {}
|
||||
for check in self._checks.values():
|
||||
if check.category not in by_category:
|
||||
by_category[check.category] = []
|
||||
by_category[check.category].append(check)
|
||||
|
||||
for category in CheckCategory:
|
||||
if category in by_category:
|
||||
lines.append(f" {category.value.upper()}:")
|
||||
for check in sorted(by_category[category], key=lambda c: c.id):
|
||||
deep_marker = " [deep]" if check.requires_deep else ""
|
||||
lines.append(f" {check.id:<30} {check.description}{deep_marker}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Global registry instance
|
||||
_registry = CheckRegistry()
|
||||
|
||||
|
||||
def register_check(
|
||||
id: str,
|
||||
title: str,
|
||||
description: str,
|
||||
category: CheckCategory,
|
||||
severity: CheckSeverity = CheckSeverity.MEDIUM,
|
||||
requires_deep: bool = False,
|
||||
dependencies: Optional[List[str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
Decorator to register a doctor check.
|
||||
|
||||
Usage:
|
||||
@register_check(
|
||||
id="python_version",
|
||||
title="Python Version",
|
||||
description="Check Python version is 3.9+",
|
||||
category=CheckCategory.ENVIRONMENT,
|
||||
)
|
||||
def check_python_version(config: DoctorConfig) -> CheckResult:
|
||||
...
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
_registry.register(
|
||||
id=id,
|
||||
title=title,
|
||||
description=description,
|
||||
category=category,
|
||||
implementation=func,
|
||||
severity=severity,
|
||||
requires_deep=requires_deep,
|
||||
dependencies=dependencies,
|
||||
tags=tags,
|
||||
)
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
def get_registry() -> CheckRegistry:
|
||||
"""Get the global check registry."""
|
||||
return _registry
|
||||
@@ -0,0 +1,578 @@
|
||||
"""
|
||||
Endpoints CLI Feature Handler
|
||||
|
||||
Provides CLI commands for interacting with recipe endpoints:
|
||||
- list: List available recipe endpoints
|
||||
- describe: Show endpoint details and schema
|
||||
- invoke: Call an endpoint
|
||||
- health: Check endpoint server health
|
||||
|
||||
All commands use the canonical `praisonai endpoints` prefix.
|
||||
|
||||
Why this feature is valuable:
|
||||
- DX: Client invocation from any language/script
|
||||
- Ops: Health checks, monitoring, automation
|
||||
- Polyglot: Non-Python clients can invoke recipes via HTTP
|
||||
- Testing: Easy endpoint verification without code
|
||||
|
||||
Architecture notes:
|
||||
- Optional extras only (no server deps in core)
|
||||
- Lazy imports for all HTTP client code
|
||||
- No impact on praisonaiagents import time
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class EndpointsHandler:
|
||||
"""
|
||||
CLI handler for endpoints operations.
|
||||
|
||||
Commands:
|
||||
- list: List available recipe endpoints
|
||||
- describe: Show endpoint details
|
||||
- invoke: Call an endpoint
|
||||
- health: Check server health
|
||||
"""
|
||||
|
||||
# Exit codes
|
||||
EXIT_SUCCESS = 0
|
||||
EXIT_GENERAL_ERROR = 1
|
||||
EXIT_VALIDATION_ERROR = 2
|
||||
EXIT_RUNTIME_ERROR = 3
|
||||
EXIT_AUTH_ERROR = 4
|
||||
EXIT_NOT_FOUND = 7
|
||||
EXIT_CONNECTION_ERROR = 8
|
||||
|
||||
DEFAULT_URL = "http://localhost:8765"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the handler."""
|
||||
self._base_url = None
|
||||
self._api_key = None
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
"""Get base URL from env or default."""
|
||||
if self._base_url:
|
||||
return self._base_url
|
||||
return os.environ.get("PRAISONAI_ENDPOINTS_URL", self.DEFAULT_URL)
|
||||
|
||||
@property
|
||||
def api_key(self) -> Optional[str]:
|
||||
"""Get API key from env."""
|
||||
if self._api_key:
|
||||
return self._api_key
|
||||
return os.environ.get("PRAISONAI_ENDPOINTS_API_KEY")
|
||||
|
||||
def handle(self, args: List[str]) -> int:
|
||||
"""
|
||||
Handle endpoints subcommand.
|
||||
|
||||
Args:
|
||||
args: Command arguments
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
if not args:
|
||||
self._print_help()
|
||||
return self.EXIT_SUCCESS
|
||||
|
||||
command = args[0]
|
||||
remaining = args[1:]
|
||||
|
||||
commands = {
|
||||
"list": self.cmd_list,
|
||||
"describe": self.cmd_describe,
|
||||
"invoke": self.cmd_invoke,
|
||||
"health": self.cmd_health,
|
||||
"help": lambda _: self._print_help() or self.EXIT_SUCCESS,
|
||||
"--help": lambda _: self._print_help() or self.EXIT_SUCCESS,
|
||||
"-h": lambda _: self._print_help() or self.EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
if command in commands:
|
||||
return commands[command](remaining)
|
||||
else:
|
||||
self._print_error(f"Unknown command: {command}")
|
||||
self._print_help()
|
||||
return self.EXIT_GENERAL_ERROR
|
||||
|
||||
def _print_help(self):
|
||||
"""Print help message."""
|
||||
help_text = """
|
||||
[bold cyan]PraisonAI Endpoints[/bold cyan]
|
||||
|
||||
Client CLI for interacting with recipe endpoints.
|
||||
|
||||
[bold]Usage:[/bold]
|
||||
praisonai endpoints <command> [options]
|
||||
|
||||
[bold]Commands:[/bold]
|
||||
list List available recipe endpoints
|
||||
describe <recipe> Show endpoint details and schema
|
||||
invoke <recipe> Call an endpoint
|
||||
health Check server health
|
||||
|
||||
[bold]List Options:[/bold]
|
||||
--format json Output as JSON
|
||||
--tags <a,b> Filter by tags (comma-separated)
|
||||
--url <url> Server URL (default: http://localhost:8765)
|
||||
|
||||
[bold]Describe Options:[/bold]
|
||||
--schema Show input/output schema only
|
||||
--url <url> Server URL
|
||||
|
||||
[bold]Invoke Options:[/bold]
|
||||
--input <path> Input file path
|
||||
--input-json <j> Input as JSON string
|
||||
--config k=v Config override (repeatable)
|
||||
--json Output as JSON
|
||||
--stream Stream output events (SSE)
|
||||
--url <url> Server URL
|
||||
--api-key <key> API key for auth (or set PRAISONAI_ENDPOINTS_API_KEY)
|
||||
--dry-run Validate without executing
|
||||
|
||||
[bold]Health Options:[/bold]
|
||||
--url <url> Server URL
|
||||
|
||||
[bold]Environment Variables:[/bold]
|
||||
PRAISONAI_ENDPOINTS_URL Default server URL
|
||||
PRAISONAI_ENDPOINTS_API_KEY API key for authentication
|
||||
|
||||
[bold]Examples:[/bold]
|
||||
praisonai endpoints list
|
||||
praisonai endpoints list --format json --tags audio,video
|
||||
praisonai endpoints describe ai-podcast-cleaner
|
||||
praisonai endpoints describe ai-podcast-cleaner --schema
|
||||
praisonai endpoints invoke ai-podcast-cleaner --input ./audio.mp3
|
||||
praisonai endpoints invoke ai-podcast-cleaner --input-json '{"text": "hello"}'
|
||||
praisonai endpoints invoke ai-podcast-cleaner --input ./audio.mp3 --stream
|
||||
praisonai endpoints health
|
||||
praisonai endpoints health --url http://localhost:8000
|
||||
"""
|
||||
self._print_rich(help_text)
|
||||
|
||||
def _print_rich(self, text: str):
|
||||
"""Print with rich formatting if available."""
|
||||
try:
|
||||
from rich import print as rprint
|
||||
rprint(text)
|
||||
except ImportError:
|
||||
import re
|
||||
plain = re.sub(r'\[/?[^\]]+\]', '', text)
|
||||
print(plain)
|
||||
|
||||
def _print_error(self, message: str):
|
||||
"""Print error message."""
|
||||
try:
|
||||
from rich import print as rprint
|
||||
rprint(f"[red]Error: {message}[/red]")
|
||||
except ImportError:
|
||||
print(f"Error: {message}", file=sys.stderr)
|
||||
|
||||
def _print_success(self, message: str):
|
||||
"""Print success message."""
|
||||
try:
|
||||
from rich import print as rprint
|
||||
rprint(f"[green]✓ {message}[/green]")
|
||||
except ImportError:
|
||||
print(f"✓ {message}")
|
||||
|
||||
def _print_json(self, data: Any):
|
||||
"""Print JSON output."""
|
||||
print(json.dumps(data, indent=2, default=str))
|
||||
|
||||
def _parse_args(self, args: List[str], spec: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Parse command arguments based on spec."""
|
||||
result = {k: v.get("default") for k, v in spec.items()}
|
||||
positional_keys = [k for k, v in spec.items() if v.get("positional")]
|
||||
positional_idx = 0
|
||||
|
||||
# Handle repeatable args
|
||||
for k, v in spec.items():
|
||||
if v.get("repeatable"):
|
||||
result[k] = []
|
||||
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
|
||||
if arg.startswith("--"):
|
||||
key = arg[2:].replace("-", "_")
|
||||
if key in spec:
|
||||
if spec[key].get("flag"):
|
||||
result[key] = True
|
||||
elif spec[key].get("repeatable") and i + 1 < len(args):
|
||||
result[key].append(args[i + 1])
|
||||
i += 1
|
||||
elif i + 1 < len(args):
|
||||
result[key] = args[i + 1]
|
||||
i += 1
|
||||
i += 1
|
||||
elif arg.startswith("-") and len(arg) == 2:
|
||||
for key, val in spec.items():
|
||||
if val.get("short") == arg:
|
||||
if val.get("flag"):
|
||||
result[key] = True
|
||||
elif i + 1 < len(args):
|
||||
result[key] = args[i + 1]
|
||||
i += 1
|
||||
break
|
||||
i += 1
|
||||
else:
|
||||
if positional_idx < len(positional_keys):
|
||||
result[positional_keys[positional_idx]] = arg
|
||||
positional_idx += 1
|
||||
i += 1
|
||||
|
||||
return result
|
||||
|
||||
def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
json_data: Optional[Dict] = None,
|
||||
stream: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Make HTTP request to endpoint server."""
|
||||
try:
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
except ImportError:
|
||||
return {"error": "urllib not available"}
|
||||
|
||||
base = url or self.base_url
|
||||
full_url = f"{base.rstrip('/')}{path}"
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
key = api_key or self.api_key
|
||||
if key:
|
||||
headers["X-API-Key"] = key
|
||||
|
||||
data = None
|
||||
if json_data:
|
||||
data = json.dumps(json_data).encode("utf-8")
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
full_url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
method=method,
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
body = response.read().decode("utf-8")
|
||||
return {"status": response.status, "data": json.loads(body) if body else {}}
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8") if e.fp else ""
|
||||
try:
|
||||
error_data = json.loads(body) if body else {}
|
||||
except json.JSONDecodeError:
|
||||
error_data = {"message": body}
|
||||
return {"status": e.code, "error": error_data}
|
||||
except urllib.error.URLError as e:
|
||||
return {"status": 0, "error": {"message": f"Connection error: {e.reason}"}}
|
||||
except Exception as e:
|
||||
return {"status": 0, "error": {"message": str(e)}}
|
||||
|
||||
def _stream_request(
|
||||
self,
|
||||
path: str,
|
||||
url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
json_data: Optional[Dict] = None,
|
||||
):
|
||||
"""Make streaming HTTP request."""
|
||||
try:
|
||||
import urllib.request
|
||||
except ImportError:
|
||||
yield {"error": "urllib not available"}
|
||||
return
|
||||
|
||||
base = url or self.base_url
|
||||
full_url = f"{base.rstrip('/')}{path}"
|
||||
|
||||
headers = {"Content-Type": "application/json", "Accept": "text/event-stream"}
|
||||
key = api_key or self.api_key
|
||||
if key:
|
||||
headers["X-API-Key"] = key
|
||||
|
||||
data = json.dumps(json_data).encode("utf-8") if json_data else None
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(full_url, data=data, headers=headers, method="POST")
|
||||
|
||||
with urllib.request.urlopen(req, timeout=300) as response:
|
||||
buffer = ""
|
||||
for line in response:
|
||||
line = line.decode("utf-8")
|
||||
buffer += line
|
||||
|
||||
if buffer.endswith("\n\n"):
|
||||
# Parse SSE event
|
||||
event_type = "message"
|
||||
event_data = ""
|
||||
|
||||
for part in buffer.strip().split("\n"):
|
||||
if part.startswith("event:"):
|
||||
event_type = part[6:].strip()
|
||||
elif part.startswith("data:"):
|
||||
event_data = part[5:].strip()
|
||||
|
||||
if event_data:
|
||||
try:
|
||||
yield {"event": event_type, "data": json.loads(event_data)}
|
||||
except json.JSONDecodeError:
|
||||
yield {"event": event_type, "data": event_data}
|
||||
|
||||
buffer = ""
|
||||
|
||||
except Exception as e:
|
||||
yield {"error": str(e)}
|
||||
|
||||
def cmd_list(self, args: List[str]) -> int:
|
||||
"""List available recipe endpoints."""
|
||||
spec = {
|
||||
"format": {"default": "table"},
|
||||
"tags": {"default": None},
|
||||
"url": {"default": None},
|
||||
}
|
||||
parsed = self._parse_args(args, spec)
|
||||
|
||||
# Build query params
|
||||
path = "/v1/recipes"
|
||||
if parsed["tags"]:
|
||||
path += f"?tags={parsed['tags']}"
|
||||
|
||||
result = self._make_request("GET", path, url=parsed["url"])
|
||||
|
||||
if result.get("error"):
|
||||
self._print_error(result["error"].get("message", str(result["error"])))
|
||||
return self.EXIT_CONNECTION_ERROR
|
||||
|
||||
recipes = result.get("data", {}).get("recipes", [])
|
||||
|
||||
if parsed["format"] == "json":
|
||||
self._print_json(recipes)
|
||||
return self.EXIT_SUCCESS
|
||||
|
||||
if not recipes:
|
||||
print("No endpoints available.")
|
||||
return self.EXIT_SUCCESS
|
||||
|
||||
try:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
table = Table(title="Available Endpoints")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Version", style="green")
|
||||
table.add_column("Description")
|
||||
table.add_column("Tags", style="yellow")
|
||||
|
||||
for r in recipes:
|
||||
tags = r.get("tags", [])
|
||||
table.add_row(
|
||||
r.get("name", ""),
|
||||
r.get("version", ""),
|
||||
(r.get("description", "")[:50] + "...") if len(r.get("description", "")) > 50 else r.get("description", ""),
|
||||
", ".join(tags[:3]) if tags else "",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
except ImportError:
|
||||
for r in recipes:
|
||||
print(f"{r.get('name')} ({r.get('version')}): {r.get('description', '')}")
|
||||
|
||||
return self.EXIT_SUCCESS
|
||||
|
||||
def cmd_describe(self, args: List[str]) -> int:
|
||||
"""Describe a recipe endpoint."""
|
||||
spec = {
|
||||
"recipe": {"positional": True, "default": ""},
|
||||
"schema": {"flag": True, "default": False},
|
||||
"url": {"default": None},
|
||||
}
|
||||
parsed = self._parse_args(args, spec)
|
||||
|
||||
if not parsed["recipe"]:
|
||||
self._print_error("Recipe name required")
|
||||
return self.EXIT_VALIDATION_ERROR
|
||||
|
||||
# Get schema if requested
|
||||
if parsed["schema"]:
|
||||
path = f"/v1/recipes/{parsed['recipe']}/schema"
|
||||
else:
|
||||
path = f"/v1/recipes/{parsed['recipe']}"
|
||||
|
||||
result = self._make_request("GET", path, url=parsed["url"])
|
||||
|
||||
if result.get("status") == 404:
|
||||
self._print_error(f"Endpoint not found: {parsed['recipe']}")
|
||||
return self.EXIT_NOT_FOUND
|
||||
|
||||
if result.get("error"):
|
||||
self._print_error(result["error"].get("message", str(result["error"])))
|
||||
return self.EXIT_CONNECTION_ERROR
|
||||
|
||||
data = result.get("data", {})
|
||||
self._print_json(data)
|
||||
return self.EXIT_SUCCESS
|
||||
|
||||
def cmd_invoke(self, args: List[str]) -> int:
|
||||
"""Invoke a recipe endpoint."""
|
||||
spec = {
|
||||
"recipe": {"positional": True, "default": ""},
|
||||
"input": {"short": "-i", "default": None},
|
||||
"input_json": {"default": None},
|
||||
"config": {"repeatable": True, "default": []},
|
||||
"json": {"flag": True, "default": False},
|
||||
"stream": {"flag": True, "default": False},
|
||||
"url": {"default": None},
|
||||
"api_key": {"default": None},
|
||||
"dry_run": {"flag": True, "default": False},
|
||||
}
|
||||
parsed = self._parse_args(args, spec)
|
||||
|
||||
if not parsed["recipe"]:
|
||||
self._print_error("Recipe name required")
|
||||
return self.EXIT_VALIDATION_ERROR
|
||||
|
||||
# Build input data
|
||||
input_data = {}
|
||||
if parsed["input_json"]:
|
||||
try:
|
||||
input_data = json.loads(parsed["input_json"])
|
||||
except json.JSONDecodeError:
|
||||
self._print_error("Invalid JSON in --input-json")
|
||||
return self.EXIT_VALIDATION_ERROR
|
||||
elif parsed["input"]:
|
||||
input_data = {"input": parsed["input"]}
|
||||
|
||||
# Build config from --config key=value pairs
|
||||
config = {}
|
||||
for cfg in parsed["config"]:
|
||||
if "=" in cfg:
|
||||
k, v = cfg.split("=", 1)
|
||||
config[k] = v
|
||||
|
||||
# Build request body
|
||||
body = {
|
||||
"recipe": parsed["recipe"],
|
||||
"input": input_data,
|
||||
"config": config,
|
||||
"options": {"dry_run": parsed["dry_run"]},
|
||||
}
|
||||
|
||||
if parsed["stream"]:
|
||||
return self._invoke_stream(body, parsed)
|
||||
|
||||
# Sync invocation
|
||||
result = self._make_request(
|
||||
"POST",
|
||||
"/v1/recipes/run",
|
||||
url=parsed["url"],
|
||||
api_key=parsed["api_key"],
|
||||
json_data=body,
|
||||
)
|
||||
|
||||
if result.get("status") == 401:
|
||||
self._print_error("Authentication required. Use --api-key or set PRAISONAI_ENDPOINTS_API_KEY")
|
||||
return self.EXIT_AUTH_ERROR
|
||||
|
||||
if result.get("status") == 404:
|
||||
self._print_error(f"Endpoint not found: {parsed['recipe']}")
|
||||
return self.EXIT_NOT_FOUND
|
||||
|
||||
if result.get("error"):
|
||||
self._print_error(result["error"].get("message", str(result["error"])))
|
||||
return self.EXIT_CONNECTION_ERROR
|
||||
|
||||
data = result.get("data", {})
|
||||
|
||||
if parsed["json"]:
|
||||
self._print_json(data)
|
||||
else:
|
||||
if data.get("ok"):
|
||||
self._print_success(f"Endpoint '{parsed['recipe']}' invoked successfully")
|
||||
print(f" Run ID: {data.get('run_id')}")
|
||||
print(f" Status: {data.get('status')}")
|
||||
if data.get("output"):
|
||||
print(f" Output: {data.get('output')}")
|
||||
else:
|
||||
self._print_error(f"Invocation failed: {data.get('error')}")
|
||||
return self.EXIT_RUNTIME_ERROR
|
||||
|
||||
return self.EXIT_SUCCESS if data.get("ok") else self.EXIT_RUNTIME_ERROR
|
||||
|
||||
def _invoke_stream(self, body: Dict, parsed: Dict) -> int:
|
||||
"""Handle streaming invocation."""
|
||||
print("Streaming output...")
|
||||
|
||||
for event in self._stream_request(
|
||||
"/v1/recipes/stream",
|
||||
url=parsed["url"],
|
||||
api_key=parsed["api_key"],
|
||||
json_data=body,
|
||||
):
|
||||
if event.get("error"):
|
||||
self._print_error(event["error"])
|
||||
return self.EXIT_CONNECTION_ERROR
|
||||
|
||||
event_type = event.get("event", "message")
|
||||
data = event.get("data", {})
|
||||
|
||||
if parsed["json"]:
|
||||
print(json.dumps({"event": event_type, "data": data}))
|
||||
else:
|
||||
if event_type == "started":
|
||||
print(f" Started: {data.get('run_id')}")
|
||||
elif event_type == "progress":
|
||||
print(f" [{data.get('step')}] {data.get('message', '')}")
|
||||
elif event_type == "completed":
|
||||
self._print_success(f"Completed: {data.get('status')}")
|
||||
elif event_type == "error":
|
||||
self._print_error(data.get("message", "Unknown error"))
|
||||
return self.EXIT_RUNTIME_ERROR
|
||||
|
||||
return self.EXIT_SUCCESS
|
||||
|
||||
def cmd_health(self, args: List[str]) -> int:
|
||||
"""Check endpoint server health."""
|
||||
spec = {
|
||||
"url": {"default": None},
|
||||
}
|
||||
parsed = self._parse_args(args, spec)
|
||||
|
||||
result = self._make_request("GET", "/health", url=parsed["url"])
|
||||
|
||||
if result.get("error"):
|
||||
self._print_error(f"Server unhealthy: {result['error'].get('message', str(result['error']))}")
|
||||
return self.EXIT_CONNECTION_ERROR
|
||||
|
||||
data = result.get("data", {})
|
||||
|
||||
if data.get("status") == "healthy":
|
||||
self._print_success("Server healthy")
|
||||
print(f" Service: {data.get('service')}")
|
||||
print(f" Version: {data.get('version')}")
|
||||
return self.EXIT_SUCCESS
|
||||
else:
|
||||
self._print_error(f"Server unhealthy: {data}")
|
||||
return self.EXIT_RUNTIME_ERROR
|
||||
|
||||
|
||||
def handle_endpoints_command(args: List[str]) -> int:
|
||||
"""Entry point for endpoints command."""
|
||||
handler = EndpointsHandler()
|
||||
return handler.handle(args)
|
||||
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
Lite Agent Handler for CLI.
|
||||
|
||||
Provides access to the lightweight praisonaiagents.lite subpackage.
|
||||
Usage: praisonai lite [run|info]
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Any, Dict, Tuple
|
||||
from .base import FlagHandler
|
||||
|
||||
|
||||
class LiteHandler(FlagHandler):
|
||||
"""
|
||||
Handler for lite agent commands.
|
||||
|
||||
The lite subpackage provides a minimal agent framework without heavy
|
||||
dependencies like litellm. Users can bring their own LLM client.
|
||||
|
||||
Commands:
|
||||
praisonai lite run "prompt" - Run a lite agent with custom LLM
|
||||
praisonai lite info - Show lite package info
|
||||
praisonai lite example - Show example usage
|
||||
"""
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
return "lite"
|
||||
|
||||
@property
|
||||
def flag_name(self) -> str:
|
||||
return "lite"
|
||||
|
||||
@property
|
||||
def flag_help(self) -> str:
|
||||
return "Use lightweight agent without heavy dependencies"
|
||||
|
||||
def check_dependencies(self) -> Tuple[bool, str]:
|
||||
"""Check if lite package is available."""
|
||||
import importlib.util
|
||||
if importlib.util.find_spec("praisonaiagents.lite") is not None:
|
||||
return True, ""
|
||||
return False, "Lite package requires praisonaiagents>=0.5.0"
|
||||
|
||||
def get_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get information about the lite package.
|
||||
|
||||
Returns:
|
||||
Dict with package info
|
||||
"""
|
||||
import importlib.util
|
||||
if importlib.util.find_spec("praisonaiagents.lite") is None:
|
||||
return {
|
||||
"available": False,
|
||||
"error": "praisonaiagents.lite not found"
|
||||
}
|
||||
|
||||
return {
|
||||
"available": True,
|
||||
"classes": ["LiteAgent", "LiteTask", "LiteToolResult"],
|
||||
"decorators": ["@tool"],
|
||||
"helpers": ["create_openai_llm_fn", "create_anthropic_llm_fn"],
|
||||
"features": [
|
||||
"BYO-LLM (Bring Your Own LLM)",
|
||||
"Thread-safe chat history",
|
||||
"Tool execution",
|
||||
"No litellm dependency",
|
||||
"Minimal memory footprint"
|
||||
]
|
||||
}
|
||||
|
||||
def show_example(self) -> str:
|
||||
"""
|
||||
Show example usage of lite package.
|
||||
|
||||
Returns:
|
||||
Example code string
|
||||
"""
|
||||
example = '''
|
||||
# Example: Using praisonaiagents.lite with custom LLM
|
||||
|
||||
from praisonaiagents.lite import LiteAgent, tool
|
||||
|
||||
# Define a custom LLM function
|
||||
def my_llm(messages):
|
||||
"""Your custom LLM implementation."""
|
||||
# Example: Call OpenAI directly
|
||||
import openai
|
||||
client = openai.OpenAI()
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=messages
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
|
||||
# Or use the built-in OpenAI adapter
|
||||
from praisonaiagents.lite import create_openai_llm_fn
|
||||
llm_fn = create_openai_llm_fn(model="gpt-4o-mini")
|
||||
|
||||
# Create a lite agent
|
||||
agent = LiteAgent(
|
||||
name="MyAgent",
|
||||
llm_fn=llm_fn,
|
||||
instructions="You are a helpful assistant."
|
||||
)
|
||||
|
||||
# Chat with the agent
|
||||
response = agent.chat("Hello!")
|
||||
print(response)
|
||||
|
||||
# Define and use tools
|
||||
@tool
|
||||
def add_numbers(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
agent_with_tools = LiteAgent(
|
||||
name="MathAgent",
|
||||
llm_fn=llm_fn,
|
||||
tools=[add_numbers]
|
||||
)
|
||||
|
||||
# Execute tool directly
|
||||
result = agent_with_tools.execute_tool("add_numbers", a=5, b=3)
|
||||
print(f"Result: {result.output}") # Output: 8
|
||||
'''
|
||||
return example
|
||||
|
||||
def run_lite_agent(
|
||||
self,
|
||||
prompt: str,
|
||||
model: str = "gpt-4o-mini",
|
||||
provider: str = "openai"
|
||||
) -> str:
|
||||
"""
|
||||
Run a lite agent with the given prompt.
|
||||
|
||||
Args:
|
||||
prompt: The prompt to send to the agent
|
||||
model: Model name to use
|
||||
provider: LLM provider (openai, anthropic)
|
||||
|
||||
Returns:
|
||||
Agent response
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents.lite import LiteAgent, create_openai_llm_fn, create_anthropic_llm_fn
|
||||
|
||||
# Create LLM function based on provider
|
||||
if provider == "openai":
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
return "Error: OPENAI_API_KEY not set"
|
||||
llm_fn = create_openai_llm_fn(model=model)
|
||||
elif provider == "anthropic":
|
||||
if not os.environ.get("ANTHROPIC_API_KEY"):
|
||||
return "Error: ANTHROPIC_API_KEY not set"
|
||||
llm_fn = create_anthropic_llm_fn(model=model)
|
||||
else:
|
||||
return f"Error: Unknown provider '{provider}'. Use 'openai' or 'anthropic'."
|
||||
|
||||
# Create and run agent
|
||||
agent = LiteAgent(
|
||||
name="LiteCLIAgent",
|
||||
llm_fn=llm_fn,
|
||||
instructions="You are a helpful assistant. Keep responses concise."
|
||||
)
|
||||
|
||||
return agent.chat(prompt)
|
||||
|
||||
except ImportError as e:
|
||||
return f"Error: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def handle(self, subcommand: str = "info", **kwargs) -> Any:
|
||||
"""
|
||||
Handle lite subcommands.
|
||||
|
||||
Args:
|
||||
subcommand: One of run, info, example
|
||||
**kwargs: Additional options (prompt, model, provider)
|
||||
|
||||
Returns:
|
||||
Results
|
||||
"""
|
||||
if subcommand == "info":
|
||||
info = self.get_info()
|
||||
if info.get("available"):
|
||||
print("praisonaiagents.lite - Lightweight Agent Package")
|
||||
print("=" * 50)
|
||||
print("\nClasses:", ", ".join(info["classes"]))
|
||||
print("Decorators:", ", ".join(info["decorators"]))
|
||||
print("Helpers:", ", ".join(info["helpers"]))
|
||||
print("\nFeatures:")
|
||||
for feature in info["features"]:
|
||||
print(f" • {feature}")
|
||||
else:
|
||||
print(f"Lite package not available: {info.get('error')}")
|
||||
return info
|
||||
|
||||
elif subcommand == "example":
|
||||
example = self.show_example()
|
||||
print(example)
|
||||
return example
|
||||
|
||||
elif subcommand == "run":
|
||||
prompt = kwargs.get("prompt", "Hello!")
|
||||
model = kwargs.get("model", "gpt-4o-mini")
|
||||
provider = kwargs.get("provider", "openai")
|
||||
|
||||
response = self.run_lite_agent(prompt, model, provider)
|
||||
print(response)
|
||||
return response
|
||||
|
||||
else:
|
||||
print(f"Unknown subcommand: {subcommand}")
|
||||
print("Available: info, example, run")
|
||||
return None
|
||||
|
||||
|
||||
def run_lite_command(args) -> int:
|
||||
"""
|
||||
Run lite command from CLI args.
|
||||
|
||||
Args:
|
||||
args: Parsed CLI arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success)
|
||||
"""
|
||||
handler = LiteHandler()
|
||||
subcommand = getattr(args, 'lite_command', 'info')
|
||||
|
||||
kwargs = {}
|
||||
if hasattr(args, 'prompt'):
|
||||
kwargs['prompt'] = args.prompt
|
||||
if hasattr(args, 'model'):
|
||||
kwargs['model'] = args.model
|
||||
if hasattr(args, 'provider'):
|
||||
kwargs['provider'] = args.provider
|
||||
|
||||
result = handler.handle(subcommand, **kwargs)
|
||||
return 0 if result else 1
|
||||
@@ -0,0 +1,308 @@
|
||||
"""
|
||||
Performance Handler for CLI.
|
||||
|
||||
Provides performance benchmarking and regression testing.
|
||||
Usage: praisonai perf [benchmark|check|import-time|memory]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from typing import Any, Dict, Tuple
|
||||
from .base import FlagHandler
|
||||
|
||||
|
||||
class PerformanceHandler(FlagHandler):
|
||||
"""
|
||||
Handler for performance benchmarking commands.
|
||||
|
||||
Commands:
|
||||
praisonai perf benchmark - Run full performance benchmark
|
||||
praisonai perf import-time - Measure import time only
|
||||
praisonai perf memory - Measure memory usage only
|
||||
praisonai perf check - Run regression check against targets
|
||||
praisonai perf lazy-check - Verify lazy imports are working
|
||||
"""
|
||||
|
||||
# Performance targets
|
||||
IMPORT_TIME_TARGET_MS = 200
|
||||
IMPORT_TIME_HARD_FAIL_MS = 300
|
||||
MEMORY_TARGET_MB = 30
|
||||
MEMORY_HARD_FAIL_MB = 45
|
||||
|
||||
@property
|
||||
def feature_name(self) -> str:
|
||||
return "performance"
|
||||
|
||||
@property
|
||||
def flag_name(self) -> str:
|
||||
return "perf"
|
||||
|
||||
@property
|
||||
def flag_help(self) -> str:
|
||||
return "Run performance benchmarks and regression checks"
|
||||
|
||||
def check_dependencies(self) -> Tuple[bool, str]:
|
||||
"""Check if performance testing is available."""
|
||||
return True, ""
|
||||
|
||||
def measure_import_time(self, runs: int = 5) -> Dict[str, Any]:
|
||||
"""
|
||||
Measure import time for praisonaiagents.
|
||||
|
||||
Args:
|
||||
runs: Number of runs to average
|
||||
|
||||
Returns:
|
||||
Dict with timing statistics
|
||||
"""
|
||||
times = []
|
||||
|
||||
for _ in range(runs):
|
||||
code = """
|
||||
import time
|
||||
start = time.perf_counter()
|
||||
import praisonaiagents
|
||||
end = time.perf_counter()
|
||||
print(f"{(end - start) * 1000:.1f}")
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", code],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
|
||||
)
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
times.append(float(result.stdout.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not times:
|
||||
return {"error": "Failed to measure import time"}
|
||||
|
||||
times.sort()
|
||||
median = times[len(times) // 2]
|
||||
|
||||
return {
|
||||
"times": times,
|
||||
"min": min(times),
|
||||
"max": max(times),
|
||||
"median": median,
|
||||
"mean": sum(times) / len(times),
|
||||
"pass": median < self.IMPORT_TIME_TARGET_MS,
|
||||
"hard_fail": median > self.IMPORT_TIME_HARD_FAIL_MS
|
||||
}
|
||||
|
||||
def measure_memory(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Measure memory usage after importing praisonaiagents.
|
||||
|
||||
Returns:
|
||||
Dict with memory statistics
|
||||
"""
|
||||
code = """
|
||||
import tracemalloc
|
||||
tracemalloc.start()
|
||||
import praisonaiagents
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
print(f"{current / 1024 / 1024:.1f},{peak / 1024 / 1024:.1f}")
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", code],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return {"error": f"Failed to measure memory: {result.stderr}"}
|
||||
|
||||
try:
|
||||
current, peak = result.stdout.strip().split(",")
|
||||
current_mb = float(current)
|
||||
peak_mb = float(peak)
|
||||
|
||||
return {
|
||||
"current_mb": current_mb,
|
||||
"peak_mb": peak_mb,
|
||||
"pass": current_mb < self.MEMORY_TARGET_MB,
|
||||
"hard_fail": current_mb > self.MEMORY_HARD_FAIL_MB
|
||||
}
|
||||
except (ValueError, IndexError) as e:
|
||||
return {"error": f"Failed to parse memory output: {e}"}
|
||||
|
||||
def check_lazy_imports(self) -> Dict[str, bool]:
|
||||
"""
|
||||
Check that heavy dependencies are not loaded at import time.
|
||||
|
||||
Returns:
|
||||
Dict mapping module name to whether it's lazy (True = good)
|
||||
"""
|
||||
heavy_modules = ["litellm", "chromadb", "mem0", "requests"]
|
||||
results = {}
|
||||
|
||||
for module in heavy_modules:
|
||||
code = f"""
|
||||
import sys
|
||||
# Clear any cached imports
|
||||
for key in list(sys.modules.keys()):
|
||||
if key.startswith('{module}'):
|
||||
del sys.modules[key]
|
||||
|
||||
import praisonaiagents
|
||||
print('{module}' not in sys.modules)
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", code],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
results[module] = result.stdout.strip() == "True"
|
||||
else:
|
||||
results[module] = False
|
||||
|
||||
return results
|
||||
|
||||
def run_benchmark(self, verbose: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Run full performance benchmark.
|
||||
|
||||
Args:
|
||||
verbose: Print results to console
|
||||
|
||||
Returns:
|
||||
Dict with all benchmark results
|
||||
"""
|
||||
results = {}
|
||||
|
||||
if verbose:
|
||||
print("=" * 60)
|
||||
print("PraisonAI Agents Performance Benchmark")
|
||||
print("=" * 60)
|
||||
|
||||
# Import time
|
||||
if verbose:
|
||||
print("\n[1/3] Measuring import time...")
|
||||
results["import_time"] = self.measure_import_time()
|
||||
|
||||
if verbose and "error" not in results["import_time"]:
|
||||
median = results["import_time"]["median"]
|
||||
status = "PASS" if results["import_time"]["pass"] else "FAIL"
|
||||
print(f" Median: {median:.1f}ms [{status}]")
|
||||
|
||||
# Memory
|
||||
if verbose:
|
||||
print("\n[2/3] Measuring memory usage...")
|
||||
results["memory"] = self.measure_memory()
|
||||
|
||||
if verbose and "error" not in results["memory"]:
|
||||
current = results["memory"]["current_mb"]
|
||||
status = "PASS" if results["memory"]["pass"] else "WARN" if not results["memory"]["hard_fail"] else "FAIL"
|
||||
print(f" Current: {current:.1f}MB [{status}]")
|
||||
|
||||
# Lazy imports
|
||||
if verbose:
|
||||
print("\n[3/3] Checking lazy imports...")
|
||||
results["lazy_imports"] = self.check_lazy_imports()
|
||||
|
||||
if verbose:
|
||||
all_lazy = all(results["lazy_imports"].values())
|
||||
status = "PASS" if all_lazy else "FAIL"
|
||||
print(f" All lazy: {all_lazy} [{status}]")
|
||||
for module, is_lazy in results["lazy_imports"].items():
|
||||
symbol = "✓" if is_lazy else "✗"
|
||||
print(f" {symbol} {module}")
|
||||
|
||||
# Overall
|
||||
overall_pass = (
|
||||
results["import_time"].get("pass", False) and
|
||||
not results["memory"].get("hard_fail", True) and
|
||||
all(results["lazy_imports"].values())
|
||||
)
|
||||
results["overall_pass"] = overall_pass
|
||||
|
||||
if verbose:
|
||||
print("\n" + "=" * 60)
|
||||
status = "PASS" if overall_pass else "FAIL"
|
||||
print(f"Overall: [{status}]")
|
||||
print("=" * 60)
|
||||
|
||||
return results
|
||||
|
||||
def run_check(self) -> bool:
|
||||
"""
|
||||
Run regression check against targets.
|
||||
|
||||
Returns:
|
||||
True if all checks pass
|
||||
"""
|
||||
results = self.run_benchmark(verbose=True)
|
||||
return results.get("overall_pass", False)
|
||||
|
||||
def handle(self, subcommand: str = "benchmark", **kwargs) -> Any:
|
||||
"""
|
||||
Handle performance subcommands.
|
||||
|
||||
Args:
|
||||
subcommand: One of benchmark, import-time, memory, check, lazy-check
|
||||
**kwargs: Additional options
|
||||
|
||||
Returns:
|
||||
Results dict or bool
|
||||
"""
|
||||
if subcommand == "benchmark":
|
||||
return self.run_benchmark(verbose=True)
|
||||
elif subcommand == "import-time":
|
||||
result = self.measure_import_time()
|
||||
if "error" not in result:
|
||||
print(f"Import time: {result['median']:.1f}ms (median)")
|
||||
print(f"Target: <{self.IMPORT_TIME_TARGET_MS}ms")
|
||||
print(f"Status: {'PASS' if result['pass'] else 'FAIL'}")
|
||||
return result
|
||||
elif subcommand == "memory":
|
||||
result = self.measure_memory()
|
||||
if "error" not in result:
|
||||
print(f"Memory: {result['current_mb']:.1f}MB")
|
||||
print(f"Target: <{self.MEMORY_TARGET_MB}MB")
|
||||
print(f"Status: {'PASS' if result['pass'] else 'WARN' if not result['hard_fail'] else 'FAIL'}")
|
||||
return result
|
||||
elif subcommand == "check":
|
||||
return self.run_check()
|
||||
elif subcommand == "lazy-check":
|
||||
results = self.check_lazy_imports()
|
||||
print("Lazy Import Check:")
|
||||
for module, is_lazy in results.items():
|
||||
status = "LAZY (good)" if is_lazy else "EAGER (bad)"
|
||||
print(f" {module}: {status}")
|
||||
return all(results.values())
|
||||
else:
|
||||
print(f"Unknown subcommand: {subcommand}")
|
||||
print("Available: benchmark, import-time, memory, check, lazy-check")
|
||||
return False
|
||||
|
||||
|
||||
# Convenience function for CLI integration
|
||||
def run_performance_command(args) -> int:
|
||||
"""
|
||||
Run performance command from CLI args.
|
||||
|
||||
Args:
|
||||
args: Parsed CLI arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success)
|
||||
"""
|
||||
handler = PerformanceHandler()
|
||||
subcommand = getattr(args, 'perf_command', 'benchmark')
|
||||
result = handler.handle(subcommand)
|
||||
|
||||
if isinstance(result, bool):
|
||||
return 0 if result else 1
|
||||
elif isinstance(result, dict):
|
||||
return 0 if result.get("overall_pass", result.get("pass", False)) else 1
|
||||
return 0
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user