Model 3: Local HTTP Sidecar

HTTP API running locally for polyglot microservices

Expose recipes over localhost HTTP so polyglot services call the same sidecar process.

python
from praisonaiagents import Agent

agent = Agent(
    name="Sidecar Client",
    instructions="Explain local HTTP sidecar integration for microservices.",
)
agent.start("Call a recipe from Go via localhost.")

The user POSTs to the sidecar; the agent runtime executes the recipe and returns JSON.

Diagram

HTTP Sidecar

📋 HTTP Request

🔌 Sidecar :8765

✅ JSON Result

How It Works

Diagram
RecipeSidecarYour AppRecipeSidecarYour AppPOST /v1/recipes/runExecute recipeOutputJSON response
Diagram

Local Machine / Container

HTTP

Your App
Any Language

PraisonAI
Sidecar :8765

Recipe Engine

The sidecar runs as a local HTTP server, exposing recipes via REST API. Your application communicates over localhost—no external network required.

Pros & Cons

Pros

  • Polyglot – Any language with HTTP client can use it
  • Process isolation – Recipes run in separate process
  • Standard HTTP – Familiar REST/JSON interface
  • Streaming support – SSE for real-time events
  • Auth built-in – API key and JWT authentication
  • Hot reload – Update recipes without restart

Cons

  • Network overhead – HTTP adds latency (~1-5ms)
  • Port management – Need to manage port allocation
  • Process lifecycle – Must start/stop sidecar
  • Local only – Not suitable for distributed systems

Step-by-Step Tutorial

  1. Install with Serve Dependencies
    bash
    pip install "praisonai[serve]"
  2. Start the Sidecar Server
    bash
    # Basic start
    praisonai serve recipe --port 8765
    
    # With API key authentication
    praisonai serve recipe --port 8765 --auth api-key
    
    # With hot reload for development
    praisonai serve recipe --port 8765 --reload
  3. Verify Server Health
    bash
    curl http://localhost:8765/health

    Expected response:

    json
    {"status": "healthy", "service": "praisonai-recipe-runner", "version": "..."}
  4. List Available Recipes
    bash
    curl http://localhost:8765/v1/recipes
  5. Run a Recipe
    bash
    curl -X POST http://localhost:8765/v1/recipes/run \
      -H "Content-Type: application/json" \
      -d '{
        "recipe": "my-recipe",
        "input": {"query": "Hello"}
      }'

API Reference

EndpointMethodDescription
/healthGETHealth check
/v1/recipesGETList recipes
/v1/recipes/{name}GETDescribe recipe
/v1/recipes/{name}/schemaGETGet recipe schema
/v1/recipes/runPOSTRun recipe (sync)
/v1/recipes/streamPOSTRun recipe (SSE)
/v1/recipes/validatePOSTValidate recipe

Production-Ready Example

Python (requests)
import requests
from typing import Any, Dict, Optional

class RecipeClient:
    """HTTP client for PraisonAI recipe sidecar."""

    def __init__(
        self,
        base_url: str = "http://localhost:8765",
        api_key: Optional[str] = None,
        timeout: int = 60
    ):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.headers = {"Content-Type": "application/json"}
        if api_key:
            self.headers["X-API-Key"] = api_key

    def health(self) -> Dict[str, Any]:
        """Check server health."""
        resp = requests.get(
            f"{self.base_url}/health",
            headers=self.headers,
            timeout=5
        )
        resp.raise_for_status()
        return resp.json()

    def list_recipes(self, tags: list = None) -> list:
        """List available recipes."""
        params = {}
        if tags:
            params["tags"] = ",".join(tags)

        resp = requests.get(
            f"{self.base_url}/v1/recipes",
            headers=self.headers,
            params=params,
            timeout=self.timeout
        )
        resp.raise_for_status()
        return resp.json().get("recipes", [])

    def run(
        self,
        recipe_name: str,
        input_data: Dict[str, Any],
        config: Dict[str, Any] = None,
        session_id: str = None
    ) -> Dict[str, Any]:
        """Run a recipe."""
        body = {
            "recipe": recipe_name,
            "input": input_data,
        }
        if config:
            body["config"] = config
        if session_id:
            body["session_id"] = session_id

        resp = requests.post(
            f"{self.base_url}/v1/recipes/run",
            headers=self.headers,
            json=body,
            timeout=self.timeout
        )
        resp.raise_for_status()
        return resp.json()

    def run_stream(self, recipe_name: str, input_data: Dict[str, Any]):
        """Run a recipe with streaming."""
        body = {"recipe": recipe_name, "input": input_data}

        with requests.post(
            f"{self.base_url}/v1/recipes/stream",
            headers={**self.headers, "Accept": "text/event-stream"},
            json=body,
            stream=True,
            timeout=self.timeout
        ) as resp:
            resp.raise_for_status()
            for line in resp.iter_lines():
                if line:
                    line = line.decode("utf-8")
                    if line.startswith("data:"):
                        yield line[5:].strip()


