Skip to main content

Model 2: CLI Invocation (Subprocess)

When to Use: Shell scripts, CI/CD pipelines, batch processing, or when you need language-agnostic recipe invocation without running a server.

How It Works

The CLI model spawns a subprocess to run recipes. Output is captured via stdout in JSON format for easy parsing.

Pros & Cons

  • Language-agnostic - Works from any language that can spawn processes
  • Simple JSON output - Easy to parse in any language
  • No SDK dependency - Calling app doesn’t need praisonai installed
  • Process isolation - Recipe runs in separate process
  • Easy debugging - Run commands manually to test

Step-by-Step Tutorial

1

Verify CLI Installation

praisonai --help
If not found, install:
pip install praisonai
2

List Available Recipes

# Human-readable format
praisonai recipe list

# JSON format for parsing
praisonai recipe list --json
3

Get Recipe Info

praisonai recipe info my-recipe --json
4

Run Recipe with JSON Output

praisonai recipe run my-recipe \
  --input '{"query": "Hello"}' \
  --json
5

Parse Output in Your Application

import subprocess
import json

result = subprocess.run(
    ["praisonai", "recipe", "run", "my-recipe",
     "--input", '{"query": "Hello"}', "--json"],
    capture_output=True,
    text=True,
    timeout=60
)

if result.returncode == 0:
    data = json.loads(result.stdout)
    print(f"Run ID: {data['run_id']}")
    print(f"Output: {data['output']}")
else:
    print(f"Error: {result.stderr}")

Production-Ready Example

import subprocess
import json
import logging
from typing import Any, Dict, Optional

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class RecipeCLIClient:
    """Production-ready CLI client for recipe invocation."""
    
    def __init__(self, timeout: int = 60, retries: int = 3):
        self.timeout = timeout
        self.retries = retries
    
    def run(
        self,
        recipe_name: str,
        input_data: Dict[str, Any],
        dry_run: bool = False
    ) -> Dict[str, Any]:
        """Run a recipe via CLI with retries."""
        
        cmd = [
            "praisonai", "recipe", "run", recipe_name,
            "--input", json.dumps(input_data),
            "--json"
        ]
        
        if dry_run:
            cmd.append("--dry-run")
        
        last_error = None
        
        for attempt in range(self.retries):
            try:
                result = subprocess.run(
                    cmd,
                    capture_output=True,
                    text=True,
                    timeout=self.timeout
                )
                
                if result.returncode == 0:
                    data = json.loads(result.stdout)
                    logger.info(f"Recipe {recipe_name} completed: {data.get('run_id')}")
                    return data
                else:
                    logger.warning(f"Recipe failed (attempt {attempt + 1}): {result.stderr}")
                    last_error = result.stderr
                    
            except subprocess.TimeoutExpired:
                logger.error(f"Recipe timeout (attempt {attempt + 1})")
                last_error = "Timeout"
            except json.JSONDecodeError as e:
                logger.error(f"JSON parse error: {e}")
                last_error = str(e)
        
        raise RuntimeError(f"Recipe {recipe_name} failed: {last_error}")
    
    def list_recipes(self) -> list:
        """List available recipes."""
        result = subprocess.run(
            ["praisonai", "recipe", "list", "--json"],
            capture_output=True,
            text=True,
            timeout=30
        )
        
        if result.returncode == 0:
            return json.loads(result.stdout)
        return []


# Usage
if __name__ == "__main__":
    client = RecipeCLIClient(timeout=60, retries=3)
    
    # List recipes
    recipes = client.list_recipes()
    print(f"Found {len(recipes)} recipes")
    
    # Run a recipe
    result = client.run(
        "support-reply-drafter",
        {"ticket_id": "T-123", "message": "I need help"}
    )
    print(result["output"])

CI/CD Integration

GitHub Actions

name: Run Recipe
on: [push]

jobs:
  run-recipe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      
      - name: Install PraisonAI
        run: pip install praisonai
      
      - name: Run Recipe
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          praisonai recipe run code-review \
            --input '{"diff": "..."}' \
            --json > result.json
      
      - name: Upload Result
        uses: actions/upload-artifact@v4
        with:
          name: recipe-result
          path: result.json

GitLab CI

run-recipe:
  image: python:3.11
  script:
    - pip install praisonai
    - praisonai recipe run my-recipe --input '{"query": "test"}' --json
  variables:
    OPENAI_API_KEY: $OPENAI_API_KEY

Troubleshooting

Add praisonai to your PATH or use the full path:
# Find installation path
python -c "import praisonai; print(praisonai.__file__)"

# Or use python -m
python -m praisonai recipe run my-recipe --json
Ensure you’re using the --json flag and only parsing stdout:
result = subprocess.run(cmd, capture_output=True, text=True)
# Only parse stdout, not stderr
data = json.loads(result.stdout)
Check stderr for error details:
if result.returncode != 0:
    print(f"Exit code: {result.returncode}")
    print(f"Error: {result.stderr}")
Common exit codes:
  • 1: General error
  • 2: Validation error
  • 7: Recipe not found
Explicitly pass environment variables:
import os

result = subprocess.run(
    cmd,
    capture_output=True,
    text=True,
    env={**os.environ, "OPENAI_API_KEY": "sk-..."}
)

Security & Ops Notes

Security Considerations
  • Input sanitization: Never pass unsanitized user input directly to CLI
  • Shell injection: Use list form of subprocess.run, not shell=True
  • API keys: Pass via environment variables, not command line arguments
  • Timeouts: Always set timeouts to prevent hanging processes
# GOOD: List form, env vars
subprocess.run(
    ["praisonai", "recipe", "run", name, "--input", json.dumps(data)],
    env={"OPENAI_API_KEY": key}
)

# BAD: Shell form, key in command
subprocess.run(
    f"OPENAI_API_KEY={key} praisonai recipe run {name}",
    shell=True  # Dangerous!
)

CLI Reference

praisonai recipe run <name> [options]

Options:
  --input <json>      Input data as JSON string
  --output <dir>      Output directory
  --json              Output as JSON
  --dry-run           Show what would be done
  --write             Execute (for dry-run-default recipes)
  --force             Force execution despite missing deps
  --consent           Acknowledge consent requirements
  --skip-checks       Skip dependency checks

Next Steps