mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cb1676b7c | ||
|
|
e2e9c3f9ee |
No files matched your search
@@ -278,6 +278,12 @@ npm install praisonai
|
||||
| ↳ Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/docs/cli/git-integration) |
|
||||
| ↳ Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/docs/cli/sandbox-execution) |
|
||||
| ↳ CLI Compare | [Example](examples/compare/cli_compare_basic.py) | [📖](https://docs.praison.ai/docs/cli/compare) |
|
||||
| ↳ Profile/Benchmark | [Example](#profile-benchmark) | [📖](https://docs.praison.ai/docs/cli/profile) |
|
||||
| ↳ Auto Mode | [Example](#auto-mode) | [📖](https://docs.praison.ai/docs/cli/auto) |
|
||||
| ↳ Init | [Example](#init) | [📖](https://docs.praison.ai/docs/cli/init) |
|
||||
| ↳ File Input | [Example](#file-input) | [📖](https://docs.praison.ai/docs/cli/file-input) |
|
||||
| ↳ Final Agent | [Example](#final-agent) | [📖](https://docs.praison.ai/docs/cli/final-agent) |
|
||||
| ↳ Max Tokens | [Example](#max-tokens) | [📖](https://docs.praison.ai/docs/cli/max-tokens) |
|
||||
| **🧪 Evaluation** | | |
|
||||
| ↳ Accuracy Evaluation | [Example](examples/eval/accuracy_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
|
||||
| ↳ Performance Evaluation | [Example](examples/eval/performance_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
|
||||
|
||||
@@ -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.3.80" \
|
||||
"praisonai>=2.3.82" \
|
||||
"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.3.80" \
|
||||
"praisonai>=2.3.82" \
|
||||
"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.3.80" \
|
||||
"praisonai>=2.3.82" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Creating Skills with PraisonAI CLI
|
||||
|
||||
This example demonstrates how to create Agent Skills using the CLI.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def example_create_skill_with_template():
|
||||
"""Create a skill using template mode (no API key required)."""
|
||||
print("=" * 60)
|
||||
print("Example 1: Create skill with template")
|
||||
print("=" * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
result = subprocess.run([
|
||||
"praisonai", "skills", "create",
|
||||
"--name", "my-template-skill",
|
||||
"--description", "A skill for processing text files",
|
||||
"--author", "example-team",
|
||||
"--license", "MIT",
|
||||
"--template",
|
||||
"--output-dir", tmpdir
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
if result.returncode == 0:
|
||||
skill_md = Path(tmpdir) / "my-template-skill" / "SKILL.md"
|
||||
print(f"\nGenerated SKILL.md:\n{skill_md.read_text()[:500]}...")
|
||||
else:
|
||||
print(f"Error: {result.stderr}")
|
||||
|
||||
|
||||
def example_create_skill_with_ai():
|
||||
"""Create a skill using AI generation (requires API key)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 2: Create skill with AI generation")
|
||||
print("=" * 60)
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Skipping: OPENAI_API_KEY not set")
|
||||
return
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
result = subprocess.run([
|
||||
"praisonai", "skills", "create",
|
||||
"--name", "csv-analyzer",
|
||||
"--description", "Analyze CSV files and generate statistical reports",
|
||||
"--output-dir", tmpdir
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
if result.returncode == 0:
|
||||
skill_md = Path(tmpdir) / "csv-analyzer" / "SKILL.md"
|
||||
print(f"\nGenerated SKILL.md:\n{skill_md.read_text()[:800]}...")
|
||||
|
||||
# Check if script was generated
|
||||
script_path = Path(tmpdir) / "csv-analyzer" / "scripts" / "skill.py"
|
||||
if script_path.exists():
|
||||
print(f"\nGenerated skill.py:\n{script_path.read_text()[:500]}...")
|
||||
|
||||
|
||||
def example_create_skill_with_script():
|
||||
"""Create a skill with auto-generated Python script."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 3: Create skill with script")
|
||||
print("=" * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
result = subprocess.run([
|
||||
"praisonai", "skills", "create",
|
||||
"--name", "data-processor",
|
||||
"--description", "Process and transform data files",
|
||||
"--template",
|
||||
"--script",
|
||||
"--output-dir", tmpdir
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
if result.returncode == 0:
|
||||
script_path = Path(tmpdir) / "data-processor" / "scripts" / "skill.py"
|
||||
if script_path.exists():
|
||||
print(f"\nGenerated skill.py:\n{script_path.read_text()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
example_create_skill_with_template()
|
||||
example_create_skill_with_ai()
|
||||
example_create_skill_with_script()
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Multi-Agent CLI Usage
|
||||
|
||||
This example demonstrates how to use the praisonai agents command
|
||||
to define and run multiple agents from the command line.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
|
||||
def example_single_agent():
|
||||
"""Run a single agent with a simple task."""
|
||||
print("=" * 60)
|
||||
print("Example 1: Single Agent")
|
||||
print("=" * 60)
|
||||
|
||||
result = subprocess.run([
|
||||
"praisonai", "agents", "run",
|
||||
"--agent", "assistant:Helper",
|
||||
"--task", "What is the capital of France?"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
if result.returncode != 0:
|
||||
print(f"Error: {result.stderr}")
|
||||
|
||||
|
||||
def example_agent_with_tools():
|
||||
"""Run an agent with tools."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 2: Agent with Tools")
|
||||
print("=" * 60)
|
||||
|
||||
result = subprocess.run([
|
||||
"praisonai", "agents", "run",
|
||||
"--agent", "researcher:Research Analyst:internet_search",
|
||||
"--task", "Find the latest news about renewable energy"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
|
||||
|
||||
def example_multiple_agents_sequential():
|
||||
"""Run multiple agents in sequential mode."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 3: Multiple Agents (Sequential)")
|
||||
print("=" * 60)
|
||||
|
||||
result = subprocess.run([
|
||||
"praisonai", "agents", "run",
|
||||
"--agent", "researcher:Research Analyst:internet_search",
|
||||
"--agent", "writer:Content Writer:write_file",
|
||||
"--task", "Research AI trends and write a brief summary"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
|
||||
|
||||
def example_multiple_agents_parallel():
|
||||
"""Run multiple agents in parallel mode."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 4: Multiple Agents (Parallel)")
|
||||
print("=" * 60)
|
||||
|
||||
result = subprocess.run([
|
||||
"praisonai", "agents", "run",
|
||||
"--agent", "analyst1:Market Analyst",
|
||||
"--agent", "analyst2:Tech Analyst",
|
||||
"--process", "parallel",
|
||||
"--task", "Analyze the current state of AI industry"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
|
||||
|
||||
def example_with_custom_llm():
|
||||
"""Run agents with a custom LLM model."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 5: Custom LLM Model")
|
||||
print("=" * 60)
|
||||
|
||||
result = subprocess.run([
|
||||
"praisonai", "agents", "run",
|
||||
"--agent", "coder:Developer:execute_command",
|
||||
"--llm", "gpt-4o-mini",
|
||||
"--task", "Write a simple hello world Python script"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
|
||||
|
||||
def example_with_instructions():
|
||||
"""Run agents with additional instructions."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 6: With Additional Instructions")
|
||||
print("=" * 60)
|
||||
|
||||
result = subprocess.run([
|
||||
"praisonai", "agents", "run",
|
||||
"--agent", "analyst:Data Analyst",
|
||||
"--instructions", "Be concise and provide only key insights",
|
||||
"--task", "Explain the benefits of using AI in healthcare"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
|
||||
|
||||
def example_list_tools():
|
||||
"""List available tools for agents."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 7: List Available Tools")
|
||||
print("=" * 60)
|
||||
|
||||
result = subprocess.run([
|
||||
"praisonai", "agents", "list"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
print(result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check for API key
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("Warning: OPENAI_API_KEY not set. Some examples may fail.")
|
||||
print()
|
||||
|
||||
# Run examples
|
||||
example_list_tools()
|
||||
example_single_agent()
|
||||
# Uncomment to run more examples:
|
||||
# example_agent_with_tools()
|
||||
# example_multiple_agents_sequential()
|
||||
# example_multiple_agents_parallel()
|
||||
# example_with_custom_llm()
|
||||
# example_with_instructions()
|
||||
@@ -278,6 +278,12 @@ npm install praisonai
|
||||
| ↳ Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/docs/cli/git-integration) |
|
||||
| ↳ Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/docs/cli/sandbox-execution) |
|
||||
| ↳ CLI Compare | [Example](examples/compare/cli_compare_basic.py) | [📖](https://docs.praison.ai/docs/cli/compare) |
|
||||
| ↳ Profile/Benchmark | [Example](#profile-benchmark) | [📖](https://docs.praison.ai/docs/cli/profile) |
|
||||
| ↳ Auto Mode | [Example](#auto-mode) | [📖](https://docs.praison.ai/docs/cli/auto) |
|
||||
| ↳ Init | [Example](#init) | [📖](https://docs.praison.ai/docs/cli/init) |
|
||||
| ↳ File Input | [Example](#file-input) | [📖](https://docs.praison.ai/docs/cli/file-input) |
|
||||
| ↳ Final Agent | [Example](#final-agent) | [📖](https://docs.praison.ai/docs/cli/final-agent) |
|
||||
| ↳ Max Tokens | [Example](#max-tokens) | [📖](https://docs.praison.ai/docs/cli/max-tokens) |
|
||||
| **🧪 Evaluation** | | |
|
||||
| ↳ Accuracy Evaluation | [Example](examples/eval/accuracy_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
|
||||
| ↳ Performance Evaluation | [Example](examples/eval/performance_example.py) | [📖](https://docs.praison.ai/docs/cli/eval) |
|
||||
|
||||
@@ -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.3.80.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.80.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.82.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.82.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
+251
-17
@@ -62,7 +62,7 @@ except ImportError:
|
||||
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO').upper(), format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# =============================================================================
|
||||
# Available Tools List (shared between generators)
|
||||
# Available Tools List (shared between generators) - Legacy for praisonai_tools
|
||||
# =============================================================================
|
||||
AVAILABLE_TOOLS = [
|
||||
"CodeDocsSearchTool", "CSVSearchTool", "DirectorySearchTool", "DOCXSearchTool",
|
||||
@@ -72,6 +72,206 @@ AVAILABLE_TOOLS = [
|
||||
"YoutubeChannelSearchTool", "YoutubeVideoSearchTool"
|
||||
]
|
||||
|
||||
# =============================================================================
|
||||
# Enhanced Tool Discovery from praisonaiagents.tools
|
||||
# =============================================================================
|
||||
|
||||
# Tool categories with their tools from praisonaiagents.tools
|
||||
TOOL_CATEGORIES = {
|
||||
'web_search': [
|
||||
'internet_search', 'duckduckgo', 'tavily_search', 'exa_search',
|
||||
'search_web', 'ydc_search', 'searxng_search'
|
||||
],
|
||||
'web_scraping': [
|
||||
'scrape_page', 'extract_links', 'crawl', 'extract_text',
|
||||
'crawl4ai', 'crawl4ai_extract', 'get_article'
|
||||
],
|
||||
'file_operations': [
|
||||
'read_file', 'write_file', 'list_files', 'get_file_info',
|
||||
'copy_file', 'move_file', 'delete_file'
|
||||
],
|
||||
'code_execution': [
|
||||
'execute_command', 'execute_code', 'analyze_code', 'format_code'
|
||||
],
|
||||
'data_processing': [
|
||||
'read_csv', 'write_csv', 'analyze_csv', 'read_json', 'write_json',
|
||||
'read_excel', 'write_excel', 'read_yaml', 'write_yaml', 'read_xml'
|
||||
],
|
||||
'research': [
|
||||
'search_arxiv', 'get_arxiv_paper', 'wiki_search', 'wiki_summary',
|
||||
'get_news_sources', 'get_trending_topics'
|
||||
],
|
||||
'finance': [
|
||||
'get_stock_price', 'get_stock_info', 'get_historical_data'
|
||||
],
|
||||
'math': [
|
||||
'evaluate', 'solve_equation', 'convert_units', 'calculate_statistics'
|
||||
],
|
||||
'database': [
|
||||
'query', 'create_table', 'load_data', 'find_documents', 'vector_search'
|
||||
]
|
||||
}
|
||||
|
||||
# Keywords that map to tool categories
|
||||
TASK_KEYWORD_TO_TOOLS = {
|
||||
# Web search keywords
|
||||
'search': 'web_search',
|
||||
'find': 'web_search',
|
||||
'look up': 'web_search',
|
||||
'google': 'web_search',
|
||||
'internet': 'web_search',
|
||||
'online': 'web_search',
|
||||
'web': 'web_search',
|
||||
|
||||
# Web scraping keywords
|
||||
'scrape': 'web_scraping',
|
||||
'crawl': 'web_scraping',
|
||||
'extract from website': 'web_scraping',
|
||||
'get from url': 'web_scraping',
|
||||
'fetch page': 'web_scraping',
|
||||
|
||||
# File operation keywords
|
||||
'read file': 'file_operations',
|
||||
'write file': 'file_operations',
|
||||
'save': 'file_operations',
|
||||
'load': 'file_operations',
|
||||
'open file': 'file_operations',
|
||||
'create file': 'file_operations',
|
||||
|
||||
# Code execution keywords
|
||||
'execute': 'code_execution',
|
||||
'run code': 'code_execution',
|
||||
'python': 'code_execution',
|
||||
'script': 'code_execution',
|
||||
'command': 'code_execution',
|
||||
'shell': 'code_execution',
|
||||
|
||||
# Data processing keywords
|
||||
'csv': 'data_processing',
|
||||
'excel': 'data_processing',
|
||||
'json': 'data_processing',
|
||||
'yaml': 'data_processing',
|
||||
'xml': 'data_processing',
|
||||
'data': 'data_processing',
|
||||
'spreadsheet': 'data_processing',
|
||||
|
||||
# Research keywords
|
||||
'research': 'research',
|
||||
'paper': 'research',
|
||||
'arxiv': 'research',
|
||||
'wikipedia': 'research',
|
||||
'academic': 'research',
|
||||
'news': 'research',
|
||||
|
||||
# Finance keywords
|
||||
'stock': 'finance',
|
||||
'price': 'finance',
|
||||
'market': 'finance',
|
||||
'financial': 'finance',
|
||||
'trading': 'finance',
|
||||
|
||||
# Math keywords
|
||||
'calculate': 'math',
|
||||
'math': 'math',
|
||||
'equation': 'math',
|
||||
'compute': 'math',
|
||||
'statistics': 'math',
|
||||
|
||||
# Database keywords
|
||||
'database': 'database',
|
||||
'sql': 'database',
|
||||
'query': 'database',
|
||||
'mongodb': 'database',
|
||||
'vector': 'database'
|
||||
}
|
||||
|
||||
|
||||
def get_all_available_tools() -> Dict[str, List[str]]:
|
||||
"""
|
||||
Get all available tools organized by category.
|
||||
|
||||
Returns:
|
||||
Dict mapping category names to lists of tool names
|
||||
"""
|
||||
return TOOL_CATEGORIES.copy()
|
||||
|
||||
|
||||
def get_tools_for_task(task_description: str) -> List[str]:
|
||||
"""
|
||||
Analyze a task description and return appropriate tools.
|
||||
|
||||
Args:
|
||||
task_description: The task to analyze
|
||||
|
||||
Returns:
|
||||
List of tool names appropriate for the task
|
||||
"""
|
||||
task_lower = task_description.lower()
|
||||
matched_categories = set()
|
||||
|
||||
# Match keywords to categories
|
||||
for keyword, category in TASK_KEYWORD_TO_TOOLS.items():
|
||||
if keyword in task_lower:
|
||||
matched_categories.add(category)
|
||||
|
||||
# Collect tools from matched categories
|
||||
tools = []
|
||||
for category in matched_categories:
|
||||
if category in TOOL_CATEGORIES:
|
||||
tools.extend(TOOL_CATEGORIES[category])
|
||||
|
||||
# Always include core tools for flexibility
|
||||
core_tools = ['read_file', 'write_file', 'execute_command']
|
||||
for tool in core_tools:
|
||||
if tool not in tools:
|
||||
tools.append(tool)
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
seen = set()
|
||||
unique_tools = []
|
||||
for tool in tools:
|
||||
if tool not in seen:
|
||||
seen.add(tool)
|
||||
unique_tools.append(tool)
|
||||
|
||||
return unique_tools
|
||||
|
||||
|
||||
def recommend_agent_count(task_description: str) -> int:
|
||||
"""
|
||||
Recommend the optimal number of agents based on task complexity.
|
||||
|
||||
Args:
|
||||
task_description: The task to analyze
|
||||
|
||||
Returns:
|
||||
Recommended number of agents (1-4)
|
||||
"""
|
||||
complexity = BaseAutoGenerator.analyze_complexity(task_description)
|
||||
|
||||
if complexity == 'simple':
|
||||
return 1
|
||||
elif complexity == 'moderate':
|
||||
return 2
|
||||
else: # complex
|
||||
# Count distinct aspects of the task
|
||||
task_lower = task_description.lower()
|
||||
aspects = 0
|
||||
|
||||
aspect_keywords = [
|
||||
['research', 'search', 'find', 'gather'],
|
||||
['analyze', 'evaluate', 'assess', 'review'],
|
||||
['write', 'create', 'generate', 'produce'],
|
||||
['edit', 'refine', 'improve', 'polish'],
|
||||
['coordinate', 'manage', 'orchestrate', 'delegate']
|
||||
]
|
||||
|
||||
for keyword_group in aspect_keywords:
|
||||
if any(kw in task_lower for kw in keyword_group):
|
||||
aspects += 1
|
||||
|
||||
return min(max(aspects, 2), 4) # Between 2 and 4 agents
|
||||
|
||||
# =============================================================================
|
||||
# Base Generator Class (DRY - shared functionality)
|
||||
# =============================================================================
|
||||
@@ -458,9 +658,19 @@ Tools are not available for {framework}. To use tools, install:
|
||||
|
||||
return merged_data
|
||||
|
||||
def discover_tools_for_topic(self) -> List[str]:
|
||||
"""
|
||||
Discover appropriate tools for the topic using intelligent matching.
|
||||
|
||||
Returns:
|
||||
List of tool names appropriate for this topic
|
||||
"""
|
||||
return get_tools_for_task(self.topic)
|
||||
|
||||
def get_user_content(self):
|
||||
"""
|
||||
Generates a prompt for the OpenAI API to generate a team structure.
|
||||
Uses intelligent tool discovery based on task analysis.
|
||||
|
||||
Args:
|
||||
None
|
||||
@@ -484,42 +694,65 @@ Tools are not available for {framework}. To use tools, install:
|
||||
|
||||
workflow_guidance = pattern_guidance.get(self.pattern, pattern_guidance["sequential"])
|
||||
|
||||
# Get recommended tools based on task analysis
|
||||
recommended_tools = self.discover_tools_for_topic()
|
||||
recommended_agent_count = recommend_agent_count(self.topic)
|
||||
complexity = self.analyze_complexity(self.topic)
|
||||
|
||||
# Build comprehensive tool list with categories
|
||||
all_tools_by_category = []
|
||||
for category, tools in TOOL_CATEGORIES.items():
|
||||
all_tools_by_category.append(f" {category}: {', '.join(tools)}")
|
||||
tools_reference = "\n".join(all_tools_by_category)
|
||||
|
||||
# Also include legacy tools for backward compatibility
|
||||
legacy_tools = ", ".join(AVAILABLE_TOOLS)
|
||||
|
||||
user_content = f"""Analyze and generate a team structure for: "{self.topic}"
|
||||
|
||||
STEP 1: TASK COMPLEXITY ANALYSIS
|
||||
First, analyze the task complexity:
|
||||
- Is this a simple task that 1-2 agents could handle?
|
||||
- Does it require multiple specialized perspectives?
|
||||
- Are there clear sequential dependencies between steps?
|
||||
TASK ANALYSIS (Pre-computed):
|
||||
- Complexity: {complexity}
|
||||
- Recommended agents: {recommended_agent_count}
|
||||
- Recommended tools based on task keywords: {', '.join(recommended_tools)}
|
||||
|
||||
STEP 1: VALIDATE TASK ANALYSIS
|
||||
Review the pre-computed analysis above. Adjust if needed based on your understanding.
|
||||
|
||||
STEP 2: DETERMINE OPTIMAL TEAM SIZE
|
||||
Based on your analysis, create the minimum number of agents needed:
|
||||
- Simple tasks: 1-2 agents
|
||||
- Moderate tasks: 2-3 agents
|
||||
- Complex tasks: 3-4 agents (maximum)
|
||||
Based on complexity analysis:
|
||||
- Simple tasks: 1 agent (single focused agent)
|
||||
- Moderate tasks: 2 agents (researcher + executor pattern)
|
||||
- Complex tasks: 3-4 agents (specialized team)
|
||||
|
||||
IMPORTANT: Only create agents that provide meaningful specialization. Avoid unnecessary complexity.
|
||||
Recommended for this task: {recommended_agent_count} agent(s)
|
||||
|
||||
STEP 3: DESIGN THE TEAM (Pattern: {self.pattern})
|
||||
{workflow_guidance}
|
||||
|
||||
Each agent should have:
|
||||
- A clear, distinct role
|
||||
- A specific goal
|
||||
- Relevant backstory
|
||||
- 1 focused task with clear description and expected output
|
||||
- Appropriate tools (only if needed)
|
||||
- Appropriate tools from the recommended list
|
||||
|
||||
Available Tools: CodeDocsSearchTool, CSVSearchTool, DirectorySearchTool, DOCXSearchTool, DirectoryReadTool, FileReadTool, TXTSearchTool, JSONSearchTool, MDXSearchTool, PDFSearchTool, RagTool, ScrapeElementFromWebsiteTool, ScrapeWebsiteTool, WebsiteSearchTool, XMLSearchTool, YoutubeChannelSearchTool, YoutubeVideoSearchTool.
|
||||
Only use these tools if the task requires them. Use empty list [] if no tools needed.
|
||||
AVAILABLE TOOLS BY CATEGORY:
|
||||
{tools_reference}
|
||||
|
||||
Example structure (2 agents for a writing task):
|
||||
LEGACY TOOLS (for backward compatibility):
|
||||
{legacy_tools}
|
||||
|
||||
RECOMMENDED TOOLS FOR THIS TASK: {', '.join(recommended_tools)}
|
||||
Prioritize using the recommended tools. Only add others if specifically needed.
|
||||
|
||||
Example structure (2 agents for a research + writing task):
|
||||
{{
|
||||
"roles": {{
|
||||
"researcher": {{
|
||||
"role": "Research Analyst",
|
||||
"goal": "Gather comprehensive information on the topic",
|
||||
"backstory": "Expert researcher skilled at finding and synthesizing information.",
|
||||
"tools": ["WebsiteSearchTool"],
|
||||
"tools": ["internet_search", "read_file"],
|
||||
"tasks": {{
|
||||
"research_task": {{
|
||||
"description": "Research key information about the topic and compile findings.",
|
||||
@@ -531,7 +764,7 @@ Example structure (2 agents for a writing task):
|
||||
"role": "Content Writer",
|
||||
"goal": "Create polished final content",
|
||||
"backstory": "Skilled writer who transforms research into engaging content.",
|
||||
"tools": [],
|
||||
"tools": ["write_file"],
|
||||
"tasks": {{
|
||||
"writing_task": {{
|
||||
"description": "Write the final content based on research findings.",
|
||||
@@ -543,6 +776,7 @@ Example structure (2 agents for a writing task):
|
||||
}}
|
||||
|
||||
Now generate the optimal team structure for: {self.topic}
|
||||
Use the recommended tools: {', '.join(recommended_tools)}
|
||||
"""
|
||||
return user_content
|
||||
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
"""
|
||||
Multi-Agent CLI Feature Handler
|
||||
|
||||
Provides CLI commands for defining and running multiple agents:
|
||||
- praisonai agents run --agent "name:role:tools" --agent "..." --task "..."
|
||||
|
||||
Example:
|
||||
praisonai agents run \
|
||||
--agent "researcher:Research Analyst:internet_search" \
|
||||
--agent "writer:Content Writer:write_file" \
|
||||
--task "Research AI trends and write a report"
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
|
||||
def parse_agent_definition(definition: str) -> Dict[str, Any]:
|
||||
"""Parse an agent definition string.
|
||||
|
||||
Format: name:role:tools:goal (tools and goal are optional)
|
||||
|
||||
Args:
|
||||
definition: Agent definition string
|
||||
|
||||
Returns:
|
||||
Dictionary with name, role, tools, and optionally goal
|
||||
"""
|
||||
parts = definition.split(':')
|
||||
|
||||
result = {
|
||||
'name': parts[0].strip() if len(parts) > 0 else 'agent',
|
||||
'role': parts[1].strip() if len(parts) > 1 else 'Assistant',
|
||||
'tools': [],
|
||||
'goal': None
|
||||
}
|
||||
|
||||
# Parse tools (comma-separated)
|
||||
if len(parts) > 2 and parts[2].strip():
|
||||
result['tools'] = [t.strip() for t in parts[2].split(',') if t.strip()]
|
||||
|
||||
# Parse goal if provided
|
||||
if len(parts) > 3:
|
||||
result['goal'] = parts[3].strip()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class MultiAgentHandler:
|
||||
"""Handler for multi-agent CLI commands."""
|
||||
|
||||
def __init__(self, verbose: bool = True):
|
||||
"""Initialize the multi-agent handler.
|
||||
|
||||
Args:
|
||||
verbose: Whether to print verbose output
|
||||
"""
|
||||
self.verbose = verbose
|
||||
|
||||
def create_agents_from_definitions(
|
||||
self,
|
||||
definitions: List[str],
|
||||
llm: Optional[str] = None,
|
||||
instructions: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Create agent configurations from definition strings.
|
||||
|
||||
Args:
|
||||
definitions: List of agent definition strings
|
||||
llm: LLM model to use for all agents
|
||||
instructions: Additional instructions for all agents
|
||||
|
||||
Returns:
|
||||
List of agent configuration dictionaries
|
||||
"""
|
||||
agents = []
|
||||
|
||||
for definition in definitions:
|
||||
agent_config = parse_agent_definition(definition)
|
||||
|
||||
if llm:
|
||||
agent_config['llm'] = llm
|
||||
|
||||
if instructions:
|
||||
agent_config['instructions'] = instructions
|
||||
|
||||
agents.append(agent_config)
|
||||
|
||||
return agents
|
||||
|
||||
def prepare_execution_config(
|
||||
self,
|
||||
agent_definitions: List[str],
|
||||
task: str,
|
||||
process: str = "sequential",
|
||||
llm: Optional[str] = None,
|
||||
instructions: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Prepare configuration for agent execution.
|
||||
|
||||
Args:
|
||||
agent_definitions: List of agent definition strings
|
||||
task: Task description
|
||||
process: Execution process (sequential, parallel)
|
||||
llm: LLM model to use
|
||||
instructions: Additional instructions
|
||||
|
||||
Returns:
|
||||
Execution configuration dictionary
|
||||
"""
|
||||
agents = self.create_agents_from_definitions(
|
||||
agent_definitions, llm, instructions
|
||||
)
|
||||
|
||||
return {
|
||||
'agents': agents,
|
||||
'task': task,
|
||||
'process': process,
|
||||
'llm': llm
|
||||
}
|
||||
|
||||
def _load_tools(self, tool_names: List[str]) -> List:
|
||||
"""Load tool functions by name.
|
||||
|
||||
Args:
|
||||
tool_names: List of tool names to load
|
||||
|
||||
Returns:
|
||||
List of tool functions
|
||||
"""
|
||||
tools = []
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import (
|
||||
internet_search, read_file, write_file, list_files,
|
||||
execute_command, read_csv, write_csv, analyze_csv
|
||||
)
|
||||
|
||||
tool_map = {
|
||||
'internet_search': internet_search,
|
||||
'read_file': read_file,
|
||||
'write_file': write_file,
|
||||
'list_files': list_files,
|
||||
'execute_command': execute_command,
|
||||
'read_csv': read_csv,
|
||||
'write_csv': write_csv,
|
||||
'analyze_csv': analyze_csv,
|
||||
}
|
||||
|
||||
for name in tool_names:
|
||||
if name in tool_map:
|
||||
tools.append(tool_map[name])
|
||||
else:
|
||||
if self.verbose:
|
||||
print(f"⚠ Tool '{name}' not found, skipping")
|
||||
except ImportError as e:
|
||||
if self.verbose:
|
||||
print(f"⚠ Could not load tools: {e}")
|
||||
|
||||
return tools
|
||||
|
||||
def _execute_agents(
|
||||
self,
|
||||
agents_config: List[Dict[str, Any]],
|
||||
task: str,
|
||||
process: str = "sequential",
|
||||
llm: Optional[str] = None
|
||||
) -> str:
|
||||
"""Execute agents with the given configuration.
|
||||
|
||||
Args:
|
||||
agents_config: List of agent configurations
|
||||
task: Task to execute
|
||||
process: Execution process
|
||||
llm: LLM model to use
|
||||
|
||||
Returns:
|
||||
Execution result
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
# Create Agent objects
|
||||
agents = []
|
||||
tasks = []
|
||||
|
||||
for i, config in enumerate(agents_config):
|
||||
# Load tools for this agent
|
||||
tools = self._load_tools(config.get('tools', []))
|
||||
|
||||
agent = Agent(
|
||||
name=config['name'],
|
||||
role=config['role'],
|
||||
goal=config.get('goal') or f"Complete tasks as {config['role']}",
|
||||
backstory=f"You are a skilled {config['role']}.",
|
||||
instructions=config.get('instructions', ''),
|
||||
tools=tools if tools else None,
|
||||
llm=config.get('llm') or llm or os.environ.get('OPENAI_MODEL_NAME', 'gpt-4o-mini'),
|
||||
verbose=self.verbose
|
||||
)
|
||||
agents.append(agent)
|
||||
|
||||
# Create task for this agent
|
||||
agent_task = Task(
|
||||
name=f"task_{config['name']}",
|
||||
description=task if i == 0 else f"Continue the work: {task}",
|
||||
expected_output="Completed task output",
|
||||
agent=agent
|
||||
)
|
||||
tasks.append(agent_task)
|
||||
|
||||
# Run agents
|
||||
if self.verbose:
|
||||
print(f"\n🚀 Running {len(agents)} agents ({process} mode)...")
|
||||
for agent in agents:
|
||||
print(f" - {agent.name}: {agent.role}")
|
||||
print()
|
||||
|
||||
praison_agents = PraisonAIAgents(
|
||||
agents=agents,
|
||||
tasks=tasks,
|
||||
process=process,
|
||||
verbose=self.verbose
|
||||
)
|
||||
|
||||
result = praison_agents.start()
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"❌ Execution failed: {e}")
|
||||
raise
|
||||
|
||||
def run(
|
||||
self,
|
||||
agent_definitions: List[str],
|
||||
task: str,
|
||||
process: str = "sequential",
|
||||
llm: Optional[str] = None,
|
||||
instructions: Optional[str] = None
|
||||
) -> str:
|
||||
"""Run multiple agents with the given task.
|
||||
|
||||
Args:
|
||||
agent_definitions: List of agent definition strings
|
||||
task: Task to execute
|
||||
process: Execution process (sequential, parallel)
|
||||
llm: LLM model to use
|
||||
instructions: Additional instructions
|
||||
|
||||
Returns:
|
||||
Execution result
|
||||
"""
|
||||
config = self.prepare_execution_config(
|
||||
agent_definitions, task, process, llm, instructions
|
||||
)
|
||||
|
||||
return self._execute_agents(
|
||||
config['agents'],
|
||||
config['task'],
|
||||
config['process'],
|
||||
config['llm']
|
||||
)
|
||||
|
||||
|
||||
def handle_agents_command(args) -> int:
|
||||
"""Handle agents subcommand from CLI.
|
||||
|
||||
Args:
|
||||
args: Parsed command line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, non-zero for error)
|
||||
"""
|
||||
handler = MultiAgentHandler(verbose=True)
|
||||
|
||||
try:
|
||||
if args.agents_command == "run":
|
||||
if not hasattr(args, 'agent') or not args.agent:
|
||||
print("Error: At least one --agent is required")
|
||||
return 1
|
||||
|
||||
if not hasattr(args, 'task') or not args.task:
|
||||
print("Error: --task is required")
|
||||
return 1
|
||||
|
||||
result = handler.run(
|
||||
agent_definitions=args.agent,
|
||||
task=args.task,
|
||||
process=getattr(args, 'process', 'sequential'),
|
||||
llm=getattr(args, 'llm', None),
|
||||
instructions=getattr(args, 'instructions', None)
|
||||
)
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("RESULT:")
|
||||
print("="*50)
|
||||
print(result)
|
||||
|
||||
elif args.agents_command == "list":
|
||||
# List available tools
|
||||
print("\nAvailable tools for agents:")
|
||||
print("-" * 30)
|
||||
tools = [
|
||||
("internet_search", "Search the web"),
|
||||
("read_file", "Read file contents"),
|
||||
("write_file", "Write to a file"),
|
||||
("list_files", "List directory contents"),
|
||||
("execute_command", "Execute shell commands"),
|
||||
("read_csv", "Read CSV files"),
|
||||
("write_csv", "Write CSV files"),
|
||||
("analyze_csv", "Analyze CSV data"),
|
||||
]
|
||||
for name, desc in tools:
|
||||
print(f" {name}: {desc}")
|
||||
|
||||
else:
|
||||
print(f"Unknown agents command: {args.agents_command}")
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_agents_parser(subparsers) -> None:
|
||||
"""Add agents subcommand to argument parser.
|
||||
|
||||
Args:
|
||||
subparsers: Subparsers object from argparse
|
||||
"""
|
||||
# run command
|
||||
run_parser = subparsers.add_parser(
|
||||
'run',
|
||||
help='Run multiple agents with a task'
|
||||
)
|
||||
run_parser.add_argument(
|
||||
'--agent', '-a',
|
||||
action='append',
|
||||
required=True,
|
||||
help='Agent definition (format: name:role:tools). Can be used multiple times.'
|
||||
)
|
||||
run_parser.add_argument(
|
||||
'--task', '-t',
|
||||
required=True,
|
||||
help='Task for the agents to complete'
|
||||
)
|
||||
run_parser.add_argument(
|
||||
'--process', '-p',
|
||||
choices=['sequential', 'parallel'],
|
||||
default='sequential',
|
||||
help='Execution process (default: sequential)'
|
||||
)
|
||||
run_parser.add_argument(
|
||||
'--llm', '-m',
|
||||
help='LLM model to use for all agents'
|
||||
)
|
||||
run_parser.add_argument(
|
||||
'--instructions', '-i',
|
||||
help='Additional instructions for all agents'
|
||||
)
|
||||
run_parser.add_argument(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
help='Enable verbose output'
|
||||
)
|
||||
|
||||
# list command - list available tools
|
||||
list_parser = subparsers.add_parser(
|
||||
'list',
|
||||
help='List available tools for agents'
|
||||
)
|
||||
@@ -134,14 +134,14 @@ class ImageHandler(FlagHandler):
|
||||
config["use_vision"] = True
|
||||
return config
|
||||
|
||||
def describe_image(self, prompt: str, image_path: str, llm: str = None) -> str:
|
||||
def describe_image(self, prompt: str, image_path: str, llm = None) -> str:
|
||||
"""
|
||||
Describe an image using vision-capable LLM.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/question about the image
|
||||
image_path: Path to the image file
|
||||
llm: Optional LLM model name (must be vision-capable)
|
||||
llm: Optional LLM model name (must be vision-capable), can be string or dict
|
||||
|
||||
Returns:
|
||||
Description of the image
|
||||
@@ -156,7 +156,11 @@ class ImageHandler(FlagHandler):
|
||||
|
||||
import litellm
|
||||
|
||||
model = llm or DEFAULT_VISION_MODEL
|
||||
# Handle llm being a dict (from agent_config) or string
|
||||
if isinstance(llm, dict):
|
||||
model = llm.get('model', DEFAULT_VISION_MODEL)
|
||||
else:
|
||||
model = llm or DEFAULT_VISION_MODEL
|
||||
|
||||
try:
|
||||
# Build message with image
|
||||
@@ -176,14 +180,14 @@ class ImageHandler(FlagHandler):
|
||||
logger.error(f"Error describing image: {e}")
|
||||
return f"Error describing image: {e}"
|
||||
|
||||
def describe_multiple_images(self, prompt: str, image_paths: List[str], llm: str = None) -> str:
|
||||
def describe_multiple_images(self, prompt: str, image_paths: List[str], llm = None) -> str:
|
||||
"""
|
||||
Describe multiple images using vision-capable LLM.
|
||||
|
||||
Args:
|
||||
prompt: The prompt/question about the images
|
||||
image_paths: List of paths to image files
|
||||
llm: Optional LLM model name
|
||||
llm: Optional LLM model name, can be string or dict
|
||||
|
||||
Returns:
|
||||
Description of the images
|
||||
@@ -199,7 +203,11 @@ class ImageHandler(FlagHandler):
|
||||
|
||||
import litellm
|
||||
|
||||
model = llm or DEFAULT_VISION_MODEL
|
||||
# Handle llm being a dict (from agent_config) or string
|
||||
if isinstance(llm, dict):
|
||||
model = llm.get('model', DEFAULT_VISION_MODEL)
|
||||
else:
|
||||
model = llm or DEFAULT_VISION_MODEL
|
||||
|
||||
try:
|
||||
# Build message with all images
|
||||
|
||||
@@ -94,14 +94,26 @@ class SkillsHandler:
|
||||
self,
|
||||
name: str,
|
||||
description: str = "A custom skill",
|
||||
output_dir: Optional[str] = None
|
||||
output_dir: Optional[str] = None,
|
||||
author: Optional[str] = None,
|
||||
license: Optional[str] = None,
|
||||
compatibility: Optional[str] = None,
|
||||
template: bool = False,
|
||||
use_ai: bool = True,
|
||||
generate_script: bool = False
|
||||
) -> str:
|
||||
"""Create a new skill from template.
|
||||
"""Create a new skill from template or AI generation.
|
||||
|
||||
Args:
|
||||
name: Skill name (kebab-case)
|
||||
description: Skill description
|
||||
description: Skill description (also used as prompt for AI)
|
||||
output_dir: Directory to create skill in (default: current dir)
|
||||
author: Author name for metadata
|
||||
license: License type (default: Apache-2.0)
|
||||
compatibility: Compatibility information
|
||||
template: If True, use template only (no AI)
|
||||
use_ai: If True, try to use AI to generate content
|
||||
generate_script: If True, generate scripts/skill.py
|
||||
|
||||
Returns:
|
||||
Path to created skill directory
|
||||
@@ -127,18 +139,74 @@ class SkillsHandler:
|
||||
(skill_dir / "references").mkdir()
|
||||
(skill_dir / "assets").mkdir()
|
||||
|
||||
# Create SKILL.md
|
||||
skill_md_content = f"""---
|
||||
# Set defaults
|
||||
author = author or "user"
|
||||
license = license or "Apache-2.0"
|
||||
compatibility = compatibility or "Works with PraisonAI Agents"
|
||||
|
||||
# Try AI generation if requested and not in template mode
|
||||
ai_content = None
|
||||
if use_ai and not template:
|
||||
ai_content = self._generate_skill_content_with_ai(name, description)
|
||||
|
||||
if ai_content and ai_content.get("skill_md"):
|
||||
# Use AI-generated content
|
||||
skill_md_content = ai_content["skill_md"]
|
||||
if self.verbose:
|
||||
print("✓ Generated SKILL.md with AI")
|
||||
else:
|
||||
# Use template
|
||||
skill_md_content = self._generate_template_content(
|
||||
name, description, author, license, compatibility
|
||||
)
|
||||
if self.verbose and use_ai and not template:
|
||||
print("⚠ No API key found, using template")
|
||||
|
||||
(skill_dir / "SKILL.md").write_text(skill_md_content)
|
||||
|
||||
# Generate scripts/skill.py if requested or AI provided it
|
||||
if generate_script or (ai_content and ai_content.get("skill_py")):
|
||||
script_content = (ai_content or {}).get("skill_py") or self._generate_template_script(name, description)
|
||||
(skill_dir / "scripts" / "skill.py").write_text(script_content)
|
||||
if self.verbose:
|
||||
print(" - scripts/skill.py")
|
||||
else:
|
||||
(skill_dir / "scripts" / ".gitkeep").write_text("")
|
||||
|
||||
# Create placeholder files
|
||||
(skill_dir / "references" / ".gitkeep").write_text("")
|
||||
(skill_dir / "assets" / ".gitkeep").write_text("")
|
||||
|
||||
if self.verbose:
|
||||
print(f"✓ Created skill at {skill_dir}")
|
||||
print(" - SKILL.md")
|
||||
print(" - scripts/")
|
||||
print(" - references/")
|
||||
print(" - assets/")
|
||||
|
||||
return str(skill_dir)
|
||||
|
||||
def _generate_template_content(
|
||||
self,
|
||||
name: str,
|
||||
description: str,
|
||||
author: str,
|
||||
license: str,
|
||||
compatibility: str
|
||||
) -> str:
|
||||
"""Generate template SKILL.md content."""
|
||||
title = name.replace('-', ' ').title()
|
||||
return f"""---
|
||||
name: {name}
|
||||
description: {description}
|
||||
license: Apache-2.0
|
||||
compatibility: Works with PraisonAI Agents
|
||||
license: {license}
|
||||
compatibility: {compatibility}
|
||||
metadata:
|
||||
author: user
|
||||
author: {author}
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# {name.replace('-', ' ').title()}
|
||||
# {title}
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -154,21 +222,229 @@ Describe how to use this skill.
|
||||
2. Step two
|
||||
3. Step three
|
||||
"""
|
||||
(skill_dir / "SKILL.md").write_text(skill_md_content)
|
||||
|
||||
def _generate_template_script(self, name: str, description: str) -> str:
|
||||
"""Generate template scripts/skill.py content."""
|
||||
func_name = name.replace('-', '_')
|
||||
return f'''"""
|
||||
{name} - {description}
|
||||
|
||||
This script provides functionality for the {name} skill.
|
||||
"""
|
||||
|
||||
def {func_name}(input_data: str) -> str:
|
||||
"""
|
||||
Process input data for {name}.
|
||||
|
||||
Args:
|
||||
input_data: The input to process
|
||||
|
||||
# Create placeholder files
|
||||
(skill_dir / "scripts" / ".gitkeep").write_text("")
|
||||
(skill_dir / "references" / ".gitkeep").write_text("")
|
||||
(skill_dir / "assets" / ".gitkeep").write_text("")
|
||||
Returns:
|
||||
Processed result
|
||||
"""
|
||||
# TODO: Implement your skill logic here
|
||||
result = f"Processed: {{input_data}}"
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the skill."""
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
result = {func_name}(sys.argv[1])
|
||||
print(result)
|
||||
else:
|
||||
print("Usage: python skill.py <input>")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
def _generate_skill_content_with_ai(
|
||||
self,
|
||||
name: str,
|
||||
description: str
|
||||
) -> Optional[dict]:
|
||||
"""Generate SKILL.md and optional skill.py using AI.
|
||||
|
||||
if self.verbose:
|
||||
print(f"✓ Created skill at {skill_dir}")
|
||||
print(" - SKILL.md")
|
||||
print(" - scripts/")
|
||||
print(" - references/")
|
||||
print(" - assets/")
|
||||
Args:
|
||||
name: Skill name
|
||||
description: Skill description/prompt
|
||||
|
||||
Returns:
|
||||
Dict with 'skill_md' and optionally 'skill_py', or None if no API key
|
||||
"""
|
||||
# Check for API keys
|
||||
api_key = (
|
||||
os.environ.get("OPENAI_API_KEY") or
|
||||
os.environ.get("ANTHROPIC_API_KEY") or
|
||||
os.environ.get("GOOGLE_API_KEY") or
|
||||
os.environ.get("GEMINI_API_KEY")
|
||||
)
|
||||
|
||||
return str(skill_dir)
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Create prompt for AI to generate skill content
|
||||
prompt = f"""Create a comprehensive SKILL.md file for an Agent Skill with the following details:
|
||||
|
||||
Name: {name}
|
||||
Description: {description}
|
||||
|
||||
The SKILL.md must follow this exact format:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {name}
|
||||
description: {description}
|
||||
license: Apache-2.0
|
||||
compatibility: Works with PraisonAI Agents, Claude Code, and other Agent Skills compatible tools
|
||||
metadata:
|
||||
author: user
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# [Title]
|
||||
|
||||
## Overview
|
||||
[Detailed overview of what this skill does]
|
||||
|
||||
## When to Use
|
||||
[Describe when this skill should be activated]
|
||||
|
||||
## Instructions
|
||||
[Step-by-step instructions for the agent]
|
||||
|
||||
## Examples
|
||||
[Provide concrete examples]
|
||||
|
||||
## Best Practices
|
||||
[List best practices]
|
||||
```
|
||||
|
||||
Generate detailed, practical content based on the description. Make it comprehensive but concise.
|
||||
The skill should be immediately useful for an AI agent.
|
||||
|
||||
Also, if this skill requires any Python code to function, provide a skill.py script that implements the core functionality.
|
||||
Format your response as:
|
||||
---SKILL.MD---
|
||||
[content]
|
||||
---SKILL.PY---
|
||||
[content or "NONE" if no script needed]
|
||||
"""
|
||||
|
||||
agent = Agent(
|
||||
name="SkillGenerator",
|
||||
role="Skill Content Generator",
|
||||
goal="Generate high-quality Agent Skill content",
|
||||
instructions="You are an expert at creating Agent Skills. Generate comprehensive, practical skill content.",
|
||||
llm=os.environ.get("OPENAI_MODEL_NAME", "gpt-4o-mini"),
|
||||
verbose=False
|
||||
)
|
||||
|
||||
result = agent.start(prompt)
|
||||
|
||||
# Parse the response
|
||||
if result and "---SKILL.MD---" in result:
|
||||
parts = result.split("---SKILL.MD---")
|
||||
if len(parts) > 1:
|
||||
skill_md_part = parts[1]
|
||||
skill_py = None
|
||||
|
||||
if "---SKILL.PY---" in skill_md_part:
|
||||
md_parts = skill_md_part.split("---SKILL.PY---")
|
||||
skill_md = md_parts[0].strip()
|
||||
skill_py_content = md_parts[1].strip() if len(md_parts) > 1 else None
|
||||
if skill_py_content and skill_py_content.upper() != "NONE":
|
||||
skill_py = skill_py_content
|
||||
else:
|
||||
skill_md = skill_md_part.strip()
|
||||
|
||||
return {"skill_md": skill_md, "skill_py": skill_py}
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"⚠ AI generation failed: {e}")
|
||||
return None
|
||||
|
||||
def upload_skill(
|
||||
self,
|
||||
skill_path: str,
|
||||
display_title: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Upload a skill to Anthropic Skills API.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill directory
|
||||
display_title: Optional display title for the skill
|
||||
|
||||
Returns:
|
||||
Skill ID if successful, None otherwise
|
||||
"""
|
||||
path = Path(skill_path).expanduser().resolve()
|
||||
|
||||
if not path.exists():
|
||||
raise ValueError(f"Skill path does not exist: {path}")
|
||||
|
||||
skill_md_path = path / "SKILL.md"
|
||||
if not skill_md_path.exists():
|
||||
raise ValueError(f"SKILL.md not found in {path}")
|
||||
|
||||
# Check for Anthropic API key
|
||||
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||
if not api_key:
|
||||
print("Error: ANTHROPIC_API_KEY environment variable is required")
|
||||
return None
|
||||
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
|
||||
# Read skill name from SKILL.md
|
||||
skill_md_content = skill_md_path.read_text()
|
||||
skill_name = path.name
|
||||
|
||||
# Extract name from frontmatter if possible
|
||||
if "---" in skill_md_content:
|
||||
import re
|
||||
match = re.search(r'^name:\s*(.+)$', skill_md_content, re.MULTILINE)
|
||||
if match:
|
||||
skill_name = match.group(1).strip()
|
||||
|
||||
title = display_title or skill_name.replace('-', ' ').title()
|
||||
|
||||
if self.verbose:
|
||||
print(f"Uploading skill '{skill_name}' to Anthropic...")
|
||||
|
||||
# Create skill using Anthropic API
|
||||
with open(skill_md_path, 'rb') as f:
|
||||
response = client.beta.skills.create(
|
||||
display_title=title,
|
||||
files=[(f"{skill_name}/SKILL.md", f, "text/markdown")],
|
||||
betas=["skills-2025-10-02"]
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
print(f"✓ Skill uploaded successfully!")
|
||||
print(f" ID: {response.id}")
|
||||
print(f" Title: {response.display_title}")
|
||||
|
||||
return response.id
|
||||
|
||||
except ImportError:
|
||||
print("Error: anthropic package is required. Install with: pip install anthropic")
|
||||
return None
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"❌ Upload failed: {e}")
|
||||
return None
|
||||
|
||||
def generate_prompt(
|
||||
self,
|
||||
@@ -228,13 +504,38 @@ def handle_skills_command(args) -> int:
|
||||
if not hasattr(args, 'name') or not args.name:
|
||||
print("Error: --name is required for create command")
|
||||
return 1
|
||||
description = args.description if hasattr(args, 'description') else "A custom skill"
|
||||
output_dir = args.output if hasattr(args, 'output') else None
|
||||
handler.create_skill(args.name, description, output_dir)
|
||||
|
||||
# Get all optional arguments
|
||||
description = getattr(args, 'description', None) or "A custom skill"
|
||||
output_dir = getattr(args, 'output_dir', None) or getattr(args, 'output', None)
|
||||
author = getattr(args, 'author', None)
|
||||
license_type = getattr(args, 'license', None)
|
||||
compatibility = getattr(args, 'compatibility', None)
|
||||
template = getattr(args, 'template', False)
|
||||
generate_script = getattr(args, 'script', False)
|
||||
|
||||
handler.create_skill(
|
||||
name=args.name,
|
||||
description=description,
|
||||
output_dir=output_dir,
|
||||
author=author,
|
||||
license=license_type,
|
||||
compatibility=compatibility,
|
||||
template=template,
|
||||
use_ai=not template,
|
||||
generate_script=generate_script
|
||||
)
|
||||
|
||||
elif args.skills_command == "prompt":
|
||||
dirs = args.dirs if hasattr(args, 'dirs') and args.dirs else None
|
||||
handler.generate_prompt(dirs, include_defaults=True)
|
||||
|
||||
elif args.skills_command == "upload":
|
||||
if not hasattr(args, 'path') or not args.path:
|
||||
print("Error: --path is required for upload command")
|
||||
return 1
|
||||
title = getattr(args, 'title', None)
|
||||
handler.upload_skill(args.path, title)
|
||||
|
||||
else:
|
||||
print(f"Unknown skills command: {args.skills_command}")
|
||||
@@ -281,22 +582,45 @@ def add_skills_parser(subparsers) -> None:
|
||||
# create command
|
||||
create_parser = subparsers.add_parser(
|
||||
'create',
|
||||
help='Create a new skill from template'
|
||||
help='Create a new skill (uses AI by default, --template for template only)'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'--name',
|
||||
required=True,
|
||||
help='Skill name (kebab-case)'
|
||||
help='Skill name (kebab-case, e.g., my-skill)'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'--description',
|
||||
default='A custom skill',
|
||||
help='Skill description'
|
||||
help='Skill description (used as prompt for AI generation)'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'--output',
|
||||
'--output-dir', '--output',
|
||||
dest='output_dir',
|
||||
help='Output directory (default: current directory)'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'--author',
|
||||
help='Author name for skill metadata'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'--license',
|
||||
help='License type (default: Apache-2.0)'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'--compatibility',
|
||||
help='Compatibility information'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'--template',
|
||||
action='store_true',
|
||||
help='Use template only, skip AI generation'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'--script',
|
||||
action='store_true',
|
||||
help='Generate scripts/skill.py with template code'
|
||||
)
|
||||
|
||||
# prompt command
|
||||
prompt_parser = subparsers.add_parser(
|
||||
@@ -308,3 +632,18 @@ def add_skills_parser(subparsers) -> None:
|
||||
nargs='+',
|
||||
help='Directories to scan for skills'
|
||||
)
|
||||
|
||||
# upload command - upload skill to Anthropic
|
||||
upload_parser = subparsers.add_parser(
|
||||
'upload',
|
||||
help='Upload skill to Anthropic Skills API'
|
||||
)
|
||||
upload_parser.add_argument(
|
||||
'--path',
|
||||
required=True,
|
||||
help='Path to skill directory'
|
||||
)
|
||||
upload_parser.add_argument(
|
||||
'--title',
|
||||
help='Display title for the skill'
|
||||
)
|
||||
@@ -661,7 +661,7 @@ class PraisonAI:
|
||||
return default_args
|
||||
|
||||
# Define special commands
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval']
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents']
|
||||
|
||||
parser = argparse.ArgumentParser(prog="praisonai", description="praisonAI command-line interface")
|
||||
parser.add_argument("--framework", choices=["crewai", "autogen", "praisonai"], help="Specify the framework")
|
||||
@@ -1122,6 +1122,24 @@ class PraisonAI:
|
||||
from .features.eval import handle_eval_command
|
||||
exit_code = handle_eval_command(unknown_args)
|
||||
sys.exit(exit_code)
|
||||
|
||||
elif args.command == 'agents':
|
||||
# Agents command - run multiple agents with custom definitions
|
||||
if not PRAISONAI_AVAILABLE:
|
||||
print("[red]ERROR: PraisonAI Agents is not installed. Install with:[/red]")
|
||||
print("\npip install praisonaiagents\n")
|
||||
sys.exit(1)
|
||||
|
||||
from .features.agents import handle_agents_command, add_agents_parser
|
||||
|
||||
# Create a parser for agents command
|
||||
agents_parser = argparse.ArgumentParser(prog="praisonai agents")
|
||||
agents_subparsers = agents_parser.add_subparsers(dest='agents_command', help='Agents commands')
|
||||
add_agents_parser(agents_subparsers)
|
||||
agents_args = agents_parser.parse_args(unknown_args)
|
||||
|
||||
exit_code = handle_agents_command(agents_args)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# Only check framework availability for agent-related operations
|
||||
if not args.command and (args.init or args.auto or args.framework):
|
||||
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.80 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.82 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.80"
|
||||
__version__ = "2.3.82"
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Tests for enhanced --auto and --init modes with dynamic tool discovery.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the praisonai package to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
|
||||
class TestToolDiscovery:
|
||||
"""Tests for dynamic tool discovery functionality."""
|
||||
|
||||
def test_get_all_available_tools_returns_dict(self):
|
||||
"""Test that get_all_available_tools returns a dictionary."""
|
||||
from praisonai.auto import get_all_available_tools
|
||||
tools = get_all_available_tools()
|
||||
assert isinstance(tools, dict)
|
||||
|
||||
def test_get_all_available_tools_has_categories(self):
|
||||
"""Test that tools are organized by category."""
|
||||
from praisonai.auto import get_all_available_tools
|
||||
tools = get_all_available_tools()
|
||||
# Should have categories like 'file', 'web', 'data', etc.
|
||||
assert len(tools) > 0
|
||||
for category, tool_list in tools.items():
|
||||
assert isinstance(category, str)
|
||||
assert isinstance(tool_list, list)
|
||||
|
||||
def test_get_tools_for_task_returns_list(self):
|
||||
"""Test that get_tools_for_task returns appropriate tools."""
|
||||
from praisonai.auto import get_tools_for_task
|
||||
tools = get_tools_for_task("Search the web for information")
|
||||
assert isinstance(tools, list)
|
||||
|
||||
def test_get_tools_for_task_web_search(self):
|
||||
"""Test that web search tasks get search tools."""
|
||||
from praisonai.auto import get_tools_for_task
|
||||
tools = get_tools_for_task("Search the internet for AI news")
|
||||
tool_names = [t if isinstance(t, str) else t.__name__ for t in tools]
|
||||
# Should include search-related tools
|
||||
assert any('search' in name.lower() for name in tool_names)
|
||||
|
||||
def test_get_tools_for_task_file_operations(self):
|
||||
"""Test that file tasks get file tools."""
|
||||
from praisonai.auto import get_tools_for_task
|
||||
tools = get_tools_for_task("Read and analyze the config file")
|
||||
tool_names = [t if isinstance(t, str) else t.__name__ for t in tools]
|
||||
# Should include file-related tools
|
||||
assert any('file' in name.lower() or 'read' in name.lower() for name in tool_names)
|
||||
|
||||
def test_get_tools_for_task_code_execution(self):
|
||||
"""Test that code tasks get execution tools."""
|
||||
from praisonai.auto import get_tools_for_task
|
||||
tools = get_tools_for_task("Execute Python code to analyze data")
|
||||
tool_names = [t if isinstance(t, str) else t.__name__ for t in tools]
|
||||
# Should include code execution tools
|
||||
assert any('execute' in name.lower() or 'code' in name.lower() or 'command' in name.lower() for name in tool_names)
|
||||
|
||||
|
||||
class TestEnhancedAutoGenerator:
|
||||
"""Tests for enhanced AutoGenerator with tool discovery."""
|
||||
|
||||
def test_enhanced_generator_has_tool_discovery(self):
|
||||
"""Test that EnhancedAutoGenerator has tool discovery method."""
|
||||
from praisonai.auto import AutoGenerator
|
||||
generator = AutoGenerator(topic="Test task", framework="praisonai")
|
||||
assert hasattr(generator, 'discover_tools_for_topic')
|
||||
|
||||
def test_analyze_task_complexity(self):
|
||||
"""Test task complexity analysis."""
|
||||
from praisonai.auto import BaseAutoGenerator
|
||||
|
||||
# Simple task
|
||||
assert BaseAutoGenerator.analyze_complexity("Write a haiku") == 'simple'
|
||||
|
||||
# Complex task
|
||||
assert BaseAutoGenerator.analyze_complexity("Comprehensive multi-step analysis") == 'complex'
|
||||
|
||||
# Moderate task (needs a keyword that's not in simple or complex lists)
|
||||
assert BaseAutoGenerator.analyze_complexity("Analyze the market trends") == 'moderate'
|
||||
|
||||
def test_recommend_agent_count(self):
|
||||
"""Test agent count recommendation based on complexity."""
|
||||
from praisonai.auto import recommend_agent_count
|
||||
|
||||
# Simple tasks should get 1 agent
|
||||
assert recommend_agent_count("Write a poem") == 1
|
||||
|
||||
# Complex tasks should get more agents
|
||||
count = recommend_agent_count("Comprehensive research and analysis with multiple perspectives")
|
||||
assert count >= 2
|
||||
|
||||
|
||||
class TestToolCategoryMapping:
|
||||
"""Tests for tool category mapping."""
|
||||
|
||||
def test_tool_categories_defined(self):
|
||||
"""Test that tool categories are properly defined."""
|
||||
from praisonai.auto import TOOL_CATEGORIES
|
||||
assert isinstance(TOOL_CATEGORIES, dict)
|
||||
assert 'web_search' in TOOL_CATEGORIES
|
||||
assert 'file_operations' in TOOL_CATEGORIES
|
||||
assert 'code_execution' in TOOL_CATEGORIES
|
||||
|
||||
def test_task_keywords_mapping(self):
|
||||
"""Test that task keywords map to tool categories."""
|
||||
from praisonai.auto import TASK_KEYWORD_TO_TOOLS
|
||||
assert isinstance(TASK_KEYWORD_TO_TOOLS, dict)
|
||||
# Keywords like 'search', 'find', 'look up' should map to web search
|
||||
assert 'search' in TASK_KEYWORD_TO_TOOLS or any('search' in k for k in TASK_KEYWORD_TO_TOOLS)
|
||||
|
||||
|
||||
class TestEnhancedYAMLGeneration:
|
||||
"""Tests for enhanced YAML generation."""
|
||||
|
||||
def test_generated_yaml_includes_tools(self):
|
||||
"""Test that generated YAML includes appropriate tools."""
|
||||
from praisonai.auto import AutoGenerator
|
||||
|
||||
# Test that the generator has tool discovery capability
|
||||
generator = AutoGenerator(
|
||||
topic="Search the web for AI news and write a summary",
|
||||
agent_file="test_agents.yaml",
|
||||
framework="praisonai"
|
||||
)
|
||||
|
||||
# Verify tool discovery works
|
||||
tools = generator.discover_tools_for_topic()
|
||||
assert isinstance(tools, list)
|
||||
assert len(tools) > 0
|
||||
# Should include search tools for this task
|
||||
tool_names = [t if isinstance(t, str) else str(t) for t in tools]
|
||||
assert any('search' in name.lower() for name in tool_names)
|
||||
|
||||
|
||||
class TestIntegrationWithPraisonAIAgentsTools:
|
||||
"""Tests for integration with praisonaiagents.tools."""
|
||||
|
||||
def test_can_import_praisonaiagents_tools(self):
|
||||
"""Test that we can import tools from praisonaiagents."""
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
assert isinstance(TOOL_MAPPINGS, dict)
|
||||
assert len(TOOL_MAPPINGS) > 0
|
||||
except ImportError:
|
||||
pytest.skip("praisonaiagents not installed")
|
||||
|
||||
def test_tool_mappings_include_core_tools(self):
|
||||
"""Test that core tools are available."""
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
core_tools = ['read_file', 'write_file', 'execute_command', 'internet_search']
|
||||
for tool in core_tools:
|
||||
assert tool in TOOL_MAPPINGS, f"Core tool {tool} not found"
|
||||
except ImportError:
|
||||
pytest.skip("praisonaiagents not installed")
|
||||
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
Unit tests for Multi-Agent CLI feature.
|
||||
|
||||
Tests for:
|
||||
- praisonai agents command with multiple --agent flags
|
||||
- Agent definition parsing (name:role:tools format)
|
||||
- Task execution with multiple agents
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
import argparse
|
||||
|
||||
|
||||
class TestMultiAgentCLIArguments:
|
||||
"""Tests for multi-agent CLI argument parsing."""
|
||||
|
||||
def test_agents_command_recognized(self):
|
||||
"""Test that 'agents' is a recognized command."""
|
||||
# Just verify the agents module loads without error
|
||||
from praisonai.cli.features.agents import MultiAgentHandler, parse_agent_definition
|
||||
assert MultiAgentHandler is not None
|
||||
assert parse_agent_definition is not None
|
||||
|
||||
def test_agent_argument_parsing(self):
|
||||
"""Test --agent argument can be used multiple times."""
|
||||
from praisonai.cli.features.agents import add_agents_parser
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='agents_command')
|
||||
add_agents_parser(subparsers)
|
||||
|
||||
args = parser.parse_args([
|
||||
'run',
|
||||
'--agent', 'researcher:Research Analyst:internet_search',
|
||||
'--agent', 'writer:Content Writer:write_file',
|
||||
'--task', 'Research AI trends and write a report'
|
||||
])
|
||||
|
||||
assert len(args.agent) == 2
|
||||
assert args.task == 'Research AI trends and write a report'
|
||||
|
||||
def test_agent_with_instructions(self):
|
||||
"""Test --agent with instructions."""
|
||||
from praisonai.cli.features.agents import add_agents_parser
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='agents_command')
|
||||
add_agents_parser(subparsers)
|
||||
|
||||
args = parser.parse_args([
|
||||
'run',
|
||||
'--agent', 'analyst:Data Analyst:read_csv,analyze_csv',
|
||||
'--instructions', 'Be thorough and accurate',
|
||||
'--task', 'Analyze the sales data'
|
||||
])
|
||||
|
||||
assert args.instructions == 'Be thorough and accurate'
|
||||
|
||||
def test_agent_with_llm(self):
|
||||
"""Test --llm argument for model selection."""
|
||||
from praisonai.cli.features.agents import add_agents_parser
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='agents_command')
|
||||
add_agents_parser(subparsers)
|
||||
|
||||
args = parser.parse_args([
|
||||
'run',
|
||||
'--agent', 'coder:Developer:execute_command',
|
||||
'--llm', 'gpt-4o',
|
||||
'--task', 'Write a Python script'
|
||||
])
|
||||
|
||||
assert args.llm == 'gpt-4o'
|
||||
|
||||
|
||||
class TestAgentDefinitionParsing:
|
||||
"""Tests for parsing agent definitions."""
|
||||
|
||||
def test_parse_agent_definition_basic(self):
|
||||
"""Test parsing basic agent definition."""
|
||||
from praisonai.cli.features.agents import parse_agent_definition
|
||||
|
||||
result = parse_agent_definition("researcher:Research Analyst:internet_search")
|
||||
|
||||
assert result['name'] == 'researcher'
|
||||
assert result['role'] == 'Research Analyst'
|
||||
assert result['tools'] == ['internet_search']
|
||||
|
||||
def test_parse_agent_definition_multiple_tools(self):
|
||||
"""Test parsing agent with multiple tools."""
|
||||
from praisonai.cli.features.agents import parse_agent_definition
|
||||
|
||||
result = parse_agent_definition("analyst:Data Analyst:read_csv,write_csv,analyze_csv")
|
||||
|
||||
assert result['name'] == 'analyst'
|
||||
assert result['role'] == 'Data Analyst'
|
||||
assert result['tools'] == ['read_csv', 'write_csv', 'analyze_csv']
|
||||
|
||||
def test_parse_agent_definition_no_tools(self):
|
||||
"""Test parsing agent without tools."""
|
||||
from praisonai.cli.features.agents import parse_agent_definition
|
||||
|
||||
result = parse_agent_definition("helper:Assistant")
|
||||
|
||||
assert result['name'] == 'helper'
|
||||
assert result['role'] == 'Assistant'
|
||||
assert result['tools'] == []
|
||||
|
||||
def test_parse_agent_definition_with_goal(self):
|
||||
"""Test parsing agent with goal (extended format)."""
|
||||
from praisonai.cli.features.agents import parse_agent_definition
|
||||
|
||||
# Extended format: name:role:tools:goal
|
||||
result = parse_agent_definition("writer:Writer:write_file:Create high-quality content")
|
||||
|
||||
assert result['name'] == 'writer'
|
||||
assert result['role'] == 'Writer'
|
||||
assert result['tools'] == ['write_file']
|
||||
assert result.get('goal') == 'Create high-quality content'
|
||||
|
||||
|
||||
class TestMultiAgentHandler:
|
||||
"""Tests for MultiAgentHandler class."""
|
||||
|
||||
def test_handler_initialization(self):
|
||||
"""Test handler initializes correctly."""
|
||||
from praisonai.cli.features.agents import MultiAgentHandler
|
||||
|
||||
handler = MultiAgentHandler(verbose=False)
|
||||
assert handler is not None
|
||||
|
||||
def test_create_agents_from_definitions(self):
|
||||
"""Test creating agents from definition strings."""
|
||||
from praisonai.cli.features.agents import MultiAgentHandler
|
||||
|
||||
handler = MultiAgentHandler(verbose=False)
|
||||
|
||||
definitions = [
|
||||
"researcher:Research Analyst:internet_search",
|
||||
"writer:Content Writer:write_file"
|
||||
]
|
||||
|
||||
agents = handler.create_agents_from_definitions(definitions)
|
||||
|
||||
assert len(agents) == 2
|
||||
assert agents[0]['name'] == 'researcher'
|
||||
assert agents[1]['name'] == 'writer'
|
||||
|
||||
def test_run_with_mock_agents(self):
|
||||
"""Test running multi-agent task with mocked agents."""
|
||||
from praisonai.cli.features.agents import MultiAgentHandler
|
||||
|
||||
handler = MultiAgentHandler(verbose=False)
|
||||
|
||||
with patch.object(handler, '_execute_agents') as mock_execute:
|
||||
mock_execute.return_value = "Task completed successfully"
|
||||
|
||||
result = handler.run(
|
||||
agent_definitions=[
|
||||
"researcher:Researcher:internet_search"
|
||||
],
|
||||
task="Find information about AI"
|
||||
)
|
||||
|
||||
# Should have called execute
|
||||
mock_execute.assert_called_once()
|
||||
|
||||
|
||||
class TestMultiAgentExecution:
|
||||
"""Tests for multi-agent execution."""
|
||||
|
||||
def test_sequential_execution(self):
|
||||
"""Test agents execute sequentially by default."""
|
||||
from praisonai.cli.features.agents import MultiAgentHandler
|
||||
|
||||
handler = MultiAgentHandler(verbose=False)
|
||||
|
||||
# This tests the configuration, not actual execution
|
||||
config = handler.prepare_execution_config(
|
||||
agent_definitions=[
|
||||
"agent1:Role1:tool1",
|
||||
"agent2:Role2:tool2"
|
||||
],
|
||||
task="Test task",
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
assert config['process'] == 'sequential'
|
||||
assert len(config['agents']) == 2
|
||||
|
||||
def test_parallel_execution_option(self):
|
||||
"""Test parallel execution option."""
|
||||
from praisonai.cli.features.agents import MultiAgentHandler
|
||||
|
||||
handler = MultiAgentHandler(verbose=False)
|
||||
|
||||
config = handler.prepare_execution_config(
|
||||
agent_definitions=[
|
||||
"agent1:Role1:tool1",
|
||||
"agent2:Role2:tool2"
|
||||
],
|
||||
task="Test task",
|
||||
process="parallel"
|
||||
)
|
||||
|
||||
assert config['process'] == 'parallel'
|
||||
|
||||
|
||||
class TestAgentsCLIIntegration:
|
||||
"""Integration tests for agents CLI."""
|
||||
|
||||
def test_help_command(self):
|
||||
"""Test agents --help works."""
|
||||
from praisonai.cli.features.agents import add_agents_parser
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='agents_command')
|
||||
add_agents_parser(subparsers)
|
||||
|
||||
# Should not raise
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
parser.parse_args(['run', '--help'])
|
||||
assert exc_info.value.code == 0
|
||||
|
||||
def test_missing_task_error(self):
|
||||
"""Test error when task is missing."""
|
||||
from praisonai.cli.features.agents import add_agents_parser
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='agents_command')
|
||||
add_agents_parser(subparsers)
|
||||
|
||||
# Should fail without --task
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args([
|
||||
'run',
|
||||
'--agent', 'test:Tester:test_tool'
|
||||
])
|
||||
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Unit tests for Skills CLI feature.
|
||||
|
||||
Tests for:
|
||||
- Skills create command with all arguments
|
||||
- AI-generated SKILL.md content
|
||||
- Template fallback when no API key
|
||||
- scripts/skill.py auto-generation
|
||||
- Anthropic Skills API integration
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestSkillsCreateCommand:
|
||||
"""Tests for skills create command."""
|
||||
|
||||
def test_create_skill_basic(self):
|
||||
"""Test basic skill creation with name and description."""
|
||||
from praisonai.cli.features.skills import SkillsHandler
|
||||
|
||||
handler = SkillsHandler(verbose=False)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
skill_path = handler.create_skill(
|
||||
name="test-skill",
|
||||
description="A test skill",
|
||||
output_dir=tmpdir
|
||||
)
|
||||
|
||||
assert Path(skill_path).exists()
|
||||
assert (Path(skill_path) / "SKILL.md").exists()
|
||||
assert (Path(skill_path) / "scripts").is_dir()
|
||||
assert (Path(skill_path) / "references").is_dir()
|
||||
assert (Path(skill_path) / "assets").is_dir()
|
||||
|
||||
def test_create_skill_with_author(self):
|
||||
"""Test skill creation with author metadata."""
|
||||
from praisonai.cli.features.skills import SkillsHandler
|
||||
|
||||
handler = SkillsHandler(verbose=False)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
skill_path = handler.create_skill(
|
||||
name="test-skill",
|
||||
description="A test skill",
|
||||
output_dir=tmpdir,
|
||||
author="test-author"
|
||||
)
|
||||
|
||||
skill_md = (Path(skill_path) / "SKILL.md").read_text()
|
||||
assert "author: test-author" in skill_md
|
||||
|
||||
def test_create_skill_with_license(self):
|
||||
"""Test skill creation with license."""
|
||||
from praisonai.cli.features.skills import SkillsHandler
|
||||
|
||||
handler = SkillsHandler(verbose=False)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
skill_path = handler.create_skill(
|
||||
name="test-skill",
|
||||
description="A test skill",
|
||||
output_dir=tmpdir,
|
||||
license="MIT"
|
||||
)
|
||||
|
||||
skill_md = (Path(skill_path) / "SKILL.md").read_text()
|
||||
assert "license: MIT" in skill_md
|
||||
|
||||
def test_create_skill_with_compatibility(self):
|
||||
"""Test skill creation with compatibility info."""
|
||||
from praisonai.cli.features.skills import SkillsHandler
|
||||
|
||||
handler = SkillsHandler(verbose=False)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
skill_path = handler.create_skill(
|
||||
name="test-skill",
|
||||
description="A test skill",
|
||||
output_dir=tmpdir,
|
||||
compatibility="Works with Python 3.8+"
|
||||
)
|
||||
|
||||
skill_md = (Path(skill_path) / "SKILL.md").read_text()
|
||||
assert "compatibility: Works with Python 3.8+" in skill_md
|
||||
|
||||
def test_create_skill_template_mode(self):
|
||||
"""Test skill creation in template-only mode (no AI)."""
|
||||
from praisonai.cli.features.skills import SkillsHandler
|
||||
|
||||
handler = SkillsHandler(verbose=False)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
skill_path = handler.create_skill(
|
||||
name="test-skill",
|
||||
description="A test skill",
|
||||
output_dir=tmpdir,
|
||||
template=True # Force template mode
|
||||
)
|
||||
|
||||
skill_md = (Path(skill_path) / "SKILL.md").read_text()
|
||||
# Template should have placeholder content
|
||||
assert "# Test Skill" in skill_md or "# test-skill" in skill_md.lower()
|
||||
|
||||
|
||||
class TestSkillsAIGeneration:
|
||||
"""Tests for AI-generated skill content."""
|
||||
|
||||
def test_ai_generation_with_api_key(self):
|
||||
"""Test AI generation when API key is available."""
|
||||
from praisonai.cli.features.skills import SkillsHandler
|
||||
|
||||
handler = SkillsHandler(verbose=False)
|
||||
|
||||
# Mock the LLM call
|
||||
with patch.object(handler, '_generate_skill_content_with_ai') as mock_ai:
|
||||
mock_ai.return_value = {
|
||||
"skill_md": "# AI Generated\n\nAI content here",
|
||||
"skill_py": "def process():\n pass"
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Set API key
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}):
|
||||
skill_path = handler.create_skill(
|
||||
name="ai-skill",
|
||||
description="An AI-generated skill",
|
||||
output_dir=tmpdir,
|
||||
use_ai=True
|
||||
)
|
||||
|
||||
# Should have called AI generation
|
||||
if hasattr(handler, '_generate_skill_content_with_ai'):
|
||||
mock_ai.assert_called_once()
|
||||
|
||||
def test_fallback_to_template_without_api_key(self):
|
||||
"""Test fallback to template when no API key."""
|
||||
from praisonai.cli.features.skills import SkillsHandler
|
||||
|
||||
handler = SkillsHandler(verbose=False)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Remove API keys
|
||||
env_without_keys = {k: v for k, v in os.environ.items()
|
||||
if 'API_KEY' not in k.upper()}
|
||||
|
||||
with patch.dict(os.environ, env_without_keys, clear=True):
|
||||
skill_path = handler.create_skill(
|
||||
name="fallback-skill",
|
||||
description="A fallback skill",
|
||||
output_dir=tmpdir,
|
||||
use_ai=True # Request AI but no key available
|
||||
)
|
||||
|
||||
# Should still create skill with template
|
||||
assert Path(skill_path).exists()
|
||||
assert (Path(skill_path) / "SKILL.md").exists()
|
||||
|
||||
|
||||
class TestSkillsScriptGeneration:
|
||||
"""Tests for scripts/skill.py auto-generation."""
|
||||
|
||||
def test_generate_skill_script(self):
|
||||
"""Test that scripts/skill.py is generated when requested."""
|
||||
from praisonai.cli.features.skills import SkillsHandler
|
||||
|
||||
handler = SkillsHandler(verbose=False)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
skill_path = handler.create_skill(
|
||||
name="script-skill",
|
||||
description="A skill with script",
|
||||
output_dir=tmpdir,
|
||||
generate_script=True
|
||||
)
|
||||
|
||||
script_path = Path(skill_path) / "scripts" / "skill.py"
|
||||
assert script_path.exists()
|
||||
|
||||
script_content = script_path.read_text()
|
||||
assert "def " in script_content # Has at least one function
|
||||
|
||||
|
||||
class TestSkillsCLIArguments:
|
||||
"""Tests for CLI argument parsing."""
|
||||
|
||||
def test_output_dir_argument(self):
|
||||
"""Test --output-dir argument is recognized."""
|
||||
from praisonai.cli.features.skills import add_skills_parser
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='skills_command')
|
||||
add_skills_parser(subparsers)
|
||||
|
||||
# Test with --output-dir
|
||||
args = parser.parse_args(['create', '--name', 'test', '--output-dir', '/tmp/test'])
|
||||
assert args.output_dir == '/tmp/test'
|
||||
|
||||
def test_author_argument(self):
|
||||
"""Test --author argument is recognized."""
|
||||
from praisonai.cli.features.skills import add_skills_parser
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='skills_command')
|
||||
add_skills_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(['create', '--name', 'test', '--author', 'myteam'])
|
||||
assert args.author == 'myteam'
|
||||
|
||||
def test_license_argument(self):
|
||||
"""Test --license argument is recognized."""
|
||||
from praisonai.cli.features.skills import add_skills_parser
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='skills_command')
|
||||
add_skills_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(['create', '--name', 'test', '--license', 'MIT'])
|
||||
assert args.license == 'MIT'
|
||||
|
||||
def test_compatibility_argument(self):
|
||||
"""Test --compatibility argument is recognized."""
|
||||
from praisonai.cli.features.skills import add_skills_parser
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='skills_command')
|
||||
add_skills_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(['create', '--name', 'test', '--compatibility', 'Python 3.8+'])
|
||||
assert args.compatibility == 'Python 3.8+'
|
||||
|
||||
def test_template_argument(self):
|
||||
"""Test --template argument is recognized."""
|
||||
from praisonai.cli.features.skills import add_skills_parser
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='skills_command')
|
||||
add_skills_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(['create', '--name', 'test', '--template'])
|
||||
assert args.template == True
|
||||
|
||||
def test_prompt_argument_for_ai_generation(self):
|
||||
"""Test prompt argument for AI generation."""
|
||||
from praisonai.cli.features.skills import add_skills_parser
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='skills_command')
|
||||
add_skills_parser(subparsers)
|
||||
|
||||
# Prompt can be positional or via --description
|
||||
args = parser.parse_args(['create', '--name', 'test', '--description', 'Process CSV files'])
|
||||
assert args.description == 'Process CSV files'
|
||||
|
||||
|
||||
class TestMultiAgentCLI:
|
||||
"""Tests for multi-agent CLI command."""
|
||||
|
||||
def test_agents_command_exists(self):
|
||||
"""Test that agents command is recognized."""
|
||||
# This will be implemented after skills fixes
|
||||
pass
|
||||
|
||||
def test_define_multiple_agents(self):
|
||||
"""Test defining multiple agents with instructions and tools."""
|
||||
# This will be implemented after skills fixes
|
||||
pass
|
||||
|
||||
|
||||
class TestAnthropicSkillsAPI:
|
||||
"""Tests for Anthropic Skills API integration."""
|
||||
|
||||
def test_upload_skill_command(self):
|
||||
"""Test uploading skill to Anthropic."""
|
||||
# This will be implemented after skills fixes
|
||||
pass
|
||||
|
||||
def test_list_skills_command(self):
|
||||
"""Test listing skills from Anthropic."""
|
||||
# This will be implemented after skills fixes
|
||||
pass
|
||||
Reference in new issue
Block a user