|
1 | 1 | #!/usr/bin/env python |
2 | 2 |
|
| 3 | +import asyncio |
3 | 4 | import base64 |
| 5 | +import inspect |
4 | 6 | import json |
5 | 7 | import sys |
6 | 8 | from contextlib import asynccontextmanager |
7 | 9 | from os import environ |
8 | | -from typing import Annotated, List, Optional |
| 10 | +from typing import Annotated, Callable, List, Optional, Union |
9 | 11 |
|
10 | 12 | import aiohttp |
| 13 | +import bot as bot_module |
11 | 14 | from bot import bot |
12 | 15 | from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Query, Request, WebSocket |
| 16 | +from fastapi.responses import JSONResponse |
13 | 17 | from fastapi.websockets import WebSocketState |
14 | 18 | from feature_manager import FeatureKeys, FeatureManager |
15 | 19 | from loguru import logger |
|
22 | 26 | from pipecatcloud_system import add_lifespan_to_app, app |
23 | 27 | from waiting_server import Config, WaitingServer |
24 | 28 |
|
| 29 | +# ------------------------------------------------------------ |
| 30 | +# Health check functions (readyz can be overridden by customer bot.py) |
| 31 | +# ------------------------------------------------------------ |
| 32 | +ReadyzResult = Union[bool, dict] |
| 33 | + |
| 34 | + |
| 35 | +def _default_readyz() -> bool: |
| 36 | + """Default readiness check - always returns True.""" |
| 37 | + return True |
| 38 | + |
| 39 | + |
| 40 | +# Try to import customer-defined readyz function, fall back to default |
| 41 | +readyz_func: Callable[[], ReadyzResult] = getattr(bot_module, "readyz", _default_readyz) |
| 42 | + |
| 43 | + |
| 44 | +async def _call_readyz_func(func: Callable[[], ReadyzResult]) -> ReadyzResult: |
| 45 | + """Call readyz function, handling both sync and async.""" |
| 46 | + try: |
| 47 | + if asyncio.iscoroutinefunction(func) or inspect.iscoroutinefunction(func): |
| 48 | + return await func() |
| 49 | + else: |
| 50 | + return func() |
| 51 | + except Exception as e: |
| 52 | + logger.warning(f"Health check function raised exception: {e}") |
| 53 | + return {"ready": False, "error": str(e)} |
| 54 | + |
| 55 | + |
25 | 56 | # Global state dictionary |
26 | 57 | GLOBALS = {} |
27 | 58 |
|
@@ -100,7 +131,47 @@ async def run_bot(args: SessionArguments, transport_type: Optional[str] = None): |
100 | 131 | GLOBALS["pipecat_session_body"] = None |
101 | 132 |
|
102 | 133 |
|
| 134 | +# ------------------------------------------------------------ |
| 135 | +# Health check routes (Kubernetes probes) |
| 136 | +# ------------------------------------------------------------ |
| 137 | +@app.get("/readyz") |
| 138 | +async def readyz(): |
| 139 | + """Kubernetes readiness probe endpoint. |
| 140 | +
|
| 141 | + Override by defining a `readyz()` function in your bot.py that returns either: |
| 142 | + - bool: True for ready, False for not ready |
| 143 | + - dict: Must contain "ready" key (bool), can include additional info |
| 144 | + """ |
| 145 | + result = await _call_readyz_func(readyz_func) |
| 146 | + |
| 147 | + # Handle bool return type |
| 148 | + if isinstance(result, bool): |
| 149 | + if result: |
| 150 | + return JSONResponse(content={"status": "ok"}, status_code=200) |
| 151 | + return JSONResponse(content={"status": "not ready"}, status_code=503) |
| 152 | + |
| 153 | + # Handle dict return type |
| 154 | + if isinstance(result, dict): |
| 155 | + is_ready = result.get("ready", False) |
| 156 | + status_code = 200 if is_ready else 503 |
| 157 | + return JSONResponse(content=result, status_code=status_code) |
| 158 | + |
| 159 | + # Unexpected return type |
| 160 | + logger.warning(f"readyz() returned unexpected type: {type(result)}") |
| 161 | + return JSONResponse( |
| 162 | + content={"status": "error", "error": "invalid readyz return type"}, status_code=503 |
| 163 | + ) |
| 164 | + |
| 165 | + |
| 166 | +@app.get("/livez") |
| 167 | +async def livez(): |
| 168 | + """Kubernetes liveness probe endpoint.""" |
| 169 | + return JSONResponse(content={"status": "ok"}, status_code=200) |
| 170 | + |
| 171 | + |
| 172 | +# ------------------------------------------------------------ |
103 | 173 | # Basic routes (always available) |
| 174 | +# ------------------------------------------------------------ |
104 | 175 | @app.post("/bot") |
105 | 176 | async def handle_bot_request( |
106 | 177 | body: dict, |
|
0 commit comments