Compare commits

...
2 Commits
Author SHA1 Message Date
Mervin Praison 657fd93743 Release v3.9.2 2026-01-16 04:37:53 +00:00
github-actions[bot] f0a2936b7f docs: auto-update api.md [skip ci] 2026-01-15 20:08:49 +00:00
14 changed files with 1817 additions and 8 deletions

No files matched your search

+3
View File
@@ -199,6 +199,7 @@ Methods:
* <code title="cli">praisonai docs run-all <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs serve <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs stats <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai doctor cleanup <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor config <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor db <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor doctor-callback <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
@@ -208,6 +209,7 @@ Methods:
* <code title="cli">praisonai doctor performance <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor selftest <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor tools <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor troubleshoot <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai endpoints list <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
* <code title="cli">praisonai endpoints test <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
* <code title="cli">praisonai environment check <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
@@ -238,6 +240,7 @@ Methods:
* <code title="cli">praisonai mcp list <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp mcp-callback <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp remove <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp run <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp status <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp sync <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp test <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
+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>=3.9.1" \
"praisonai>=3.9.2" \
"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>=3.9.1" \
"praisonai>=3.9.2" \
"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>=3.9.1" \
"praisonai>=3.9.2" \
"praisonai[ui]" \
"praisonai[crewai]"
+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/v3.9.1.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.9.1.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.9.2.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.9.2.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+2
View File
@@ -215,6 +215,7 @@ def register_commands():
from .commands.endpoints import app as endpoints_app
from .commands.test import app as test_app
from .commands.examples import app as examples_app
from .commands.batch import app as batch_app
# Import TUI and queue commands
from .features.tui.debug import create_debug_app as create_tui_debug_app
@@ -272,6 +273,7 @@ def register_commands():
app.add_typer(endpoints_app, name="endpoints", help="API endpoint management")
app.add_typer(test_app, name="test", help="Run test suite with tier and provider options")
app.add_typer(examples_app, name="examples", help="Run and manage example files")
app.add_typer(batch_app, name="batch", help="Run all PraisonAI scripts in current folder")
# Register standardise command
try:
@@ -0,0 +1,848 @@
"""
PraisonAI Batch CLI Command.
Run all Python files with PraisonAI imports in the current directory.
Designed for quick debugging - running multiple local scripts at once.
Usage:
praisonai batch # Run all PraisonAI scripts in current dir
praisonai batch --sub # Include subdirectories
praisonai batch --sub --depth 2 # Limit recursion depth
praisonai batch --parallel # Run in parallel with async reporting
praisonai batch list # List discovered files
praisonai batch stats # Show statistics
praisonai batch report # View latest report
"""
from datetime import datetime
from pathlib import Path
from typing import Optional
import typer
app = typer.Typer(
help="Run all PraisonAI scripts in current folder for quick debugging",
no_args_is_help=False,
)
def _get_default_report_dir() -> Path:
"""Get default report directory with timestamp."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return Path.home() / "Downloads" / "reports" / "batch" / timestamp
def _get_latest_report_dir() -> Optional[Path]:
"""Get the latest report directory."""
base_dir = Path.home() / "Downloads" / "reports" / "batch"
if not base_dir.exists():
return None
# Find most recent timestamp directory
dirs = [d for d in base_dir.iterdir() if d.is_dir()]
if not dirs:
return None
return max(dirs, key=lambda d: d.name)
@app.callback(invoke_without_command=True)
def batch_run(
ctx: typer.Context,
path: Optional[Path] = typer.Option(
None,
"--path", "-p",
help="Path to search (default: current directory)",
),
recursive: bool = typer.Option(
False,
"--sub", "--recursive", "-r",
help="Include subdirectories",
),
depth: Optional[int] = typer.Option(
None,
"--depth", "-d",
help="Maximum recursion depth (only with --sub)",
),
include_tests: bool = typer.Option(
False,
"--include-tests",
help="Include test files (test_*.py, *_test.py)",
),
timeout: int = typer.Option(
60,
"--timeout", "-t",
help="Per-script timeout in seconds",
),
max_items: Optional[int] = typer.Option(
None,
"--max-items",
help="Maximum scripts to run",
),
fail_fast: bool = typer.Option(
False,
"--fail-fast", "-x",
help="Stop on first failure",
),
parallel: bool = typer.Option(
False,
"--parallel", "--async",
help="Run in parallel with async reporting (background)",
),
max_workers: int = typer.Option(
4,
"--workers", "-w",
help="Max parallel workers (only with --parallel)",
),
no_stream: bool = typer.Option(
False,
"--no-stream",
help="Don't stream output to terminal",
),
report_dir: Optional[Path] = typer.Option(
None,
"--report-dir",
help="Directory for reports (default: ~/Downloads/reports/batch/<timestamp>)",
),
no_report: bool = typer.Option(
False,
"--no-report",
help="Skip report generation",
),
quiet: bool = typer.Option(
False,
"--quiet", "-q",
help="Minimal output",
),
server: bool = typer.Option(
False,
"--server",
help="Run only server scripts (uvicorn, Flask, streamlit, etc.) with 10s timeout",
),
filter_type: Optional[str] = typer.Option(
None,
"--filter", "-f",
help="Filter by type: 'agent', 'agents', or 'workflow'",
),
ci: bool = typer.Option(
False,
"--ci",
help="CI-friendly output (no colors, proper exit codes)",
),
):
"""
Run all PraisonAI scripts in the current folder.
Quick debugging tool for running multiple Python files that use
praisonaiagents or praisonai. Unlike 'praisonai run' (which runs
agent configs) or 'praisonai examples' (which runs repo examples),
this command finds and executes local scripts for rapid testing.
Only files containing 'from praisonaiagents' or 'from praisonai'
are executed. Test files (test_*.py) are excluded by default.
Examples:
praisonai batch # Run all in current dir
praisonai batch --sub # Include subdirectories
praisonai batch --sub --depth 2 # Limit depth
praisonai batch --parallel # Run in parallel
praisonai batch --timeout 120 # Custom timeout
"""
# If a subcommand was invoked, skip the default run
if ctx.invoked_subcommand is not None:
return
# Lazy import to avoid loading at CLI startup
from praisonai.suite_runner import BatchSource, SuiteExecutor, RunResult
# Resolve paths
search_path = Path(path).resolve() if path else Path.cwd().resolve()
output_dir = report_dir or _get_default_report_dir()
if not search_path.exists():
if ci:
print(f"ERROR: Path not found: {search_path}")
else:
typer.echo(f"❌ Path not found: {search_path}")
raise typer.Exit(2)
# Validate filter_type
if filter_type and filter_type not in ("agent", "agents", "workflow"):
if ci:
print(f"ERROR: Invalid filter type: {filter_type}. Use 'agent', 'agents', or 'workflow'")
else:
typer.echo(f"❌ Invalid filter type: {filter_type}. Use 'agent', 'agents', or 'workflow'")
raise typer.Exit(2)
# Server mode uses 10s timeout by default
effective_timeout = 10 if server else timeout
# Create source
source = BatchSource(
root=search_path,
recursive=recursive,
depth=depth,
exclude_tests=not include_tests,
exclude_servers=not server, # Exclude servers by default, include when --server
server_only=server, # Only servers when --server flag
filter_type=filter_type,
)
# Discover items
items = source.discover()
if not items:
if ci:
print(f"INFO: No PraisonAI scripts found in {search_path}")
else:
typer.echo(f"ℹ️ No PraisonAI scripts found in {search_path}")
if not recursive:
typer.echo(" Tip: Use --sub to include subdirectories")
raise typer.Exit(0)
# Parallel execution
if parallel:
_run_parallel(
items=items,
source=source,
search_path=search_path,
output_dir=output_dir,
timeout=effective_timeout,
max_workers=max_workers,
quiet=quiet,
ci=ci,
)
return
# Sequential execution (default)
executor = SuiteExecutor(
suite="batch",
source_path=search_path,
timeout=effective_timeout,
fail_fast=fail_fast,
stream_output=not no_stream,
max_items=max_items,
report_dir=output_dir if not no_report else None,
generate_json=not no_report,
generate_md=not no_report,
generate_csv=not no_report,
pythonpath_additions=source.get_pythonpath(),
)
# Status icons (CI mode uses text)
if ci:
icons = {
"passed": "PASS",
"failed": "FAIL",
"skipped": "SKIP",
"timeout": "TIMEOUT",
"not_run": "NOT_RUN",
"xfail": "XFAIL",
}
else:
icons = {
"passed": "✅",
"failed": "❌",
"skipped": "⏭️",
"timeout": "⏱️",
"not_run": "📝",
"xfail": "⚠️",
}
def on_item_start(item, idx: int, total: int):
if not quiet:
if ci:
print(f"[{idx}/{total}] Running: {item.display_name}")
else:
typer.echo(f"\n[{idx}/{total}] Running: {item.display_name}")
def on_item_end(result: RunResult, idx: int, total: int):
icon = icons.get(result.status, "?")
duration = f"{result.duration_seconds:.2f}s" if result.duration_seconds else ""
if quiet:
if ci:
print(f"{icon} {result.display_name}")
else:
typer.echo(f"{icon} {result.display_name}")
else:
msg = f" {icon} {result.status.upper()}"
if duration:
msg += f" ({duration})"
if result.skip_reason:
msg += f" - {result.skip_reason}"
if result.error_message and not no_stream:
msg += f"\n Error: {result.error_message[:100]}"
if ci:
print(msg)
else:
typer.echo(msg)
def on_output(line: str, stream: str):
if not quiet and not no_stream:
prefix = " | " if ci else " │ "
if stream != "stdout":
prefix += "[err] "
if ci:
print(f"{prefix}{line.rstrip()}")
else:
typer.echo(f"{prefix}{line.rstrip()}")
# Print header
if not quiet:
if ci:
print("=" * 60)
print("PraisonAI Batch Runner")
print("=" * 60)
print(f"Path: {search_path}")
print(f"Recursive: {recursive}")
if depth:
print(f"Depth: {depth}")
print(f"Timeout: {effective_timeout}s")
print(f"Scripts: {len(items)}")
if server:
print("Mode: Server scripts only")
if filter_type:
print(f"Filter: {filter_type}")
if not no_report:
print(f"Reports: {output_dir}")
else:
typer.echo("=" * 60)
typer.echo("PraisonAI Batch Runner")
typer.echo("=" * 60)
typer.echo(f"Path: {search_path}")
typer.echo(f"Recursive: {recursive}")
if depth:
typer.echo(f"Depth: {depth}")
typer.echo(f"Timeout: {effective_timeout}s")
typer.echo(f"Scripts: {len(items)}")
if server:
typer.echo("Mode: Server scripts only")
if filter_type:
typer.echo(f"Filter: {filter_type}")
if not no_report:
typer.echo(f"Reports: {output_dir}")
# Run
report = executor.run(
items=items,
on_item_start=on_item_start,
on_item_end=on_item_end,
on_output=on_output if not no_stream else None,
)
# Print summary
totals = report.totals
total_count = sum(totals.values())
if not quiet:
if ci:
print("")
print("=" * 60)
print("SUMMARY")
print("=" * 60)
print(f" PASSED: {totals['passed']}")
print(f" FAILED: {totals['failed']}")
print(f" SKIPPED: {totals['skipped']}")
print(f" TIMEOUT: {totals['timeout']}")
print(" -----------------")
print(f" TOTAL: {total_count}")
print("=" * 60)
if not no_report and output_dir.exists():
print(f"Reports: {output_dir}")
else:
typer.echo("\n" + "=" * 60)
typer.echo("SUMMARY")
typer.echo("=" * 60)
typer.echo(f" ✅ Passed: {totals['passed']}")
typer.echo(f" ❌ Failed: {totals['failed']}")
typer.echo(f" ⏭️ Skipped: {totals['skipped']}")
typer.echo(f" ⏱️ Timeout: {totals['timeout']}")
typer.echo(" ─────────────────")
typer.echo(f" Total: {total_count}")
typer.echo("=" * 60)
if not no_report and output_dir.exists():
typer.echo(f"\n📁 Reports saved to: {output_dir}")
# Exit code
if totals['failed'] > 0 or totals['timeout'] > 0:
raise typer.Exit(1)
raise typer.Exit(0)
def _run_parallel(
items,
source,
search_path: Path,
output_dir: Path,
timeout: int,
max_workers: int,
quiet: bool,
ci: bool = False,
):
"""Run items in parallel with async reporting."""
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from praisonai.suite_runner import RunResult, RunReport, SuiteReporter
from datetime import timezone
if ci:
print("=" * 60)
print("PraisonAI Batch Runner (Parallel Mode)")
print("=" * 60)
print(f"Path: {search_path}")
print(f"Workers: {max_workers}")
print(f"Timeout: {timeout}s")
print(f"Scripts: {len(items)}")
print(f"Reports: {output_dir}")
print("")
print("Running... Progress:")
print("-" * 60)
else:
typer.echo("=" * 60)
typer.echo("PraisonAI Batch Runner (Parallel Mode)")
typer.echo("=" * 60)
typer.echo(f"Path: {search_path}")
typer.echo(f"Workers: {max_workers}")
typer.echo(f"Timeout: {timeout}s")
typer.echo(f"Scripts: {len(items)}")
typer.echo(f"Reports: {output_dir}")
typer.echo("")
typer.echo("Running in background... Progress:")
typer.echo("-" * 60)
results = []
completed = 0
total = len(items)
# Build environment
import os
env = os.environ.copy()
pythonpath = source.get_pythonpath()
if pythonpath:
existing = env.get('PYTHONPATH', '')
env['PYTHONPATH'] = os.pathsep.join(pythonpath + ([existing] if existing else []))
def run_item(item):
"""Run a single item and return result."""
nonlocal completed
if not item.runnable:
completed += 1
return RunResult(
item_id=item.item_id,
suite="batch",
group=item.group,
source_path=item.source_path,
status="skipped" if item.skip else "not_run",
skip_reason=item.skip_reason,
code_hash=item.code_hash,
)
start_time = datetime.now(timezone.utc)
try:
result = subprocess.run(
[sys.executable, '-u', str(item.script_path)],
capture_output=True,
text=True,
timeout=timeout,
env=env,
cwd=item.script_path.parent,
)
end_time = datetime.now(timezone.utc)
duration = (end_time - start_time).total_seconds()
status = "passed" if result.returncode == 0 else "failed"
if item.xfail and result.returncode != 0:
status = "xfail"
error_message = None
if result.returncode != 0 and result.stderr:
lines = result.stderr.strip().split('\n')
error_message = lines[-1][:200] if lines else None
completed += 1
# Progress indicator
if ci:
icon = "PASS" if status == "passed" else "FAIL" if status == "failed" else "XFAIL"
else:
icon = "✅" if status == "passed" else "❌" if status == "failed" else "⚠️"
if not quiet:
if ci:
print(f" [{completed}/{total}] {icon} {item.display_name} ({duration:.2f}s)")
else:
typer.echo(f" [{completed}/{total}] {icon} {item.display_name} ({duration:.2f}s)")
return RunResult(
item_id=item.item_id,
suite="batch",
group=item.group,
source_path=item.source_path,
status=status,
exit_code=result.returncode,
duration_seconds=duration,
start_time=start_time.isoformat(),
end_time=end_time.isoformat(),
error_message=error_message,
stdout=result.stdout,
stderr=result.stderr,
code_hash=item.code_hash,
)
except subprocess.TimeoutExpired:
completed += 1
end_time = datetime.now(timezone.utc)
duration = (end_time - start_time).total_seconds()
if not quiet:
if ci:
print(f" [{completed}/{total}] TIMEOUT {item.display_name} (timeout)")
else:
typer.echo(f" [{completed}/{total}] ⏱️ {item.display_name} (timeout)")
return RunResult(
item_id=item.item_id,
suite="batch",
group=item.group,
source_path=item.source_path,
status="timeout",
duration_seconds=duration,
error_message=f"Exceeded {timeout}s timeout",
code_hash=item.code_hash,
)
except Exception as e:
completed += 1
if not quiet:
if ci:
print(f" [{completed}/{total}] FAIL {item.display_name} (error: {e})")
else:
typer.echo(f" [{completed}/{total}] ❌ {item.display_name} (error: {e})")
return RunResult(
item_id=item.item_id,
suite="batch",
group=item.group,
source_path=item.source_path,
status="failed",
error_message=str(e),
code_hash=item.code_hash,
)
# Run in parallel
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(run_item, item): item for item in items}
for future in as_completed(futures):
result = future.result()
results.append(result)
# Create report
report = RunReport(
results=results,
suite="batch",
source_path=search_path,
groups_run=[],
)
# Generate reports
output_dir.mkdir(parents=True, exist_ok=True)
reporter = SuiteReporter(output_dir)
reporter.save_logs(results)
reporter.generate_json(report)
reporter.generate_markdown(report)
reporter.generate_csv(report)
# Print summary
totals = report.totals
total_count = sum(totals.values())
if ci:
print("-" * 60)
print("")
print("SUMMARY")
print("=" * 60)
print(f" PASSED: {totals['passed']}")
print(f" FAILED: {totals['failed']}")
print(f" SKIPPED: {totals['skipped']}")
print(f" TIMEOUT: {totals['timeout']}")
print(" -----------------")
print(f" TOTAL: {total_count}")
print("=" * 60)
print(f"Reports: {output_dir}")
else:
typer.echo("-" * 60)
typer.echo("\nSUMMARY")
typer.echo("=" * 60)
typer.echo(f" ✅ Passed: {totals['passed']}")
typer.echo(f" ❌ Failed: {totals['failed']}")
typer.echo(f" ⏭️ Skipped: {totals['skipped']}")
typer.echo(f" ⏱️ Timeout: {totals['timeout']}")
typer.echo(" ─────────────────")
typer.echo(f" Total: {total_count}")
typer.echo("=" * 60)
typer.echo(f"\n📁 Reports saved to: {output_dir}")
# Exit code
if totals['failed'] > 0 or totals['timeout'] > 0:
raise typer.Exit(1)
raise typer.Exit(0)
@app.command("list")
def batch_list(
path: Optional[Path] = typer.Option(
None,
"--path", "-p",
help="Path to search",
),
recursive: bool = typer.Option(
False,
"--sub", "--recursive", "-r",
help="Include subdirectories",
),
depth: Optional[int] = typer.Option(
None,
"--depth", "-d",
help="Maximum recursion depth",
),
include_tests: bool = typer.Option(
False,
"--include-tests",
help="Include test files",
),
show_groups: bool = typer.Option(
False,
"--groups",
help="Show available groups only",
),
):
"""
List discovered PraisonAI scripts without running them.
Examples:
praisonai batch list
praisonai batch list --sub
praisonai batch list --groups
"""
from praisonai.suite_runner import BatchSource
search_path = Path(path).resolve() if path else Path.cwd().resolve()
if not search_path.exists():
typer.echo(f"❌ Path not found: {search_path}")
raise typer.Exit(2)
source = BatchSource(
root=search_path,
recursive=recursive,
depth=depth,
exclude_tests=not include_tests,
)
# Show groups only
if show_groups:
groups = source.get_groups()
if not groups:
typer.echo(f"No PraisonAI scripts found in {search_path}")
return
typer.echo(f"Available groups in {search_path}:\n")
for g in groups:
typer.echo(f" - {g}")
return
items = source.discover()
if not items:
typer.echo(f"No PraisonAI scripts found in {search_path}")
if not recursive:
typer.echo("Tip: Use --sub to include subdirectories")
return
typer.echo(f"Found {len(items)} PraisonAI scripts in {search_path}\n")
for idx, item in enumerate(items, 1):
rel_path = item.source_path.relative_to(search_path)
flags = []
if item.skip:
flags.append("skip")
if item.uses_agent:
flags.append("Agent")
if item.uses_agents:
flags.append("Agents")
if item.uses_workflow:
flags.append("Workflow")
flag_str = f" [{', '.join(flags)}]" if flags else ""
typer.echo(f" {idx:3}. [{item.group}] {rel_path}{flag_str}")
@app.command("stats")
def batch_stats(
path: Optional[Path] = typer.Option(
None,
"--path", "-p",
help="Path to search",
),
recursive: bool = typer.Option(
False,
"--sub", "--recursive", "-r",
help="Include subdirectories",
),
depth: Optional[int] = typer.Option(
None,
"--depth", "-d",
help="Maximum recursion depth",
),
include_tests: bool = typer.Option(
False,
"--include-tests",
help="Include test files",
),
):
"""
Show statistics for PraisonAI scripts.
Displays counts by group, runnable status, and agent-centric usage.
Examples:
praisonai batch stats
praisonai batch stats --sub
"""
from collections import Counter
from praisonai.suite_runner import BatchSource
search_path = Path(path).resolve() if path else Path.cwd().resolve()
if not search_path.exists():
typer.echo(f"❌ Path not found: {search_path}")
raise typer.Exit(2)
source = BatchSource(
root=search_path,
recursive=recursive,
depth=depth,
exclude_tests=not include_tests,
)
items = source.discover()
if not items:
typer.echo(f"No PraisonAI scripts found in {search_path}")
return
# Calculate stats
group_counts = Counter(item.group for item in items)
runnable_by_group = Counter(item.group for item in items if item.runnable)
# Agent-centric stats
agent_count = sum(1 for item in items if item.uses_agent)
agents_count = sum(1 for item in items if item.uses_agents)
workflow_count = sum(1 for item in items if item.uses_workflow)
agent_by_group = Counter(item.group for item in items if item.uses_agent)
agents_by_group = Counter(item.group for item in items if item.uses_agents)
workflow_by_group = Counter(item.group for item in items if item.uses_workflow)
typer.echo("\n📊 Batch Execution Statistics")
typer.echo(f"Path: {search_path}")
typer.echo("=" * 80)
typer.echo(f"\n{'Group':<20} {'Total':>8} {'Runnable':>10} {'Agent':>8} {'Agents':>8} {'Workflow':>10}")
typer.echo("-" * 80)
for g in sorted(group_counts.keys()):
typer.echo(
f"{g:<20} {group_counts[g]:>8} {runnable_by_group[g]:>10} "
f"{agent_by_group[g]:>8} {agents_by_group[g]:>8} {workflow_by_group[g]:>10}"
)
typer.echo("-" * 80)
typer.echo(
f"{'TOTAL':<20} {len(items):>8} {sum(1 for i in items if i.runnable):>10} "
f"{agent_count:>8} {agents_count:>8} {workflow_count:>10}"
)
typer.echo()
@app.command("report")
def batch_report(
report_dir: Optional[Path] = typer.Option(
None,
"--dir", "-d",
help="Specific report directory (default: latest)",
),
format: str = typer.Option(
"summary",
"--format", "-f",
help="Output format: summary, failures, full",
),
):
"""
View the latest batch execution report.
Examples:
praisonai batch report
praisonai batch report --format failures
praisonai batch report --dir ~/Downloads/reports/batch/20240115_120000
"""
import json
# Find report directory
if report_dir:
target_dir = Path(report_dir)
else:
target_dir = _get_latest_report_dir()
if not target_dir or not target_dir.exists():
typer.echo("❌ No batch reports found.")
typer.echo(" Run 'praisonai batch' first to generate a report.")
raise typer.Exit(1)
# Load JSON report
json_path = target_dir / "report.json"
if not json_path.exists():
typer.echo(f"❌ Report not found: {json_path}")
raise typer.Exit(1)
report_data = json.loads(json_path.read_text())
typer.echo(f"\n📊 Batch Report: {target_dir.name}")
typer.echo("=" * 60)
# Summary
totals = report_data.get("totals", {})
typer.echo(f" ✅ Passed: {totals.get('passed', 0)}")
typer.echo(f" ❌ Failed: {totals.get('failed', 0)}")
typer.echo(f" ⏭️ Skipped: {totals.get('skipped', 0)}")
typer.echo(f" ⏱️ Timeout: {totals.get('timeout', 0)}")
typer.echo("=" * 60)
if format in ("failures", "full"):
results = report_data.get("results", [])
failures = [r for r in results if r.get("status") == "failed"]
if failures:
typer.echo("\n❌ FAILURES:")
typer.echo("-" * 60)
for r in failures:
typer.echo(f"\n {r.get('source_path', 'unknown')}")
if r.get("error_message"):
typer.echo(f" Error: {r['error_message'][:200]}")
if format == "full":
results = report_data.get("results", [])
typer.echo("\n📋 ALL RESULTS:")
typer.echo("-" * 60)
for r in results:
status = r.get("status", "unknown")
icon = {"passed": "✅", "failed": "❌", "skipped": "⏭️", "timeout": "⏱️"}.get(status, "?")
duration = r.get("duration_seconds", 0)
typer.echo(f" {icon} {r.get('source_path', 'unknown')} ({duration:.2f}s)")
typer.echo(f"\n📁 Full report: {target_dir}")
+1 -1
View File
@@ -33,7 +33,7 @@ TYPER_COMMANDS = {
'chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research',
'memory', 'workflow', 'tools', 'knowledge', 'deploy', 'agents', 'skills',
'eval', 'templates', 'recipe', 'todo', 'docs', 'commit', 'hooks', 'rules',
'registry', 'package', 'endpoints',
'registry', 'package', 'endpoints', 'test', 'examples', 'batch',
# Standardisation commands
'standardise', 'standardize',
}
+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==3.9.1 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==3.9.2 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
@@ -25,6 +25,7 @@ __all__ = [
# Sources
"ExamplesSource",
"DocsSource",
"BatchSource",
# Executor
"SuiteExecutor",
]
@@ -56,6 +57,10 @@ def __getattr__(name: str):
from .docs_source import DocsSource
return DocsSource
if name == "BatchSource":
from .batch_source import BatchSource
return BatchSource
if name == "SuiteExecutor":
from .executor import SuiteExecutor
return SuiteExecutor
@@ -0,0 +1,351 @@
"""
Batch Source - Adapter for running local Python files with PraisonAI imports.
Discovers and prepares Python files in the current directory (or specified path)
that contain imports from praisonaiagents or praisonai packages.
This is designed for quick debugging - running multiple local scripts at once.
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import List, Optional
from .models import RunItem
from .discovery import FileDiscovery, get_pythonpath_for_dev
# Import detection patterns - matches 'from praisonai...' or 'import praisonai...'
PRAISONAI_IMPORT_PATTERN = re.compile(
r'^(?:from|import)\s+praisonai(?:agents)?\b',
re.MULTILINE
)
# Agent-centric detection patterns (reused from examples_source)
AGENT_PATTERN = re.compile(r'\bAgent\s*\(')
AGENTS_PATTERN = re.compile(r'\b(?:Agents|PraisonAIAgents)\s*\(')
WORKFLOW_PATTERN = re.compile(r'\bWorkflow\s*\(')
# Directive parsing regex (reused from examples_source)
DIRECTIVE_PATTERN = re.compile(r'^#\s*praisonai:\s*(\w+)=(.+)$', re.MULTILINE)
INPUT_PATTERN = re.compile(r'\binput\s*\(')
# Test file patterns to exclude by default
TEST_FILE_PATTERNS = {'test_*.py', '*_test.py', 'conftest.py'}
# Server detection patterns - scripts that start long-running servers
SERVER_PATTERNS = [
re.compile(r'uvicorn\.run\s*\('), # uvicorn.run()
re.compile(r'\.launch\s*\('), # agent.launch(), demo.launch()
re.compile(r'app\.run\s*\('), # Flask app.run()
re.compile(r'^\s*import\s+streamlit\b', re.MULTILINE), # import streamlit
re.compile(r'^\s*from\s+streamlit\b', re.MULTILINE), # from streamlit import
re.compile(r'^\s*import\s+gradio\b', re.MULTILINE), # import gradio
re.compile(r'^\s*from\s+gradio\b', re.MULTILINE), # from gradio import
re.compile(r'FastAPI\s*\('), # FastAPI()
re.compile(r'Flask\s*\('), # Flask()
re.compile(r'\.serve\s*\('), # server.serve()
re.compile(r'mcp\.run\s*\('), # mcp.run()
re.compile(r'server\.run\s*\('), # server.run()
]
class BatchSource:
"""
Discovers and prepares local Python files with PraisonAI imports.
Unlike ExamplesSource (for repo examples) or DocsSource (for docs),
this source is designed for quick local debugging of user scripts.
Only files containing 'from praisonaiagents' or 'from praisonai'
are included. Test files are excluded by default.
Parses directives from comments:
- # praisonai: skip=true
- # praisonai: timeout=120
- # praisonai: require_env=KEY1,KEY2
- # praisonai: xfail=reason
"""
def __init__(
self,
root: Optional[Path] = None,
recursive: bool = False,
depth: Optional[int] = None,
exclude_tests: bool = True,
include_patterns: Optional[List[str]] = None,
exclude_patterns: Optional[List[str]] = None,
groups: Optional[List[str]] = None,
exclude_servers: bool = True,
server_only: bool = False,
filter_type: Optional[str] = None,
):
"""
Initialize batch source.
Args:
root: Root directory to search (default: current directory).
recursive: Whether to search subdirectories.
depth: Maximum recursion depth (None = unlimited when recursive).
exclude_tests: Whether to exclude test files (test_*.py, *_test.py).
include_patterns: Glob patterns to include.
exclude_patterns: Glob patterns to exclude.
groups: Specific groups (subdirs) to include.
exclude_servers: Whether to exclude server scripts (default: True).
server_only: Only include server scripts (for --server mode).
filter_type: Filter by agent type ('agent', 'agents', 'workflow').
"""
self.root = Path(root).resolve() if root else Path.cwd().resolve()
self.recursive = recursive
self.depth = depth
self.exclude_tests = exclude_tests
self.include_patterns = include_patterns
self.exclude_patterns = exclude_patterns or []
self.groups = groups
self.exclude_servers = exclude_servers
self.server_only = server_only
self.filter_type = filter_type
# Add test exclusions if enabled
if exclude_tests:
self.exclude_patterns = list(self.exclude_patterns) + [
'test_*.py', '*_test.py', 'conftest.py'
]
self._discovery = FileDiscovery(
root=self.root,
extensions=['.py'],
include_patterns=include_patterns,
exclude_patterns=self.exclude_patterns,
ignore_underscore=True,
)
def _has_praisonai_import(self, content: str) -> bool:
"""
Check if file content contains PraisonAI imports.
Args:
content: File content to check.
Returns:
True if file contains 'from praisonai...' or 'import praisonai...'.
"""
# Check each line to avoid matching commented imports
for line in content.split('\n'):
stripped = line.strip()
# Skip comments
if stripped.startswith('#'):
continue
# Check for import
if PRAISONAI_IMPORT_PATTERN.match(stripped):
return True
return False
def _is_server_script(self, content: str) -> bool:
"""
Check if file content contains server patterns that would hang.
Detects: uvicorn.run, .launch(), app.run, streamlit, gradio, FastAPI, Flask, etc.
Args:
content: File content to check.
Returns:
True if file contains server patterns.
"""
for pattern in SERVER_PATTERNS:
if pattern.search(content):
return True
return False
def _get_files_at_depth(self, max_depth: Optional[int] = None) -> List[Path]:
"""
Get files respecting depth limit.
Args:
max_depth: Maximum depth (0 = root only, 1 = root + immediate subdirs, etc.)
Returns:
List of file paths.
"""
if not self.recursive:
# Non-recursive: only files in root directory
files = []
for path in self.root.glob('*.py'):
if path.is_file() and not path.name.startswith('_'):
# Apply exclude patterns
if self.exclude_patterns:
import fnmatch
if any(fnmatch.fnmatch(path.name, p) for p in self.exclude_patterns):
continue
files.append(path)
return sorted(files)
# Recursive with optional depth limit
all_files = self._discovery.discover()
if max_depth is None:
return all_files
# Filter by depth
filtered = []
for path in all_files:
rel_path = path.relative_to(self.root)
# Depth is number of parent directories
depth = len(rel_path.parts) - 1 # -1 because last part is filename
if depth <= max_depth:
filtered.append(path)
return filtered
def discover(self) -> List[RunItem]:
"""
Discover all Python files with PraisonAI imports.
Returns:
List of RunItem objects for files that contain PraisonAI imports.
"""
files = self._get_files_at_depth(self.depth)
# Filter by groups if specified
if self.groups:
filtered_files = []
for path in files:
group = self._get_group_for_path(path)
if group in self.groups:
filtered_files.append(path)
files = filtered_files
items = []
for path in files:
# Read file content
try:
content = path.read_text(encoding='utf-8', errors='ignore')
except Exception:
continue
# Check for PraisonAI imports
if not self._has_praisonai_import(content):
continue
# Create RunItem
item = self._create_item(path, content)
# Apply server filtering
if self.server_only:
# Only include server scripts
if not item.is_server:
continue
elif self.exclude_servers:
# Exclude server scripts (default behavior)
if item.is_server:
continue
# Apply agent type filtering
if self.filter_type:
if self.filter_type == "agent" and not item.uses_agent:
continue
elif self.filter_type == "agents" and not item.uses_agents:
continue
elif self.filter_type == "workflow" and not item.uses_workflow:
continue
items.append(item)
return items
def _get_group_for_path(self, path: Path) -> str:
"""Get group name for a file path."""
try:
rel_path = path.relative_to(self.root)
if len(rel_path.parts) > 1:
return rel_path.parts[0]
return "root"
except ValueError:
return "unknown"
def _create_item(self, path: Path, content: str) -> RunItem:
"""Create RunItem from file."""
group = self._get_group_for_path(path)
rel_path = path.relative_to(self.root).as_posix()
item_id = rel_path.replace('/', '__').replace('.py', '')
# Parse directives from first 30 lines
lines = content.split('\n')[:30]
header = '\n'.join(lines)
skip = False
skip_reason = None
timeout = None
require_env = []
xfail = None
for match in DIRECTIVE_PATTERN.finditer(header):
key, value = match.group(1), match.group(2).strip()
if key == 'skip':
skip = value.lower() in ('true', '1', 'yes')
if skip:
skip_reason = "skip=true directive"
elif key == 'timeout':
try:
timeout = int(value)
except ValueError:
pass
elif key == 'require_env':
require_env = [k.strip() for k in value.split(',') if k.strip()]
elif key == 'xfail':
xfail = value
# Detect interactive input() calls
is_interactive = bool(INPUT_PATTERN.search(content))
if is_interactive and not skip:
skip = True
skip_reason = "Interactive script (contains input())"
# Detect agent-centric usage
uses_agent = bool(AGENT_PATTERN.search(content))
uses_agents = bool(AGENTS_PATTERN.search(content))
uses_workflow = bool(WORKFLOW_PATTERN.search(content))
# Detect server scripts
is_server = self._is_server_script(content)
# Determine runnable status
runnable = not skip
runnable_decision = "runnable" if runnable else (skip_reason or "skipped")
return RunItem(
item_id=item_id,
suite="batch",
group=group,
source_path=path,
block_index=0,
language="python",
code=content,
script_path=path, # Run directly
runnable=runnable,
runnable_decision=runnable_decision,
skip=skip,
skip_reason=skip_reason,
timeout=timeout,
require_env=require_env,
xfail=xfail,
is_interactive=is_interactive,
uses_agent=uses_agent,
uses_agents=uses_agents,
uses_workflow=uses_workflow,
is_server=is_server,
)
def get_groups(self) -> List[str]:
"""Get available groups (directories containing matching files)."""
items = self.discover()
groups = set(item.group for item in items)
return sorted(groups)
def get_pythonpath(self) -> List[str]:
"""Get PYTHONPATH additions for dev mode."""
return get_pythonpath_for_dev(self.root)
@@ -59,6 +59,9 @@ class RunItem:
uses_agents: bool = False # Agents() / PraisonAIAgents class
uses_workflow: bool = False # Workflow class
# Server detection
is_server: bool = False # Contains server code (uvicorn, Flask, streamlit, etc.)
@property
def agent_type(self) -> str:
"""Get agent type used in this item."""
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.9.1"
__version__ = "3.9.2"
@@ -0,0 +1,597 @@
"""
Tests for BatchSource - discovers Python files with PraisonAI imports.
TDD: These tests define the expected behavior before implementation.
"""
from pathlib import Path
import tempfile
class TestBatchSourceImportDetection:
"""Test import pattern detection."""
def test_detects_praisonaiagents_import(self):
"""Should detect 'from praisonaiagents import ...'"""
from praisonai.suite_runner.batch_source import BatchSource
content = """
from praisonaiagents import Agent
agent = Agent(instructions="Test")
"""
source = BatchSource(root=Path("."))
assert source._has_praisonai_import(content) is True
def test_detects_praisonai_import(self):
"""Should detect 'from praisonai import ...'"""
from praisonai.suite_runner.batch_source import BatchSource
content = """
from praisonai import Agent
agent = Agent()
"""
source = BatchSource(root=Path("."))
assert source._has_praisonai_import(content) is True
def test_detects_import_praisonaiagents(self):
"""Should detect 'import praisonaiagents'"""
from praisonai.suite_runner.batch_source import BatchSource
content = """
import praisonaiagents
agent = praisonaiagents.Agent()
"""
source = BatchSource(root=Path("."))
assert source._has_praisonai_import(content) is True
def test_rejects_non_praisonai_file(self):
"""Should reject files without PraisonAI imports."""
from praisonai.suite_runner.batch_source import BatchSource
content = """
import os
import sys
print("Hello world")
"""
source = BatchSource(root=Path("."))
assert source._has_praisonai_import(content) is False
def test_rejects_commented_import(self):
"""Should reject commented imports."""
from praisonai.suite_runner.batch_source import BatchSource
content = """
# from praisonaiagents import Agent
print("Hello")
"""
source = BatchSource(root=Path("."))
# Commented imports should not match
assert source._has_praisonai_import(content) is False
class TestBatchSourceDiscovery:
"""Test file discovery with filtering."""
def test_discovers_files_in_current_directory(self):
"""Should discover .py files in root directory only (non-recursive)."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# Create a file with praisonai import (not a test file name)
(root / "my_agent.py").write_text("from praisonaiagents import Agent\n")
# Create a file without praisonai import
(root / "other.py").write_text("print('hello')\n")
# Create a subdirectory with a file (should not be found without recursive)
subdir = root / "subdir"
subdir.mkdir()
(subdir / "nested.py").write_text("from praisonaiagents import Agent\n")
source = BatchSource(root=root, recursive=False)
items = source.discover()
# Should only find my_agent.py (has praisonai import, in root)
assert len(items) == 1
assert items[0].source_path.name == "my_agent.py"
def test_discovers_files_recursively(self):
"""Should discover .py files recursively when recursive=True."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# Create files
(root / "root_agent.py").write_text("from praisonaiagents import Agent\n")
subdir = root / "subdir"
subdir.mkdir()
(subdir / "nested_agent.py").write_text("from praisonai import Agent\n")
source = BatchSource(root=root, recursive=True)
items = source.discover()
# Should find both files
assert len(items) == 2
names = {item.source_path.name for item in items}
assert names == {"root_agent.py", "nested_agent.py"}
def test_respects_depth_limit(self):
"""Should respect depth limit when recursive."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# Create nested structure
(root / "level0.py").write_text("from praisonaiagents import Agent\n")
level1 = root / "level1"
level1.mkdir()
(level1 / "level1.py").write_text("from praisonaiagents import Agent\n")
level2 = level1 / "level2"
level2.mkdir()
(level2 / "level2.py").write_text("from praisonaiagents import Agent\n")
# Depth 1 should only find level0 and level1
source = BatchSource(root=root, recursive=True, depth=1)
items = source.discover()
names = {item.source_path.name for item in items}
assert "level0.py" in names
assert "level1.py" in names
assert "level2.py" not in names
def test_excludes_test_files_by_default(self):
"""Should exclude test_*.py and *_test.py by default."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# Create test files
(root / "test_agent.py").write_text("from praisonaiagents import Agent\n")
(root / "agent_test.py").write_text("from praisonaiagents import Agent\n")
(root / "conftest.py").write_text("from praisonaiagents import Agent\n")
# Create regular file
(root / "my_agent.py").write_text("from praisonaiagents import Agent\n")
source = BatchSource(root=root, exclude_tests=True)
items = source.discover()
# Should only find my_agent.py
assert len(items) == 1
assert items[0].source_path.name == "my_agent.py"
def test_includes_test_files_when_disabled(self):
"""Should include test files when exclude_tests=False."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "test_agent.py").write_text("from praisonaiagents import Agent\n")
(root / "my_agent.py").write_text("from praisonaiagents import Agent\n")
source = BatchSource(root=root, exclude_tests=False)
items = source.discover()
assert len(items) == 2
class TestBatchSourceRunItem:
"""Test RunItem creation."""
def test_creates_runitem_with_correct_fields(self):
"""Should create RunItem with correct metadata."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
content = """
from praisonaiagents import Agent
agent = Agent(instructions="Test")
agent.start()
"""
(root / "my_agent.py").write_text(content)
source = BatchSource(root=root)
items = source.discover()
assert len(items) == 1
item = items[0]
assert item.suite == "batch"
assert item.language == "python"
assert item.runnable is True
assert item.uses_agent is True
def test_detects_agent_usage(self):
"""Should detect Agent class usage."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "agent.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
""")
source = BatchSource(root=root)
items = source.discover()
assert items[0].uses_agent is True
def test_detects_agents_usage(self):
"""Should detect Agents/PraisonAIAgents class usage."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "agents.py").write_text("""
from praisonaiagents import Agent, Agents
agents = Agents(agents=[Agent()])
""")
source = BatchSource(root=root)
items = source.discover()
assert items[0].uses_agents is True
class TestBatchSourceGroups:
"""Test group detection."""
def test_assigns_root_group_for_top_level_files(self):
"""Files in root should have group 'root'."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "agent.py").write_text("from praisonaiagents import Agent\n")
source = BatchSource(root=root)
items = source.discover()
assert items[0].group == "root"
def test_assigns_directory_group_for_nested_files(self):
"""Files in subdirs should have group = subdir name."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
subdir = root / "my_agents"
subdir.mkdir()
(subdir / "agent.py").write_text("from praisonaiagents import Agent\n")
source = BatchSource(root=root, recursive=True)
items = source.discover()
assert items[0].group == "my_agents"
def test_get_groups_returns_all_groups(self):
"""get_groups() should return all discovered groups."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "root.py").write_text("from praisonaiagents import Agent\n")
dir1 = root / "agents"
dir1.mkdir()
(dir1 / "a.py").write_text("from praisonaiagents import Agent\n")
dir2 = root / "tools"
dir2.mkdir()
(dir2 / "b.py").write_text("from praisonaiagents import Agent\n")
source = BatchSource(root=root, recursive=True)
groups = source.get_groups()
assert set(groups) == {"root", "agents", "tools"}
class TestBatchSourceServerDetection:
"""Test server script detection."""
def test_detects_uvicorn_server(self):
"""Should detect uvicorn.run() as server script."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "server.py").write_text("""
from praisonaiagents import Agent
import uvicorn
from fastapi import FastAPI
app = FastAPI()
uvicorn.run(app, host="0.0.0.0", port=8000)
""")
# Need exclude_servers=False to include server scripts
source = BatchSource(root=root, exclude_servers=False)
items = source.discover()
assert len(items) == 1
assert items[0].is_server is True
def test_detects_launch_method(self):
"""Should detect .launch() as server script."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "api.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
agent.launch(path="/ask", port=3030)
""")
source = BatchSource(root=root, exclude_servers=False)
items = source.discover()
assert len(items) == 1
assert items[0].is_server is True
def test_detects_flask_app_run(self):
"""Should detect app.run() (Flask) as server script."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "flask_app.py").write_text("""
from praisonaiagents import Agent
from flask import Flask
app = Flask(__name__)
app.run(debug=True)
""")
source = BatchSource(root=root, exclude_servers=False)
items = source.discover()
assert len(items) == 1
assert items[0].is_server is True
def test_detects_streamlit_import(self):
"""Should detect streamlit import as server script."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "streamlit_app.py").write_text("""
from praisonaiagents import Agent
import streamlit as st
st.title("Agent App")
""")
source = BatchSource(root=root, exclude_servers=False)
items = source.discover()
assert len(items) == 1
assert items[0].is_server is True
def test_detects_gradio_import(self):
"""Should detect gradio import as server script."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "gradio_app.py").write_text("""
from praisonaiagents import Agent
import gradio as gr
demo = gr.Interface(fn=lambda x: x, inputs="text", outputs="text")
demo.launch()
""")
source = BatchSource(root=root, exclude_servers=False)
items = source.discover()
assert len(items) == 1
assert items[0].is_server is True
def test_non_server_script(self):
"""Should not mark regular scripts as server."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "agent.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
result = agent.start("Hello")
print(result)
""")
source = BatchSource(root=root)
items = source.discover()
assert len(items) == 1
assert items[0].is_server is False
class TestBatchSourceServerExclusion:
"""Test server script exclusion."""
def test_excludes_servers_by_default(self):
"""Should exclude server scripts by default."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# Server script
(root / "server.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
agent.launch(path="/ask", port=3030)
""")
# Regular script
(root / "agent.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
print(agent.start("Hello"))
""")
source = BatchSource(root=root, exclude_servers=True)
items = source.discover()
# Should only find agent.py
assert len(items) == 1
assert items[0].source_path.name == "agent.py"
def test_includes_servers_when_disabled(self):
"""Should include server scripts when exclude_servers=False."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "server.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
agent.launch(path="/ask", port=3030)
""")
(root / "agent.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
""")
source = BatchSource(root=root, exclude_servers=False)
items = source.discover()
assert len(items) == 2
def test_server_only_mode(self):
"""Should only return server scripts when server_only=True."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "server.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
agent.launch(path="/ask", port=3030)
""")
(root / "agent.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
""")
source = BatchSource(root=root, server_only=True)
items = source.discover()
# Should only find server.py
assert len(items) == 1
assert items[0].source_path.name == "server.py"
class TestBatchSourceFiltering:
"""Test filtering by agent type."""
def test_filter_agent_only(self):
"""Should filter to only scripts using Agent class."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# Agent only
(root / "single.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
""")
# Agents (multi-agent)
(root / "multi.py").write_text("""
from praisonaiagents import Agent, Agents
agents = Agents(agents=[Agent()])
""")
source = BatchSource(root=root, filter_type="agent")
items = source.discover()
# Should find both (both use Agent)
assert len(items) == 2
def test_filter_agents_only(self):
"""Should filter to only scripts using Agents/PraisonAIAgents class."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# Agent only
(root / "single.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
""")
# Agents (multi-agent)
(root / "multi.py").write_text("""
from praisonaiagents import Agent, Agents
agents = Agents(agents=[Agent()])
""")
source = BatchSource(root=root, filter_type="agents")
items = source.discover()
# Should only find multi.py
assert len(items) == 1
assert items[0].source_path.name == "multi.py"
def test_filter_workflow_only(self):
"""Should filter to only scripts using Workflow class."""
from praisonai.suite_runner.batch_source import BatchSource
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# Agent only
(root / "agent.py").write_text("""
from praisonaiagents import Agent
agent = Agent(instructions="Test")
""")
# Workflow
(root / "workflow.py").write_text("""
from praisonaiagents import Agent, Workflow
workflow = Workflow(agents=[Agent()])
""")
source = BatchSource(root=root, filter_type="workflow")
items = source.discover()
# Should only find workflow.py
assert len(items) == 1
assert items[0].source_path.name == "workflow.py"