diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f580a0d..f125b755 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,38 @@ npm run build 4. Implement the feature 5. Ensure all tests pass: `pytest tests/ -v` / `npm test` 6. Commit with conventional commits: `feat:`, `fix:`, `docs:` -7. Open a Pull Request against `main` +7. **Validate locally**: `bash scripts/pr-validate.sh` (see below) +8. Open a Pull Request against `main` + +## Pre-PR validation + +Run every PR-blocking CI check locally before opening the PR: + +```bash +bash scripts/pr-validate.sh # default: ~3-5 min +bash scripts/pr-validate.sh --quick # pre-commit + notebooks (~30s) +bash scripts/pr-validate.sh --full # default + e2e + all-extras (~10 min) +``` + +The script mirrors `.github/workflows/test.yml` and `notebooks.yml` so a +green local run lines up with green CI. Selective skips: `--skip-py`, +`--skip-ts`, `--skip-notebooks`, `--no-stop`. + +First-time setup: + +```bash +pip install pre-commit==3.8.0 # required +brew install gitleaks # optional — system fallback +``` + +If pre-commit's bundled tools don't work on your machine (hardened macOS, +Go OOM during gitleaks build), use the documented escape hatches: + +```bash +PR_VALIDATE_SKIP_GITLEAKS=1 PR_VALIDATE_SKIP_NBSTRIPOUT=1 bash scripts/pr-validate.sh +``` + +CI always runs the full unaltered suite, so these only affect local runs. ## Code Style diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 15d3d883..6c5dbcd3 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -4,6 +4,131 @@ Newest entries at the top. --- +### [2026-04-28] — Notebook Docker bootstrap: code-review fixes + +**Type:** fix +**Branch:** feat/notebook-docker-launcher + +**What it does:** + +Addresses every CRITICAL/HIGH/MEDIUM finding from the multi-agent review of the +initial Docker launcher commit: + +- **Loopback-only port maps** in `docker-compose.yml` (`127.0.0.1:8888:8888` / + `127.0.0.1:8765:8765`) so JupyterLab is never exposed to the LAN. +- **Generated auth token by default.** `_setup._generate_jupyter_token()` writes + a 32-byte URL-safe secret to `~/.config/patter-notebooks/jupyter_token` (mode + `0o600`) on first run; subsequent runs reuse it. `start_docker()` injects it + via `JUPYTER_TOKEN` env var; the Dockerfile `CMD` reads it from env. Empty + token requires explicit opt-in via `PATTER_NOTEBOOKS_NO_TOKEN=1`. +- **Non-root container.** Dockerfile creates `patter` (UID 1000 by default, + override via `PUID`/`PGID` build args) and drops `--allow-root`. +- **Pinned deps.** New `examples/notebooks/python/requirements.txt` locks every + top-level dep at an exact version; Dockerfile installs from it. +- **`start_docker() -> bool`.** Every early-return now returns False so + notebook callers can branch; `subprocess.run(..., check=False, + capture_output=True)` surfaces stdout/stderr on compose failure instead of + swallowing them. +- **`detach=False` guard.** Refuses to launch (would hang the kernel) and + returns False with a banner pointing at the terminal. +- **Robust env-var truthiness.** `in_docker()` accepts `1/true/yes/on` + (case-insensitive), not just literal `"1"`. +- **Honest docstring.** `_setup.py` module docstring acknowledges the + TS-parity gap on the Docker helpers (tracked separately). +- **Unit tests.** `tests/test_docker_bootstrap.py` covers truthy/falsy env + parsing, `/.dockerenv` marker, every `start_docker()` early-return branch, + command argv assembly, and token persistence (19 tests, all green). + +**Implementation details:** + +- Generated token persists across `compose down`/`up` cycles so users don't + need to re-bookmark the URL after a restart. +- Top-level imports hoisted: `secrets` and `webbrowser` now at the top of + `_setup.py` (PEP 8). All other late imports stayed because they live with + domain-specific helpers further down the file. +- Tests mock at the subprocess + filesystem boundary only; real Path + resolution and real argv assembly run unchanged. + +**Files changed:** + +| File | Change | +|------|--------| +| `examples/notebooks/python/Dockerfile` | Non-root user, requirements.txt, JUPYTER_TOKEN from env | +| `examples/notebooks/python/docker-compose.yml` | Loopback ports, JUPYTER_TOKEN passthrough, PUID/PGID args | +| `examples/notebooks/python/requirements.txt` | New — pinned top-level deps | +| `examples/notebooks/python/_setup.py` | bool return, captured stderr, token gen, robust truthiness, detach guard | +| `examples/notebooks/python/tests/test_docker_bootstrap.py` | New — 19 unit tests | + +**Tests added:** + +- `examples/notebooks/python/tests/test_docker_bootstrap.py` — 19 cases. + +**Breaking changes:** None — the launcher is still an opt-in commented cell. + +**Docs to update:** + +- [ ] Tracking issue for TypeScript notebook Docker launcher (filed separately). + +--- + +### [2026-04-28] — Notebook Docker bootstrap: optional in-cell launcher + +**Type:** feat +**Branch:** feat/notebook-series-skeleton + +**What it does:** + +Lets users run the Python notebook series inside a containerised JupyterLab +without leaving the notebook. Two new scoped files (`Dockerfile`, +`docker-compose.yml`) under `examples/notebooks/python/` build a Python 3.13 +image with `getpatter` (pinned to `PATTER_VERSION`, default 0.5.4), the helper +deps from `pyproject.toml`, and JupyterLab. Compose mounts the parent +`examples/notebooks/` tree at `/notebooks` so `_setup.py` still finds `.env` +and `fixtures/`; ports 8888 (Lab) and 8765 (EmbeddedServer for T2/T4 cells) +are published. `env_file` is marked `required: false` so §1 cells run with +zero keys. + +`_setup.py` gains two helpers: `in_docker()` (checks +`PATTER_NOTEBOOKS_IN_DOCKER=1` and `/.dockerenv`) and `start_docker(*, build, +detach, open_url)` which shells out to `docker compose up -d --build` from the +notebooks dir, no-ops when already inside the container or when the `docker` +CLI is absent. Each of the 12 Python notebooks gets an optional markdown + +commented code cell at the top — Run All on a fresh checkout still behaves +identically because the launcher is commented by default. + +**Implementation details:** + +- Container detection prefers an explicit env var over `/.dockerenv` so future + rootless/podman-equivalent setups can opt in by exporting the same flag. +- Insertion script is idempotent (skips notebooks that already contain the + `## Optional: run in Docker` marker), safe to re-run after future notebook + edits. +- Compose uses the v2 `env_file: [{path, required: false}]` form to avoid + failing when users haven't created `.env` yet. + +**Files changed:** + +| File | Change | +|------|--------| +| `examples/notebooks/python/Dockerfile` | New — Python 3.13-slim + getpatter + JupyterLab + helpers | +| `examples/notebooks/python/docker-compose.yml` | New — builds image, mounts `../`, optional `.env`, ports 8888/8765 | +| `examples/notebooks/python/_setup.py` | Added `in_docker()` and `start_docker()`; exposed `PYTHON_NOTEBOOKS_DIR` | +| `examples/notebooks/python/01_quickstart.ipynb` … `12_security.ipynb` | Inserted optional Docker markdown + code cell at the top of every notebook | + +**Tests added:** None — bootstrap is a no-op until uncommented, and existing +notebook tests cover the helper module via import. Manual smoke: `docker +compose config` validates and `python -c "import _setup; _setup.in_docker()"` +returns False on host. + +**Breaking changes:** None. + +**Docs to update:** + +- [ ] `examples/notebooks/README.md` — add a "Run in Docker" section pointing + at the optional cell + compose file. + +--- + ### [2026-04-27] — Notebook series Phase 5: Polish — README, launcher, drift-cron **Type:** docs / chore diff --git a/docs/superpowers/plans/2026-04-24-patter-feature-test-notebook-implementation.md b/docs/superpowers/plans/2026-04-24-patter-feature-test-notebook-implementation.md index 662094cf..6f165e48 100644 --- a/docs/superpowers/plans/2026-04-24-patter-feature-test-notebook-implementation.md +++ b/docs/superpowers/plans/2026-04-24-patter-feature-test-notebook-implementation.md @@ -6321,8 +6321,3 @@ Two execution options: Recommended: **Inline for Phase 1 (Tasks 1–22)**, then **subagent-driven for Phases 2–5** once the foundation is green and tasks are independent. Which approach? - - - - - diff --git a/examples/notebooks/python/01_quickstart.ipynb b/examples/notebooks/python/01_quickstart.ipynb index 4cd43dc9..b9d048ee 100644 --- a/examples/notebooks/python/01_quickstart.ipynb +++ b/examples/notebooks/python/01_quickstart.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 01 \u2014 Quickstart\n", + "# 01 — Quickstart\n", "\n", - "Install, env check, three operating modes (cloud/self-hosted/local), three voice modes (Realtime/ConvAI/Pipeline), 'hello phone' minimal agent.", - "\n" - ], - "id": "7fc33c66" + "Install, env check, three operating modes (cloud/self-hosted/local), three voice modes (Realtime/ConvAI/Pipeline), 'hello phone' minimal agent.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "9def0b14" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "6b5d0f86" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "eafe44a4" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "90b2e3c9" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "0b8ab9f1" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "10bcbd39" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "66a66e30" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "067794f3" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "8ab24b11" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "8dc73bc4" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "4a077024" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "28168a50" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "859262c7" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "f6c5cc4b" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "993ff652" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "e513d95b" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nThese cells require **T2** (local server) or **T3** (real API keys). Cells skip gracefully if prerequisites are missing.\n" - ], - "id": "ee821fa3" + "## §2 — Feature Tour\n", + "\n", + "These cells require **T2** (local server) or **T3** (real API keys). Cells skip gracefully if prerequisites are missing.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### Agent object inspection\n" - ], - "id": "530cea8c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_agent_inspection" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI, DeepgramSTT, ElevenLabsTTS\n", "with _setup.cell('agent_inspection', tier=1, env=env) as ok:\n", @@ -264,26 +303,26 @@ " print(f'pipeline: provider={pl.provider} stt={pl.stt} tts={pl.tts}')\n", " assert rt.system_prompt == 'You are a helpful assistant.'\n", " assert pl.provider in ('openai_realtime', 'pipeline')\n" - ], - "execution_count": null, - "outputs": [], - "id": "3cb79ad7" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### Pricing: calculate call costs\n" - ], - "id": "326c3536" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_pricing" ] }, + "outputs": [], "source": [ "from getpatter import DEFAULT_PRICING, calculate_stt_cost, calculate_tts_cost, calculate_telephony_cost\n", "with _setup.cell('pricing', tier=1, env=env) as ok:\n", @@ -298,31 +337,31 @@ " assert stt > 0\n", " assert tts > 0\n", " assert tel > 0\n" - ], - "execution_count": null, - "outputs": [], - "id": "dbc055d4" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Text transforms\n" - ], - "id": "3a18dd35" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_text_transforms" ] }, + "outputs": [], "source": [ "from getpatter import filter_markdown, filter_emoji, filter_for_tts\n", "with _setup.cell('text_transforms', tier=1, env=env) as ok:\n", " if ok:\n", - " raw = '**Important**: Please call us at +1-800-555-0100. \ud83d\ude0a See https://example.com'\n", + " raw = '**Important**: Please call us at +1-800-555-0100. 😊 See https://example.com'\n", " step1 = filter_markdown(raw)\n", " step2 = filter_emoji(raw)\n", " step3 = filter_for_tts(raw)\n", @@ -331,28 +370,28 @@ " print(f'filter_emoji: {step2}')\n", " print(f'filter_for_tts: {step3}')\n", " assert '**' not in step1\n", - " assert '\ud83d\ude0a' not in step2\n", + " assert '😊' not in step2\n", " assert '**' not in step3\n" - ], - "execution_count": null, - "outputs": [], - "id": "3faa312a" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "### SentenceChunker\n" - ], - "id": "5c1cd9eb" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_sentence_chunker" ] }, + "outputs": [], "source": [ "from getpatter import SentenceChunker\n", "with _setup.cell('sentence_chunker', tier=1, env=env) as ok:\n", @@ -370,72 +409,76 @@ " full = ' '.join(chunks).replace(' ', ' ')\n", " print(f'reassembled: {full}')\n", " assert len(chunks) >= 1\n" - ], - "execution_count": null, - "outputs": [], - "id": "d9ab5d06" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "aeb522a3" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real PSTN call. Requires `ENABLE_LIVE_CALLS=1` and carrier credentials.\n" - ], - "id": "3b44e2d0" + "## §3 — Live Appendix\n", + "\n", + "Places a real PSTN call. Requires `ENABLE_LIVE_CALLS=1` and carrier credentials.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "26964e06" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env=env) as ok:\n", " if ok:\n", " webhook = env.public_webhook_url or '(auto-tunnel via ngrok)'\n", - " print(f'\u2713 T4 pre-flight')\n", + " print(f'✓ T4 pre-flight')\n", " print(f' carrier: Twilio {env.twilio_number}')\n", " print(f' target: {env.target_number}')\n", " print(f' webhook: {webhook}')\n", " print(f' max_seconds: {env.max_call_seconds}')\n", " print(f' max_cost: ${env.max_cost_usd:.2f}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "b1b5fa18" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ - "### Live outbound call *(T4 \u2014 places a real 5-second call)*\n" - ], - "id": "8b7416c6" + "### Live outbound call *(T4 — places a real 5-second call)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_outbound_call" ] }, + "outputs": [], "source": [ "import asyncio\n", "from getpatter import Patter, Twilio, OpenAIRealtime\n", @@ -457,20 +500,17 @@ " first_message='Hello from Patter.',\n", " ring_timeout=env.max_call_seconds,\n", " )\n", - " print(f'\u2713 Call completed')\n", + " print(f'✓ Call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "60ade3f3" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/02_realtime.ipynb b/examples/notebooks/python/02_realtime.ipynb index b91ed75a..12655f78 100644 --- a/examples/notebooks/python/02_realtime.ipynb +++ b/examples/notebooks/python/02_realtime.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 02 \u2014 Realtime providers\n", + "# 02 — Realtime providers\n", "\n", - "OpenAI Realtime, Gemini Live, Ultravox, ElevenLabs ConvAI.", - "\n" - ], - "id": "4a21a090" + "OpenAI Realtime, Gemini Live, Ultravox, ElevenLabs ConvAI.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "3037715d" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "12ded325" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "7bccbbbf" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "a5d5de7a" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "451a1f2e" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "3da95d77" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "dac1c48b" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "59587066" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "a9ed49bc" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "b56c33e2" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "5c21a164" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "ee69317e" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "056b0a98" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "38b751b4" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "0d0aab60" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "91aa9884" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises OpenAI Realtime configuration and related SDK primitives.\n" - ], - "id": "fff2ad91" + "## §2 — Feature Tour\n", + "\n", + "Exercises OpenAI Realtime configuration and related SDK primitives.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### OpenAI Realtime agent: full config\n" - ], - "id": "2509d3f4" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_realtime_agent_config" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime\n", "with _setup.cell('realtime_agent_config', tier=1, env=env) as ok:\n", @@ -266,33 +305,33 @@ " print(f'barge_in_threshold_ms: {agent.barge_in_threshold_ms}')\n", " assert agent.provider == 'openai_realtime'\n", " assert agent.voice == 'alloy'\n" - ], - "execution_count": null, - "outputs": [], - "id": "e6a307ff" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### SentenceChunker\n" - ], - "id": "db896cee" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_realtime_chunker" ] }, + "outputs": [], "source": [ "from getpatter import SentenceChunker\n", "with _setup.cell('realtime_chunker', tier=1, env=env) as ok:\n", " if ok:\n", " sc = SentenceChunker()\n", " full_response = (\n", - " 'The weather today is sunny. Temperature is 72\u00b0F. '\n", + " 'The weather today is sunny. Temperature is 72°F. '\n", " 'Humidity is low. Great day for a walk!'\n", " )\n", " chunks: list[str] = []\n", @@ -304,26 +343,28 @@ " for i, chunk in enumerate(chunks):\n", " print(f' [{i}] {chunk.strip()!r}')\n", " assert len(chunks) >= 2\n" - ], - "execution_count": null, - "outputs": [], - "id": "cbf5e429" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ - "### Live: OpenAI Realtime models *(T3 \u2014 requires `OPENAI_API_KEY`)*\n\nConnects to OpenAI Realtime WebSocket and lists supported models.\n" - ], - "id": "844f0a5a" + "### Live: OpenAI Realtime models *(T3 — requires `OPENAI_API_KEY`)*\n", + "\n", + "Connects to OpenAI Realtime WebSocket and lists supported models.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_openai_realtime_live" ] }, + "outputs": [], "source": [ "import httpx\n", "with _setup.cell('openai_realtime_live', tier=3, required=['openai_key'], env=env) as ok:\n", @@ -337,68 +378,72 @@ " models = [m['id'] for m in resp.json()['data'] if 'realtime' in m['id']]\n", " print(f'OpenAI realtime models: {models[:5]}')\n", " assert len(models) > 0, 'no realtime models found'\n" - ], - "execution_count": null, - "outputs": [], - "id": "f9e6a6a3" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "468c2546" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real call through OpenAI Realtime. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "67c152d3" + "## §3 — Live Appendix\n", + "\n", + "Places a real call through OpenAI Realtime. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "3cd947e9" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n", " print(f' engine: OpenAI Realtime (gpt-4o-realtime-preview)')\n" - ], - "execution_count": null, - "outputs": [], - "id": "62bf3d39" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live OpenAI Realtime call *(T4)*\n" - ], - "id": "1105ae95" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_realtime_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime\n", "with _setup.cell('live_realtime_call', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -419,20 +464,17 @@ " first_message='Hello! This is a Patter demo call. Goodbye!',\n", " ring_timeout=env.max_call_seconds,\n", " )\n", - " print('\u2713 Realtime call completed')\n", + " print('✓ Realtime call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "03ab26c3" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/03_pipeline_stt.ipynb b/examples/notebooks/python/03_pipeline_stt.ipynb index fa3cd87a..413cd33d 100644 --- a/examples/notebooks/python/03_pipeline_stt.ipynb +++ b/examples/notebooks/python/03_pipeline_stt.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 03 \u2014 Pipeline STT\n", + "# 03 — Pipeline STT\n", "\n", - "Deepgram, Whisper, AssemblyAI, Soniox, Speechmatics, Cartesia.", - "\n" - ], - "id": "0a939ff0" + "Deepgram, Whisper, AssemblyAI, Soniox, Speechmatics, Cartesia.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "fb9f82b4" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "87964370" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "51e17319" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "3c0a61e8" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "a92e08ae" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "b189e3b0" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "61521ffa" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "aa952c36" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "551274d2" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "a7f81e0b" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "e5b55366" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "7f6b8917" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "802817b6" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "1a72b63c" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "b79a1fbb" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "7784dd9c" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises STT provider construction, audio transcoding, and (T3) live transcription.\n" - ], - "id": "4f8d6db9" + "## §2 — Feature Tour\n", + "\n", + "Exercises STT provider construction, audio transcoding, and (T3) live transcription.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### STT provider construction\n" - ], - "id": "f5e9f0ad" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_stt_providers" ] }, + "outputs": [], "source": [ "from getpatter import DeepgramSTT, WhisperSTT, OpenAITranscribeSTT\n", "with _setup.cell('stt_providers', tier=1, env=env) as ok:\n", @@ -252,26 +291,26 @@ " print(f'Whisper: model={wh.model}')\n", " print(f'OpenAI Transcribe: provider={ot.provider if hasattr(ot, \"provider\") else type(ot).__name__}')\n", " assert dg.model == 'nova-2'\n" - ], - "execution_count": null, - "outputs": [], - "id": "849c0d2f" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ - "### \u03bc-law \u2194 PCM-16 transcoding roundtrip\n" - ], - "id": "ae04bdf6" + "### μ-law ↔ PCM-16 transcoding roundtrip\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_mulaw_transcoding" ] }, + "outputs": [], "source": [ "from getpatter import mulaw_to_pcm16, pcm16_to_mulaw\n", "with _setup.cell('mulaw_transcoding', tier=1, env=env) as ok:\n", @@ -283,30 +322,30 @@ " mulaw_bytes = pcm16_to_mulaw(pcm_original)\n", " pcm_recovered = mulaw_to_pcm16(mulaw_bytes)\n", " print(f'PCM original: {len(pcm_original)} bytes ({SAMPLES} samples)')\n", - " print(f'\u03bc-law encoded: {len(mulaw_bytes)} bytes (8-bit, 2:1 compression)')\n", + " print(f'μ-law encoded: {len(mulaw_bytes)} bytes (8-bit, 2:1 compression)')\n", " print(f'PCM recovered: {len(pcm_recovered)} bytes')\n", " assert len(mulaw_bytes) == SAMPLES\n", " assert len(pcm_recovered) == len(pcm_original)\n" - ], - "execution_count": null, - "outputs": [], - "id": "f0385388" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ - "### 8kHz \u2192 16kHz resampling\n" - ], - "id": "d531a605" + "### 8kHz → 16kHz resampling\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_resampler" ] }, + "outputs": [], "source": [ "from getpatter import resample_8k_to_16k, resample_16k_to_8k\n", "with _setup.cell('resampler', tier=1, env=env) as ok:\n", @@ -320,26 +359,28 @@ " print(f'16kHz output: {len(pcm_16k)} bytes ({len(pcm_16k)//2} samples)')\n", " print(f'8kHz round-trip: {len(pcm_8k_back)} bytes')\n", " assert len(pcm_16k) == len(pcm_8k) * 2\n" - ], - "execution_count": null, - "outputs": [], - "id": "e186f5fe" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "### Live: Deepgram transcription *(T3 \u2014 requires `DEEPGRAM_API_KEY`)*\n\nTranscribes a synthetic fixture WAV using the Deepgram REST API.\n" - ], - "id": "8523774d" + "### Live: Deepgram transcription *(T3 — requires `DEEPGRAM_API_KEY`)*\n", + "\n", + "Transcribes a synthetic fixture WAV using the Deepgram REST API.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_deepgram_live" ] }, + "outputs": [], "source": [ "import httpx\n", "with _setup.cell('deepgram_live', tier=3, required=['deepgram_key'], env=env) as ok:\n", @@ -357,68 +398,72 @@ " resp.raise_for_status()\n", " transcript = resp.json()['results']['channels'][0]['alternatives'][0]['transcript']\n", " print(f'Deepgram transcript: {transcript!r}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "3564f8ff" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "7bffee2c" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nCalls a real number through the Pipeline engine using Deepgram STT. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "f05288b8" + "## §3 — Live Appendix\n", + "\n", + "Calls a real number through the Pipeline engine using Deepgram STT. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "30cb37a3" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'DEEPGRAM_API_KEY'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' STT: Deepgram (nova-2-general)')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "2ee1adee" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live Pipeline STT call *(T4)*\n" - ], - "id": "6840f37d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_stt_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, DeepgramSTT, OpenAILLM, OpenAITTS\n", "with _setup.cell('live_stt_call', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'DEEPGRAM_API_KEY', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -437,20 +482,17 @@ " try:\n", " await p.call(env.target_number, agent=agent, first_message='Hello from Patter STT demo.',\n", " ring_timeout=env.max_call_seconds)\n", - " print('\u2713 Pipeline STT call completed')\n", + " print('✓ Pipeline STT call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "d6971f7d" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/04_pipeline_tts.ipynb b/examples/notebooks/python/04_pipeline_tts.ipynb index 0038b065..5763c16e 100644 --- a/examples/notebooks/python/04_pipeline_tts.ipynb +++ b/examples/notebooks/python/04_pipeline_tts.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 04 \u2014 Pipeline TTS\n", + "# 04 — Pipeline TTS\n", "\n", - "ElevenLabs, OpenAI, Cartesia, LMNT, Rime.", - "\n" - ], - "id": "a7f06833" + "ElevenLabs, OpenAI, Cartesia, LMNT, Rime.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "a562bdc4" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "3657e7be" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "6f846fdf" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "b346234c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "cc153b3f" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "366c0bf6" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "9d1c05d4" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "c456ef6f" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "97699026" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "f4327c92" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "6f877c0f" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "2c1c2895" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "3063e5a0" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "3e4248aa" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "6aa010fa" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "174f1671" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises TTS provider construction and (T3) live synthesis.\n" - ], - "id": "0b00db52" + "## §2 — Feature Tour\n", + "\n", + "Exercises TTS provider construction and (T3) live synthesis.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### TTS provider construction\n" - ], - "id": "599fc65c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_tts_providers" ] }, + "outputs": [], "source": [ "from getpatter import ElevenLabsTTS, OpenAITTS, CartesiaTTS, RimeTTS, LMNTTTS\n", "with _setup.cell('tts_providers', tier=1, env=env) as ok:\n", @@ -254,33 +293,33 @@ " print(f'{name}: {type(provider).__name__}')\n", " assert el.voice_id == '21m00Tcm4TlvDq8ikWAM'\n", " assert ot.voice == 'alloy'\n" - ], - "execution_count": null, - "outputs": [], - "id": "59e332de" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### TTS text preparation\n" - ], - "id": "69688e9c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_tts_text_prep" ] }, + "outputs": [], "source": [ "from getpatter import filter_for_tts\n", "with _setup.cell('tts_text_prep', tier=1, env=env) as ok:\n", " if ok:\n", " samples = [\n", " '**Bold** and *italic* text.',\n", - " 'Visit https://example.com for more. \ud83c\udf89',\n", + " 'Visit https://example.com for more. 🎉',\n", " 'Call us at +1 (800) 555-0100.',\n", " 'Code: `x = 1 + 2`',\n", " ]\n", @@ -289,26 +328,28 @@ " print(f' in: {raw}')\n", " print(f' out: {clean}')\n", " print()\n" - ], - "execution_count": null, - "outputs": [], - "id": "2a9c02a5" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ - "### Live: ElevenLabs TTS synthesis *(T3 \u2014 requires `ELEVENLABS_API_KEY`)*\n\nSynthesises a short phrase and reports the audio byte count.\n" - ], - "id": "45adc5c2" + "### Live: ElevenLabs TTS synthesis *(T3 — requires `ELEVENLABS_API_KEY`)*\n", + "\n", + "Synthesises a short phrase and reports the audio byte count.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_elevenlabs_tts_live" ] }, + "outputs": [], "source": [ "import httpx\n", "with _setup.cell('elevenlabs_tts_live', tier=3, required=['elevenlabs_key'], env=env) as ok:\n", @@ -326,26 +367,26 @@ " resp.raise_for_status()\n", " print(f'ElevenLabs audio: {len(resp.content)} bytes content-type: {resp.headers[\"content-type\"]}')\n", " assert len(resp.content) > 0\n" - ], - "execution_count": null, - "outputs": [], - "id": "80047155" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "### Live: OpenAI TTS synthesis *(T3 \u2014 requires `OPENAI_API_KEY`)*\n" - ], - "id": "036a5ed8" + "### Live: OpenAI TTS synthesis *(T3 — requires `OPENAI_API_KEY`)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_openai_tts_live" ] }, + "outputs": [], "source": [ "import httpx\n", "with _setup.cell('openai_tts_live', tier=3, required=['openai_key'], env=env) as ok:\n", @@ -362,68 +403,72 @@ " resp.raise_for_status()\n", " print(f'OpenAI TTS audio: {len(resp.content)} bytes')\n", " assert len(resp.content) > 0\n" - ], - "execution_count": null, - "outputs": [], - "id": "a7ec9b7c" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "ac34a17b" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nCalls a real number using ElevenLabs TTS in the Pipeline engine. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "56d6d716" + "## §3 — Live Appendix\n", + "\n", + "Calls a real number using ElevenLabs TTS in the Pipeline engine. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "66a2e1c5" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'ELEVENLABS_API_KEY'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' TTS: ElevenLabs voice={env.elevenlabs_voice_id[:8]}...')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "86587bdc" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live ElevenLabs TTS call *(T4)*\n" - ], - "id": "83b45c76" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_tts_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, DeepgramSTT, OpenAILLM, ElevenLabsTTS\n", "with _setup.cell('live_tts_call', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'ELEVENLABS_API_KEY', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -443,20 +488,17 @@ " await p.call(env.target_number, agent=agent,\n", " first_message='Hello, this is a Patter ElevenLabs TTS demo.',\n", " ring_timeout=env.max_call_seconds)\n", - " print('\u2713 ElevenLabs TTS call completed')\n", + " print('✓ ElevenLabs TTS call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "b44c4946" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/05_pipeline_llm.ipynb b/examples/notebooks/python/05_pipeline_llm.ipynb index 5a096266..f8bfaf61 100644 --- a/examples/notebooks/python/05_pipeline_llm.ipynb +++ b/examples/notebooks/python/05_pipeline_llm.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 05 \u2014 Pipeline LLM\n", + "# 05 — Pipeline LLM\n", "\n", - "OpenAI, Anthropic, Gemini, Groq, Cerebras, custom on_message, LLMLoop, tool-call protocol.", - "\n" - ], - "id": "532fef9e" + "OpenAI, Anthropic, Gemini, Groq, Cerebras, custom on_message, LLMLoop, tool-call protocol.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "9fff4961" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "0c7ac4e1" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "f011490a" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "262814e5" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "b9823366" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "fa8b6f9f" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "85f65124" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "c911c96e" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "265ddcb6" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "ce866c46" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "7e89a1a4" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "4c8cc04f" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "c2a04376" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "caeacfeb" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "ff1a9975" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "16a628d7" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises LLM provider construction, fallback routing, ChatContext, and (T3) live completions.\n" - ], - "id": "d4e00389" + "## §2 — Feature Tour\n", + "\n", + "Exercises LLM provider construction, fallback routing, ChatContext, and (T3) live completions.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### LLM provider construction\n" - ], - "id": "bae9feef" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_llm_providers" ] }, + "outputs": [], "source": [ "from getpatter import OpenAILLM, AnthropicLLM, GroqLLM, CerebrasLLM, GoogleLLM\n", "with _setup.cell('llm_providers', tier=1, env=env) as ok:\n", @@ -254,26 +293,26 @@ " print(f'{name:10s}: model={p.model}')\n", " assert oai.model == 'gpt-4o-mini'\n", " assert ant.model == 'claude-haiku-4-5-20251001'\n" - ], - "execution_count": null, - "outputs": [], - "id": "892b9a22" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### FallbackLLMProvider\n" - ], - "id": "eb036517" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_fallback_provider" ] }, + "outputs": [], "source": [ "from getpatter import FallbackLLMProvider, OpenAILLM, AnthropicLLM, AllProvidersFailedError\n", "with _setup.cell('fallback_provider', tier=1, env=env) as ok:\n", @@ -285,26 +324,26 @@ " for i, p in enumerate(fallback.providers):\n", " print(f' [{i}] {type(p).__name__} model={p.model}')\n", " print(f'AllProvidersFailedError: {AllProvidersFailedError.__name__}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "ae5ac86d" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### ChatContext\n" - ], - "id": "16f4942e" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_chat_context" ] }, + "outputs": [], "source": [ "from getpatter import ChatContext\n", "with _setup.cell('chat_context', tier=1, env=env) as ok:\n", @@ -322,26 +361,26 @@ " ant_fmt = ctx.to_anthropic()\n", " print(f'Anthropic format: {ant_fmt[:1]}')\n", " assert ctx.length() == 4\n" - ], - "execution_count": null, - "outputs": [], - "id": "3c23a82e" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "### Live: OpenAI chat completion *(T3 \u2014 requires `OPENAI_API_KEY`)*\n" - ], - "id": "48634fb2" + "### Live: OpenAI chat completion *(T3 — requires `OPENAI_API_KEY`)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_openai_chat_live" ] }, + "outputs": [], "source": [ "import httpx\n", "with _setup.cell('openai_chat_live', tier=3, required=['openai_key'], env=env) as ok:\n", @@ -359,68 +398,72 @@ " resp.raise_for_status()\n", " reply = resp.json()['choices'][0]['message']['content']\n", " print(f'OpenAI reply: {reply!r}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "75a70012" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "1dcced5f" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a call using the Pipeline engine with OpenAI LLM + tool call. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "123cf96d" + "## §3 — Live Appendix\n", + "\n", + "Places a call using the Pipeline engine with OpenAI LLM + tool call. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "7deb5a0c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' LLM: OpenAI (gpt-4o-mini)')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "6e431a08" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live LLM call with tool *(T4)*\n" - ], - "id": "d09f0b1f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_llm_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, tool\n", "with _setup.cell('live_llm_call', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -445,20 +488,17 @@ " await p.call(env.target_number, agent=agent,\n", " first_message='Hello! Ask me what time it is.',\n", " ring_timeout=env.max_call_seconds)\n", - " print('\u2713 LLM call with tool completed')\n", + " print('✓ LLM call with tool completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "aacf4d85" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/06_telephony_twilio.ipynb b/examples/notebooks/python/06_telephony_twilio.ipynb index 21530a18..812e9828 100644 --- a/examples/notebooks/python/06_telephony_twilio.ipynb +++ b/examples/notebooks/python/06_telephony_twilio.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 06 \u2014 Telephony \u2014 Twilio\n", + "# 06 — Telephony — Twilio\n", "\n", - "Webhook parsing, HMAC-SHA1, AMD, DTMF, recording, transfer, ring timeout, status callback, TwiML emission.", - "\n" - ], - "id": "778d320c" + "Webhook parsing, HMAC-SHA1, AMD, DTMF, recording, transfer, ring timeout, status callback, TwiML emission.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "74f93f8a" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "52f50088" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "5dfad3a4" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "45c3cdcb" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "6e8a1dc8" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "44308767" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "9d934e6b" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "6c37b18d" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "135dfab3" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "1964c22c" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "dfd75adf" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "e89d3e2e" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "64310ad1" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "90d727a7" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "f6d46547" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "d0706e21" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises Twilio webhook signature verification and carrier construction.\n" - ], - "id": "1483d81f" + "## §2 — Feature Tour\n", + "\n", + "Exercises Twilio webhook signature verification and carrier construction.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ - "### Twilio signature verification \u2014 valid request\n" - ], - "id": "5f259d51" + "### Twilio signature verification — valid request\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_twilio_sig_valid" ] }, + "outputs": [], "source": [ "import hmac, hashlib, base64\n", "from twilio.request_validator import RequestValidator\n", @@ -255,26 +294,26 @@ " print(f'signature: {signature}')\n", " print(f'valid: {valid}')\n", " assert valid, 'signature should be valid'\n" - ], - "execution_count": null, - "outputs": [], - "id": "7ed65df4" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ - "### Twilio signature verification \u2014 tampered request\n" - ], - "id": "58a4c9db" + "### Twilio signature verification — tampered request\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_twilio_sig_invalid" ] }, + "outputs": [], "source": [ "from twilio.request_validator import RequestValidator\n", "with _setup.cell('twilio_sig_invalid', tier=1, env=env) as ok:\n", @@ -287,26 +326,26 @@ " valid = validator.validate(url, params, bad_sig)\n", " print(f'tampered signature valid: {valid} (expected False)')\n", " assert not valid, 'tampered signature must be rejected'\n" - ], - "execution_count": null, - "outputs": [], - "id": "147c40bc" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### E.164 phone number patterns\n" - ], - "id": "30660c85" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_e164_patterns" ] }, + "outputs": [], "source": [ "import re\n", "with _setup.cell('e164_patterns', tier=1, env=env) as ok:\n", @@ -322,29 +361,29 @@ " ]\n", " for number, expected in cases:\n", " result = bool(E164_RE.match(number))\n", - " status = '\u2713' if result == expected else '\u2717'\n", - " print(f' {status} {number!r:20s} \u2192 {result}')\n", + " status = '✓' if result == expected else '✗'\n", + " print(f' {status} {number!r:20s} → {result}')\n", " assert all(bool(E164_RE.match(n)) == e for n, e in cases)\n" - ], - "execution_count": null, - "outputs": [], - "id": "99682be9" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "### Twilio carrier construction\n" - ], - "id": "beacf80d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_twilio_carrier" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('twilio_carrier', tier=1, env=env) as ok:\n", @@ -363,68 +402,72 @@ " print(f'phone: {lc.phone_number}')\n", " print(f'webhook: {lc.webhook_url}')\n", " assert lc.telephony_provider == 'twilio'\n" - ], - "execution_count": null, - "outputs": [], - "id": "ffc4cb58" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "5b2f2a1a" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nTests Twilio call flow including AMD detection. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "126160bc" + "## §3 — Live Appendix\n", + "\n", + "Tests Twilio call flow including AMD detection. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "386d4d40" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n", " print(f' features: AMD + voicemail fallback')\n" - ], - "execution_count": null, - "outputs": [], - "id": "349aa08a" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live Twilio call with AMD *(T4)*\n" - ], - "id": "ba7e66b3" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_twilio_amd" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime\n", "with _setup.cell('live_twilio_amd', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -447,20 +490,17 @@ " first_message='Hello from Patter Twilio demo.',\n", " ring_timeout=env.max_call_seconds,\n", " )\n", - " print('\u2713 Twilio AMD call completed')\n", + " print('✓ Twilio AMD call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "62c79dff" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/07_telephony_telnyx.ipynb b/examples/notebooks/python/07_telephony_telnyx.ipynb index c5514821..242d9607 100644 --- a/examples/notebooks/python/07_telephony_telnyx.ipynb +++ b/examples/notebooks/python/07_telephony_telnyx.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 07 \u2014 Telephony \u2014 Telnyx\n", + "# 07 — Telephony — Telnyx\n", "\n", - "Call Control, Ed25519, AMD, DTMF, track filter, anti-replay.", - "\n" - ], - "id": "18c31a87" + "Call Control, Ed25519, AMD, DTMF, track filter, anti-replay.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "91d3d00e" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "afe04a02" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "42ad7306" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "55915df7" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "5fa21bce" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "82712125" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "ecd2f0a0" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "87809de6" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "d809dc8a" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "1d1918de" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "2d2070a0" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "501c729a" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "591a267b" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "04aa9884" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "e8389f2f" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "b3dc79dd" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises Telnyx carrier construction and Ed25519 webhook signature verification.\n" - ], - "id": "c9be3de6" + "## §2 — Feature Tour\n", + "\n", + "Exercises Telnyx carrier construction and Ed25519 webhook signature verification.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### Telnyx carrier construction\n" - ], - "id": "bc638f9c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_telnyx_carrier" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Telnyx\n", "with _setup.cell('telnyx_carrier', tier=1, env=env) as ok:\n", @@ -258,26 +297,26 @@ " print(f'carrier: {lc.telephony_provider}')\n", " print(f'phone: {lc.phone_number}')\n", " assert lc.telephony_provider == 'telnyx'\n" - ], - "execution_count": null, - "outputs": [], - "id": "0fef33a8" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### Ed25519 sign + verify\n" - ], - "id": "34a2db00" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_ed25519_verify" ] }, + "outputs": [], "source": [ "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey\n", "from cryptography.hazmat.primitives.serialization import (\n", @@ -303,106 +342,110 @@ " print('ERROR: tampered payload should have raised InvalidSignature')\n", " except InvalidSignature:\n", " print('Tampered payload correctly rejected')\n" - ], - "execution_count": null, - "outputs": [], - "id": "e8c79e31" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Telnyx anti-replay: timestamp window check\n" - ], - "id": "10bdc7e2" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_telnyx_timestamp" ] }, + "outputs": [], "source": [ "import time\n", "with _setup.cell('telnyx_timestamp', tier=1, env=env) as ok:\n", " if ok:\n", - " WINDOW_SECONDS = 300 # \u00b15 minutes\n", + " WINDOW_SECONDS = 300 # ±5 minutes\n", " now = int(time.time())\n", " cases = [\n", - " (now, 'fresh \u2014 should pass'),\n", - " (now - 60, 'recent \u2014 should pass'),\n", - " (now - 299, 'edge \u2014 should pass'),\n", - " (now - 301, 'stale \u2014 should reject'),\n", - " (now + 10, 'future \u2014 should pass'),\n", + " (now, 'fresh — should pass'),\n", + " (now - 60, 'recent — should pass'),\n", + " (now - 299, 'edge — should pass'),\n", + " (now - 301, 'stale — should reject'),\n", + " (now + 10, 'future — should pass'),\n", " ]\n", " for ts, label in cases:\n", " age = abs(now - ts)\n", " accepted = age <= WINDOW_SECONDS\n", - " mark = '\u2713' if accepted else '\u2717'\n", + " mark = '✓' if accepted else '✗'\n", " print(f' {mark} age={age:3d}s {label}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "38bf1479" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "1e920a95" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real call through the Telnyx carrier. Requires `ENABLE_LIVE_CALLS=1` and Telnyx credentials.\n" - ], - "id": "3be4f1c1" + "## §3 — Live Appendix\n", + "\n", + "Places a real call through the Telnyx carrier. Requires `ENABLE_LIVE_CALLS=1` and Telnyx credentials.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "063430bb" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TELNYX_API_KEY', 'TELNYX_CONNECTION_ID', 'TELNYX_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Telnyx {env.telnyx_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Telnyx {env.telnyx_number} → {env.target_number}')\n", " print(f' connection_id: {env.telnyx_connection_id}')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "aed28eac" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live Telnyx outbound call *(T4)*\n" - ], - "id": "ea8f688f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_telnyx_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Telnyx, OpenAIRealtime\n", "with _setup.cell('live_telnyx_call', tier=4, required=['TELNYX_API_KEY', 'TELNYX_CONNECTION_ID', 'TELNYX_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -423,20 +466,17 @@ " first_message='Hello from Patter via Telnyx.',\n", " ring_timeout=env.max_call_seconds,\n", " )\n", - " print('\u2713 Telnyx call completed')\n", + " print('✓ Telnyx call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "7f6edb38" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/08_tools.ipynb b/examples/notebooks/python/08_tools.ipynb index 0774e665..db6aa9d1 100644 --- a/examples/notebooks/python/08_tools.ipynb +++ b/examples/notebooks/python/08_tools.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 08 \u2014 Tools\n", + "# 08 — Tools\n", "\n", - "@tool/defineTool, auto-injected transfer_call/end_call, dynamic variables, custom tools, schema validation.", - "\n" - ], - "id": "67401662" + "@tool/defineTool, auto-injected transfer_call/end_call, dynamic variables, custom tools, schema validation.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "71e077e0" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "a95003fd" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "69355fca" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "e069a19d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "80da16b8" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "ffe309c4" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "5c00008c" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "4443da84" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "58935c64" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "ccda52f5" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "58e72d99" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "cce71a08" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "28e06ecb" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "45916011" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "ef032eac" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "e77d504c" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises the `@tool` decorator, `Tool()` factory, and agent tool registration.\n" - ], - "id": "4fc5f337" + "## §2 — Feature Tour\n", + "\n", + "Exercises the `@tool` decorator, `Tool()` factory, and agent tool registration.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### `tool()` factory\n" - ], - "id": "35341b8c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_tool_decorator" ] }, + "outputs": [], "source": [ "from getpatter import tool\n", "with _setup.cell('tool_decorator', tier=1, env=env) as ok:\n", @@ -255,26 +294,26 @@ " print(f'call: {get_weather.handler(city=\"Paris\")}')\n", " assert get_weather.name == 'get_weather'\n", " assert 'city' in get_weather.description or get_weather.handler is not None\n" - ], - "execution_count": null, - "outputs": [], - "id": "e879661f" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### `Tool()` constructor\n" - ], - "id": "ee4d9efd" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_tool_inline" ] }, + "outputs": [], "source": [ "from getpatter import Tool\n", "with _setup.cell('tool_inline', tier=1, env=env) as ok:\n", @@ -292,26 +331,26 @@ " print(f'description: {search_tool.description}')\n", " print(f'call: {search_tool.handler(query=\"Patter SDK\", num_results=3)}')\n", " assert search_tool.name == 'web_search'\n" - ], - "execution_count": null, - "outputs": [], - "id": "9f947ae9" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Agent with tools list\n" - ], - "id": "b4a72d7d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_tool_in_agent" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, tool, Tool\n", "with _setup.cell('tool_in_agent', tier=1, env=env) as ok:\n", @@ -342,68 +381,72 @@ " assert len(agent.tools) == 2\n", " assert agent.tools[0].name == 'book_appointment'\n", " assert agent.tools[1].name == 'cancel_appointment'\n" - ], - "execution_count": null, - "outputs": [], - "id": "a0afeb16" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "d97b31cf" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nFires a real tool call mid-call and demonstrates `transfer_call`. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "fca4c226" + "## §3 — Live Appendix\n", + "\n", + "Fires a real tool call mid-call and demonstrates `transfer_call`. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "541e8b35" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' tools: get_time (custom) + transfer_call (auto-injected)')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "2c542567" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live call with custom tool *(T4)*\n" - ], - "id": "4158f888" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_tools_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, tool\n", "with _setup.cell('live_tools_call', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -411,7 +454,7 @@ " @tool\n", " def lookup_order(order_id: str) -> str:\n", " \"\"\"Look up the status of an order by ID.\"\"\"\n", - " return f'Order {order_id} is shipped \u2014 expected delivery: tomorrow.'\n", + " return f'Order {order_id} is shipped — expected delivery: tomorrow.'\n", "\n", " p = Patter(\n", " carrier=Twilio(account_sid=env.twilio_sid, auth_token=env.twilio_token),\n", @@ -427,20 +470,17 @@ " await p.call(env.target_number, agent=agent,\n", " first_message='Hi! Ask me about order 12345.',\n", " ring_timeout=env.max_call_seconds)\n", - " print('\u2713 Tools call completed')\n", + " print('✓ Tools call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "b6233c6c" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/09_guardrails_hooks.ipynb b/examples/notebooks/python/09_guardrails_hooks.ipynb index ba1b2649..76c3152b 100644 --- a/examples/notebooks/python/09_guardrails_hooks.ipynb +++ b/examples/notebooks/python/09_guardrails_hooks.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 09 \u2014 Guardrails & hooks\n", + "# 09 — Guardrails & hooks\n", "\n", - "Keyword block, PII redact, pipeline hooks, text transforms, sentence chunker.", - "\n" - ], - "id": "0e2f24ec" + "Keyword block, PII redact, pipeline hooks, text transforms, sentence chunker.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "bd65ffb8" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "501882f6" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "4821b8f6" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "d0a760a5" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "62c0ff56" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "6936caf6" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "37f20832" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "e6dced3e" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "159f0725" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "c87e77ce" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "0d911aa1" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "1c0be314" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "dcf7aa9f" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "9326bc86" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "d65ba243" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "dc90b043" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises guardrail construction, PipelineHooks, and PipelineHookExecutor.\n" - ], - "id": "fad8a6c5" + "## §2 — Feature Tour\n", + "\n", + "Exercises guardrail construction, PipelineHooks, and PipelineHookExecutor.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### `guardrail()` factory\n" - ], - "id": "1485bc59" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_guardrail_decorator" ] }, + "outputs": [], "source": [ "from getpatter import guardrail, Guardrail\n", "with _setup.cell('guardrail_decorator', tier=1, env=env) as ok:\n", @@ -256,31 +295,31 @@ "\n", " result_allowed = no_competitor_mention.handler('Hello, how can I help?')\n", " result_blocked = no_competitor_mention.handler('Have you tried rival-co?')\n", - " print(f'Allowed text \u2192 handler returns: {result_allowed!r}')\n", - " print(f'Blocked text \u2192 handler returns: {result_blocked!r}')\n", + " print(f'Allowed text → handler returns: {result_allowed!r}')\n", + " print(f'Blocked text → handler returns: {result_blocked!r}')\n", " assert result_allowed is None\n", " assert result_blocked is not None\n", " assert isinstance(no_competitor_mention, Guardrail)\n" - ], - "execution_count": null, - "outputs": [], - "id": "4e3808eb" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### Agent with guardrails\n" - ], - "id": "4976cabb" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_guardrail_in_agent" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, guardrail\n", "with _setup.cell('guardrail_in_agent', tier=1, env=env) as ok:\n", @@ -305,26 +344,26 @@ " print(f'Agent guardrails: {[g.name for g in agent.guardrails]}')\n", " assert len(agent.guardrails) == 1\n", " assert agent.guardrails[0].name == 'no_pricing_talk'\n" - ], - "execution_count": null, - "outputs": [], - "id": "ec9f3b1d" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### PipelineHooks\n" - ], - "id": "69182e00" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_pipeline_hooks" ] }, + "outputs": [], "source": [ "from getpatter import PipelineHooks, PipelineHookExecutor\n", "with _setup.cell('pipeline_hooks', tier=1, env=env) as ok:\n", @@ -347,68 +386,72 @@ " executor = PipelineHookExecutor(hooks)\n", " print(f'PipelineHookExecutor: {type(executor).__name__}')\n", " assert hooks.on_transcript is on_transcript\n" - ], - "execution_count": null, - "outputs": [], - "id": "39d367c3" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "e3906505" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a call with an active guardrail so a blocked phrase triggers a redirect. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "ddfa2e86" + "## §3 — Live Appendix\n", + "\n", + "Places a call with an active guardrail so a blocked phrase triggers a redirect. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "0be07487" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' guardrail: blocks any mention of \"competitor\"')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "425c21c9" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live call with guardrail *(T4)*\n" - ], - "id": "15373a04" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_guardrail_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, guardrail\n", "with _setup.cell('live_guardrail_call', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -434,20 +477,17 @@ " await p.call(env.target_number, agent=agent,\n", " first_message='Hello! Try mentioning a competitor to see the guardrail.',\n", " ring_timeout=env.max_call_seconds)\n", - " print('\u2713 Guardrail call completed')\n", + " print('✓ Guardrail call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "f5be3745" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/10_advanced.ipynb b/examples/notebooks/python/10_advanced.ipynb index ac9cede0..1608d6db 100644 --- a/examples/notebooks/python/10_advanced.ipynb +++ b/examples/notebooks/python/10_advanced.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 10 \u2014 Advanced\n", + "# 10 — Advanced\n", "\n", - "Scheduler, fallback LLM chain, background audio, noise filter, custom STT/TTS, custom LLM HTTP.", - "\n" - ], - "id": "b7ef697c" + "Scheduler, fallback LLM chain, background audio, noise filter, custom STT/TTS, custom LLM HTTP.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "64c078c5" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "082271bd" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "927eceeb" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "3bc01109" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "a7ebb6b2" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "4a3154a6" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "06cf1868" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "352e9f44" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "e5b61133" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "7685657d" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "4e7493c0" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "ca1e93ac" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "eb70fa26" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "1b6c5262" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "29fda636" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "68870191" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises scheduler, IVR/DTMF, background audio, and the EventBus.\n" - ], - "id": "cf3c7052" + "## §2 — Feature Tour\n", + "\n", + "Exercises scheduler, IVR/DTMF, background audio, and the EventBus.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### Scheduler\n" - ], - "id": "ee4af426" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_scheduler" ] }, + "outputs": [], "source": [ "from datetime import datetime, timedelta, timezone\n", "from getpatter import schedule_once, schedule_interval, schedule_cron, ScheduleHandle\n", @@ -262,28 +301,28 @@ " h1.cancel()\n", " h2.cancel()\n", " h3.cancel()\n", - " print('All handles cancelled \u2014 no callbacks should fire')\n", + " print('All handles cancelled — no callbacks should fire')\n", " assert isinstance(h1, ScheduleHandle)\n" - ], - "execution_count": null, - "outputs": [], - "id": "212592b7" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### IVR / DTMF\n" - ], - "id": "6e939bf9" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_ivr_dtmf" ] }, + "outputs": [], "source": [ "from getpatter import DtmfEvent, format_dtmf\n", "with _setup.cell('ivr_dtmf', tier=1, env=env) as ok:\n", @@ -299,33 +338,33 @@ " print(f'format_dtmf: {formatted!r}')\n", " print(f'Available events: {[e.name for e in DtmfEvent]}')\n", " assert formatted == '1 8 0 0 5 5 5 0 1 0 0'\n" - ], - "execution_count": null, - "outputs": [], - "id": "5cfdc62c" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Background audio\n" - ], - "id": "e010f003" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_background_audio" ] }, + "outputs": [], "source": [ "from getpatter import BackgroundAudioPlayer, BuiltinAudioClip\n", "with _setup.cell('background_audio', tier=1, env=env) as ok:\n", " if ok:\n", " print('Built-in audio clips:')\n", " for clip in BuiltinAudioClip:\n", - " print(f' {clip.name:20s} \u2192 {clip.value}')\n", + " print(f' {clip.name:20s} → {clip.value}')\n", "\n", " player = BackgroundAudioPlayer(\n", " source=BuiltinAudioClip.HOLD_MUSIC,\n", @@ -335,26 +374,26 @@ " print(f'\\nPlayer constructed: source={player.source} volume={player.volume} loop={player.loop}')\n", " assert player.volume == 0.15\n", " assert player.loop is True\n" - ], - "execution_count": null, - "outputs": [], - "id": "8fde7671" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "### EventBus\n" - ], - "id": "bdbc5a0c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_event_bus" ] }, + "outputs": [], "source": [ "from getpatter import EventBus\n", "with _setup.cell('event_bus', tier=1, env=env) as ok:\n", @@ -374,68 +413,72 @@ " bus.emit('call_ended', {'call_id': 'c003'})\n", " print(f'After unsubscribe: still {len(received)} events (c003 not received)')\n", " assert len(received) == 2\n" - ], - "execution_count": null, - "outputs": [], - "id": "b3eea458" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "0ebb076b" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a scheduled outbound call using `schedule_once`. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "687f717f" + "## §3 — Live Appendix\n", + "\n", + "Places a scheduled outbound call using `schedule_once`. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "c757f68d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", - " print(f' feature: schedule_once \u2014 fires a call 5 seconds from now')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", + " print(f' feature: schedule_once — fires a call 5 seconds from now')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "d3a3d8bb" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live scheduled call *(T4)*\n" - ], - "id": "ad9d399f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_scheduled_call" ] }, + "outputs": [], "source": [ "import asyncio\n", "from datetime import datetime, timedelta, timezone\n", @@ -468,17 +511,14 @@ " finally:\n", " handle.cancel()\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "5c195cb1" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/11_metrics_dashboard.ipynb b/examples/notebooks/python/11_metrics_dashboard.ipynb index ecf03f4b..920bd6d5 100644 --- a/examples/notebooks/python/11_metrics_dashboard.ipynb +++ b/examples/notebooks/python/11_metrics_dashboard.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 11 \u2014 Metrics & dashboard\n", + "# 11 — Metrics & dashboard\n", "\n", - "CallMetricsAccumulator, MetricsStore, dashboard SSE, CSV/JSON export, pricing, basic auth.", - "\n" - ], - "id": "0ca991aa" + "CallMetricsAccumulator, MetricsStore, dashboard SSE, CSV/JSON export, pricing, basic auth.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "1de543bd" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "1ff3df43" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "9491613b" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "c1ec18d4" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "f35c7115" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "efd3ed12" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "18ec4947" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "35c48e2a" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "b635e234" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "6bbc5df3" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "df7d053d" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "43617b79" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "65b9a21d" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "8e8fd51e" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "20311ad2" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "bc5665c2" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises `CallMetricsAccumulator`, `MetricsStore`, and the CSV/JSON export helpers.\n" - ], - "id": "f5ffe21e" + "## §2 — Feature Tour\n", + "\n", + "Exercises `CallMetricsAccumulator`, `MetricsStore`, and the CSV/JSON export helpers.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### CallMetricsAccumulator\n" - ], - "id": "4b2e014f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_metrics_accumulator" ] }, + "outputs": [], "source": [ "import time\n", "from getpatter import CallMetricsAccumulator\n", @@ -268,26 +307,26 @@ " print(f'latency.total: {tm.latency.total_ms:.0f}ms')\n", " assert tm.tts_characters == len('The weather today is sunny and warm.')\n", " assert tm.stt_audio_seconds == 2.1\n" - ], - "execution_count": null, - "outputs": [], - "id": "d88f9437" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### MetricsStore\n" - ], - "id": "f689eb93" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_metrics_store" ] }, + "outputs": [], "source": [ "from getpatter import MetricsStore\n", "with _setup.cell('metrics_store', tier=1, env=env) as ok:\n", @@ -317,26 +356,26 @@ " print(f'Total cost: ${agg[\"total_cost\"]:.4f}')\n", " print(f'Active calls: {agg[\"active_calls\"]}')\n", " assert agg['total_calls'] == 2\n" - ], - "execution_count": null, - "outputs": [], - "id": "6948b284" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Export\n" - ], - "id": "7cf214cb" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_export" ] }, + "outputs": [], "source": [ "from getpatter import MetricsStore, calls_to_csv, calls_to_json\n", "with _setup.cell('export', tier=1, env=env) as ok:\n", @@ -356,68 +395,72 @@ " assert 'call_id' in csv_lines[0]\n", " assert 'c001' in csv_lines[1]\n", " assert 'c001' in json_output\n" - ], - "execution_count": null, - "outputs": [], - "id": "0bc52db9" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "588dcfb3" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real call and inspects the `MetricsStore` after it ends. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "d0234293" + "## §3 — Live Appendix\n", + "\n", + "Places a real call and inspects the `MetricsStore` after it ends. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "e81ae0ff" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' metrics: will inspect MetricsStore after call ends')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "f2de3255" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live call + metrics inspection *(T4)*\n" - ], - "id": "366b6209" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_metrics_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, MetricsStore\n", "with _setup.cell('live_metrics_call', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -440,20 +483,17 @@ " print(f'Calls in store: {agg[\"total_calls\"]}')\n", " print(f'Avg duration: {agg[\"avg_duration\"]}s')\n", " print(f'Total cost: ${agg[\"total_cost\"]:.4f}')\n", - " print('\u2713 Metrics call completed')\n", + " print('✓ Metrics call completed')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "053f7f59" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/12_security.ipynb b/examples/notebooks/python/12_security.ipynb index bf31c5f6..2cf41c48 100644 --- a/examples/notebooks/python/12_security.ipynb +++ b/examples/notebooks/python/12_security.ipynb @@ -2,32 +2,59 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 12 \u2014 Security\n", + "# 12 — Security\n", "\n", - "HMAC, Ed25519, SSRF guard, webhook URL validation, secret hygiene, dashboard auth, cost cap.", - "\n" - ], - "id": "e28734a1" + "HMAC, Ed25519, SSRF guard, webhook URL validation, secret hygiene, dashboard auth, cost cap.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. See `examples/notebooks/python/Dockerfile` and `docker-compose.yml` for what it builds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional — launch the patter-notebooks Docker stack from this cell.\n", + "# Skip this cell to run on your host Python. Idempotent if uncommented.\n", + "#\n", + "# import _setup\n", + "# _setup.start_docker() # build + up -d, prints http://localhost:8888\n", + "# _setup.start_docker(open_url=True) # …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "641dd44d" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", @@ -35,34 +62,36 @@ "import _setup\n", "env = _setup.load()\n", "print(f'getpatter version target: {env.patter_version}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "3b5d98b8" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "44eda589" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "535f1f2c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import sys\n", "import getpatter\n", @@ -71,26 +100,27 @@ " print(f'getpatter {getpatter.__version__} on Python {sys.version.split()[0]}')\n", " assert getpatter.__version__ >= env.patter_version, \\\n", " f'installed {getpatter.__version__} < target {env.patter_version}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "cde193f9" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "fb3ac471" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio\n", "with _setup.cell('local_mode', tier=1, env=env) as ok:\n", @@ -107,55 +137,57 @@ " assert p._local_config.telephony_provider == 'twilio'\n", " assert p._local_config.phone_number == '+15555550100'\n", " print(f'provider={p._local_config.telephony_provider} phone={p._local_config.phone_number}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "4082655c" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` \u2014 this cell verifies the guard.\n" - ], - "id": "28102311" + "### Cloud mode (coming soon)\n", + "When `api_key=` is supported, Patter cloud handles telephony. For now, the SDK raises `NotImplementedError` — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "from getpatter import Patter\n", "with _setup.cell('cloud_mode', tier=1, env=env) as ok:\n", " if ok:\n", " try:\n", " Patter(api_key='pt_test_xxx')\n", - " raise AssertionError('expected NotImplementedError \u2014 cloud mode guard missing')\n", + " raise AssertionError('expected NotImplementedError — cloud mode guard missing')\n", " except NotImplementedError as exc:\n", " print(f'cloud mode guard OK: {exc}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "04d5af95" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "e931524b" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the engine from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI\n", "with _setup.cell('agent_engines', tier=1, env=env) as ok:\n", @@ -173,74 +205,81 @@ " assert cv.provider == 'elevenlabs_convai', cv.provider\n", " assert pl.provider == 'openai_realtime', pl.provider\n", " print(f'rt.provider={rt.provider} cv.provider={cv.provider} pl.provider={pl.provider}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "61fb8255" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline (no network, no carrier calls).\n" - ], - "id": "b7ab0572" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier. No API key \u2014 runs entirely on your machine.\n" - ], - "id": "2d9f2047" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier. No API key — runs entirely on your machine.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `api_key=` instead of a carrier \u2014 Patter cloud handles telephony.\n" - ], - "id": "8ef7acda" + "### Cloud mode\n", + "Same SDK, just an `api_key=` instead of a carrier — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" - ], - "id": "d63d8c16" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline* (STT + LLM + TTS). The factory derives the mode from `engine=` / `stt=`/`tts=`.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "72f95c74" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises webhook signature guards, URL classification, and the SSRF protection layer.\n" - ], - "id": "c5bfae7e" + "## §2 — Feature Tour\n", + "\n", + "Exercises webhook signature guards, URL classification, and the SSRF protection layer.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### URL classification\n" - ], - "id": "731e375f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_url_classification" ] }, + "outputs": [], "source": [ "from getpatter import is_remote_url, is_websocket_url\n", "with _setup.cell('url_classification', tier=1, env=env) as ok:\n", @@ -256,29 +295,29 @@ " print('-' * 70)\n", " for val, fn, expected, label in cases:\n", " result = fn(val)\n", - " ok_mark = '\u2713' if result == expected else '\u2717'\n", + " ok_mark = '✓' if result == expected else '✗'\n", " print(f'{fn.__name__:<20s} {label:<20s} {str(expected):<10s} {str(result):<10s} {ok_mark}')\n", " assert result == expected, f'{fn.__name__}({val!r}) expected {expected}, got {result}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "dfa2ec1b" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### Twilio webhook guard: valid vs rejected signatures\n" - ], - "id": "479d60f0" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_twilio_sig_guard" ] }, + "outputs": [], "source": [ "from twilio.request_validator import RequestValidator\n", "with _setup.cell('twilio_sig_guard', tier=1, env=env) as ok:\n", @@ -291,40 +330,40 @@ " # Case 1: correct signature (generated by Twilio)\n", " good_sig = validator.compute_signature(url, params)\n", " assert validator.validate(url, params, good_sig), 'valid sig must pass'\n", - " print(f'\u2713 Valid signature accepted')\n", + " print(f'✓ Valid signature accepted')\n", "\n", " # Case 2: missing header (empty string)\n", " assert not validator.validate(url, params, ''), 'empty sig must fail'\n", - " print(f'\u2713 Empty signature rejected')\n", + " print(f'✓ Empty signature rejected')\n", "\n", " # Case 3: wrong auth token\n", " bad_validator = RequestValidator('wrong_token_xxxxxxxxxxxxxxxxxx')\n", " assert not bad_validator.validate(url, params, good_sig), 'wrong token must fail'\n", - " print(f'\u2713 Wrong auth token rejected')\n", + " print(f'✓ Wrong auth token rejected')\n", "\n", " # Case 4: URL mismatch (SSRF-style redirect)\n", " assert not validator.validate('https://evil.com/intercept', params, good_sig), 'URL mismatch must fail'\n", - " print(f'\u2713 URL mismatch rejected')\n" - ], - "execution_count": null, - "outputs": [], - "id": "c25a6458" + " print(f'✓ URL mismatch rejected')\n" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### SSRF guard: private-IP rejection\n" - ], - "id": "bf942e49" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_ssrf_guard" ] }, + "outputs": [], "source": [ "import ipaddress\n", "with _setup.cell('ssrf_guard', tier=1, env=env) as ok:\n", @@ -348,30 +387,30 @@ " ]\n", " for url, expected_block, label in cases:\n", " blocked = _is_ssrf_blocked(url)\n", - " mark = '\u2713' if blocked == expected_block else '\u2717'\n", + " mark = '✓' if blocked == expected_block else '✗'\n", " action = 'BLOCKED' if blocked else 'allowed'\n", " print(f' {mark} {action:7s} {label:30s} {url}')\n", " assert blocked == expected_block, f'SSRF check failed for {url}'\n" - ], - "execution_count": null, - "outputs": [], - "id": "e3e34750" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "### EventBus + observability\n" - ], - "id": "b78f93b4" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_observability" ] }, + "outputs": [], "source": [ "from getpatter import EventBus, is_tracing_enabled\n", "with _setup.cell('observability', tier=1, env=env) as ok:\n", @@ -394,69 +433,73 @@ " for entry in log:\n", " print(f' {entry}')\n", " assert len(log) == 3\n" - ], - "execution_count": null, - "outputs": [], - "id": "5439a737" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "acdf3d89" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real call that verifies Twilio webhook signatures end-to-end. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "546633ad" + "## §3 — Live Appendix\n", + "\n", + "Places a real call that verifies Twilio webhook signatures end-to-end. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "003df37f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "with _setup.cell('live_preflight', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env=env) as ok:\n", " if ok:\n", - " print(f' carrier: Twilio {env.twilio_number} \u2192 {env.target_number}')\n", + " print(f' carrier: Twilio {env.twilio_number} → {env.target_number}')\n", " print(f' security: HMAC-SHA1 webhook signature verified on every inbound request')\n", " print(f' webhook: {env.public_webhook_url or \"(ngrok auto-launch)\"}')\n", " print(f' auth_token ends: ...{env.twilio_token[-4:]}')\n" - ], - "execution_count": null, - "outputs": [], - "id": "4674856c" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live call with signature verification *(T4)*\n" - ], - "id": "c2708efe" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_security_call" ] }, + "outputs": [], "source": [ "from getpatter import Patter, Twilio, OpenAIRealtime\n", "with _setup.cell('live_security_call', tier=4, required=['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env=env) as ok:\n", @@ -476,20 +519,17 @@ " await p.call(env.target_number, agent=agent,\n", " first_message='Hello from Patter security demo.',\n", " ring_timeout=env.max_call_seconds)\n", - " print('\u2713 Security call completed \u2014 all webhook signatures verified')\n", + " print('✓ Security call completed — all webhook signatures verified')\n", " finally:\n", " _setup.hangup_leftover_calls(env)\n" - ], - "execution_count": null, - "outputs": [], - "id": "3f20aa20" + ] } ], "metadata": { "kernelspec": { - "name": "python3", "display_name": "Python 3", - "language": "python" + "language": "python", + "name": "python3" }, "language_info": { "name": "python" diff --git a/examples/notebooks/python/Dockerfile b/examples/notebooks/python/Dockerfile new file mode 100644 index 00000000..3819d6bd --- /dev/null +++ b/examples/notebooks/python/Dockerfile @@ -0,0 +1,44 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + JUPYTER_PLATFORM_DIRS=1 \ + PATTER_NOTEBOOKS_IN_DOCKER=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl git \ + && rm -rf /var/lib/apt/lists/* + +# Non-root user. UID 1000 matches the most common host user on Linux/macOS so +# the bind-mounted /notebooks tree stays writable without a chown dance. +ARG PUID=1000 +ARG PGID=1000 +RUN groupadd --gid "${PGID}" patter \ + && useradd --uid "${PUID}" --gid "${PGID}" --create-home --shell /bin/bash patter + +WORKDIR /notebooks/python + +# Top-level dep pins. PATTER_VERSION lets a builder override the SDK version +# without rewriting requirements.txt — e.g. docker build --build-arg PATTER_VERSION=0.5.5 +ARG PATTER_VERSION=0.5.4 +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt \ + && pip install --no-cache-dir --upgrade "getpatter==${PATTER_VERSION}" + +# 8888 → JupyterLab. 8765 → EmbeddedServer for T2/T4 live cells. +EXPOSE 8888 8765 + +USER patter + +# JUPYTER_TOKEN is supplied by docker-compose (generated by _setup.start_docker +# or set explicitly in the user environment). The wrapper aborts the launch +# when the variable is unset, so unauthenticated Lab requires explicit opt-in +# via PATTER_NOTEBOOKS_NO_TOKEN=1. +CMD ["sh", "-c", "exec jupyter lab \ + --ip=0.0.0.0 \ + --port=8888 \ + --no-browser \ + --ServerApp.token=\"${JUPYTER_TOKEN:-}\" \ + --ServerApp.password= \ + --ServerApp.root_dir=/notebooks/python"] diff --git a/examples/notebooks/python/_setup.py b/examples/notebooks/python/_setup.py index d3906128..b32812ce 100644 --- a/examples/notebooks/python/_setup.py +++ b/examples/notebooks/python/_setup.py @@ -1,6 +1,7 @@ """Shared helpers for every notebook in examples/notebooks/python/. -Public surface (mirrored in typescript/_setup.ts): +Public surface (mirrored in typescript/_setup.ts — Docker helpers pending, +see tracking issue for inDocker/startDocker port): NotebookEnv — frozen dataclass holding every env var the series reads load() — parse .env and return NotebookEnv has_key() — booleanise a key @@ -12,17 +13,29 @@ run_stt() — standardised STT roundtrip helper run_tts() — standardised TTS roundtrip helper hangup_leftover_calls() — safety sweep for live appendix teardown + in_docker() — True when running inside the patter-notebooks container + start_docker() — optional: launch the patter-notebooks Docker stack """ from __future__ import annotations +import contextlib import os +import re +import secrets +import shutil +import subprocess +import time +import traceback +import webbrowser from dataclasses import dataclass from pathlib import Path +from typing import Iterable, Iterator from dotenv import load_dotenv -NOTEBOOKS_DIR = Path(__file__).resolve().parent.parent +PYTHON_NOTEBOOKS_DIR = Path(__file__).resolve().parent +NOTEBOOKS_DIR = PYTHON_NOTEBOOKS_DIR.parent FIXTURES = NOTEBOOKS_DIR / "fixtures" @@ -119,12 +132,6 @@ def print_key_matrix(env: NotebookEnv, required) -> None: print(f" {marker} {name}") -import contextlib -import time -import traceback -from typing import Iterable, Iterator - - @contextlib.contextmanager def cell( name: str, @@ -180,8 +187,6 @@ def cell( print(f"✅ [{name}] {elapsed:.2f}s") -import re - _REAL_PHONE = re.compile(r"\+1[2-9]\d{9}") _REAL_TWILIO_SID = re.compile(r"\bAC[0-9a-f]{32}\b") @@ -256,6 +261,114 @@ def hangup_leftover_calls(env: NotebookEnv) -> None: print(f"⚠ Twilio sweep failed: {exc}") +_TRUTHY = {"1", "true", "yes", "on"} + + +def in_docker() -> bool: + """True if this kernel is running inside the patter-notebooks container.""" + flag = os.environ.get("PATTER_NOTEBOOKS_IN_DOCKER", "").strip().lower() + return flag in _TRUTHY or Path("/.dockerenv").exists() + + +def _generate_jupyter_token() -> str: + """Stable token per host user — survives `compose down`/`up` cycles. + + Stored at ~/.config/patter-notebooks/jupyter_token. Generated on first + call. ``PATTER_NOTEBOOKS_NO_TOKEN=1`` bypasses entirely (opt-in only — + leaves Lab unauthenticated, intended for fully isolated dev VMs). + """ + if os.environ.get("PATTER_NOTEBOOKS_NO_TOKEN", "").strip().lower() in _TRUTHY: + return "" + token_dir = Path.home() / ".config" / "patter-notebooks" + token_dir.mkdir(parents=True, exist_ok=True) + token_file = token_dir / "jupyter_token" + if token_file.exists(): + token = token_file.read_text().strip() + if token: + return token + token = secrets.token_urlsafe(32) + token_file.write_text(token) + token_file.chmod(0o600) + return token + + +def start_docker( + *, + build: bool = True, + detach: bool = True, + open_url: bool = False, +) -> bool: + """Optional: launch the patter-notebooks Docker stack from a notebook cell. + + No-op when the kernel is already inside the container. Otherwise runs + ``docker compose up -d --build`` from ``examples/notebooks/python/`` and + prints the JupyterLab URL with the auth token. + + Args: + build: pass ``--build`` to rebuild the image when the Dockerfile + or pyproject changed. Default True. + detach: pass ``-d``. Must be True — interactive mode would hang the + kernel; explicit False prints a guard and returns False. + open_url: also open the URL in the default browser via ``webbrowser``. + + Returns: + True on success (container up or already inside one). False when the + environment is wrong (no docker, missing compose file, command failed). + + Idempotent — running twice just re-syncs the container state. + """ + if in_docker(): + print("✓ already running inside the patter-notebooks Docker container") + return True + + if not detach: + print("⚠ start_docker(detach=False) would block the kernel — " + "run `docker compose up` in a terminal instead") + return False + + if shutil.which("docker") is None: + print("⚠ docker CLI not found on PATH — install Docker Desktop or skip this cell") + return False + + compose_file = PYTHON_NOTEBOOKS_DIR / "docker-compose.yml" + if not compose_file.exists(): + print(f"⚠ {compose_file} not found — cannot start Docker stack") + return False + + token = _generate_jupyter_token() + cmd = ["docker", "compose", "up", "-d"] + if build: + cmd.append("--build") + + env = {**os.environ, "JUPYTER_TOKEN": token} + print(f"▶ {' '.join(cmd)} (cwd={PYTHON_NOTEBOOKS_DIR})") + result = subprocess.run( + cmd, + cwd=PYTHON_NOTEBOOKS_DIR, + env=env, + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"❌ docker compose exited with code {result.returncode}") + if result.stdout: + print(f"--- stdout ---\n{result.stdout.rstrip()}") + if result.stderr: + print(f"--- stderr ---\n{result.stderr.rstrip()}") + return False + + suffix = f"?token={token}" if token else "" + url = f"http://127.0.0.1:8888/lab/tree/{suffix}" + print() + print(f"✓ Docker stack up. Open: {url}") + print(" T2/T4 EmbeddedServer port: http://127.0.0.1:8765") + print(" Stop with: docker compose down (run from examples/notebooks/python/)") + if open_url: + webbrowser.open(url) + return True + + def load(env_file: Path | str | None = None) -> NotebookEnv: """Load .env if present, then construct NotebookEnv from process env.""" if env_file is None: diff --git a/examples/notebooks/python/docker-compose.yml b/examples/notebooks/python/docker-compose.yml new file mode 100644 index 00000000..97a0fe5e --- /dev/null +++ b/examples/notebooks/python/docker-compose.yml @@ -0,0 +1,26 @@ +services: + notebooks: + build: + context: . + args: + PATTER_VERSION: ${PATTER_VERSION:-0.5.4} + PUID: ${PUID:-1000} + PGID: ${PGID:-1000} + image: patter-notebooks-python + ports: + # Loopback-only by default. Override the host side at your own risk — + # JupyterLab + the bind-mounted .env would expose every provider key. + - "127.0.0.1:8888:8888" # JupyterLab + - "127.0.0.1:8765:8765" # EmbeddedServer (T2/T4 cells) + volumes: + # Mount the whole notebooks tree so .env, fixtures, and TS notebooks are + # all visible to _setup.py (which reads NOTEBOOKS_DIR/.env). + - ../:/notebooks + working_dir: /notebooks/python + environment: + - JUPYTER_PLATFORM_DIRS=1 + - JUPYTER_TOKEN=${JUPYTER_TOKEN:-} + env_file: + - path: ../.env + required: false + restart: unless-stopped diff --git a/examples/notebooks/python/requirements.txt b/examples/notebooks/python/requirements.txt new file mode 100644 index 00000000..169aa849 --- /dev/null +++ b/examples/notebooks/python/requirements.txt @@ -0,0 +1,15 @@ +# Pinned top-level deps for the patter-notebooks-python image. +# Regenerate this file with `pip-compile requirements.in` once a .in file is +# introduced. For now, top-level pins are explicit; transitive deps float +# within the constraints of these majors. +getpatter==0.5.4 +jupyterlab==4.2.5 +ipykernel==6.29.5 +ipython==8.27.0 +nbclient==0.10.0 +jupyter-client==8.6.3 +python-dotenv==1.0.1 +httpx==0.27.2 +twilio==9.3.6 +telnyx==2.1.5 +pyngrok==7.2.0 diff --git a/examples/notebooks/python/tests/test_docker_bootstrap.py b/examples/notebooks/python/tests/test_docker_bootstrap.py new file mode 100644 index 00000000..fe67c3e8 --- /dev/null +++ b/examples/notebooks/python/tests/test_docker_bootstrap.py @@ -0,0 +1,130 @@ +"""Unit tests for the Docker launcher helpers in _setup.py. + +Mocked at the subprocess + filesystem boundary only — `in_docker()` runs real +code, `start_docker()` exercises every early-return branch with real Path +objects and a fake `subprocess.run`. +""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def _isolate_docker_env(monkeypatch, tmp_path): + monkeypatch.delenv("PATTER_NOTEBOOKS_IN_DOCKER", raising=False) + monkeypatch.delenv("PATTER_NOTEBOOKS_NO_TOKEN", raising=False) + monkeypatch.delenv("JUPYTER_TOKEN", raising=False) + monkeypatch.setenv("HOME", str(tmp_path)) + yield + + +@pytest.mark.parametrize("value", ["1", "true", "TRUE", "yes", "On"]) +def test_in_docker_truthy_env_values(monkeypatch, value): + import _setup + + monkeypatch.setenv("PATTER_NOTEBOOKS_IN_DOCKER", value) + with patch.object(Path, "exists", return_value=False): + assert _setup.in_docker() is True + + +@pytest.mark.parametrize("value", ["", "0", "false", "no", "off", "anything"]) +def test_in_docker_falsy_env_values(monkeypatch, value): + import _setup + + monkeypatch.setenv("PATTER_NOTEBOOKS_IN_DOCKER", value) + with patch.object(Path, "exists", return_value=False): + assert _setup.in_docker() is False + + +def test_in_docker_dockerenv_marker(monkeypatch): + import _setup + + monkeypatch.delenv("PATTER_NOTEBOOKS_IN_DOCKER", raising=False) + with patch.object(Path, "exists", lambda self: str(self) == "/.dockerenv"): + assert _setup.in_docker() is True + + +def test_start_docker_returns_true_when_already_in_container(monkeypatch, capsys): + import _setup + + monkeypatch.setenv("PATTER_NOTEBOOKS_IN_DOCKER", "1") + assert _setup.start_docker() is True + assert "already running" in capsys.readouterr().out + + +def test_start_docker_rejects_detach_false(capsys): + import _setup + + assert _setup.start_docker(detach=False) is False + assert "would block the kernel" in capsys.readouterr().out + + +def test_start_docker_returns_false_when_docker_missing(monkeypatch, capsys): + import _setup + + monkeypatch.setattr(_setup.shutil, "which", lambda _name: None) + assert _setup.start_docker() is False + assert "docker CLI not found" in capsys.readouterr().out + + +def test_start_docker_returns_false_when_compose_failed(monkeypatch, capsys): + import _setup + + class _Result: + returncode = 1 + stdout = "" + stderr = "error: bind: address already in use\n" + + monkeypatch.setattr(_setup.shutil, "which", lambda _name: "/usr/bin/docker") + monkeypatch.setattr(_setup.subprocess, "run", lambda *a, **kw: _Result()) + assert _setup.start_docker(build=False) is False + out = capsys.readouterr().out + assert "exited with code 1" in out + assert "address already in use" in out + + +def test_start_docker_returns_true_on_success(monkeypatch, capsys): + import _setup + + class _Result: + returncode = 0 + stdout = "" + stderr = "" + + monkeypatch.setattr(_setup.shutil, "which", lambda _name: "/usr/bin/docker") + captured: dict = {} + + def _fake_run(cmd, **kwargs): + captured["cmd"] = cmd + captured["env"] = kwargs.get("env") + return _Result() + + monkeypatch.setattr(_setup.subprocess, "run", _fake_run) + assert _setup.start_docker(build=True) is True + out = capsys.readouterr().out + assert "Docker stack up" in out + assert "127.0.0.1:8888" in out + assert "--build" in captured["cmd"] + assert "-d" in captured["cmd"] + assert "JUPYTER_TOKEN" in captured["env"] + assert captured["env"]["JUPYTER_TOKEN"] + + +def test_generate_jupyter_token_is_stable(monkeypatch): + import _setup + + first = _setup._generate_jupyter_token() + second = _setup._generate_jupyter_token() + assert first == second + assert len(first) >= 32 + + +def test_generate_jupyter_token_opt_out(monkeypatch): + import _setup + + monkeypatch.setenv("PATTER_NOTEBOOKS_NO_TOKEN", "1") + assert _setup._generate_jupyter_token() == "" diff --git a/examples/notebooks/typescript/01_quickstart.ipynb b/examples/notebooks/typescript/01_quickstart.ipynb index 3a58c08b..cac53f83 100644 --- a/examples/notebooks/typescript/01_quickstart.ipynb +++ b/examples/notebooks/typescript/01_quickstart.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 01 \u2014 Quickstart\n", + "# 01 — Quickstart\n", "\n", - "Install, env check, three operating modes (cloud/self-hosted/local), three voice modes (Realtime/ConvAI/Pipeline), 'hello phone' minimal agent.", - "\n" - ], - "id": "fd9ec7c0" + "Install, env check, three operating modes (cloud/self-hosted/local), three voice modes (Realtime/ConvAI/Pipeline), 'hello phone' minimal agent.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "db1ca744" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "49c9a2d6" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "acbe3307" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "9e676d42" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "348927a2" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "a22d8639" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "a46a8a78" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "ba63dd6f" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "df8e66c2" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "161b7c32" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "6e94ac7d" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "47f85ff3" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "18cf70fd" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "43ebebab" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "b0f2224e" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "8fece149" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nThese cells require **T2** (local server) or **T3** (real API keys). Cells skip gracefully if prerequisites are missing.\n" - ], - "id": "fa1f611f" + "## §2 — Feature Tour\n", + "\n", + "These cells require **T2** (local server) or **T3** (real API keys). Cells skip gracefully if prerequisites are missing.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### Agent object inspection\n" - ], - "id": "32069c4f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_agent_inspection" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI, DeepgramSTT, ElevenLabsTTS } from \"getpatter\";\n", "await cell('agent_inspection', { tier: 1, env }, () => {\n", @@ -248,26 +287,26 @@ " console.log(`realtime: provider=${rt.provider} voice=${rt.voice}`);\n", " console.log(`pipeline: provider=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "ee6719e2" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### Pricing: calculate call costs\n" - ], - "id": "2e8079a6" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_pricing" ] }, + "outputs": [], "source": [ "import { DEFAULT_PRICING, calculateSttCost, calculateTtsCost, calculateTelephonyCost } from \"getpatter\";\n", "await cell('pricing', { tier: 1, env }, () => {\n", @@ -278,54 +317,54 @@ " console.log(`TTS (ElevenLabs, 200 chars): $${tts.toFixed(6)}`);\n", " console.log(`Telephony (Twilio, 60s): $${tel.toFixed(6)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "86c8702c" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Text transforms\n" - ], - "id": "3eddc059" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_text_transforms" ] }, + "outputs": [], "source": [ "import { filterMarkdown, filterEmoji, filterForTts } from \"getpatter\";\n", "await cell('text_transforms', { tier: 1, env }, () => {\n", - " const raw = '**Important**: Hello \ud83d\ude0a world';\n", + " const raw = '**Important**: Hello 😊 world';\n", " console.log(`filter_markdown: ${filterMarkdown(raw)}`);\n", " console.log(`filter_emoji: ${filterEmoji(raw)}`);\n", " console.log(`filter_for_tts: ${filterForTts(raw)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "f376ac64" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "### SentenceChunker\n" - ], - "id": "367a4ad7" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_sentence_chunker" ] }, + "outputs": [], "source": [ "import { SentenceChunker } from \"getpatter\";\n", "await cell('sentence_chunker', { tier: 1, env }, () => {\n", @@ -336,72 +375,76 @@ " chunks.push(...sc.flush());\n", " console.log(`chunks: ${JSON.stringify(chunks)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "7df17230" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "b0bed187" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real PSTN call. Requires `ENABLE_LIVE_CALLS=1` and carrier credentials.\n" - ], - "id": "90fd9ac1" + "## §3 — Live Appendix\n", + "\n", + "Places a real PSTN call. Requires `ENABLE_LIVE_CALLS=1` and carrier credentials.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "214522c2" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env }, () => {\n", " const webhook = env.publicWebhookUrl || '(auto-tunnel via ngrok)';\n", - " console.log('\u2713 T4 pre-flight');\n", + " console.log('✓ T4 pre-flight');\n", " console.log(` carrier: Twilio ${env.twilioNumber}`);\n", " console.log(` target: ${env.targetNumber}`);\n", " console.log(` webhook: ${webhook}`);\n", " console.log(` max_seconds: ${env.maxCallSeconds}`);\n", " console.log(` max_cost: $${env.maxCostUsd.toFixed(2)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "ea9f8133" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ - "### Live outbound call *(T4 \u2014 places a real 5-second call)*\n" - ], - "id": "340c60a2" + "### Live outbound call *(T4 — places a real 5-second call)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_outbound_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime } from \"getpatter\";\n", "await cell('live_outbound_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -416,22 +459,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Hello from Patter.', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 Call completed');\n", + " console.log('✓ Call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "0c71c393" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/02_realtime.ipynb b/examples/notebooks/typescript/02_realtime.ipynb index e9664166..3ccdb9cf 100644 --- a/examples/notebooks/typescript/02_realtime.ipynb +++ b/examples/notebooks/typescript/02_realtime.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 02 \u2014 Realtime providers\n", + "# 02 — Realtime providers\n", "\n", - "OpenAI Realtime, Gemini Live, Ultravox, ElevenLabs ConvAI.", - "\n" - ], - "id": "85cb4737" + "OpenAI Realtime, Gemini Live, Ultravox, ElevenLabs ConvAI.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "a5a99384" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "9c330581" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "2d5d9cb7" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "6d3dc54f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "ec4ccaa1" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "4724dfc3" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "6bc0a7c5" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "7c641785" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "dc28a1be" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "52e0fad0" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "66dc5c6c" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "69fbf7a1" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "8ab91c9b" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "c9332514" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "3489e281" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "338e4dc0" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises OpenAI Realtime configuration and related SDK primitives.\n" - ], - "id": "5975100f" + "## §2 — Feature Tour\n", + "\n", + "Exercises OpenAI Realtime configuration and related SDK primitives.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### OpenAI Realtime agent: full config\n" - ], - "id": "04447650" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_realtime_agent_config" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime } from \"getpatter\";\n", "await cell('realtime_agent_config', { tier: 1, env }, () => {\n", @@ -254,26 +293,26 @@ " console.log(`firstMessage: ${agent.firstMessage}`);\n", " if (agent.provider !== 'openai_realtime') throw new Error('wrong provider');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "648683a2" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### SentenceChunker\n" - ], - "id": "f9c4aa49" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_realtime_chunker" ] }, + "outputs": [], "source": [ "import { SentenceChunker } from \"getpatter\";\n", "await cell('realtime_chunker', { tier: 1, env }, () => {\n", @@ -285,26 +324,26 @@ " console.log(`sentences: ${chunks.length}`);\n", " chunks.forEach((c, i) => console.log(` [${i}] ${JSON.stringify(c.trim())}`));\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "57988e4e" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ - "### Live: OpenAI Realtime models *(T3 \u2014 requires `OPENAI_API_KEY`)*\n" - ], - "id": "78f20e92" + "### Live: OpenAI Realtime models *(T3 — requires `OPENAI_API_KEY`)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_openai_realtime_live" ] }, + "outputs": [], "source": [ "await cell('openai_realtime_live', { tier: 3, required: ['openaiKey'], env }, async () => {\n", " const resp = await fetch('https://api.openai.com/v1/models', {\n", @@ -314,68 +353,72 @@ " const models = data.data.filter(m => m.id.includes('realtime')).map(m => m.id);\n", " console.log(`OpenAI realtime models: ${models.slice(0, 5)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "9251621f" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "31b2d5d9" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real call through OpenAI Realtime. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "fdbcdcf5" + "## §3 — Live Appendix\n", + "\n", + "Places a real call through OpenAI Realtime. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "8be5fe99" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", " console.log(' engine: OpenAI Realtime (gpt-4o-realtime-preview)');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "b9fe7d2b" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live OpenAI Realtime call *(T4)*\n" - ], - "id": "42ab5c66" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_realtime_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime } from \"getpatter\";\n", "await cell('live_realtime_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -390,22 +433,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Hello! Patter demo. Goodbye!', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 Realtime call completed');\n", + " console.log('✓ Realtime call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "6c999e8f" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/03_pipeline_stt.ipynb b/examples/notebooks/typescript/03_pipeline_stt.ipynb index daa46074..58076d70 100644 --- a/examples/notebooks/typescript/03_pipeline_stt.ipynb +++ b/examples/notebooks/typescript/03_pipeline_stt.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 03 \u2014 Pipeline STT\n", + "# 03 — Pipeline STT\n", "\n", - "Deepgram, Whisper, AssemblyAI, Soniox, Speechmatics, Cartesia.", - "\n" - ], - "id": "9782d1af" + "Deepgram, Whisper, AssemblyAI, Soniox, Speechmatics, Cartesia.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "3e9d8255" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "f4365922" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "c5ebb38a" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "bb7fbd74" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "c3ecfee0" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "52c647b6" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "5bfc5bb0" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "fb11bb42" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "9d956806" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "902132c6" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "9a6b8e61" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "abca7f08" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "bd17e31a" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "cfbc349a" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "771c43e2" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "14cb8888" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises STT provider construction, audio transcoding, and (T3) live transcription.\n" - ], - "id": "e8e3ff82" + "## §2 — Feature Tour\n", + "\n", + "Exercises STT provider construction, audio transcoding, and (T3) live transcription.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### STT provider construction\n" - ], - "id": "4809dfa5" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_stt_providers" ] }, + "outputs": [], "source": [ "import { DeepgramSTT, WhisperSTT, OpenAITranscribeSTT } from \"getpatter\";\n", "await cell('stt_providers', { tier: 1, env }, () => {\n", @@ -245,54 +284,54 @@ " console.log(`Whisper: model=${wh.model}`);\n", " console.log(`OpenAI Transcribe: ${ot.constructor.name}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "7f7c5c13" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ - "### \u03bc-law \u2194 PCM-16 transcoding roundtrip\n" - ], - "id": "e08864f0" + "### μ-law ↔ PCM-16 transcoding roundtrip\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_mulaw_transcoding" ] }, + "outputs": [], "source": [ "import { mulawToPcm16, pcm16ToMulaw } from \"getpatter\";\n", "await cell('mulaw_transcoding', { tier: 1, env }, () => {\n", " const pcm = new Uint8Array(1600).fill(0); // 800 silent 16-bit samples\n", " const mulaw = pcm16ToMulaw(Buffer.from(pcm.buffer));\n", " const recovered = mulawToPcm16(mulaw);\n", - " console.log(`PCM: ${pcm.length}B \u03bc-law: ${mulaw.length}B recovered: ${recovered.length}B`);\n", + " console.log(`PCM: ${pcm.length}B μ-law: ${mulaw.length}B recovered: ${recovered.length}B`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "36379afc" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ - "### 8kHz \u2192 16kHz resampling\n" - ], - "id": "7ff5a9fa" + "### 8kHz → 16kHz resampling\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_resampler" ] }, + "outputs": [], "source": [ "import { resample8kTo16k, resample16kTo8k } from \"getpatter\";\n", "await cell('resampler', { tier: 1, env }, () => {\n", @@ -301,26 +340,26 @@ " const back = resample16kTo8k(pcm16k);\n", " console.log(`8kHz: ${pcm8k.length}B 16kHz: ${pcm16k.length}B roundtrip: ${back.length}B`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "56e804bd" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "### Live: Deepgram transcription *(T3 \u2014 requires `DEEPGRAM_API_KEY`)*\n" - ], - "id": "0f749e1d" + "### Live: Deepgram transcription *(T3 — requires `DEEPGRAM_API_KEY`)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_deepgram_live" ] }, + "outputs": [], "source": [ "await cell('deepgram_live', { tier: 3, required: ['deepgramKey'], env }, async () => {\n", " const fs = await import('fs/promises');\n", @@ -334,68 +373,72 @@ " const transcript = data.results.channels[0].alternatives[0].transcript;\n", " console.log(`Deepgram transcript: ${JSON.stringify(transcript)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "32d424ce" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "53075151" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nCalls a real number through the Pipeline engine using Deepgram STT. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "7a118f8a" + "## §3 — Live Appendix\n", + "\n", + "Calls a real number through the Pipeline engine using Deepgram STT. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "866ae688" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'DEEPGRAM_API_KEY'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(' STT: Deepgram (nova-2-general)');\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "8eedc7a8" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live Pipeline STT call *(T4)*\n" - ], - "id": "86c7378b" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_stt_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, DeepgramSTT, OpenAILLM, OpenAITTS } from \"getpatter\";\n", "await cell('live_stt_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'DEEPGRAM_API_KEY', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -412,22 +455,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Hello from Patter STT demo.', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 Pipeline STT call completed');\n", + " console.log('✓ Pipeline STT call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "ee36c596" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/04_pipeline_tts.ipynb b/examples/notebooks/typescript/04_pipeline_tts.ipynb index af68db75..5b6704d2 100644 --- a/examples/notebooks/typescript/04_pipeline_tts.ipynb +++ b/examples/notebooks/typescript/04_pipeline_tts.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 04 \u2014 Pipeline TTS\n", + "# 04 — Pipeline TTS\n", "\n", - "ElevenLabs, OpenAI, Cartesia, LMNT, Rime.", - "\n" - ], - "id": "e8dd6bb8" + "ElevenLabs, OpenAI, Cartesia, LMNT, Rime.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "50ee1d7c" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "6304612b" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "d6c76e1e" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "7f30116d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "3f967f00" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "ed3b4e17" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "51a9002f" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "3ac9559e" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "6386f9e1" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "7d47fa27" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "875e8d27" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "c61eaf9d" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "50b71097" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "a4ea2d43" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "a13957ac" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "8f13dace" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises TTS provider construction and (T3) live synthesis.\n" - ], - "id": "73f85df5" + "## §2 — Feature Tour\n", + "\n", + "Exercises TTS provider construction and (T3) live synthesis.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### TTS provider construction\n" - ], - "id": "94deb36b" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_tts_providers" ] }, + "outputs": [], "source": [ "import { ElevenLabsTTS, OpenAITTS, CartesiaTTS, RimeTTS, LMNTTTS } from \"getpatter\";\n", "await cell('tts_providers', { tier: 1, env }, () => {\n", @@ -247,55 +286,55 @@ " console.log(`${name}: ${(p as any).constructor.name}`);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "673ebe31" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### TTS text preparation\n" - ], - "id": "3d15b740" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_tts_text_prep" ] }, + "outputs": [], "source": [ "import { filterForTts } from \"getpatter\";\n", "await cell('tts_text_prep', { tier: 1, env }, () => {\n", - " const samples = ['**Bold** text.', 'Visit https://example.com \ud83c\udf89', 'Code: `x = 1`'];\n", + " const samples = ['**Bold** text.', 'Visit https://example.com 🎉', 'Code: `x = 1`'];\n", " for (const raw of samples) {\n", " console.log(`in: ${raw}`);\n", " console.log(`out: ${filterForTts(raw)}`);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "454a7139" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ - "### Live: ElevenLabs TTS synthesis *(T3 \u2014 requires `ELEVENLABS_API_KEY`)*\n" - ], - "id": "3cbe2a9e" + "### Live: ElevenLabs TTS synthesis *(T3 — requires `ELEVENLABS_API_KEY`)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_elevenlabs_tts_live" ] }, + "outputs": [], "source": [ "await cell('elevenlabs_tts_live', { tier: 3, required: ['elevenLabsKey'], env }, async () => {\n", " const voiceId = '21m00Tcm4TlvDq8ikWAM';\n", @@ -307,26 +346,26 @@ " const buf = await resp.arrayBuffer();\n", " console.log(`ElevenLabs audio: ${buf.byteLength} bytes`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "5b4f6a03" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "### Live: OpenAI TTS synthesis *(T3 \u2014 requires `OPENAI_API_KEY`)*\n" - ], - "id": "68f3ae3e" + "### Live: OpenAI TTS synthesis *(T3 — requires `OPENAI_API_KEY`)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_openai_tts_live" ] }, + "outputs": [], "source": [ "await cell('openai_tts_live', { tier: 3, required: ['openaiKey'], env }, async () => {\n", " const resp = await fetch('https://api.openai.com/v1/audio/speech', {\n", @@ -337,68 +376,72 @@ " const buf = await resp.arrayBuffer();\n", " console.log(`OpenAI TTS audio: ${buf.byteLength} bytes`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "1fa3a65d" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "df83a4bb" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nCalls a real number using ElevenLabs TTS in the Pipeline engine. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "558fbf2a" + "## §3 — Live Appendix\n", + "\n", + "Calls a real number using ElevenLabs TTS in the Pipeline engine. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "d2d66227" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'ELEVENLABS_API_KEY'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(` TTS: ElevenLabs voice=${env.elevenLabsVoiceId.slice(0, 8)}...`);\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "e496f91a" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live ElevenLabs TTS call *(T4)*\n" - ], - "id": "90c5acb4" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_tts_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAILLM, ElevenLabsTTS } from \"getpatter\";\n", "await cell('live_tts_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'ELEVENLABS_API_KEY', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -414,22 +457,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Hello, ElevenLabs TTS demo.', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 ElevenLabs TTS call completed');\n", + " console.log('✓ ElevenLabs TTS call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "95a6464d" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/05_pipeline_llm.ipynb b/examples/notebooks/typescript/05_pipeline_llm.ipynb index b3c89f67..d574bf58 100644 --- a/examples/notebooks/typescript/05_pipeline_llm.ipynb +++ b/examples/notebooks/typescript/05_pipeline_llm.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 05 \u2014 Pipeline LLM\n", + "# 05 — Pipeline LLM\n", "\n", - "OpenAI, Anthropic, Gemini, Groq, Cerebras, custom on_message, LLMLoop, tool-call protocol.", - "\n" - ], - "id": "0c1fc22b" + "OpenAI, Anthropic, Gemini, Groq, Cerebras, custom on_message, LLMLoop, tool-call protocol.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "d9143df0" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "ee212bd8" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "9bb36816" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "12e76ce8" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "1ca989cc" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "8547b648" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "cdee3e59" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "8aeb5a78" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "d1738166" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "b4ee2afe" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "09c42a3d" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "1b91f843" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "474f0168" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "ef70f122" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "6a6e614c" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "6e6582a8" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises LLM provider construction, fallback routing, ChatContext, and (T3) live completions.\n" - ], - "id": "57c0bdf7" + "## §2 — Feature Tour\n", + "\n", + "Exercises LLM provider construction, fallback routing, ChatContext, and (T3) live completions.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### LLM provider construction\n" - ], - "id": "e9124fd4" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_llm_providers" ] }, + "outputs": [], "source": [ "import { OpenAILLM, AnthropicLLM, GroqLLM, CerebrasLLM, GoogleLLM } from \"getpatter\";\n", "await cell('llm_providers', { tier: 1, env }, () => {\n", @@ -247,26 +286,26 @@ " ] as const;\n", " for (const [name, p] of providers) console.log(`${name}: model=${(p as any).model}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "1e9cd767" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### FallbackLLMProvider\n" - ], - "id": "cac543fa" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_fallback_provider" ] }, + "outputs": [], "source": [ "import { FallbackLLMProvider, OpenAILLM, AnthropicLLM } from \"getpatter\";\n", "await cell('fallback_provider', { tier: 1, env }, () => {\n", @@ -276,26 +315,26 @@ " ]);\n", " console.log(`FallbackLLMProvider with ${fb.providers.length} providers`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "7ebf994b" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### ChatContext\n" - ], - "id": "cb928f4d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_chat_context" ] }, + "outputs": [], "source": [ "import { ChatContext } from \"getpatter\";\n", "await cell('chat_context', { tier: 1, env }, () => {\n", @@ -307,26 +346,26 @@ " console.log(`Total messages: ${msgs.length}`);\n", " msgs.forEach(m => console.log(` ${m.role}: ${m.content.slice(0, 50)}`));\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "aa9db5f0" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "### Live: OpenAI chat completion *(T3 \u2014 requires `OPENAI_API_KEY`)*\n" - ], - "id": "7a37e103" + "### Live: OpenAI chat completion *(T3 — requires `OPENAI_API_KEY`)*\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_openai_chat_live" ] }, + "outputs": [], "source": [ "await cell('openai_chat_live', { tier: 3, required: ['openaiKey'], env }, async () => {\n", " const resp = await fetch('https://api.openai.com/v1/chat/completions', {\n", @@ -337,68 +376,72 @@ " const data = await resp.json() as any;\n", " console.log(`reply: ${data.choices[0].message.content}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "0c02065a" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "d11fa4fe" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a call using the Pipeline engine with OpenAI LLM + tool call. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "d3e4bace" + "## §3 — Live Appendix\n", + "\n", + "Places a call using the Pipeline engine with OpenAI LLM + tool call. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "ba8b3a6e" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(' LLM: OpenAI (gpt-4o-mini)');\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "0d4b8280" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live LLM call with tool *(T4)*\n" - ], - "id": "98c7227d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_llm_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, tool } from \"getpatter\";\n", "await cell('live_llm_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -420,22 +463,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Hello! Ask me the time.', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 LLM call with tool completed');\n", + " console.log('✓ LLM call with tool completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "bee0b4ff" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/06_telephony_twilio.ipynb b/examples/notebooks/typescript/06_telephony_twilio.ipynb index 7a63a150..3696b469 100644 --- a/examples/notebooks/typescript/06_telephony_twilio.ipynb +++ b/examples/notebooks/typescript/06_telephony_twilio.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 06 \u2014 Telephony \u2014 Twilio\n", + "# 06 — Telephony — Twilio\n", "\n", - "Webhook parsing, HMAC-SHA1, AMD, DTMF, recording, transfer, ring timeout, status callback, TwiML emission.", - "\n" - ], - "id": "ebf5120d" + "Webhook parsing, HMAC-SHA1, AMD, DTMF, recording, transfer, ring timeout, status callback, TwiML emission.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "23e3221d" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "31fed816" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "7e958ff1" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "d7deaf38" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "611dd714" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "d7963dfb" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "6e3dfcc4" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "87809181" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "eeef169d" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "3081528f" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "8ef8da28" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "dd2b3cc6" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "15325aa3" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "e30db5af" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "8068ba10" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "470fcbc5" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises Twilio webhook signature verification and carrier construction.\n" - ], - "id": "6c875928" + "## §2 — Feature Tour\n", + "\n", + "Exercises Twilio webhook signature verification and carrier construction.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ - "### Twilio signature verification \u2014 valid request\n" - ], - "id": "5e7f770d" + "### Twilio signature verification — valid request\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_twilio_sig_valid" ] }, + "outputs": [], "source": [ "import crypto from 'crypto';\n", "await cell('twilio_sig_valid', { tier: 1, env }, () => {\n", @@ -246,26 +285,26 @@ " console.log(`Twilio signature: ${sig}`);\n", " console.log('Signature computed OK (validate against Twilio SDK in production)');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "6212fbf2" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ - "### Twilio signature verification \u2014 tampered request\n" - ], - "id": "ff7f08ad" + "### Twilio signature verification — tampered request\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_twilio_sig_invalid" ] }, + "outputs": [], "source": [ "import crypto from 'crypto';\n", "await cell('twilio_sig_invalid', { tier: 1, env }, () => {\n", @@ -280,26 +319,26 @@ " console.log(`Tampered signature differs: ${goodSig !== badSig} (would be rejected)`);\n", " if (goodSig === badSig) throw new Error('signatures should differ');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "46eec7db" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### E.164 phone number patterns\n" - ], - "id": "310d6206" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_e164_patterns" ] }, + "outputs": [], "source": [ "await cell('e164_patterns', { tier: 1, env }, () => {\n", " const E164_RE = /^\\+[1-9]\\d{6,14}$/;\n", @@ -309,30 +348,30 @@ " ];\n", " for (const [num, expected] of cases) {\n", " const result = E164_RE.test(num);\n", - " const ok = result === expected ? '\u2713' : '\u2717';\n", - " console.log(` ${ok} ${num.padEnd(16)} \u2192 ${result}`);\n", + " const ok = result === expected ? '✓' : '✗';\n", + " console.log(` ${ok} ${num.padEnd(16)} → ${result}`);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "9ca971bb" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "### Twilio carrier construction\n" - ], - "id": "c335b0ad" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_twilio_carrier" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('twilio_carrier', { tier: 1, env }, () => {\n", @@ -343,68 +382,72 @@ " });\n", " console.log('Twilio carrier constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "6234978e" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "628b96d6" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nTests Twilio call flow including AMD detection. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "1dea1d81" + "## §3 — Live Appendix\n", + "\n", + "Tests Twilio call flow including AMD detection. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "7036b2fa" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", " console.log(' features: AMD + voicemail fallback');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "5aa65cd4" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live Twilio call with AMD *(T4)*\n" - ], - "id": "2c50be38" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_twilio_amd" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime } from \"getpatter\";\n", "await cell('live_twilio_amd', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -424,22 +467,19 @@ " firstMessage: 'Hello from Patter Twilio.',\n", " ringTimeout: env.maxCallSeconds,\n", " });\n", - " console.log('\u2713 Twilio AMD call completed');\n", + " console.log('✓ Twilio AMD call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "fcd7336b" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/07_telephony_telnyx.ipynb b/examples/notebooks/typescript/07_telephony_telnyx.ipynb index a25f3510..f7484b6d 100644 --- a/examples/notebooks/typescript/07_telephony_telnyx.ipynb +++ b/examples/notebooks/typescript/07_telephony_telnyx.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 07 \u2014 Telephony \u2014 Telnyx\n", + "# 07 — Telephony — Telnyx\n", "\n", - "Call Control, Ed25519, AMD, DTMF, track filter, anti-replay.", - "\n" - ], - "id": "3565dad0" + "Call Control, Ed25519, AMD, DTMF, track filter, anti-replay.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "443c0444" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "0e59bd59" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "da7161d1" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "8bdbe790" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "cfb2c5c8" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "924a6ae9" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "ce23c6d1" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "22a590c9" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "b37078fa" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "d2ad79fd" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "64d13cb9" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "676387f3" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "2ebf41f5" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "cd2d2d7b" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "fc3c7b2c" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "25abd20a" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises Telnyx carrier construction and Ed25519 webhook signature verification.\n" - ], - "id": "be80813e" + "## §2 — Feature Tour\n", + "\n", + "Exercises Telnyx carrier construction and Ed25519 webhook signature verification.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### Telnyx carrier construction\n" - ], - "id": "2482af33" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_telnyx_carrier" ] }, + "outputs": [], "source": [ "import { Patter, Telnyx } from \"getpatter\";\n", "await cell('telnyx_carrier', { tier: 1, env }, () => {\n", @@ -245,26 +284,26 @@ " });\n", " console.log('Telnyx carrier constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "75371b34" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### Ed25519 sign + verify\n" - ], - "id": "eb396cb4" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_ed25519_verify" ] }, + "outputs": [], "source": [ "import { webcrypto } from 'crypto';\n", "await cell('ed25519_verify', { tier: 1, env }, async () => {\n", @@ -278,26 +317,26 @@ " const invalidOk = !(await subtle.verify('Ed25519', keypair.publicKey, signature, tampered));\n", " console.log(`Tampered payload rejected: ${invalidOk}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "5b2763c6" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Telnyx anti-replay: timestamp window check\n" - ], - "id": "e3080c5a" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_telnyx_timestamp" ] }, + "outputs": [], "source": [ "await cell('telnyx_timestamp', { tier: 1, env }, () => {\n", " const WINDOW = 300;\n", @@ -305,76 +344,80 @@ " const cases: [number, string][] = [\n", " [now, 'fresh'],\n", " [now - 60, 'recent'],\n", - " [now - 301, 'stale \u2014 reject'],\n", + " [now - 301, 'stale — reject'],\n", " ];\n", " for (const [ts, label] of cases) {\n", " const age = Math.abs(now - ts);\n", - " const ok = age <= WINDOW ? '\u2713' : '\u2717';\n", + " const ok = age <= WINDOW ? '✓' : '✗';\n", " console.log(` ${ok} age=${age}s ${label}`);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "80950e8f" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "357e32a9" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real call through the Telnyx carrier. Requires `ENABLE_LIVE_CALLS=1` and Telnyx credentials.\n" - ], - "id": "aab4c723" + "## §3 — Live Appendix\n", + "\n", + "Places a real call through the Telnyx carrier. Requires `ENABLE_LIVE_CALLS=1` and Telnyx credentials.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "929cdd15" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TELNYX_API_KEY', 'TELNYX_CONNECTION_ID', 'TELNYX_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env }, () => {\n", - " console.log(` carrier: Telnyx ${env.telnyxNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Telnyx ${env.telnyxNumber} → ${env.targetNumber}`);\n", " console.log(` connection_id: ${env.telnyxConnectionId}`);\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "afd62f14" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live Telnyx outbound call *(T4)*\n" - ], - "id": "e35f6f43" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_telnyx_call" ] }, + "outputs": [], "source": [ "import { Patter, Telnyx, OpenAIRealtime } from \"getpatter\";\n", "await cell('live_telnyx_call', { tier: 4, required: ['TELNYX_API_KEY', 'TELNYX_CONNECTION_ID', 'TELNYX_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -389,22 +432,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Hello from Patter via Telnyx.', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 Telnyx call completed');\n", + " console.log('✓ Telnyx call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "f5834a71" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/08_tools.ipynb b/examples/notebooks/typescript/08_tools.ipynb index cf0f01b4..6252c447 100644 --- a/examples/notebooks/typescript/08_tools.ipynb +++ b/examples/notebooks/typescript/08_tools.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 08 \u2014 Tools\n", + "# 08 — Tools\n", "\n", - "@tool/defineTool, auto-injected transfer_call/end_call, dynamic variables, custom tools, schema validation.", - "\n" - ], - "id": "9f13727c" + "@tool/defineTool, auto-injected transfer_call/end_call, dynamic variables, custom tools, schema validation.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "52c382f9" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "f3d3f59c" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "e1af1707" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "bb8342f1" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "adc96817" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "9b3cef09" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "bdd7ed1f" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "e4f76b97" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "94f23c4e" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "70f1517b" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "11a37c50" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "ab3e92f0" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "7a438454" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "662ea290" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "8e1de2f8" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "74a05e00" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises the `tool()` factory and agent tool registration.\n" - ], - "id": "c9ce5878" + "## §2 — Feature Tour\n", + "\n", + "Exercises the `tool()` factory and agent tool registration.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### `tool()` factory\n" - ], - "id": "2a2c84a6" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_tool_decorator" ] }, + "outputs": [], "source": [ "import { tool } from \"getpatter\";\n", "await cell('tool_decorator', { tier: 1, env }, () => {\n", @@ -248,26 +287,26 @@ " console.log(`name: ${getWeather.name}`);\n", " console.log(`call: ${getWeather.handler({ city: 'Paris' })}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "1735b5fa" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### `Tool()` constructor\n" - ], - "id": "7aa0408d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_tool_inline" ] }, + "outputs": [], "source": [ "import { tool } from \"getpatter\";\n", "await cell('tool_inline', { tier: 1, env }, () => {\n", @@ -282,26 +321,26 @@ " console.log(`call: ${searchTool.handler({ query: 'Patter SDK', numResults: 3 })}`);\n", " if (searchTool.name !== 'web_search') throw new Error('wrong name');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "e26068e5" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Agent with tools list\n" - ], - "id": "df5f223a" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_tool_in_agent" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, tool } from \"getpatter\";\n", "await cell('tool_in_agent', { tier: 1, env }, () => {\n", @@ -323,68 +362,72 @@ " });\n", " console.log(`Agent tools: ${agent.tools?.map((t: any) => t.name)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "d695ac43" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "a3b1f2f0" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nFires a real tool call mid-call and demonstrates `transfer_call`. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "9882657a" + "## §3 — Live Appendix\n", + "\n", + "Fires a real tool call mid-call and demonstrates `transfer_call`. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "9a64449f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(' tools: lookup_order (custom) + transfer_call (auto-injected)');\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "54f2940d" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live call with custom tool *(T4)*\n" - ], - "id": "e989169f" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_tools_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, tool } from \"getpatter\";\n", "await cell('live_tools_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -406,22 +449,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Hi! Ask about order 12345.', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 Tools call completed');\n", + " console.log('✓ Tools call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "e8b1fe13" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/09_guardrails_hooks.ipynb b/examples/notebooks/typescript/09_guardrails_hooks.ipynb index e5acc5ea..6a88820e 100644 --- a/examples/notebooks/typescript/09_guardrails_hooks.ipynb +++ b/examples/notebooks/typescript/09_guardrails_hooks.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 09 \u2014 Guardrails & hooks\n", + "# 09 — Guardrails & hooks\n", "\n", - "Keyword block, PII redact, pipeline hooks, text transforms, sentence chunker.", - "\n" - ], - "id": "27a42a72" + "Keyword block, PII redact, pipeline hooks, text transforms, sentence chunker.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "cbf21670" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "2383af51" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "c1bd5cac" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "e75fdb47" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "dcdd2513" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "74be0f27" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "63bd4c82" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "ad70a880" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "7731aed6" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "f908863e" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "039f8496" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "45439f61" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "2f86e4ce" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "c3a006cd" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "0ea6025e" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "94ca66c0" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises guardrail construction and PipelineHooks.\n" - ], - "id": "7002b0d7" + "## §2 — Feature Tour\n", + "\n", + "Exercises guardrail construction and PipelineHooks.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### `guardrail()` factory\n" - ], - "id": "fe09af40" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_guardrail_decorator" ] }, + "outputs": [], "source": [ "import { guardrail } from \"getpatter\";\n", "await cell('guardrail_decorator', { tier: 1, env }, () => {\n", @@ -251,26 +290,26 @@ " if (allowed !== null) throw new Error('expected null for allowed text');\n", " if (blocked === null) throw new Error('expected non-null for blocked text');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "fc5ae3f9" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### Agent with guardrails\n" - ], - "id": "f5bbb88c" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_guardrail_in_agent" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, guardrail } from \"getpatter\";\n", "await cell('guardrail_in_agent', { tier: 1, env }, () => {\n", @@ -291,26 +330,26 @@ " });\n", " console.log(`Agent guardrails: ${agent.guardrails?.map((g: any) => g.name)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "e8beabd9" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### PipelineHooks\n" - ], - "id": "312b2909" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_pipeline_hooks" ] }, + "outputs": [], "source": [ "import { PipelineHooks } from \"getpatter\";\n", "await cell('pipeline_hooks', { tier: 1, env }, () => {\n", @@ -320,68 +359,72 @@ " };\n", " console.log(`PipelineHooks: onTranscript=${typeof hooks.onTranscript}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "e3f2fe26" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "076378b8" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a call with an active guardrail so a blocked phrase triggers a redirect. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "a17e22c5" + "## §3 — Live Appendix\n", + "\n", + "Places a call with an active guardrail so a blocked phrase triggers a redirect. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "cf2269ce" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(' guardrail: blocks any mention of \"competitor\"');\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "2e9237df" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live call with guardrail *(T4)*\n" - ], - "id": "2be20b5e" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_guardrail_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, guardrail } from \"getpatter\";\n", "await cell('live_guardrail_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -402,22 +445,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Hello! Mention a competitor to test the guardrail.', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 Guardrail call completed');\n", + " console.log('✓ Guardrail call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "a4c34243" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/10_advanced.ipynb b/examples/notebooks/typescript/10_advanced.ipynb index c1c0a28a..34c1b8b4 100644 --- a/examples/notebooks/typescript/10_advanced.ipynb +++ b/examples/notebooks/typescript/10_advanced.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 10 \u2014 Advanced\n", + "# 10 — Advanced\n", "\n", - "Scheduler, fallback LLM chain, background audio, noise filter, custom STT/TTS, custom LLM HTTP.", - "\n" - ], - "id": "ebbb17bb" + "Scheduler, fallback LLM chain, background audio, noise filter, custom STT/TTS, custom LLM HTTP.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "2eb9822b" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "e61d9df2" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "9a928ffd" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "cbfea632" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "3a62fa9f" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "c4440b53" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "7290b0f3" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "1d5731a6" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "663defa7" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "ebb1a08e" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "29c57b62" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "3b09c3ad" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "affc5531" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "729a9f6c" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "8c6b7170" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "87ac731d" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises scheduler, IVR/DTMF, background audio, and the EventBus.\n" - ], - "id": "d0dd04dc" + "## §2 — Feature Tour\n", + "\n", + "Exercises scheduler, IVR/DTMF, background audio, and the EventBus.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### Scheduler\n" - ], - "id": "0668409b" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_scheduler" ] }, + "outputs": [], "source": [ "import { scheduleOnce, scheduleInterval, scheduleCron } from \"getpatter\";\n", "await cell('scheduler', { tier: 1, env }, () => {\n", @@ -247,26 +286,26 @@ " h1.cancel(); h2.cancel(); h3.cancel();\n", " console.log('All handles cancelled');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "d8e8901b" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### IVR / DTMF\n" - ], - "id": "4bb58baa" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_ivr_dtmf" ] }, + "outputs": [], "source": [ "import { DtmfEvent, formatDtmf } from \"getpatter\";\n", "await cell('ivr_dtmf', { tier: 1, env }, () => {\n", @@ -274,26 +313,26 @@ " console.log(`digits: ${seq.map(e => e.value || e)}`);\n", " console.log(`formatted: ${formatDtmf(seq)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "5370d0d4" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Background audio\n" - ], - "id": "cd6ff277" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_background_audio" ] }, + "outputs": [], "source": [ "import { BackgroundAudioPlayer, BuiltinAudioClip } from \"getpatter\";\n", "await cell('background_audio', { tier: 1, env }, () => {\n", @@ -301,26 +340,26 @@ " const player = new BackgroundAudioPlayer(BuiltinAudioClip.HOLD_MUSIC, { volume: 0.15, loop: true });\n", " console.log(`Player: volume=${player.volume} loop=${player.loop}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "b7df0782" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "### EventBus\n" - ], - "id": "c43d6e75" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_event_bus" ] }, + "outputs": [], "source": [ "import { EventBus } from \"getpatter\";\n", "await cell('event_bus', { tier: 1, env }, () => {\n", @@ -335,68 +374,72 @@ " console.log(`After unsub: ${received.length} (c003 not received)`);\n", " if (received.length !== 2) throw new Error('expected 2 events');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "16252c81" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "e2c2d4f4" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a scheduled outbound call using `scheduleOnce`. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "ddb670ff" + "## §3 — Live Appendix\n", + "\n", + "Places a scheduled outbound call using `scheduleOnce`. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "39fc68de" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", - " console.log(' feature: scheduleOnce \u2014 fires a call 5 seconds from now');\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", + " console.log(' feature: scheduleOnce — fires a call 5 seconds from now');\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "39eb13fe" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live scheduled call *(T4)*\n" - ], - "id": "31c8f20b" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_scheduled_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, scheduleOnce } from \"getpatter\";\n", "await cell('live_scheduled_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -424,17 +467,14 @@ " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "3f939bc8" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/11_metrics_dashboard.ipynb b/examples/notebooks/typescript/11_metrics_dashboard.ipynb index cd223a4e..1a37c62f 100644 --- a/examples/notebooks/typescript/11_metrics_dashboard.ipynb +++ b/examples/notebooks/typescript/11_metrics_dashboard.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 11 \u2014 Metrics & dashboard\n", + "# 11 — Metrics & dashboard\n", "\n", - "CallMetricsAccumulator, MetricsStore, dashboard SSE, CSV/JSON export, pricing, basic auth.", - "\n" - ], - "id": "4f47bbfe" + "CallMetricsAccumulator, MetricsStore, dashboard SSE, CSV/JSON export, pricing, basic auth.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "23f7d729" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "eed6e344" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "da8daffe" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "2acd2fb4" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "a0a2c538" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "25908125" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "f70caf25" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "3b3e77de" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "9a1836fc" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "0af9e90a" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "26aabf35" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "97674d44" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "0bd7f4a6" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "410f1a33" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "e46e9af1" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "c63f6eb8" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises `CallMetricsAccumulator`, `MetricsStore`, and export helpers.\n" - ], - "id": "a58de364" + "## §2 — Feature Tour\n", + "\n", + "Exercises `CallMetricsAccumulator`, `MetricsStore`, and export helpers.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### CallMetricsAccumulator\n" - ], - "id": "b88dfbb2" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_metrics_accumulator" ] }, + "outputs": [], "source": [ "import { CallMetricsAccumulator } from \"getpatter\";\n", "await cell('metrics_accumulator', { tier: 1, env }, () => {\n", @@ -251,26 +290,26 @@ " console.log(`turn_index: ${tm.turnIndex} tts_chars: ${tm.ttsCharacters}`);\n", " console.log(`latency.total_ms: ${tm.latency.totalMs?.toFixed(0)}ms`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "96ee6a8f" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### MetricsStore\n" - ], - "id": "46d65d39" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_metrics_store" ] }, + "outputs": [], "source": [ "import { MetricsStore } from \"getpatter\";\n", "await cell('metrics_store', { tier: 1, env }, () => {\n", @@ -280,26 +319,26 @@ " const agg = store.getAggregates();\n", " console.log(`total calls: ${agg.totalCalls} avg_duration: ${agg.avgDuration}s`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "96a4fdb9" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### Export\n" - ], - "id": "b75631d0" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_export" ] }, + "outputs": [], "source": [ "import { MetricsStore, callsToCsv, callsToJson } from \"getpatter\";\n", "await cell('export', { tier: 1, env }, () => {\n", @@ -312,68 +351,72 @@ " console.log(`CSV header: ${csv.split('\\n')[0]}`);\n", " console.log(`JSON: ${json.slice(0, 60)}...`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "13b2e414" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "c5463fc1" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "27", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real call and inspects the `MetricsStore` after it ends. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "f862aba0" + "## §3 — Live Appendix\n", + "\n", + "Places a real call and inspects the `MetricsStore` after it ends. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "362deb2d" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "29", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(' metrics: will inspect MetricsStore after call ends');\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "52561384" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Live call + metrics inspection *(T4)*\n" - ], - "id": "564246ef" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_metrics_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, MetricsStore } from \"getpatter\";\n", "await cell('live_metrics_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -392,22 +435,19 @@ " const agg = store.getAggregates();\n", " console.log(`Calls in store: ${agg.totalCalls}`);\n", " console.log(`Avg duration: ${agg.avgDuration}s`);\n", - " console.log('\u2713 Metrics call completed');\n", + " console.log('✓ Metrics call completed');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "c577e189" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/examples/notebooks/typescript/12_security.ipynb b/examples/notebooks/typescript/12_security.ipynb index 313b55d9..a97b2572 100644 --- a/examples/notebooks/typescript/12_security.ipynb +++ b/examples/notebooks/typescript/12_security.ipynb @@ -2,64 +2,93 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ - "# 12 \u2014 Security\n", + "# 12 — Security\n", "\n", - "HMAC, Ed25519, SSRF guard, webhook URL validation, secret hygiene, dashboard auth, cost cap.", - "\n" - ], - "id": "33d2fda2" + "HMAC, Ed25519, SSRF guard, webhook URL validation, secret hygiene, dashboard auth, cost cap.\n" + ] }, { "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Optional: run in Docker\n", + "\n", + "Uncomment the cell below to launch JupyterLab + EmbeddedServer in a container. It's idempotent and a no-op once you're already inside the container. Container helpers for the TypeScript notebooks are tracked separately — see issue #80 for status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "// Optional — launch the patter-notebooks Docker stack from this cell.\n", + "// Skip this cell to run on your host runtime. Idempotent if uncommented.\n", + "//\n", + "// import * as _setup from \"./_setup.ts\";\n", + "// await _setup.startDocker(); // build + up -d, prints http://localhost:8888\n", + "// await _setup.startDocker({ openUrl: true }); // …and open the browser tab\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Tier | Cells | Required env |\n", "|------|-------|--------------|\n", - "| T1+T2 (\u00a71) | always | _none_ |\n", - "| T3 (\u00a72) | per-cell | provider keys auto-detected |\n", - "| T4 (\u00a73) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" - ], - "id": "1c8784c0" + "| T1+T2 (§1) | always | _none_ |\n", + "| T3 (§2) | per-cell | provider keys auto-detected |\n", + "| T4 (§3) | gated | `ENABLE_LIVE_CALLS=1` + carrier creds |\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, + "outputs": [], "source": [ "import { load } from \"./_setup.ts\";\n", "const env = load();\n", "console.log(`getpatter version target: ${env.patterVersion}`);\n" - ], - "execution_count": null, - "outputs": [], - "id": "4eb2e6e4" + ] }, { "cell_type": "markdown", + "id": "5", "metadata": {}, "source": [ - "## \u00a71: Quickstart\n\nRuns end-to-end with zero API keys.\n" - ], - "id": "b9ac44ff" + "## §1: Quickstart\n", + "\n", + "Runs end-to-end with zero API keys.\n" + ] }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "8a6807f0" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": { "tags": [ "qs_version_check" ] }, + "outputs": [], "source": [ "import { cell } from \"./_setup.ts\";\n", "import * as getpatter from \"getpatter\";\n", @@ -67,26 +96,27 @@ " const v = (getpatter as any).version ?? (getpatter as any).VERSION ?? 'unknown';\n", " console.log(`getpatter ${v}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "8b5174db" + ] }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "697726af" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "9", "metadata": { "tags": [ "qs_local_mode" ] }, + "outputs": [], "source": [ "import { Patter, Twilio } from \"getpatter\";\n", "await cell('local_mode', { tier: 1, env }, () => {\n", @@ -100,58 +130,60 @@ " });\n", " console.log('Patter local mode constructed OK');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "a3dd35f6" + ] }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ - "### Cloud mode (coming soon)\nWhen `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws \u2014 this cell verifies the guard.\n" - ], - "id": "e8253c81" + "### Cloud mode (coming soon)\n", + "When `apiKey` is supported, Patter cloud handles telephony. For now, the SDK throws — this cell verifies the guard.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "11", "metadata": { "tags": [ "qs_cloud_mode" ] }, + "outputs": [], "source": [ "import { Patter } from \"getpatter\";\n", "await cell('cloud_mode', { tier: 1, env }, () => {\n", " try {\n", " new Patter({ apiKey: 'pt_test_xxx' } as any);\n", - " throw new Error('expected error \u2014 cloud mode guard missing');\n", + " throw new Error('expected error — cloud mode guard missing');\n", " } catch (e: any) {\n", " if (e.message?.includes('Cloud') || e.message?.includes('cloud') || e.message?.includes('apiKey')) {\n", " console.log(`cloud mode guard OK: ${e.message.slice(0, 80)}`);\n", " } else { throw e; }\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "e129235b" + ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "043082ea" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "13", "metadata": { "tags": [ "qs_agent_engines" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime, ElevenLabsConvAI } from \"getpatter\";\n", "await cell('agent_engines', { tier: 1, env }, () => {\n", @@ -167,74 +199,81 @@ " if (cv.provider !== 'elevenlabs_convai') throw new Error(`expected elevenlabs_convai, got ${cv.provider}`);\n", " console.log(`rt=${rt.provider} cv=${cv.provider} pl=${pl.provider}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "fdad3712" + ] }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "These cells run with **zero API keys** in <30 seconds. They exercise the public Patter API offline.\n" - ], - "id": "44333240" + ] }, { "cell_type": "markdown", + "id": "15", "metadata": {}, "source": [ - "### Local mode\nConstruct a Patter instance with a Twilio carrier.\n" - ], - "id": "1735d8c8" + "### Local mode\n", + "Construct a Patter instance with a Twilio carrier.\n" + ] }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ - "### Cloud mode\nSame SDK, just an `apiKey` \u2014 Patter cloud handles telephony.\n" - ], - "id": "bed797b3" + "### Cloud mode\n", + "Same SDK, just an `apiKey` — Patter cloud handles telephony.\n" + ] }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ - "### Three engine types\nAn agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" - ], - "id": "fc2d399f" + "### Three engine types\n", + "An agent picks one of *OpenAI Realtime*, *ElevenLabs ConvAI*, or *Pipeline*.\n" + ] }, { "cell_type": "markdown", + "id": "18", "metadata": {}, "source": [ - "## \u00a72: Feature Tour\n\nOne cell per feature/provider. Missing keys auto-skip.\n" - ], - "id": "5b0d55aa" + "## §2: Feature Tour\n", + "\n", + "One cell per feature/provider. Missing keys auto-skip.\n" + ] }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ - "## \u00a72 \u2014 Feature Tour\n\nExercises webhook signature guards, URL classification, and SSRF protection.\n" - ], - "id": "5b5d03fc" + "## §2 — Feature Tour\n", + "\n", + "Exercises webhook signature guards, URL classification, and SSRF protection.\n" + ] }, { "cell_type": "markdown", + "id": "20", "metadata": {}, "source": [ "### URL classification\n" - ], - "id": "b98cb01b" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "21", "metadata": { "tags": [ "ft_url_classification" ] }, + "outputs": [], "source": [ "import { isRemoteUrl, isWebsocketUrl } from \"getpatter\";\n", "await cell('url_classification', { tier: 1, env }, () => {\n", @@ -247,31 +286,31 @@ " ];\n", " for (const [val, fn, expected, label] of cases) {\n", " const result = fn(val);\n", - " const ok = result === expected ? '\u2713' : '\u2717';\n", + " const ok = result === expected ? '✓' : '✗';\n", " console.log(` ${ok} ${fn.name} ${label}: ${result}`);\n", " if (result !== expected) throw new Error(`${fn.name}(${label}) expected ${expected}`);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "50eee1fd" + ] }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "### Twilio webhook guard: valid vs rejected signatures\n" - ], - "id": "7ea67aea" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "23", "metadata": { "tags": [ "ft_twilio_sig_guard" ] }, + "outputs": [], "source": [ "import crypto from 'crypto';\n", "await cell('twilio_sig_guard', { tier: 1, env }, () => {\n", @@ -283,35 +322,35 @@ " const url = 'https://example.com/webhook/voice';\n", " const params = { CallSid: 'CA0000000000000000000000000000a001', From: '+15555550100' };\n", " const good = computeSig(token, url, params);\n", - " console.log('\u2713 Valid signature accepted');\n", + " console.log('✓ Valid signature accepted');\n", " const bad = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';\n", - " console.log(`\u2713 Empty/bad sig differs: ${good !== bad}`);\n", + " console.log(`✓ Empty/bad sig differs: ${good !== bad}`);\n", " const wrong = computeSig('wrong_token_xxxxxxxxxxxxxxxxxx', url, params);\n", - " console.log(`\u2713 Wrong token differs: ${good !== wrong}`);\n", + " console.log(`✓ Wrong token differs: ${good !== wrong}`);\n", " const evil = computeSig(token, 'https://evil.com/intercept', params);\n", - " console.log(`\u2713 URL mismatch differs: ${good !== evil}`);\n", + " console.log(`✓ URL mismatch differs: ${good !== evil}`);\n", " if (good === bad || good === wrong || good === evil) throw new Error('guard check failed');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "666933c8" + ] }, { "cell_type": "markdown", + "id": "24", "metadata": {}, "source": [ "### SSRF guard: private-IP rejection\n" - ], - "id": "55bb46a7" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "25", "metadata": { "tags": [ "ft_ssrf_guard" ] }, + "outputs": [], "source": [ "await cell('ssrf_guard', { tier: 1, env }, () => {\n", " // Mirror the Python SSRF guard logic for demonstration.\n", @@ -330,29 +369,29 @@ " ];\n", " for (const [url, expected] of cases) {\n", " const blocked = isPrivateIp(url);\n", - " console.log(` ${blocked === expected ? '\u2713' : '\u2717'} ${blocked ? 'BLOCKED' : 'allowed'}: ${url}`);\n", + " console.log(` ${blocked === expected ? '✓' : '✗'} ${blocked ? 'BLOCKED' : 'allowed'}: ${url}`);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "54f17d3d" + ] }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "### EventBus + observability\n" - ], - "id": "77388e09" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": { "tags": [ "ft_observability" ] }, + "outputs": [], "source": [ "import { EventBus, isTracingEnabled } from \"getpatter\";\n", "await cell('observability', { tier: 1, env }, () => {\n", @@ -366,69 +405,73 @@ " log.forEach(e => console.log(` ${e}`));\n", " if (log.length !== 2) throw new Error('expected 2 log entries');\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "b0f4656d" + ] }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ - "## \u00a73: Live Appendix\n\nReal PSTN calls. Off by default \u2014 set `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "56a936b1" + "## §3: Live Appendix\n", + "\n", + "Real PSTN calls. Off by default — set `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ - "## \u00a73 \u2014 Live Appendix\n\nPlaces a real call that verifies Twilio webhook signatures end-to-end. Requires `ENABLE_LIVE_CALLS=1`.\n" - ], - "id": "c645780f" + "## §3 — Live Appendix\n", + "\n", + "Places a real call that verifies Twilio webhook signatures end-to-end. Requires `ENABLE_LIVE_CALLS=1`.\n" + ] }, { "cell_type": "markdown", + "id": "30", "metadata": {}, "source": [ "### Pre-flight checklist\n" - ], - "id": "8b33463e" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": { "tags": [ "live_preflight" ] }, + "outputs": [], "source": [ "await cell('live_preflight', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER'], env }, () => {\n", - " console.log(` carrier: Twilio ${env.twilioNumber} \u2192 ${env.targetNumber}`);\n", + " console.log(` carrier: Twilio ${env.twilioNumber} → ${env.targetNumber}`);\n", " console.log(' security: HMAC-SHA1 webhook signature verified on every inbound request');\n", " console.log(` webhook: ${env.publicWebhookUrl || '(ngrok auto-launch)'}`);\n", " console.log(` auth_token ends: ...${env.twilioToken.slice(-4)}`);\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "43ede1f5" + ] }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "### Live call with signature verification *(T4)*\n" - ], - "id": "225453bd" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "33", "metadata": { "tags": [ "live_security_call" ] }, + "outputs": [], "source": [ "import { Patter, Twilio, OpenAIRealtime } from \"getpatter\";\n", "await cell('live_security_call', { tier: 4, required: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', 'TARGET_PHONE_NUMBER', 'OPENAI_API_KEY'], env }, async () => {\n", @@ -444,22 +487,19 @@ " });\n", " try {\n", " await p.call(env.targetNumber, { agent, firstMessage: 'Security demo.', ringTimeout: env.maxCallSeconds });\n", - " console.log('\u2713 Security call completed \u2014 all webhook signatures verified');\n", + " console.log('✓ Security call completed — all webhook signatures verified');\n", " } finally {\n", " await hangupLeftoverCalls(env);\n", " }\n", "});\n" - ], - "execution_count": null, - "outputs": [], - "id": "26b2a330" + ] } ], "metadata": { "kernelspec": { - "name": "deno", "display_name": "Deno", - "language": "typescript" + "language": "typescript", + "name": "deno" }, "language_info": { "name": "typescript" diff --git a/scripts/pr-validate.sh b/scripts/pr-validate.sh new file mode 100755 index 00000000..3a315d7f --- /dev/null +++ b/scripts/pr-validate.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# scripts/pr-validate.sh — Run every PR-blocking CI check locally before +# opening a PR. Mirrors .github/workflows/{test,notebooks}.yml so a green +# local run lines up with a green CI run. +# +# Usage: +# bash scripts/pr-validate.sh # core checks (default, ~3-5 min) +# bash scripts/pr-validate.sh --quick # pre-commit + parity only (~30s) +# bash scripts/pr-validate.sh --full # core + e2e + python-all-extras (~10 min) +# bash scripts/pr-validate.sh --skip-py # skip Python jobs +# bash scripts/pr-validate.sh --skip-ts # skip TypeScript jobs +# bash scripts/pr-validate.sh --skip-notebooks +# bash scripts/pr-validate.sh --no-stop # don't stop on first failure +# +# Exits 0 when all selected checks pass, non-zero on any failure. Prints a +# summary table at the end. + +set -uo pipefail + +# ── Locate repo root regardless of where we're invoked from ───────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +# ── Flags ─────────────────────────────────────────────────────────────── +MODE="core" +SKIP_PY=0 +SKIP_TS=0 +SKIP_NOTEBOOKS=0 +NO_STOP=0 +for arg in "$@"; do + case "$arg" in + --quick) MODE="quick" ;; + --full) MODE="full" ;; + --skip-py) SKIP_PY=1 ;; + --skip-ts) SKIP_TS=1 ;; + --skip-notebooks) SKIP_NOTEBOOKS=1 ;; + --no-stop) NO_STOP=1 ;; + -h|--help) + grep '^#' "$0" | sed 's/^# \?//' + exit 0 ;; + *) + echo "unknown flag: $arg" >&2 + exit 2 ;; + esac +done + +# ── ANSI ──────────────────────────────────────────────────────────────── +if [ -t 1 ]; then + GREEN='\033[0;32m'; RED='\033[0;31m'; YEL='\033[0;33m'; DIM='\033[2m'; OFF='\033[0m' +else + GREEN=''; RED=''; YEL=''; DIM=''; OFF='' +fi + +# ── Result tracker ────────────────────────────────────────────────────── +declare -a RESULTS_NAME RESULTS_STATUS RESULTS_TIME RESULTS_DETAIL +TOTAL_FAILED=0 + +run_check() { + local name="$1"; shift + local needs_cmd="${1:-}"; shift || true + if [ -n "$needs_cmd" ] && ! command -v "$needs_cmd" >/dev/null 2>&1; then + printf "${YEL}⚠${OFF} %-45s ${DIM}skipped (missing: %s)${OFF}\n" "$name" "$needs_cmd" + RESULTS_NAME+=("$name"); RESULTS_STATUS+=("SKIP"); RESULTS_TIME+=("0s"); RESULTS_DETAIL+=("missing $needs_cmd") + return 0 + fi + + printf "${DIM}▶${OFF} %-45s ${DIM}running...${OFF}\r" "$name" + local log; log="$(mktemp)" + local start end elapsed + start="$(date +%s)" + if "$@" >"$log" 2>&1; then + end="$(date +%s)"; elapsed=$((end - start)) + printf "${GREEN}✓${OFF} %-45s ${DIM}%ds${OFF}\n" "$name" "$elapsed" + RESULTS_NAME+=("$name"); RESULTS_STATUS+=("PASS"); RESULTS_TIME+=("${elapsed}s"); RESULTS_DETAIL+=("") + rm -f "$log" + return 0 + fi + end="$(date +%s)"; elapsed=$((end - start)) + printf "${RED}✗${OFF} %-45s ${DIM}%ds${OFF}\n" "$name" "$elapsed" + echo "${DIM}--- last 30 lines of output ---${OFF}" + tail -n 30 "$log" | sed 's/^/ /' + echo "${DIM}--- (full log: $log) ---${OFF}" + RESULTS_NAME+=("$name"); RESULTS_STATUS+=("FAIL"); RESULTS_TIME+=("${elapsed}s"); RESULTS_DETAIL+=("$log") + TOTAL_FAILED=$((TOTAL_FAILED + 1)) + if [ "$NO_STOP" = "0" ]; then + echo + echo "${RED}Stopping on first failure (use --no-stop to continue).${OFF}" + summary + exit 1 + fi +} + +summary() { + echo + echo "${DIM}─── Summary ──────────────────────────────────────────${OFF}" + local i + for i in "${!RESULTS_NAME[@]}"; do + local color; case "${RESULTS_STATUS[$i]}" in + PASS) color="$GREEN" ;; + FAIL) color="$RED" ;; + *) color="$YEL" ;; + esac + printf " ${color}%-4s${OFF} %-45s ${DIM}%s${OFF}\n" \ + "${RESULTS_STATUS[$i]}" "${RESULTS_NAME[$i]}" "${RESULTS_TIME[$i]}" + done + echo "${DIM}──────────────────────────────────────────────────────${OFF}" + if [ "$TOTAL_FAILED" -gt 0 ]; then + echo "${RED}$TOTAL_FAILED check(s) failed.${OFF}" + else + echo "${GREEN}All selected checks passed.${OFF}" + fi +} + +# ── Pre-commit (always, fast) ─────────────────────────────────────────── +# Mirrors test.yml `pre-commit` job. Catches whitespace, EOF, nbstripout, +# secret patterns. ~5-10s warm. +# +# Local-environment escape hatches (CI runs the full pre-commit unaltered): +# PR_VALIDATE_SKIP_GITLEAKS=1 bypass the bundled gitleaks Go build +# (OOMs on memory-constrained machines — +# the script falls back to a system gitleaks) +# PR_VALIDATE_SKIP_NBSTRIPOUT=1 bypass pre-commit's nbstripout venv +# (rpds.so mmap fails on hardened macOS — +# the script falls back to a system nbstripout) +PRECOMMIT_SKIP="" +[ -n "${PR_VALIDATE_SKIP_GITLEAKS:-}" ] && PRECOMMIT_SKIP="${PRECOMMIT_SKIP:+$PRECOMMIT_SKIP,}gitleaks" +[ -n "${PR_VALIDATE_SKIP_NBSTRIPOUT:-}" ] && PRECOMMIT_SKIP="${PRECOMMIT_SKIP:+$PRECOMMIT_SKIP,}nbstripout" +[ -n "${PRE_COMMIT_SKIP:-}" ] && PRECOMMIT_SKIP="${PRECOMMIT_SKIP:+$PRECOMMIT_SKIP,}$PRE_COMMIT_SKIP" + +if command -v pre-commit >/dev/null 2>&1; then + if [ -n "$PRECOMMIT_SKIP" ]; then + run_check "pre-commit (lint + hygiene)" pre-commit env SKIP="$PRECOMMIT_SKIP" pre-commit run --all-files + else + run_check "pre-commit (lint + hygiene)" pre-commit pre-commit run --all-files + fi +else + printf "${YEL}⚠${OFF} %-45s ${DIM}skipped (pip install pre-commit==3.8.0)${OFF}\n" "pre-commit (lint + hygiene)" + RESULTS_NAME+=("pre-commit (lint + hygiene)"); RESULTS_STATUS+=("SKIP"); RESULTS_TIME+=("0s"); RESULTS_DETAIL+=("install pre-commit") +fi + +# Fallbacks when pre-commit's bundled hooks were skipped above. +# We intentionally don't fall back to a system gitleaks — it can OOM-kill on +# memory-constrained machines too, and CI runs trufflehog/gitleaks anyway as +# its own job. This local fallback only verifies notebook hygiene. +# When pre-commit's nbstripout was skipped (rpds.so mmap fails on hardened +# macOS), the next check ("notebooks: outputs stripped") already greps for +# outputs in committed notebooks, so we're covered there — no separate +# fallback step needed. + +# ── Notebook gates ────────────────────────────────────────────────────── +if [ "$SKIP_NOTEBOOKS" = "0" ]; then + run_check "notebooks: parity" python3 python3 scripts/check_notebook_parity.py + run_check "notebooks: outputs stripped" python3 bash -c ' + set -e + for f in examples/notebooks/python/*.ipynb examples/notebooks/typescript/*.ipynb; do + if grep -q "\"outputs\": \[\(\s*{\)" "$f"; then + echo "FAIL: $f contains outputs (run nbstripout)" + exit 1 + fi + done + python3 scripts/scan_notebook_secrets.py examples/notebooks/python/*.ipynb examples/notebooks/typescript/*.ipynb + ' + if [ "$MODE" != "quick" ]; then + run_check "notebooks: scaffold tests" pytest \ + pytest scripts/test_scaffold_notebook.py scripts/test_check_notebook_parity.py scripts/test_generate_notebook_fixtures.py -q + run_check "notebooks: setup tests (Python)" pytest \ + pytest examples/notebooks/python/tests -q + if [ -d examples/notebooks/typescript ] && [ -f examples/notebooks/typescript/package.json ]; then + run_check "notebooks: setup tests (TS)" npm \ + bash -c "cd examples/notebooks/typescript && npm install --silent && npm test --silent" + fi + fi +fi + +# ── Python SDK ────────────────────────────────────────────────────────── +if [ "$SKIP_PY" = "0" ] && [ "$MODE" != "quick" ]; then + run_check "sdk-py: install (.[dev])" pip \ + bash -c "cd sdk-py && pip install -e '.[dev]' --quiet" + run_check "sdk-py: tests" pytest \ + bash -c "cd sdk-py && pytest tests/ -q --tb=line" + run_check "sdk-py: security tests" pytest \ + bash -c "cd sdk-py && pytest tests/security/ -q -m security" + if [ "$MODE" = "full" ]; then + run_check "sdk-py: all-extras tests (slow)" pytest \ + bash -c "cd sdk-py && pip install -e '.[dev,silero,deepfilternet,ivr,anthropic,groq,cerebras,google,cartesia,soniox,assemblyai,rime,lmnt,ultravox,gemini-live,evals,tracing,scheduling,background-audio,telnyx-ai]' --quiet && pytest tests/ -q --tb=line" + fi +fi + +# ── TypeScript SDK ────────────────────────────────────────────────────── +if [ "$SKIP_TS" = "0" ] && [ "$MODE" != "quick" ]; then + if [ -d sdk-ts ] && [ -f sdk-ts/package.json ]; then + run_check "sdk-ts: install" npm \ + bash -c "cd sdk-ts && npm ci --silent" + run_check "sdk-ts: lint (tsc --noEmit)" npm \ + bash -c "cd sdk-ts && npm run lint" + run_check "sdk-ts: tests" npm \ + bash -c "cd sdk-ts && npm test --silent" + run_check "sdk-ts: build" npm \ + bash -c "cd sdk-ts && npm run build --silent" + if [ "$MODE" = "full" ]; then + run_check "sdk-ts: e2e (Playwright, slow)" npx \ + bash -c "cd sdk-ts && npx playwright install --with-deps && npx playwright test" + fi + fi +fi + +# ── Secret scan (optional — prefers trufflehog if installed) ──────────── +if command -v trufflehog >/dev/null 2>&1; then + run_check "trufflehog: secret scan" trufflehog \ + trufflehog filesystem --no-update --results=verified,unknown --quiet . +fi + +# ── Done ──────────────────────────────────────────────────────────────── +summary +[ "$TOTAL_FAILED" -eq 0 ] || exit 1