Release v2.3.66

This commit is contained in:
Mervin Praison committed 2025-12-22 08:50:02 +00:00
1 parent 09f01919d2
commit cacf1097ef
17 files changed
+478 -79

No files matched your search

+36 -8
View File
@@ -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
+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.65" \
"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.65" \
"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.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
+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
+36 -8
View File
@@ -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
+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.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:
+13 -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"
@@ -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:
"""
+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.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
View File
@@ -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'])