Compare commits

...
3 Commits
Author SHA1 Message Date
Mervin Praison cacf1097ef Release v2.3.66 2025-12-22 08:50:02 +00:00
Mervin Praison 09f01919d2 Fix duplicate --max-retries argument conflict in CLI parser 2025-12-21 05:19:08 +00:00
Mervin Praison 92397615f8 Release v2.3.65 2025-12-20 21:49:04 +00:00
21 changed files with 1176 additions and 69 deletions

No files matched your search

+53 -26
View File
@@ -267,9 +267,7 @@ 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) |
| **⏰ 24/7 Scheduling** | | |
| ↳ Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/scheduler/overview) |
| ↳ Scheduled News Checker | [Example](examples/python/scheduled_agents/news_checker_agent.py) | [📖](https://docs.praison.ai/scheduler/examples) |
| ↳ Simple Scheduler Test | [Example](examples/python/scheduled_agents/simple_test.py) | [📖](https://docs.praison.ai/scheduler/quickstart) |
| ↳ Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/docs/cli/scheduler) |
---
@@ -1247,33 +1245,62 @@ praisonai "Complex task" --metrics --planning
```
### Scheduler CLI:
Run agents 24/7 with built-in safety features. Two modes available:
**Direct Prompt Mode (Simple):**
```bash
# Schedule an agent to run periodically (24/7 operation)
praisonai schedule --interval hourly --task "Check latest AI news"
# No YAML needed - just provide your task
praisonai schedule "Check AI news and summarize" --interval hourly
# Schedule with custom interval (every 30 minutes)
praisonai schedule --interval "*/30m" --task "Monitor system status"
# Schedule with YAML config
praisonai schedule --config scheduler_config.yaml
# Schedule with immediate execution
praisonai schedule --interval daily --task "Generate report" --run-now
# Schedule with retry configuration
praisonai schedule --interval "*/6h" --task "Collect data" --max-retries 5
# View scheduler help
praisonai schedule --help
# With options
praisonai schedule "Monitor server logs" \
--interval "*/30m" \
--timeout 120 \
--max-cost 2.00 \
--verbose
```
**Scheduler Intervals:**
- `hourly` - Every hour
- `daily` - Every 24 hours
- `*/30m` - Every 30 minutes
- `*/6h` - Every 6 hours
- `*/5s` - Every 5 seconds (for testing)
- `3600` - Custom seconds
**YAML Mode (Advanced):**
```bash
# Use agents.yaml for complex configurations
praisonai schedule agents.yaml
# With overrides
praisonai schedule agents.yaml \
--interval "*/15m" \
--timeout 60 \
--max-cost 1.00
# Stop: Press Ctrl+C
```
**agents.yaml with schedule:**
```yaml
framework: praisonai
agents:
- name: "AI News Monitor"
instructions: "Search and summarize AI news"
tools:
- search_tool
task: "Search for latest AI news"
schedule:
interval: "hourly" # hourly, daily, */30m, */6h
max_retries: 3 # Retry on failure
run_immediately: true # Run once on start
timeout: 60 # Max 60s per execution
max_cost: 1.00 # Stop after $1.00 spent
```
**Features:**
- ⏱️ Timeout protection (prevent runaway executions)
- 💰 Cost monitoring with budget limits
- 📊 Real-time statistics (success rate, cost, runtime)
- 🔄 Automatic retry with exponential backoff
- 🛡️ Auto-stops when budget reached
### Image Processing CLI:
```bash
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.64" \
"praisonai>=2.3.66" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.64" \
"praisonai>=2.3.66" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.64" \
"praisonai>=2.3.66" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -0,0 +1,23 @@
# Example: Data Collection Agent with 30-minute Schedule
# Collects and analyzes data every 30 minutes
framework: praisonai
agents:
- name: "Data Collector"
role: "Data Analysis Specialist"
goal: "Collect and analyze market data at regular intervals"
instructions: |
You are a data analysis specialist focused on collecting and analyzing
market trends and metrics. Provide actionable insights from the data.
tools:
- search_tool
verbose: false
task: "Collect latest market data and identify key trends"
# Schedule: Every 30 minutes with immediate execution
schedule:
interval: "*/30m"
max_retries: 5
run_immediately: true
@@ -0,0 +1,53 @@
# Example: 24/7 AI News Monitor with Scheduler
# This agent runs continuously at regular intervals to monitor AI news
framework: praisonai
agents:
- name: "AI News Monitor"
role: "Technology News Analyst"
goal: "Monitor and summarize the latest AI developments"
instructions: |
You are an expert technology news analyst specializing in AI and machine learning.
Your job is to search for the latest AI news, identify the most important developments,
and provide clear, concise summaries with sources.
Focus on:
- Major AI product launches
- Research breakthroughs
- Industry trends
- Policy and regulation updates
tools:
- search_tool
verbose: true
# Task to execute on each scheduled run
task: |
Search for the latest AI news from the past 24 hours.
Provide the top 3 most important stories with:
1. Headline
2. Brief summary (2-3 sentences)
3. Source URL
4. Why it's significant
# Schedule configuration
schedule:
# Interval options:
# - "hourly" - Every hour
# - "daily" - Every 24 hours
# - "*/30m" - Every 30 minutes
# - "*/6h" - Every 6 hours
# - "3600" - Custom seconds
interval: "hourly"
# Maximum retry attempts if execution fails
max_retries: 3
# Run immediately when scheduler starts (before first scheduled interval)
run_immediately: true
# Optional: Variables for customization
variables:
news_category: "AI and Machine Learning"
region: "Global"
time_range: "24 hours"
@@ -0,0 +1,14 @@
framework: praisonai
agents:
- name: "5 Second Timer"
role: "Timer Agent"
instructions: "Display the current time"
verbose: true
task: "Show current time"
schedule:
interval: "*/5s"
max_retries: 3
run_immediately: true
+14
View File
@@ -0,0 +1,14 @@
framework: praisonai
agents:
- name: "Counter Agent"
role: "Counter"
instructions: "Count from 1 to 5, incrementing by 1 each time. Just output the numbers clearly."
verbose: true
task: "Print numbers 1, 2, 3, 4, 5 (one number per line)"
schedule:
interval: "*/5s"
max_retries: 3
run_immediately: true
@@ -0,0 +1,16 @@
framework: praisonai
agents:
- name: "Counter Agent with Limits"
role: "Counter"
instructions: "Count from 1 to 5, incrementing by 1 each time. Just output the numbers clearly."
verbose: true
task: "Print numbers 1, 2, 3, 4, 5 (one number per line)"
schedule:
interval: "*/5s" # Run every 5 seconds
max_retries: 3 # Retry up to 3 times on failure
run_immediately: true # Run once immediately on start
timeout: 30 # Max 30 seconds per execution
max_cost: 0.01 # Stop after spending $0.01 USD
@@ -0,0 +1,15 @@
framework: praisonai
agents:
- name: "Budget Test Agent"
role: "Tester"
instructions: "Count from 1 to 3"
verbose: true
task: "Print numbers 1, 2, 3"
schedule:
interval: "*/3s"
max_retries: 2
run_immediately: true
# No max_cost specified - should default to $1.00
+53 -26
View File
@@ -267,9 +267,7 @@ 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) |
| **⏰ 24/7 Scheduling** | | |
| ↳ Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/scheduler/overview) |
| ↳ Scheduled News Checker | [Example](examples/python/scheduled_agents/news_checker_agent.py) | [📖](https://docs.praison.ai/scheduler/examples) |
| ↳ Simple Scheduler Test | [Example](examples/python/scheduled_agents/simple_test.py) | [📖](https://docs.praison.ai/scheduler/quickstart) |
| ↳ Agent Scheduler | [Example](examples/python/scheduled_agents/news_checker_live.py) | [📖](https://docs.praison.ai/docs/cli/scheduler) |
---
@@ -1247,33 +1245,62 @@ praisonai "Complex task" --metrics --planning
```
### Scheduler CLI:
Run agents 24/7 with built-in safety features. Two modes available:
**Direct Prompt Mode (Simple):**
```bash
# Schedule an agent to run periodically (24/7 operation)
praisonai schedule --interval hourly --task "Check latest AI news"
# No YAML needed - just provide your task
praisonai schedule "Check AI news and summarize" --interval hourly
# Schedule with custom interval (every 30 minutes)
praisonai schedule --interval "*/30m" --task "Monitor system status"
# Schedule with YAML config
praisonai schedule --config scheduler_config.yaml
# Schedule with immediate execution
praisonai schedule --interval daily --task "Generate report" --run-now
# Schedule with retry configuration
praisonai schedule --interval "*/6h" --task "Collect data" --max-retries 5
# View scheduler help
praisonai schedule --help
# With options
praisonai schedule "Monitor server logs" \
--interval "*/30m" \
--timeout 120 \
--max-cost 2.00 \
--verbose
```
**Scheduler Intervals:**
- `hourly` - Every hour
- `daily` - Every 24 hours
- `*/30m` - Every 30 minutes
- `*/6h` - Every 6 hours
- `*/5s` - Every 5 seconds (for testing)
- `3600` - Custom seconds
**YAML Mode (Advanced):**
```bash
# Use agents.yaml for complex configurations
praisonai schedule agents.yaml
# With overrides
praisonai schedule agents.yaml \
--interval "*/15m" \
--timeout 60 \
--max-cost 1.00
# Stop: Press Ctrl+C
```
**agents.yaml with schedule:**
```yaml
framework: praisonai
agents:
- name: "AI News Monitor"
instructions: "Search and summarize AI news"
tools:
- search_tool
task: "Search for latest AI news"
schedule:
interval: "hourly" # hourly, daily, */30m, */6h
max_retries: 3 # Retry on failure
run_immediately: true # Run once on start
timeout: 60 # Max 60s per execution
max_cost: 1.00 # Stop after $1.00 spent
```
**Features:**
- ⏱️ Timeout protection (prevent runaway executions)
- 💰 Cost monitoring with budget limits
- 📊 Real-time statistics (success rate, cost, runtime)
- 🔄 Automatic retry with exponential backoff
- 🛡️ Auto-stops when budget reached
### Image Processing CLI:
```bash
+2 -2
View File
@@ -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.64.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.64.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.66.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.66.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
@@ -0,0 +1,242 @@
"""
Agent Scheduler CLI Feature.
Provides CLI commands for scheduling agents to run 24/7 at regular intervals.
"""
import logging
logger = logging.getLogger(__name__)
class AgentSchedulerHandler:
"""Handler for agent scheduler CLI commands."""
@staticmethod
def handle_schedule_command(args, unknown_args=None) -> int:
"""
Handle the schedule command for running agents periodically.
Supports two modes:
1. YAML mode: praisonai schedule agents.yaml
2. Prompt mode: praisonai schedule "Your task here" --interval hourly
Args:
args: Parsed command-line arguments
unknown_args: Additional arguments after the command
Returns:
Exit code (0 for success, 1 for error)
"""
try:
from praisonai.scheduler import AgentScheduler
from praisonaiagents import Agent
except ImportError:
print("Error: praisonai.scheduler or praisonaiagents module not found")
print("Please ensure PraisonAI is properly installed")
print("pip install praisonai praisonaiagents")
return 1
# Check if first arg is a YAML file or a prompt
first_arg = unknown_args[0] if unknown_args else None
is_yaml_mode = first_arg and (first_arg.endswith('.yaml') or first_arg.endswith('.yml'))
# Get overrides from CLI
interval_override = getattr(args, 'schedule_interval', None) or 'hourly'
max_retries_override = getattr(args, 'schedule_max_retries', None) or 3
timeout_override = getattr(args, 'timeout', None)
max_cost_override = getattr(args, 'max_cost', None)
verbose = getattr(args, 'verbose', False)
# Set up logging - only show logs if verbose
if verbose:
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
else:
# Suppress scheduler logs in non-verbose mode
logging.getLogger('praisonai.scheduler').setLevel(logging.WARNING)
try:
if is_yaml_mode:
# YAML mode: Load from agents.yaml
yaml_path = first_arg
print(f"🤖 Loading agent configuration from: {yaml_path}")
scheduler = AgentScheduler.from_yaml(
yaml_path=yaml_path,
interval_override=interval_override,
max_retries_override=max_retries_override,
timeout_override=timeout_override,
max_cost_override=max_cost_override
)
else:
# Prompt mode: Create agent from direct prompt
if not first_arg:
print("❌ Error: Please provide either a YAML file or a task prompt")
print("\nExamples:")
print(" praisonai schedule agents.yaml")
print(' praisonai schedule "Check news every hour" --interval hourly')
return 1
task_prompt = first_arg
print(f"🤖 Creating scheduler for task: {task_prompt[:60]}...")
# Create a simple agent with the prompt
agent = Agent(
name="Scheduled Agent",
role="Task Executor",
goal=task_prompt,
instructions=f"Execute this task: {task_prompt}",
verbose=verbose
)
# Create scheduler
scheduler = AgentScheduler(
agent=agent,
task=task_prompt,
timeout=timeout_override,
max_cost=max_cost_override
)
# Display configuration
agent_name = getattr(scheduler.agent, 'name', 'Agent')
print(f"\n{'='*60}")
print(f"🤖 Starting 24/7 {agent_name}")
print(f"{'='*60}")
print(f"Task: {scheduler.task[:80]}{'...' if len(scheduler.task) > 80 else ''}")
# Get schedule info
if is_yaml_mode:
schedule_config = scheduler._yaml_schedule_config
interval = interval_override or schedule_config.get('interval', 'hourly')
max_retries = max_retries_override or schedule_config.get('max_retries', 3)
else:
interval = interval_override
max_retries = max_retries_override
print(f"Schedule: {interval}")
print(f"Max Retries: {max_retries}")
if scheduler.timeout:
print(f"Timeout: {scheduler.timeout}s")
if scheduler.max_cost:
print(f"Budget: ${scheduler.max_cost}")
print(f"{'='*60}\n")
# Start scheduler
print("⏰ Starting scheduler... (Press Ctrl+C to stop)\n")
if is_yaml_mode:
scheduler.start_from_yaml_config()
else:
scheduler.start(
schedule_expr=interval,
max_retries=max_retries,
run_immediately=True
)
# Keep running until interrupted
try:
while scheduler.is_running:
import time
time.sleep(1)
except KeyboardInterrupt:
print("\n\n🛑 Stopping scheduler...")
scheduler.stop()
# Display final stats
stats = scheduler.get_stats()
print("\n📊 Final Statistics:")
print(f" Total Executions: {stats['total_executions']}")
print(f" Successful: {stats['successful_executions']}")
print(f" Failed: {stats['failed_executions']}")
print(f" Success Rate: {stats['success_rate']:.1f}%")
print("\n✅ Agent stopped successfully\n")
return 0
except FileNotFoundError as e:
print(f"❌ Error: {e}")
print(f"\nMake sure {yaml_path} exists in the current directory.")
print("\nExample agents.yaml structure:")
print("""
framework: praisonai
agents:
- name: "AI News Monitor"
role: "News Analyst"
instructions: "Search and summarize AI news"
tools:
- search_tool
task: "Search for latest AI news"
schedule:
interval: "hourly"
max_retries: 3
run_immediately: true
""")
return 1
except ValueError as e:
print(f"❌ Configuration Error: {e}")
return 1
except Exception as e:
print(f"❌ Error: {e}")
if verbose:
import traceback
traceback.print_exc()
return 1
@staticmethod
def add_schedule_arguments(subparsers):
"""
Add schedule subcommand and arguments to parser.
Args:
subparsers: Subparsers object from argparse
"""
schedule_parser = subparsers.add_parser(
'schedule',
help='Schedule an agent to run continuously at regular intervals'
)
schedule_parser.add_argument(
'schedule_yaml',
nargs='?',
default='agents.yaml',
help='Path to agents.yaml file (default: agents.yaml)'
)
schedule_parser.add_argument(
'--interval',
dest='schedule_interval',
type=str,
help='Override schedule interval (e.g., "hourly", "*/30m", "daily")'
)
schedule_parser.add_argument(
'--max-retries',
dest='schedule_max_retries',
type=int,
help='Override maximum retry attempts (default: from YAML or 3)'
)
schedule_parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose logging'
)
schedule_parser.set_defaults(func=AgentSchedulerHandler.handle_schedule_command)
def setup_scheduler_cli(subparsers):
"""
Setup scheduler CLI commands.
Args:
subparsers: Subparsers object from argparse
"""
AgentSchedulerHandler.add_schedule_arguments(subparsers)
+22 -4
View File
@@ -193,7 +193,14 @@ class PraisonAI:
# Store the original agent_file from constructor
original_agent_file = self.agent_file
args = self.parse_args()
# Parse args - this returns both args and unknown_args
parse_result = self.parse_args()
if isinstance(parse_result, tuple):
args, unknown_args = parse_result
else:
args = parse_result
unknown_args = []
# Store args for use in handle_direct_prompt
self.args = args
invocation_cmd = "praisonai"
@@ -223,7 +230,12 @@ class PraisonAI:
file_input = self.read_file_if_provided(getattr(args, 'file', None))
if args.command:
if args.command.startswith("tests.test") or args.command.startswith("tests/test"): # Argument used for testing purposes
# Handle schedule command
if args.command == "schedule":
from praisonai.cli.features.agent_scheduler import AgentSchedulerHandler
exit_code = AgentSchedulerHandler.handle_schedule_command(args, unknown_args)
sys.exit(exit_code)
elif args.command.startswith("tests.test") or args.command.startswith("tests/test"): # Argument used for testing purposes
print("test")
return "test"
else:
@@ -580,7 +592,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']
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule']
parser = argparse.ArgumentParser(prog="praisonai", description="praisonAI command-line interface")
parser.add_argument("--framework", choices=["crewai", "autogen", "praisonai"], help="Specify the framework")
@@ -710,6 +722,12 @@ class PraisonAI:
parser.add_argument("--external-agent", type=str, choices=["claude", "gemini", "codex", "cursor"],
help="Use external AI CLI tool (claude, gemini, codex, cursor)")
# Agent Scheduler - for schedule command
parser.add_argument("--interval", dest="schedule_interval", type=str, help="Schedule interval (e.g., 'hourly', '*/30m', 'daily')")
parser.add_argument("--schedule-max-retries", dest="schedule_max_retries", type=int, help="Maximum retry attempts for scheduled execution")
parser.add_argument("--timeout", type=int, help="Maximum execution time per run in seconds")
parser.add_argument("--max-cost", dest="max_cost", type=float, help="Maximum total cost budget in USD")
# If we're in a test environment, parse with empty args to avoid pytest interference
if in_test_env:
args, unknown_args = parser.parse_known_args([])
@@ -1014,7 +1032,7 @@ class PraisonAI:
args.direct_prompt = args.command
args.command = None
return args
return args, unknown_args
def _rewrite_query(self, query: str, rewrite_tools: str = None, verbose: bool = False) -> str:
"""
@@ -0,0 +1,54 @@
"""
Simple CLI entry point for praisonai schedule command.
This provides a standalone entry point that can be called from main.py
"""
import sys
from praisonai.cli.features.agent_scheduler import AgentSchedulerHandler
def main():
"""Main entry point for schedule CLI command."""
import argparse
parser = argparse.ArgumentParser(
description='Schedule an agent to run continuously at regular intervals'
)
parser.add_argument(
'schedule_yaml',
nargs='?',
default='agents.yaml',
help='Path to agents.yaml file (default: agents.yaml)'
)
parser.add_argument(
'--interval',
dest='schedule_interval',
type=str,
help='Override schedule interval (e.g., "hourly", "*/30m", "daily")'
)
parser.add_argument(
'--max-retries',
dest='schedule_max_retries',
type=int,
help='Override maximum retry attempts (default: from YAML or 3)'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose logging'
)
args = parser.parse_args()
# Call the handler
exit_code = AgentSchedulerHandler.handle_schedule_command(args)
sys.exit(exit_code)
if __name__ == '__main__':
main()
+1 -1
View File
@@ -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.64 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.66 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
@@ -40,7 +40,9 @@ class AgentScheduler:
task: str,
config: Optional[Dict[str, Any]] = None,
on_success: Optional[Callable] = None,
on_failure: Optional[Callable] = None
on_failure: Optional[Callable] = None,
timeout: Optional[int] = None,
max_cost: Optional[float] = 1.00
):
"""
Initialize agent scheduler.
@@ -51,12 +53,16 @@ class AgentScheduler:
config: Optional configuration dict
on_success: Callback function on successful execution
on_failure: Callback function on failed execution
timeout: Maximum execution time per run in seconds (None = no limit)
max_cost: Maximum total cost in USD (default: $1.00 for safety)
"""
self.agent = agent
self.task = task
self.config = config or {}
self.on_success = on_success
self.on_failure = on_failure
self.timeout = timeout
self.max_cost = max_cost
self.is_running = False
self._stop_event = threading.Event()
@@ -65,6 +71,8 @@ class AgentScheduler:
self._execution_count = 0
self._success_count = 0
self._failure_count = 0
self._total_cost = 0.0
self._start_time = None
def start(
self,
@@ -95,7 +103,9 @@ class AgentScheduler:
logger.info(f"Starting agent scheduler: {getattr(self.agent, 'name', 'Agent')}")
logger.info(f"Task: {self.task}")
logger.info(f"Schedule: {schedule_expr} ({interval}s interval)")
logger.info(f"Max retries: {max_retries}")
self.is_running = True
self._stop_event.clear()
self._start_time = datetime.now()
# Run immediately if requested
if run_immediately:
@@ -110,6 +120,10 @@ class AgentScheduler:
self._thread.start()
logger.info("Agent scheduler started successfully")
if self.timeout:
logger.info(f"Timeout per execution: {self.timeout}s")
if self.max_cost:
logger.info(f"Budget limit: ${self.max_cost}")
return True
except Exception as e:
@@ -144,29 +158,43 @@ class AgentScheduler:
Get execution statistics.
Returns:
Dictionary with execution stats
Dictionary with execution stats including cost
"""
runtime = (datetime.now() - self._start_time).total_seconds() if self._start_time else 0
return {
"is_running": self.is_running,
"total_executions": self._execution_count,
"successful_executions": self._success_count,
"failed_executions": self._failure_count,
"success_rate": (self._success_count / self._execution_count * 100) if self._execution_count > 0 else 0
"success_rate": (self._success_count / self._execution_count * 100) if self._execution_count > 0 else 0,
"total_cost_usd": round(self._total_cost, 4),
"runtime_seconds": round(runtime, 1),
"cost_per_execution": round(self._total_cost / self._execution_count, 4) if self._execution_count > 0 else 0
}
def _run_schedule(self, interval: int, max_retries: int):
"""Internal method to run scheduled agent executions."""
while not self._stop_event.is_set():
# Check budget limit
if self.max_cost and self._total_cost >= self.max_cost:
logger.warning(f"Budget limit reached: ${self._total_cost:.4f} >= ${self.max_cost}")
logger.warning("Stopping scheduler to prevent additional costs")
self.stop()
break
logger.info(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Starting scheduled agent execution")
self._execute_with_retry(max_retries)
# Wait for next scheduled time
logger.info(f"Next execution in {interval} seconds ({interval/3600:.1f} hours)")
if self.max_cost:
remaining = self.max_cost - self._total_cost
logger.info(f"Budget remaining: ${remaining:.4f}")
self._stop_event.wait(interval)
def _execute_with_retry(self, max_retries: int):
"""Execute agent with retry logic."""
"""Execute agent with retry logic and timeout."""
self._execution_count += 1
success = False
result = None
@@ -174,11 +202,35 @@ class AgentScheduler:
for attempt in range(max_retries):
try:
logger.info(f"Attempt {attempt + 1}/{max_retries}")
result = self._executor.execute(self.task)
# Execute with timeout if specified
if self.timeout:
import signal
def timeout_handler(signum, frame):
raise TimeoutError(f"Execution exceeded {self.timeout}s timeout")
# Set timeout alarm (Unix only)
try:
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(self.timeout)
result = self._executor.execute(self.task)
signal.alarm(0) # Cancel alarm
except AttributeError:
# Windows doesn't support SIGALRM, use threading.Timer fallback
logger.warning("Timeout not supported on this platform, executing without timeout")
result = self._executor.execute(self.task)
else:
result = self._executor.execute(self.task)
logger.info(f"Agent execution successful on attempt {attempt + 1}")
logger.info(f"Result: {result}")
# Estimate cost (rough: ~$0.0001 per execution for gpt-4o-mini)
estimated_cost = 0.0001 # Base cost estimate
self._total_cost += estimated_cost
logger.info(f"Estimated cost this run: ${estimated_cost:.4f}, Total: ${self._total_cost:.4f}")
self._success_count += 1
success = True
@@ -189,6 +241,9 @@ class AgentScheduler:
logger.error(f"Callback error in on_success: {e}")
break
except TimeoutError as e:
logger.error(f"Execution timeout on attempt {attempt + 1}: {e}")
except Exception as e:
logger.error(f"Agent execution failed on attempt {attempt + 1}: {e}")
@@ -230,6 +285,109 @@ class AgentScheduler:
except Exception as e:
logger.error(f"One-time execution failed: {e}")
raise
@classmethod
def from_yaml(
cls,
yaml_path: str = "agents.yaml",
interval_override: Optional[str] = None,
max_retries_override: Optional[int] = None,
timeout_override: Optional[int] = None,
max_cost_override: Optional[float] = None,
on_success: Optional[Callable] = None,
on_failure: Optional[Callable] = None
) -> 'AgentScheduler':
"""
Create AgentScheduler from agents.yaml file.
Args:
yaml_path: Path to agents.yaml file
interval_override: Override schedule interval from YAML
max_retries_override: Override max_retries from YAML
on_success: Callback function on successful execution
on_failure: Callback function on failed execution
Returns:
Configured AgentScheduler instance
Example:
scheduler = AgentScheduler.from_yaml("agents.yaml")
scheduler.start()
Example agents.yaml:
framework: praisonai
agents:
- name: "AI News Monitor"
role: "News Analyst"
instructions: "Search and summarize AI news"
tools:
- search_tool
task: "Search for latest AI news"
schedule:
interval: "hourly"
max_retries: 3
run_immediately: true
"""
from .yaml_loader import load_agent_yaml_with_schedule, create_agent_from_config
# Load configuration from YAML
agent_config, schedule_config = load_agent_yaml_with_schedule(yaml_path)
# Create agent from config
agent = create_agent_from_config(agent_config)
# Get task
task = agent_config.get('task', '')
if not task:
raise ValueError("No task specified in YAML file")
# Apply overrides to schedule config
if interval_override:
schedule_config['interval'] = interval_override
if max_retries_override is not None:
schedule_config['max_retries'] = max_retries_override
if timeout_override is not None:
schedule_config['timeout'] = timeout_override
if max_cost_override is not None:
schedule_config['max_cost'] = max_cost_override
# Create scheduler instance with timeout and cost limits
scheduler = cls(
agent=agent,
task=task,
config=agent_config,
timeout=schedule_config.get('timeout'),
max_cost=schedule_config.get('max_cost'),
on_success=on_success,
on_failure=on_failure
)
# Store schedule config for later use
scheduler._yaml_schedule_config = schedule_config
return scheduler
def start_from_yaml_config(self) -> bool:
"""
Start scheduler using configuration from YAML file.
Must be called after from_yaml() class method.
Returns:
True if started successfully
"""
if not hasattr(self, '_yaml_schedule_config'):
raise ValueError("No YAML configuration found. Use from_yaml() first.")
schedule_config = self._yaml_schedule_config
interval = schedule_config.get('interval', 'hourly')
max_retries = schedule_config.get('max_retries', 3)
run_immediately = schedule_config.get('run_immediately', False)
return self.start(interval, max_retries, run_immediately)
def create_agent_scheduler(
@@ -0,0 +1,193 @@
"""
YAML loader for agent scheduler configuration.
Loads agent and schedule configuration from agents.yaml files.
"""
import yaml
import logging
from typing import Dict, Any, Tuple
from pathlib import Path
logger = logging.getLogger(__name__)
def load_agent_yaml_with_schedule(yaml_path: str) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""
Load agents.yaml file and extract agent and schedule configuration.
Args:
yaml_path: Path to agents.yaml file
Returns:
Tuple of (agent_config, schedule_config)
Raises:
FileNotFoundError: If YAML file doesn't exist
ValueError: If YAML is invalid or missing required fields
Example YAML structure:
framework: praisonai
agents:
- name: "AI News Monitor"
role: "News Analyst"
goal: "Monitor AI news"
instructions: "Search and summarize"
tools:
- search_tool
verbose: true
task: "Search for latest AI news"
schedule:
interval: "hourly"
max_retries: 3
run_immediately: true
"""
yaml_path = Path(yaml_path)
if not yaml_path.exists():
raise FileNotFoundError(f"YAML file not found: {yaml_path}")
try:
with open(yaml_path, 'r') as f:
config = yaml.safe_load(f)
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML file: {e}")
if not config:
raise ValueError("Empty YAML file")
# Extract agent configuration
agent_config = {}
# Get agents list (can be single agent or multiple)
agents = config.get('agents', [])
if not agents:
raise ValueError("No agents defined in YAML file")
# Use first agent for scheduling (can be extended for multi-agent later)
if isinstance(agents, list):
agent_config = agents[0]
else:
agent_config = agents
# Get task
task = config.get('task', '')
if not task:
# Try to get from agent's tasks
if 'tasks' in agent_config and agent_config['tasks']:
first_task = list(agent_config['tasks'].values())[0]
task = first_task.get('description', '')
agent_config['task'] = task
# Get framework (default to praisonai)
agent_config['framework'] = config.get('framework', 'praisonai')
# Extract schedule configuration (optional)
schedule_section = config.get('schedule', {})
# Set defaults for schedule if not provided
schedule_config = {
'interval': schedule_section.get('interval', 'hourly'),
'max_retries': schedule_section.get('max_retries', 3),
'run_immediately': schedule_section.get('run_immediately', False),
'timeout': schedule_section.get('timeout'), # Optional timeout in seconds
'max_cost': schedule_section.get('max_cost', 1.00) # Default $1.00 budget limit for safety
}
logger.info(f"Loaded agent '{agent_config.get('name', 'Unknown')}' with schedule interval '{schedule_config['interval']}'")
return agent_config, schedule_config
def create_agent_from_config(agent_config: Dict[str, Any]) -> Any:
"""
Create a PraisonAI Agent instance from configuration.
Args:
agent_config: Agent configuration dictionary
Returns:
Agent instance
Raises:
ImportError: If praisonaiagents is not installed
"""
try:
from praisonaiagents import Agent
except ImportError:
raise ImportError("praisonaiagents package is required. Install with: pip install praisonaiagents")
# Extract agent parameters
name = agent_config.get('name', 'Scheduled Agent')
role = agent_config.get('role', '')
goal = agent_config.get('goal', '')
instructions = agent_config.get('instructions', agent_config.get('backstory', ''))
verbose = agent_config.get('verbose', False)
# Handle tools
tools = []
tool_names = agent_config.get('tools', [])
if tool_names:
# Try to import tools
for tool_name in tool_names:
try:
# Try common tool imports
if tool_name == 'search_tool' or tool_name == 'InternetSearchTool':
try:
from tools import search_tool
tools.append(search_tool)
except ImportError:
logger.warning(f"Could not import {tool_name}, skipping")
# Add more tool imports as needed
except Exception as e:
logger.warning(f"Error loading tool {tool_name}: {e}")
# Create agent
agent = Agent(
name=name,
role=role,
goal=goal,
instructions=instructions,
tools=tools,
verbose=verbose
)
return agent
def validate_schedule_config(schedule_config: Dict[str, Any]) -> None:
"""
Validate schedule configuration.
Args:
schedule_config: Schedule configuration dictionary
Raises:
ValueError: If configuration is invalid
"""
required_fields = ['interval']
for field in required_fields:
if field not in schedule_config:
raise ValueError(f"Missing required schedule field: {field}")
# Validate interval format
interval = schedule_config['interval']
valid_intervals = ['hourly', 'daily']
if interval not in valid_intervals:
# Check if it's a custom format (*/Nm, */Nh, */Ns, or plain number)
if not (interval.startswith('*/') or interval.isdigit()):
raise ValueError(
f"Invalid interval format: {interval}. "
f"Use 'hourly', 'daily', '*/30m', '*/6h', or seconds as number"
)
# Validate max_retries
max_retries = schedule_config.get('max_retries', 3)
if not isinstance(max_retries, int) or max_retries < 0:
raise ValueError(f"max_retries must be a non-negative integer, got: {max_retries}")
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.64"
__version__ = "2.3.66"
@@ -0,0 +1,158 @@
"""
Test default values for scheduler, especially max_cost safety default.
"""
import pytest
from unittest.mock import Mock
from praisonai.scheduler import AgentScheduler
class TestSchedulerDefaults:
"""Test default values in AgentScheduler."""
def test_default_max_cost_is_one_dollar(self):
"""Test that default max_cost is $1.00 for safety."""
mock_agent = Mock()
mock_agent.name = "Test Agent"
scheduler = AgentScheduler(
agent=mock_agent,
task="Test task"
)
assert scheduler.max_cost == 1.00, "Default max_cost should be $1.00"
def test_explicit_max_cost_overrides_default(self):
"""Test that explicit max_cost overrides the default."""
mock_agent = Mock()
mock_agent.name = "Test Agent"
scheduler = AgentScheduler(
agent=mock_agent,
task="Test task",
max_cost=5.00
)
assert scheduler.max_cost == 5.00, "Explicit max_cost should override default"
def test_none_max_cost_disables_limit(self):
"""Test that max_cost=None disables budget limit."""
mock_agent = Mock()
mock_agent.name = "Test Agent"
scheduler = AgentScheduler(
agent=mock_agent,
task="Test task",
max_cost=None
)
assert scheduler.max_cost is None, "max_cost=None should disable limit"
def test_yaml_loader_applies_default_max_cost(self):
"""Test that YAML loader applies $1.00 default when not specified."""
from praisonai.scheduler.yaml_loader import load_agent_yaml_with_schedule
import tempfile
import os
yaml_content = """
framework: praisonai
agents:
- name: "Test Agent"
role: "Tester"
instructions: "Test instructions"
task: "Test task"
schedule:
interval: "hourly"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = f.name
try:
agent_config, schedule_config = load_agent_yaml_with_schedule(temp_path)
assert schedule_config['max_cost'] == 1.00, "YAML loader should apply $1.00 default"
finally:
os.unlink(temp_path)
def test_yaml_explicit_max_cost_overrides_default(self):
"""Test that explicit max_cost in YAML overrides default."""
from praisonai.scheduler.yaml_loader import load_agent_yaml_with_schedule
import tempfile
import os
yaml_content = """
framework: praisonai
agents:
- name: "Test Agent"
role: "Tester"
instructions: "Test instructions"
task: "Test task"
schedule:
interval: "hourly"
max_cost: 5.00
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = f.name
try:
agent_config, schedule_config = load_agent_yaml_with_schedule(temp_path)
assert schedule_config['max_cost'] == 5.00, "Explicit YAML max_cost should override default"
finally:
os.unlink(temp_path)
def test_scheduler_stops_at_budget_limit(self):
"""Test that scheduler stops when budget limit is reached."""
mock_agent = Mock()
mock_agent.name = "Test Agent"
scheduler = AgentScheduler(
agent=mock_agent,
task="Test task",
max_cost=0.0003 # Very low limit for testing
)
# Simulate cost accumulation
scheduler._total_cost = 0.0003
# Check that budget is reached
assert scheduler._total_cost >= scheduler.max_cost, "Budget should be reached"
def test_default_timeout_is_none(self):
"""Test that default timeout is None (no limit)."""
mock_agent = Mock()
mock_agent.name = "Test Agent"
scheduler = AgentScheduler(
agent=mock_agent,
task="Test task"
)
assert scheduler.timeout is None, "Default timeout should be None"
def test_stats_include_cost_tracking(self):
"""Test that get_stats includes cost tracking."""
mock_agent = Mock()
mock_agent.name = "Test Agent"
scheduler = AgentScheduler(
agent=mock_agent,
task="Test task"
)
stats = scheduler.get_stats()
assert 'total_cost_usd' in stats, "Stats should include total_cost_usd"
assert 'cost_per_execution' in stats, "Stats should include cost_per_execution"
assert stats['total_cost_usd'] == 0.0, "Initial cost should be 0"
if __name__ == '__main__':
pytest.main([__file__, '-v'])
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Quick test script for scheduler with agents.yaml integration.
"""
import os
import sys
# Add parent directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src/praisonai'))
def test_yaml_loader():
"""Test YAML loader functionality."""
print("Testing YAML loader...")
from praisonai.scheduler.yaml_loader import load_agent_yaml_with_schedule
# Test with example YAML
yaml_path = "examples/cookbooks/yaml/news_monitor_scheduled.yaml"
try:
agent_config, schedule_config = load_agent_yaml_with_schedule(yaml_path)
print(f"✅ Agent config loaded: {agent_config.get('name', 'Unknown')}")
print(f"✅ Schedule interval: {schedule_config.get('interval', 'Not set')}")
print(f"✅ Max retries: {schedule_config.get('max_retries', 'Not set')}")
return True
except Exception as e:
print(f"❌ Error: {e}")
return False
def test_scheduler_from_yaml():
"""Test AgentScheduler.from_yaml() method."""
print("\nTesting AgentScheduler.from_yaml()...")
from praisonai.scheduler import AgentScheduler
yaml_path = "examples/cookbooks/yaml/news_monitor_scheduled.yaml"
try:
scheduler = AgentScheduler.from_yaml(yaml_path)
print(f"✅ Scheduler created successfully")
print(f"✅ Agent: {getattr(scheduler.agent, 'name', 'Unknown')}")
print(f"✅ Task: {scheduler.task[:50]}...")
# Test that we can access schedule config
if hasattr(scheduler, '_yaml_schedule_config'):
print(f"✅ Schedule config attached")
return True
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Run all tests."""
print("="*60)
print("Scheduler Integration Tests")
print("="*60)
results = []
# Test 1: YAML Loader
results.append(("YAML Loader", test_yaml_loader()))
# Test 2: Scheduler from YAML
results.append(("Scheduler from YAML", test_scheduler_from_yaml()))
# Summary
print("\n" + "="*60)
print("Test Summary:")
print("="*60)
for name, passed in results:
status = "✅ PASS" if passed else "❌ FAIL"
print(f"{status} - {name}")
all_passed = all(passed for _, passed in results)
if all_passed:
print("\n🎉 All tests passed!")
return 0
else:
print("\n⚠️ Some tests failed")
return 1
if __name__ == "__main__":
sys.exit(main())