# Usage
if __name__ == "__main__":
    client = RecipeClient(api_key="your-api-key")

    # Check health
    print(client.health())

    # Run recipe
    result = client.run(
        "support-reply-drafter",
        {"ticket_id": "T-123", "message": "I need help"}
    )
    print(result["output"])
Node.js (fetch)
class RecipeClient {
  constructor(baseUrl = 'http://localhost:8765', apiKey = null) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.headers = { 'Content-Type': 'application/json' };
    if (apiKey) {
      this.headers['X-API-Key'] = apiKey;
    }
  }

  async health() {
    const resp = await fetch(`${this.baseUrl}/health`, {
      headers: this.headers,
    });
    return resp.json();
  }

  async listRecipes(tags = []) {
    const params = tags.length ? `?tags=${tags.join(',')}` : '';
    const resp = await fetch(`${this.baseUrl}/v1/recipes${params}`, {
      headers: this.headers,
    });
    const data = await resp.json();
    return data.recipes || [];
  }

  async run(recipeName, inputData, config = null) {
    const body = { recipe: recipeName, input: inputData };
    if (config) body.config = config;

    const resp = await fetch(`${this.baseUrl}/v1/recipes/run`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify(body),
    });
    return resp.json();
  }

  async *runStream(recipeName, inputData) {
    const body = { recipe: recipeName, input: inputData };

    const resp = await fetch(`${this.baseUrl}/v1/recipes/stream`, {
      method: 'POST',
      headers: { ...this.headers, Accept: 'text/event-stream' },
      body: JSON.stringify(body),
    });

    const reader = resp.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop();

      for (const line of lines) {
        if (line.startsWith('data:')) {
          yield JSON.parse(line.slice(5).trim());
        }
      }
    }
  }
}

// Usage
const client = new RecipeClient('http://localhost:8765', 'your-api-key');
const result = await client.run('my-recipe', { query: 'Hello' });
console.log(result.output);
curl
# Health check
curl http://localhost:8765/health

# List recipes
curl http://localhost:8765/v1/recipes

# Run recipe
curl -X POST http://localhost:8765/v1/recipes/run \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{"recipe": "my-recipe", "input": {"query": "Hello"}}'

# Stream recipe
curl -X POST http://localhost:8765/v1/recipes/stream \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"recipe": "my-recipe", "input": {"query": "Hello"}}'

Docker Deployment

dockerfile
FROM python:3.11-slim

WORKDIR /app

RUN pip install "praisonai[serve]"

EXPOSE 8765

CMD ["praisonai", "recipe", "serve", "--host", "0.0.0.0", "--port", "8765"]
yaml
# docker-compose.yml
version: '3.8'
services:
  praisonai-sidecar:
    build: .
    ports:
      - "8765:8765"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - PRAISONAI_API_KEY=${PRAISONAI_API_KEY}
    volumes:
      - ./recipes:/root/.praison/templates
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8765/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Troubleshooting

Connection refused

Ensure the server is running:

bash
# Check if port is in use
lsof -i :8765

# Start server
praisonai serve recipe --port 8765
401 Unauthorized

If auth is enabled, provide the API key:

bash
curl -H "X-API-Key: your-key" http://localhost:8765/v1/recipes

Or set the environment variable:

bash
export PRAISONAI_API_KEY=your-key
SSE streaming not working

Ensure you’re using the correct endpoint and headers:

bash
curl -X POST http://localhost:8765/v1/recipes/stream \
  -H "Accept: text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"recipe": "my-recipe", "input": {}}'
CORS errors in browser

Start the server with CORS enabled:

bash
# Via environment
export PRAISONAI_CORS_ORIGINS="*"
praisonai serve recipe

# Or via config file (serve.yaml)
# cors_origins: "*"

Security & Ops Notes

  • Bind to localhost – Default 127.0.0.1 prevents external access
  • API key auth – Enable --auth api-key for production
  • HTTPS – Use a reverse proxy (nginx, traefik) for TLS
  • Rate limiting – Configure rate_limit in serve.yaml
  • Request size limits – Default 10MB, adjust via max_request_size
yaml
# serve.yaml - Production configuration
host: 127.0.0.1
port: 8765
auth: api-key
api_key: ${PRAISONAI_API_KEY}
cors_origins: "https://myapp.com"
rate_limit: 100
max_request_size: 10485760
enable_metrics: true

Best Practices

Bind to localhost unless you need remote access

The default 127.0.0.1 keeps the sidecar off the network. Only bind to 0.0.0.0 behind a reverse proxy that terminates TLS and auth.

Enable API-key auth for anything beyond local dev

Start with --auth api-key and pass the key via X-API-Key. Keep the key in PRAISONAI_API_KEY, never in client code.

Stream long-running recipes

Use /v1/recipes/stream with Accept: text/event-stream so callers see incremental output instead of waiting for the full response.

Add a health check to your orchestrator

Poll /health in Docker/Compose so the platform restarts the sidecar automatically when it becomes unhealthy.


Related