diff --git a/templates/config.json b/templates/config.json index 4d013eb0..df0e8926 100644 --- a/templates/config.json +++ b/templates/config.json @@ -2571,6 +2571,34 @@ }, "tags": ["Web Data & Search", "RAG", "Developer Tools"] }, +{ + "id": "dograh", + "name": "dograh-hq/dograh", + "description": "Open source voice AI platform. Self-hosted alternative to Vapi and Retell. On Prem, BYOK across Speech to Speech or LLM/STT/TTS, with a visual workflow builder, MCP native and telephony support.", + "repo": "https://github.com/Phala-Network/phala-cloud/tree/main/templates/prebuilt/dograh", + "author": "dograh-hq", + "icon": "dograh.svg", + "envs": [ + { + "key": "DOGRAH_SDK_VERSION", + "required": false, + "default": "0.1.6", + "description": "Pinned Dograh Python SDK package version installed by the demo service at container startup." + }, + { + "key": "DOGRAH_DEMO_TITLE", + "required": false, + "default": "Phala Cloud Dograh SDK demo", + "description": "Title returned by the /demo endpoint." + } + ], + "defaultResource": { + "vCPU": 1, + "memory": 1024, + "diskSize": 10 + }, + "tags": ["AI Apps & Workflows", "Voice AI", "Developer Tools"] +}, { "id": "dify", "name": "langgenius/dify", diff --git a/templates/icons/dograh.svg b/templates/icons/dograh.svg new file mode 100644 index 00000000..6a3e57f3 --- /dev/null +++ b/templates/icons/dograh.svg @@ -0,0 +1,3 @@ + + Dograh AI + diff --git a/templates/prebuilt/dograh/README.md b/templates/prebuilt/dograh/README.md new file mode 100644 index 00000000..2685a146 --- /dev/null +++ b/templates/prebuilt/dograh/README.md @@ -0,0 +1,104 @@ +# Dograh on Phala Cloud + +This template runs a CPU-safe Dograh SDK verifier behind a public Caddy proxy. The app installs the real `dograh-sdk` Python package, imports it, builds a deterministic local Dograh voice workflow, serializes it to the SDK's React Flow-compatible JSON shape, and exposes JSON endpoints for smoke testing. + +The default deployment does not start the full Dograh voice platform, does not place telephony calls, does not call LLM/STT/TTS providers, does not download model weights, and does not require provider credentials. Upstream Dograh's full self-hosted Docker stack is a multi-service deployment for PostgreSQL, Redis, MinIO, API, UI, optional nginx, and optional coturn; the upstream remote deployment guide calls for at least 8 GB RAM and 4 vCPUs, so this prebuilt template uses a small deterministic verifier by default. + +## Metadata + +- Template id: `dograh` +- Category: AI Apps & Workflows +- Upstream repo: `https://github.com/dograh-hq/dograh` +- Upstream author: `dograh-hq` +- Package: `dograh-sdk==0.1.6` +- Python runtime: `python:3.12` from the `ghcr.io/astral-sh/uv:python3.12-bookworm-slim` image +- Icon source: upstream `docs/logo/light.svg` from `dograh-hq/dograh`, inspected at commit `8f10bcade32079af126e4e9d83061cd30936fcad` + +## Services + +- `app`: internal Python HTTP service. At startup it installs `dograh-sdk`, imports the package, builds a local three-node Dograh workflow with SDK `Workflow.add()`, `Workflow.edge()`, `Workflow.to_json()`, and `Workflow.from_json()`, then serves JSON on port `8000`. +- `proxy`: public Caddy reverse proxy. It is the only service with a host port mapping and exposes `8080:80`. + +## Deploy + +From this template directory: + +```bash +docker compose config +docker compose up -d +``` + +On Phala Cloud, deploy the prebuilt template and open the public endpoint on port `8080`. + +The first start can take a few minutes because the app installs the pinned Dograh SDK wheel and dependencies inside the container. The template does not require a persistent volume. + +## Usage + +The public HTTP API is available through Caddy on port `8080`. + +```bash +curl -fsS http://localhost:8080/healthz | jq +curl -fsS http://localhost:8080/demo | jq +curl -fsS http://localhost:8080/v1/models | jq +``` + +Endpoints: + +- `/healthz`: returns HTTP 200 only when the real `dograh-sdk` package imports successfully and the local workflow builder demo round-trips through `Workflow.from_json()`. +- `/demo`: returns package metadata, the local node-spec catalog version, a three-node workflow payload, edge labels, and flags confirming that no credentials, provider calls, model downloads, or telephony calls are used. +- `/v1/models`: returns an OpenAI-shaped model list containing a `dograh-sdk/local-workflow-demo` placeholder. It is metadata only; the default template does not host a model. + +## Verification + +Run these smoke checks after deployment: + +```bash +docker compose ps +curl -i http://localhost:8080/healthz +curl -fsS http://localhost:8080/demo | jq '.demo.roundtrip_ok, .demo.credentials_required, .demo.provider_calls' +curl -fsS http://localhost:8080/demo | jq '.demo.node_types, .demo.edge_labels' +curl -fsS http://localhost:8080/v1/models | jq '.data[0].id' +``` + +These commands verify that the real Dograh SDK imports, the local workflow demo round-trips successfully, and the template stays credential-free by default. + +Expected results: + +- `GET /healthz` returns `200 OK`. +- `.demo.roundtrip_ok` is `true`. +- `.demo.credentials_required` and `.demo.provider_calls` are `false`. +- `.demo.node_types` includes `startCall`, `agentNode`, and `endCall`. +- `/v1/models` includes `dograh-sdk/local-workflow-demo`. + +## Environment Variables + +The default template requires no credentials. + +| Variable | Default | Required | Description | +| --- | --- | --- | --- | +| `DOGRAH_SDK_VERSION` | `0.1.6` | No | Pinned Dograh Python SDK package version installed at container startup. Override only when testing another compatible SDK release. | +| `DOGRAH_DEMO_TITLE` | `Phala Cloud Dograh SDK demo` | No | Title returned by the `/demo` endpoint. | + +Dograh API credentials such as `DOGRAH_API_KEY` and live backend URLs such as `DOGRAH_API_URL` are intentionally not required and are not consumed by the default verifier. Add them only if you replace this local verifier with an application that talks to a hosted or self-hosted Dograh backend. + +## Production Notes + +- Upstream Dograh is an open source voice AI platform with a visual workflow builder, SDKs, MCP integration, telephony integrations, and bring-your-own LLM/STT/TTS provider configuration. +- For a production Dograh platform deployment, follow the upstream Docker guide and size the CVM for the full multi-service stack. The upstream remote deployment docs recommend at least 8 GB RAM and 4 vCPUs and require public HTTPS for browser microphone access. +- The upstream compose file publishes PostgreSQL, Redis, MinIO, the API, the UI, and optional remote networking services. This template deliberately avoids that stack by default so it can act as a deterministic CPU-safe verifier on small Phala Cloud resources. +- The SDK's live workflow operations normally require a Dograh backend plus a `DOGRAH_API_KEY`; outbound calls and production voice agents also require configured telephony and inference providers. + +## Security Notes + +- Only Caddy publishes a host port: `8080:80`. +- The app service is internal and uses `expose`, not `ports`. +- The template does not use privileged mode, host networking, host IPC, Docker socket mounts, host bind mounts, or external credentials. +- No API keys, tokens, private keys, OTPs, passwords, or generated secrets are baked into the compose file. + +## Cleanup + +```bash +docker compose down +``` + +No named volumes are created by this template. diff --git a/templates/prebuilt/dograh/docker-compose.yml b/templates/prebuilt/dograh/docker-compose.yml new file mode 100644 index 00000000..b96ee0e1 --- /dev/null +++ b/templates/prebuilt/dograh/docker-compose.yml @@ -0,0 +1,381 @@ +services: + app: + image: ghcr.io/astral-sh/uv:python3.12-bookworm-slim + restart: unless-stopped + init: true + expose: + - "8000" + environment: + APP_PORT: "8000" + DOGRAH_SDK_VERSION: ${DOGRAH_SDK_VERSION:-0.1.6} + DOGRAH_DEMO_TITLE: ${DOGRAH_DEMO_TITLE:-Phala Cloud Dograh SDK demo} + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_CACHE_DIR: "1" + PYTHONUNBUFFERED: "1" + UV_SYSTEM_PYTHON: "1" + configs: + - source: dograh_demo_app + target: /opt/dograh-demo/main.py + command: + - /bin/sh + - -lc + - | + set -eu + uv pip install --system --no-cache "dograh-sdk==$${DOGRAH_SDK_VERSION}" + exec python /opt/dograh-demo/main.py + healthcheck: + test: + [ + "CMD-SHELL", + "python -c \"import os, sys, urllib.request; port = os.environ.get('APP_PORT', '8000'); response = urllib.request.urlopen(f'http://127.0.0.1:{port}/healthz', timeout=5); sys.exit(0 if response.status == 200 else 1)\"", + ] + interval: 30s + timeout: 10s + retries: 10 + start_period: 180s + networks: + - internal + + proxy: + image: caddy:2.8 + restart: unless-stopped + ports: + - "8080:80" + configs: + - source: caddy_config + target: /etc/caddy/Caddyfile + depends_on: + app: + condition: service_healthy + networks: + - internal + +configs: + caddy_config: + content: | + :80 { + encode zstd gzip + header { + X-Content-Type-Options "nosniff" + Referrer-Policy "no-referrer" + -Server + } + reverse_proxy app:8000 + } + + dograh_demo_app: + content: | + import importlib + import importlib.metadata + import json + import os + import platform + import sys + import time + from http import HTTPStatus + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + from urllib.parse import urlparse + + + STARTED_AT = time.time() + PACKAGE_NAME = "dograh-sdk" + MODULE_NAME = "dograh_sdk" + SPEC_VERSION = "local-phala-demo-2026-05-30" + + + def property_spec(name, property_type, display_name, description, **extra): + spec = { + "name": name, + "type": property_type, + "display_name": display_name, + "description": description, + } + spec.update(extra) + return spec + + + NODE_SPECS = { + "startCall": { + "name": "startCall", + "display_name": "Start Call", + "description": "Entry point for a Dograh voice workflow.", + "category": "call_node", + "icon": "phone-call", + "version": "1.0.0", + "properties": [ + property_spec("name", "string", "Name", "Node name.", required=True), + property_spec("prompt", "mention_textarea", "Prompt", "Opening instructions.", required=True), + property_spec( + "greeting_type", + "options", + "Greeting Type", + "How Dograh should greet the caller.", + default="text", + options=[ + {"value": "text", "label": "Text"}, + {"value": "audio", "label": "Audio"}, + ], + ), + property_spec("greeting", "string", "Greeting", "Text greeting for the caller."), + property_spec("allow_interrupt", "boolean", "Allow Interrupt", "Whether the caller can interrupt.", default=False), + property_spec("add_global_prompt", "boolean", "Add Global Prompt", "Append the global prompt.", default=True), + ], + }, + "agentNode": { + "name": "agentNode", + "display_name": "Agent Node", + "description": "LLM-driven conversation step in a Dograh voice workflow.", + "category": "call_node", + "icon": "bot", + "version": "1.0.0", + "properties": [ + property_spec("name", "string", "Name", "Node name.", required=True), + property_spec("prompt", "mention_textarea", "Prompt", "Agent instructions.", required=True), + property_spec("allow_interrupt", "boolean", "Allow Interrupt", "Whether the caller can interrupt.", default=True), + property_spec("add_global_prompt", "boolean", "Add Global Prompt", "Append the global prompt.", default=True), + property_spec("tool_uuids", "tool_refs", "Tools", "Dograh tool UUIDs exposed to this node.", default=[]), + property_spec("document_uuids", "document_refs", "Documents", "Dograh knowledge-base document UUIDs.", default=[]), + ], + }, + "endCall": { + "name": "endCall", + "display_name": "End Call", + "description": "Terminal node that closes a Dograh voice workflow.", + "category": "call_node", + "icon": "phone-off", + "version": "1.0.0", + "properties": [ + property_spec("name", "string", "Name", "Node name.", required=True), + property_spec("prompt", "mention_textarea", "Prompt", "Final instructions before ending the call.", required=True), + property_spec("allow_interrupt", "boolean", "Allow Interrupt", "Whether the caller can interrupt.", default=False), + property_spec("add_global_prompt", "boolean", "Add Global Prompt", "Append the global prompt.", default=True), + ], + }, + } + + + def import_checks(): + result = { + "package": PACKAGE_NAME, + "distribution_version": None, + "module_version": None, + "import_ok": False, + "symbols": {}, + "error": None, + } + try: + result["distribution_version"] = importlib.metadata.version(PACKAGE_NAME) + dograh_sdk = importlib.import_module(MODULE_NAME) + result["module_version"] = getattr(dograh_sdk, "__version__", None) + for symbol in ("DograhClient", "Workflow", "NodeRef", "ValidationError"): + result["symbols"][symbol] = hasattr(dograh_sdk, symbol) + result["import_ok"] = bool(result["distribution_version"]) and all(result["symbols"].values()) + except Exception as exc: + result["error"] = f"{type(exc).__name__}: {exc}" + return result + + + def build_demo(): + from dograh_sdk import Workflow + from dograh_sdk._generated_models import NodeSpec, NodeTypesResponse + + class LocalSpecClient: + def __init__(self): + self.spec_version = SPEC_VERSION + + def get_node_type(self, name): + if name not in NODE_SPECS: + raise KeyError(f"Unknown local Dograh demo node type: {name}") + return NodeSpec.model_validate(NODE_SPECS[name]) + + def list_node_types(self): + return NodeTypesResponse( + spec_version=self.spec_version, + node_types=[ + NodeSpec.model_validate(NODE_SPECS[name]) + for name in sorted(NODE_SPECS) + ], + ) + + client = LocalSpecClient() + workflow = Workflow(client=client, name="phala_cloud_no_secret_demo") + greeting = workflow.add( + type="startCall", + name="greeting", + prompt="Greet the caller and explain this is a local Dograh SDK verifier.", + greeting_type="text", + greeting="Hello, this is the Dograh SDK smoke test running on Phala Cloud.", + position=(0, 0), + ) + qualify = workflow.add( + type="agentNode", + name="qualify", + prompt=( + "Ask one deterministic question about the deployment target, " + "then continue to the closing node. Do not call external tools." + ), + tool_uuids=[], + document_uuids=[], + position=(320, 0), + ) + done = workflow.add( + type="endCall", + name="done", + prompt="Thank the caller and end the verification call.", + position=(640, 0), + ) + workflow.edge( + greeting, + qualify, + label="ready", + condition="The greeting has finished.", + ) + workflow.edge( + qualify, + done, + label="verified", + condition="The local SDK workflow builder completed successfully.", + ) + + payload = workflow.to_json() + roundtrip = Workflow.from_json(payload, client=client, name=workflow.name) + roundtrip_payload = roundtrip.to_json() + node_types = [node["type"] for node in payload["nodes"]] + node_names = [node["data"]["name"] for node in payload["nodes"]] + + return { + "title": os.environ.get("DOGRAH_DEMO_TITLE", "Phala Cloud Dograh SDK demo"), + "workflow_name": workflow.name, + "spec_version": client.spec_version, + "node_types": node_types, + "node_names": node_names, + "edge_labels": [edge["data"]["label"] for edge in payload["edges"]], + "node_count": len(payload["nodes"]), + "edge_count": len(payload["edges"]), + "roundtrip_ok": roundtrip_payload == payload, + "workflow": payload, + "credentials_required": False, + "provider_calls": False, + "model_downloads": False, + "telephony_calls": False, + } + + + IMPORT_STATUS = import_checks() + try: + DEMO_STATUS = build_demo() if IMPORT_STATUS["import_ok"] else None + DEMO_ERROR = None + except Exception as exc: + DEMO_STATUS = None + DEMO_ERROR = f"{type(exc).__name__}: {exc}" + + + def response_body(path): + uptime_seconds = round(time.time() - STARTED_AT, 3) + ok = IMPORT_STATUS["import_ok"] and DEMO_STATUS is not None and DEMO_STATUS["roundtrip_ok"] + + if path == "/healthz": + return HTTPStatus.OK if ok else HTTPStatus.SERVICE_UNAVAILABLE, { + "status": "ok" if ok else "unhealthy", + "uptime_seconds": uptime_seconds, + "dograh_sdk": IMPORT_STATUS, + "demo_error": DEMO_ERROR, + } + + if path == "/demo": + return HTTPStatus.OK if ok else HTTPStatus.SERVICE_UNAVAILABLE, { + "service": "dograh-sdk-local-workflow-demo", + "upstream": "https://github.com/dograh-hq/dograh", + "description": ( + "Builds a deterministic Dograh voice workflow with the real " + "dograh-sdk package and local node specs. It does not call " + "LLM, STT, TTS, telephony, browser-auth, or hosted Dograh APIs." + ), + "uptime_seconds": uptime_seconds, + "dograh_sdk": IMPORT_STATUS, + "demo": DEMO_STATUS, + "demo_error": DEMO_ERROR, + } + + if path == "/v1/models": + return HTTPStatus.OK, { + "object": "list", + "data": [ + { + "id": "dograh-sdk/local-workflow-demo", + "object": "model", + "created": 0, + "owned_by": "dograh-hq", + "description": ( + "Metadata-only placeholder. The default template " + "does not host or call an LLM, STT, or TTS model." + ), + } + ], + } + + if path == "/": + return HTTPStatus.OK, { + "service": "dograh-sdk-local-workflow-demo", + "endpoints": ["/healthz", "/demo", "/v1/models"], + "python": sys.version.split()[0], + "platform": platform.platform(), + "dograh_sdk_import_ok": IMPORT_STATUS["import_ok"], + "demo_ok": ok, + } + + return HTTPStatus.NOT_FOUND, { + "error": "not_found", + "endpoints": ["/healthz", "/demo", "/v1/models"], + } + + + class Handler(BaseHTTPRequestHandler): + server_version = "dograh-sdk-demo" + + def log_message(self, fmt, *args): + print( + json.dumps( + { + "ts": time.time(), + "client": self.client_address[0], + "request": self.requestline, + "message": fmt % args, + } + ), + flush=True, + ) + + def do_GET(self): + path = urlparse(self.path).path.rstrip("/") or "/" + status, payload = response_body(path) + body = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + + if __name__ == "__main__": + port = int(os.environ.get("APP_PORT", "8000")) + print( + json.dumps( + { + "event": "startup", + "port": port, + "dograh_sdk": IMPORT_STATUS, + "demo_ok": DEMO_STATUS is not None, + "provider_calls": False, + "model_downloads": False, + "telephony_calls": False, + } + ), + flush=True, + ) + ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever() + +networks: + internal: + driver: bridge