mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-12 18:47:44 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
657fd93743 | ||
|
|
f0a2936b7f |
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -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]" \
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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}")
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
__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"
|
||||
Reference in new issue
Block a user