mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 04:35:58 -10:00
Release v2.3.66
This commit is contained in:
1 parent
09f01919d2
commit
cacf1097ef
17 files changed
+478
-79
No files matched your search
@@ -1245,15 +1245,34 @@ 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 using agents.yaml
|
||||
# No YAML needed - just provide your task
|
||||
praisonai schedule "Check AI news and summarize" --interval hourly
|
||||
|
||||
# With options
|
||||
praisonai schedule "Monitor server logs" \
|
||||
--interval "*/30m" \
|
||||
--timeout 120 \
|
||||
--max-cost 2.00 \
|
||||
--verbose
|
||||
```
|
||||
|
||||
**YAML Mode (Advanced):**
|
||||
```bash
|
||||
# Use agents.yaml for complex configurations
|
||||
praisonai schedule agents.yaml
|
||||
|
||||
# Override interval from CLI
|
||||
praisonai schedule agents.yaml --interval "*/30m"
|
||||
# With overrides
|
||||
praisonai schedule agents.yaml \
|
||||
--interval "*/15m" \
|
||||
--timeout 60 \
|
||||
--max-cost 1.00
|
||||
|
||||
# With verbose logging
|
||||
praisonai schedule agents.yaml --verbose
|
||||
# Stop: Press Ctrl+C
|
||||
```
|
||||
|
||||
**agents.yaml with schedule:**
|
||||
@@ -1269,11 +1288,20 @@ agents:
|
||||
task: "Search for latest AI news"
|
||||
|
||||
schedule:
|
||||
interval: "hourly" # hourly, daily, */30m, */6h
|
||||
max_retries: 3
|
||||
run_immediately: true
|
||||
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
|
||||
# Process images with vision-based tasks
|
||||
|
||||
@@ -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.65" \
|
||||
"praisonai>=2.3.66" \
|
||||
"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.65" \
|
||||
"praisonai>=2.3.66" \
|
||||
"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.65" \
|
||||
"praisonai>=2.3.66" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+36
-8
@@ -1245,15 +1245,34 @@ 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 using agents.yaml
|
||||
# No YAML needed - just provide your task
|
||||
praisonai schedule "Check AI news and summarize" --interval hourly
|
||||
|
||||
# With options
|
||||
praisonai schedule "Monitor server logs" \
|
||||
--interval "*/30m" \
|
||||
--timeout 120 \
|
||||
--max-cost 2.00 \
|
||||
--verbose
|
||||
```
|
||||
|
||||
**YAML Mode (Advanced):**
|
||||
```bash
|
||||
# Use agents.yaml for complex configurations
|
||||
praisonai schedule agents.yaml
|
||||
|
||||
# Override interval from CLI
|
||||
praisonai schedule agents.yaml --interval "*/30m"
|
||||
# With overrides
|
||||
praisonai schedule agents.yaml \
|
||||
--interval "*/15m" \
|
||||
--timeout 60 \
|
||||
--max-cost 1.00
|
||||
|
||||
# With verbose logging
|
||||
praisonai schedule agents.yaml --verbose
|
||||
# Stop: Press Ctrl+C
|
||||
```
|
||||
|
||||
**agents.yaml with schedule:**
|
||||
@@ -1269,11 +1288,20 @@ agents:
|
||||
task: "Search for latest AI news"
|
||||
|
||||
schedule:
|
||||
interval: "hourly" # hourly, daily, */30m, */6h
|
||||
max_retries: 3
|
||||
run_immediately: true
|
||||
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
|
||||
# Process images with vision-based tasks
|
||||
|
||||
@@ -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.65.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.65.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]"
|
||||
|
||||
@@ -13,47 +13,92 @@ class AgentSchedulerHandler:
|
||||
"""Handler for agent scheduler CLI commands."""
|
||||
|
||||
@staticmethod
|
||||
def handle_schedule_command(args) -> int:
|
||||
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 module not found")
|
||||
print("Error: praisonai.scheduler or praisonaiagents module not found")
|
||||
print("Please ensure PraisonAI is properly installed")
|
||||
print("pip install praisonai praisonaiagents")
|
||||
return 1
|
||||
|
||||
# Get YAML file path (default to agents.yaml)
|
||||
yaml_path = getattr(args, 'schedule_yaml', None) or 'agents.yaml'
|
||||
# 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)
|
||||
max_retries_override = getattr(args, 'schedule_max_retries', None)
|
||||
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
|
||||
# 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:
|
||||
# Create scheduler from YAML
|
||||
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
|
||||
)
|
||||
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')
|
||||
@@ -63,17 +108,32 @@ class AgentSchedulerHandler:
|
||||
print(f"Task: {scheduler.task[:80]}{'...' if len(scheduler.task) > 80 else ''}")
|
||||
|
||||
# Get schedule info
|
||||
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)
|
||||
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")
|
||||
scheduler.start_from_yaml_config()
|
||||
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:
|
||||
|
||||
@@ -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"
|
||||
@@ -226,7 +233,7 @@ class PraisonAI:
|
||||
# Handle schedule command
|
||||
if args.command == "schedule":
|
||||
from praisonai.cli.features.agent_scheduler import AgentSchedulerHandler
|
||||
exit_code = AgentSchedulerHandler.handle_schedule_command(args)
|
||||
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")
|
||||
@@ -585,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")
|
||||
@@ -718,6 +725,8 @@ class PraisonAI:
|
||||
# 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:
|
||||
@@ -1023,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:
|
||||
"""
|
||||
|
||||
@@ -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.65 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}")
|
||||
@@ -234,9 +289,11 @@ class AgentScheduler:
|
||||
@classmethod
|
||||
def from_yaml(
|
||||
cls,
|
||||
yaml_path: str,
|
||||
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':
|
||||
@@ -287,19 +344,29 @@ class AgentScheduler:
|
||||
if not task:
|
||||
raise ValueError("No task specified in YAML file")
|
||||
|
||||
# Create scheduler
|
||||
# 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 auto-start
|
||||
# Store schedule config for later use
|
||||
scheduler._yaml_schedule_config = schedule_config
|
||||
scheduler._interval_override = interval_override
|
||||
scheduler._max_retries_override = max_retries_override
|
||||
|
||||
return scheduler
|
||||
|
||||
@@ -310,23 +377,17 @@ class AgentScheduler:
|
||||
Must be called after from_yaml() class method.
|
||||
|
||||
Returns:
|
||||
True if scheduler started successfully
|
||||
True if started successfully
|
||||
"""
|
||||
if not hasattr(self, '_yaml_schedule_config'):
|
||||
raise RuntimeError("start_from_yaml_config() can only be called after from_yaml()")
|
||||
raise ValueError("No YAML configuration found. Use from_yaml() first.")
|
||||
|
||||
schedule_config = self._yaml_schedule_config
|
||||
|
||||
# Use overrides if provided, otherwise use YAML config
|
||||
interval = self._interval_override or schedule_config.get('interval', 'hourly')
|
||||
max_retries = self._max_retries_override or schedule_config.get('max_retries', 3)
|
||||
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(
|
||||
schedule_expr=interval,
|
||||
max_retries=max_retries,
|
||||
run_immediately=run_immediately
|
||||
)
|
||||
return self.start(interval, max_retries, run_immediately)
|
||||
|
||||
|
||||
def create_agent_scheduler(
|
||||
|
||||
@@ -87,20 +87,16 @@ def load_agent_yaml_with_schedule(yaml_path: str) -> Tuple[Dict[str, Any], Dict[
|
||||
agent_config['framework'] = config.get('framework', 'praisonai')
|
||||
|
||||
# Extract schedule configuration (optional)
|
||||
schedule_config = config.get('schedule', {})
|
||||
schedule_section = config.get('schedule', {})
|
||||
|
||||
# Set defaults for schedule if not provided
|
||||
if not schedule_config:
|
||||
schedule_config = {
|
||||
'interval': 'hourly',
|
||||
'max_retries': 3,
|
||||
'run_immediately': False
|
||||
}
|
||||
else:
|
||||
# Ensure required fields have defaults
|
||||
schedule_config.setdefault('interval', 'hourly')
|
||||
schedule_config.setdefault('max_retries', 3)
|
||||
schedule_config.setdefault('run_immediately', False)
|
||||
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']}'")
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.65"
|
||||
__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'])
|
||||
Reference in new issue
Block a user