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.
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.
How It Works
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
- Install with Serve Dependencies
bash pip install "praisonai[serve]" - 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 - Verify Server Health
bash curl http://localhost:8765/healthExpected response:
json {"status": "healthy", "service": "praisonai-recipe-runner", "version": "..."} - List Available Recipes
bash curl http://localhost:8765/v1/recipes - 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
| Endpoint | Method | Description |
|---|---|---|
/health | GET | Health check |
/v1/recipes | GET | List recipes |
/v1/recipes/{name} | GET | Describe recipe |
/v1/recipes/{name}/schema | GET | Get recipe schema |
/v1/recipes/run | POST | Run recipe (sync) |
/v1/recipes/stream | POST | Run recipe (SSE) |
/v1/recipes/validate | POST | Validate recipe |
Production-Ready Example
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"])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);# 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
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"]# 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: 3Troubleshooting
Connection refused
Ensure the server is running:
# Check if port is in use
lsof -i :8765
# Start server
praisonai serve recipe --port 8765401 Unauthorized
If auth is enabled, provide the API key:
curl -H "X-API-Key: your-key" http://localhost:8765/v1/recipesOr set the environment variable:
export PRAISONAI_API_KEY=your-keySSE streaming not working
Ensure you’re using the correct endpoint and headers:
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:
# 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.1prevents external access - API key auth – Enable
--auth api-keyfor production - HTTPS – Use a reverse proxy (nginx, traefik) for TLS
- Rate limiting – Configure
rate_limitin serve.yaml - Request size limits – Default 10MB, adjust via
max_request_size
# 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: trueBest 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.