diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec9e58243..d0d6d56d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,6 +173,38 @@ jobs: tests/tts/test_tts_openai.py tests/server/test_webrtc_routes.py + langchain-versions: + name: LangChain ${{ matrix.line }} + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - line: v0 + extra: langchain-v0 + - line: v1 + extra: langchain + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.1" + enable-cache: true + python-version: "3.12" + - name: Create isolated LangChain environment + run: uv venv --python 3.12 .venv-langchain + - name: Install documented LangChain line + run: >- + uv pip install --python .venv-langchain/bin/python + -e ".[quickstart,${{ matrix.extra }}]" + - name: Smoke examples and bridge runtime + run: >- + .venv-langchain/bin/python + scripts/smoke_langchain_versions.py --line ${{ matrix.line }} + coverage: name: Coverage + guard suite (diff-cover report-only) runs-on: ubuntu-latest diff --git a/.github/workflows/nightly-validation.yml b/.github/workflows/nightly-validation.yml index 3fa69bd47..f284e58de 100644 --- a/.github/workflows/nightly-validation.yml +++ b/.github/workflows/nightly-validation.yml @@ -158,6 +158,8 @@ jobs: # - pydantic-ai and pydantic-ai-v2 are mutually exclusive pins; they # never co-install because each cell syncs exactly one extra into a fresh # environment (uv sync is exact and removes anything not requested). + # - langchain and langchain-v0 are separate 1.x / 0.3.x lines. Each cell + # installs one complete line and runs the same exact bridge contract. # - ten-vad is deliberately excluded: its non-permissive license is an # end-user opt-in, and installing it here would have project CI accept # those terms nightly. See EXCLUDED_EXTRAS in scripts/extras_matrix.py. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69b800fd7..b042ca3ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ raw equivalent in the table below. Install it with `uv tool install rust-just`, | Lint auto-fix | `just lint-fix` | `uv run ruff check --fix .` | | Format | `just fmt` | `uv run ruff format .` | | Format check | `just fmt-check` | `uv run ruff format --check .` | -| Type gate (mypy, whole package) | `just typecheck` | `uv run mypy src/easycat` | +| Type gate (mypy + LangChain smoke script) | `just typecheck` | `uv run mypy src/easycat scripts/smoke_langchain_versions.py` | | Fast types (ty, advisory) | `just typecheck-fast` | `uvx ty check src/easycat` | | Coverage | `just cov` | `uv run pytest -n auto --dist load --cov --cov-report=term-missing -m "not integration_socket and not integration_live and not integration_external and not contract and not latency and not slow and not stress and not serial and not flaky and not guard"` | | Validate (quick) | `just validate-quick` | `uv run easycat validate quick` | @@ -63,7 +63,7 @@ raw equivalent in the table below. Install it with `uv tool install rust-just`, | Validate (release) | `just validate-release` | `uv run easycat validate release` | | Validate report | `just validate-report .easycat/validation/latest.json` | `uv run easycat validate report .easycat/validation/latest.json` | | Pre-commit hooks | `just pre-commit` | `uv run pre-commit run --all-files` | -| Pre-PR gauntlet | `just check` | `uv run pre-commit run --all-files && uv run mypy src/easycat && uv run pytest -n auto --dist loadscope -m "not integration_live and not integration_external and not serial" && uv run pytest -o faulthandler_timeout=0 -o timeout=0 -m "serial and not integration_live and not integration_external"` | +| Pre-PR gauntlet | `just check` | `uv run pre-commit run --all-files && uv run mypy src/easycat scripts/smoke_langchain_versions.py && uv run pytest -n auto --dist loadscope -m "not integration_live and not integration_external and not serial" && uv run pytest -o faulthandler_timeout=0 -o timeout=0 -m "serial and not integration_live and not integration_external"` | `just check` mirrors CI's core source-quality gates, but it is not a literal replay of the workflow: CI also covers the supported Python matrix, minimum diff --git a/docs/architecture.md b/docs/architecture.md index bbf7accb6..769378128 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -350,9 +350,10 @@ duration-drift, AEC reference, VAD/pre-roll, and transport accounting tests. can faithfully consume the committed lockfile. The `uv_build` requirement is an independent bounded build-backend range. - A dependency extra never spans behaviorally incompatible SDK majors. - PydanticAI v2 is the primary supported line; any v1 compatibility extra is - explicitly named, tested separately, and given a documented retirement - path. + PydanticAI and LangChain major lines are explicitly named and tested in + isolated environments. PydanticAI v2 is the primary supported line, and its + v1 compatibility extra has a documented retirement path; LangChain 0.3 and + 1.x remain separate supported lines. - Extras describe dependencies, not whether a capability exists. Provider discovery comes from the catalog, so empty marker extras are not a capability registry. diff --git a/docs/browser-playground.md b/docs/browser-playground.md index 871eb67a3..8e87335eb 100644 --- a/docs/browser-playground.md +++ b/docs/browser-playground.md @@ -33,8 +33,15 @@ Useful options: defaults. - `--token` (or `EASYCAT_SERVE_TOKEN`) — shared secret required by the signaling endpoints. `easycat serve` refuses a non-loopback `--host` - without a token. The printed Open URL embeds it as `?token=...` and the - bundled client forwards it as an `Authorization: Bearer` header. + without a token. For loopback, the printed Open URL embeds it in a + `#token=...` fragment, which is not sent in the HTTP request; the bundled + client removes it from the visible URL and forwards it as an + `Authorization: Bearer` header. +- `--public-url` (or `EASYCAT_SERVE_PUBLIC_URL`) — external HTTPS origin used + for the printed browser link when a non-loopback server sits behind a + TLS-terminating proxy. Without this option, EasyCat does not print a + token-bearing direct-HTTP URL; bearer credentials must not cross an + unencrypted network. - `--agent-model` / `--instructions` — swap the playground agent's OpenAI Responses API model or its guidance. - `--manifest` / `--profile` — build a manifest-backed `VoiceServer` instead @@ -93,8 +100,9 @@ Outbound (server → client): Audio flows over the Opus peer connection. Signaling is HTTP (`POST /offer`, `GET /config`, `POST /stats`, `GET /health`); when `WebRTCTransportConfig.auth_token` is set, `/config`, `/offer`, and `/stats` -require the token as `Authorization: Bearer ` or a `?token=` query -parameter. +require the token as `Authorization: Bearer `. A `?token=` query +parameter is accepted only when `allow_query_token=True`; it is disabled by +default because URLs leak more readily through logs and browser history. Session event messages arrive on a client-created data channel named `events`. diff --git a/docs/deployment/production-servers.md b/docs/deployment/production-servers.md index df27c0e87..0b7c7990e 100644 --- a/docs/deployment/production-servers.md +++ b/docs/deployment/production-servers.md @@ -210,9 +210,19 @@ ICE server configuration for the bundled browser client, `POST /stats` accepts sanitized WebRTC stats snapshots, and `/health` reports `status`, `active_sessions`, and `max_sessions` for readiness checks. For public deployments, put the signaling server behind HTTPS so -`getUserMedia()` works, configure TURN, set `SIGNALING_AUTH_TOKEN` so `/offer` -and `/stats` require a bearer/query token, and tune `WEBRTC_MAX_SESSIONS` from -load-test data before raising the default cap. +`getUserMedia()` works, configure TURN, set `WEBRTC_SIGNALING_TOKEN` so +`/config`, `/offer`, and `/stats` require a bearer token, and tune +`WEBRTC_MAX_SESSIONS` from load-test data before raising the default cap. The +bundled client can read that token from its initial `#token=` fragment, removes +it from the visible URL, and forwards it in the `Authorization` header. URL +fragments are not included in HTTP requests, while direct `?token=` query +authentication remains off unless `allow_query_token=True` is set explicitly. + +The server can use configured TURN credentials without returning TURN entries +from `/config`: the browser receives STUN-only config while the server peer can +still gather a relay candidate. Browser-side relay requires +`WEBRTC_EXPOSE_ICE_CREDENTIALS=1`; expose only short-lived TURN credentials (or +use this with a trusted demo), because every authorized client can read them. ### Flat routes vs. the `VoiceServer` `/webrtc/*` namespace @@ -232,9 +242,11 @@ implementation (`easycat.server.webrtc_routes.WebRTCRoutes`): The SAME bundled client HTML serves both. It resolves its route base from a `?webrtc=` query parameter (defaulting to `""` for the flat helper); -`VoiceServer`'s root redirect appends `?webrtc=/webrtc` (preserving any -`?token=`) so the served client targets the namespaced routes. A custom client -can target either surface by setting `?webrtc=` (or its own base) accordingly. +`VoiceServer`'s root redirect appends `?webrtc=/webrtc` so the served client +targets the namespaced routes. It drops any legacy `?token=` rather than copy a +secret into another HTTP request; bootstrap the bundled client with the +`#token=` fragment instead. A custom client can target either surface by +setting `?webrtc=` (or its own base) accordingly. ## WebTransport servers diff --git a/docs/install.md b/docs/install.md index 6ff98bd09..38545434e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -99,7 +99,8 @@ Commands below are for this repository. In an application, use the equivalent | OpenAI Agents SDK | `uv sync --extra openai-agents --group dev` | | PydanticAI stable v1 | `uv sync --extra pydantic-ai --group dev` | | PydanticAI stable v2 | `uv sync --extra pydantic-ai-v2 --group dev` | -| LangChain core | `uv sync --extra langchain --group dev` | +| LangChain 1.x | `uv sync --extra langchain --group dev` | +| LangChain 0.3 | `uv sync --extra langchain-v0 --group dev` | | LangGraph | `uv sync --extra langgraph --group dev` | | LlamaAgents/LlamaIndex workflows | `uv sync --extra llama-agents --group dev` | | OpenAI providers | `uv sync --extra openai --group dev` | @@ -121,12 +122,19 @@ uv pip install krisp_audio ``` Deepgram, ElevenLabs, and Cartesia use EasyCat's core WebSocket/HTTP stack, so -their extras are install markers and do not add vendor SDKs. LangChain model -packages such as `langchain-openai` remain application choices. +their extras are install markers and do not add vendor SDKs. The `langchain` +and `langchain-v0` extras each install a complete, compatible LangChain and +`langchain-openai` line for the maintained examples. Other LangChain model +integrations remain application choices. The `pydantic-ai` extra targets stable v1. The `pydantic-ai-v2` extra installs `pydantic-ai>=2.24.0,<3.0.0`; the two extras are mutually exclusive. +The `langchain` extra targets 1.x and `langchain-v0` targets 0.3.x. Both lines +run the same EasyCat bridge contract in isolated CI environments. They are +mutually exclusive, and `langchain-v0` also cannot be combined with `langgraph` +or `all` because those selections require `langchain-core` 1.x. + ## Broad evaluation install For a downstream application evaluating most integrations: @@ -146,8 +154,9 @@ uv sync --extra all --extra pydantic-ai-v2 --group dev ``` The `all` extra deliberately omits `ten-vad` because of its non-permissive -license and omits the mutually exclusive `pydantic-ai` and `pydantic-ai-v2` -extras. +license, omits the mutually exclusive `pydantic-ai` and `pydantic-ai-v2` +extras, and omits `langchain-v0` because the broad install includes LangChain +1.x and LangGraph. Install `langchain-v0` separately when targeting 0.3.x. ## Credentials without accidental overwrites diff --git a/docs/teaching/13-swap-providers-and-transports/README.md b/docs/teaching/13-swap-providers-and-transports/README.md index e154325e1..87bef0173 100644 --- a/docs/teaching/13-swap-providers-and-transports/README.md +++ b/docs/teaching/13-swap-providers-and-transports/README.md @@ -240,10 +240,10 @@ Provider choice and transport choice optimise **different axes**: | Axis you care about | Choose this | |-----------------------------|-------------| | First-audio latency | Provider mix — compare `easycat latency` on repeated, matched turns | -| Jitter + packet loss | Transport — WebRTC preserves UDP end-to-end | -| Codec quality | Transport — Local / WebRTC (24 kHz) vs Twilio (μ-law 8 kHz) | +| Jitter + packet loss | Transport — inspect WebRTC's selected ICE path and client stats; TURN can relay media over UDP or TCP | +| Codec quality | Transport — Local uses 24 kHz PCM; WebRTC uses 48 kHz media frames with Opus around a 16 kHz pipeline; Twilio uses μ-law at 8 kHz on the wire | | Cost per turn | Provider mix — usually the dominant cost driver | -| Offline / on-device | Provider mix — (future: Cartesia / local models) | +| Offline / on-device | Provider mix — use a custom local/self-hosted provider; the bundled STT/TTS providers are hosted | | Reach a regular phone | Transport — Twilio only | Measure the production bundles with `easycat latency`; choose with @@ -290,7 +290,7 @@ numbers from your own environment: | Phone IVR | ⭐ | ⭐ | ⭐⭐⭐ | ⭐ | `openai` on Twilio | | Retail kiosk (noisy) | ⭐⭐ | ⭐⭐⭐ | ⭐ | ⭐ | `deepgram-eleven` on Local | | Multilingual hotline | ⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | `deepgram-eleven` on Twilio | -| Offline embedded device | ⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐ | (future: local models) | +| Offline embedded device | ⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐ | Custom local providers on Local | Cost is not a measured axis in this chapter — it's an annotation from provider pricing pages. Chapter 12 deliberately stops short diff --git a/docs/teaching/15-operate-in-production/README.md b/docs/teaching/15-operate-in-production/README.md index 5154faaed..56b063003 100644 --- a/docs/teaching/15-operate-in-production/README.md +++ b/docs/teaching/15-operate-in-production/README.md @@ -677,7 +677,9 @@ Run easycat explain json-schema for CLI JSON. `Open http://localhost:8080` URL, with live transcript, interruption indicator, and per-turn latency readout in the page. Needs an OpenAI key; a non-loopback `--host` requires `--token` (or - `EASYCAT_SERVE_TOKEN`). + `EASYCAT_SERVE_TOKEN`). Put a remote browser serve behind TLS and pass its + HTTPS origin with `--public-url` (or `EASYCAT_SERVE_PUBLIC_URL`); EasyCat + will not print a token-bearing direct-HTTP link. - **`uv run easycat plan`** — resolves an `easycat.toml` profile into its provider/capability plan across all seven pipeline roles (`src/easycat/cli/plan.py`), reporting the selected provider per role plus diff --git a/docs/using-easycat/05-agent-bridges/README.md b/docs/using-easycat/05-agent-bridges/README.md index 90e83725e..59c4c5797 100644 --- a/docs/using-easycat/05-agent-bridges/README.md +++ b/docs/using-easycat/05-agent-bridges/README.md @@ -90,7 +90,7 @@ already own: |---|---|---|---| | OpenAI Agents SDK `Agent` | `OpenAIAgentsBridge` | Tools, handoffs, SDK context, previous-response chaining | `--extra quickstart` or `--extra openai-agents` | | PydanticAI `Agent` | `PydanticAIBridge` | Typed dependencies, tools, and structured results | `--extra pydantic-ai` or `--extra pydantic-ai-v2` | -| LangChain `Runnable` | `LangChainBridge` | LCEL chains, runnable events, and message history | `--extra langchain`; install the model package separately | +| LangChain `Runnable` | `LangChainBridge` | LCEL chains, runnable events, and message history | `--extra langchain` for 1.x or `--extra langchain-v0` for 0.3.x | | Compiled LangGraph graph | `LangGraphBridge` | Nodes, checkpoints, resumable state, and native state edits | `--extra langgraph`; install the model package separately | | LlamaIndex Workflow | `LlamaAgentsBridge` | Local/remote workflows and human-in-the-loop resumption | `--extra llama-agents` | | Remote Responses API base URL | `RemoteResponsesAPIBridge` | An agent running behind an HTTP/SSE service boundary | Core; provide `agent_model` and remote auth as needed | @@ -99,7 +99,8 @@ already own: `quickstart` already includes OpenAI Agents SDK. Do not redundantly add its bundled extra. PydanticAI v1 and v2 are separate, conflicting install choices; -select the one your application uses. +select the one your application uses. LangChain 0.3 and 1.x are likewise +separate, conflicting install choices, and both run the same bridge contract. ## Auto-detection is the default path diff --git a/examples/README.md b/examples/README.md index 6169f1011..d446da870 100644 --- a/examples/README.md +++ b/examples/README.md @@ -45,10 +45,9 @@ emit the run or saved report inside the standard CLI envelope. `quickstart` includes local audio, OpenAI providers, the OpenAI Agents SDK, NumPy, ONNX Runtime, and LiveKit AEC3 echo cancellation. RNNoise is opt-in through the `rnnoise` extra. It does not install every framework/provider -variant. Install cells are repo-local commands that start with EasyCat extras, -such as `uv sync --extra quickstart --group dev`; anything after a semicolon is -an additional third-party package to install in the same environment with -`uv pip install`. +variant. Install cells start with a complete repo-local `uv sync` command. +After a semicolon, `--extra ` snippets extend that sync command; packages +such as `krisp_audio` are installed in the same environment with `uv pip install`. ## Choose An Example @@ -66,11 +65,11 @@ an additional third-party package to install in the same environment with | Example | Use When | Run | Install | Env | | --- | --- | --- | --- | --- | | [voice_app.py](voice_app.py) | App-first VoiceApp with one-word mode switching (browser/local/websocket/twilio). | `uv run python examples/voice_app.py` | `uv sync --extra quickstart --extra webrtc --group dev` | `OPENAI_API_KEY` | -| [voice_app_twilio.py](voice_app_twilio.py) | App-first Twilio phone bot via `VoiceApp.run("twilio")` and the reusable `easycat.telephony.server` helper. | `uv run python examples/voice_app_twilio.py` | `uv sync --extra openai --extra telephony --extra openai-agents --group dev` | `OPENAI_API_KEY`, `TWILIO_STREAM_URL`, `TWILIO_AUTH_TOKEN`; optional `TWILIO_STREAM_TOKEN_SECRET` | +| [voice_app_twilio.py](voice_app_twilio.py) | App-first Twilio phone bot via `VoiceApp.run("twilio")` and the reusable `easycat.telephony.server` helper. | `uv run python examples/voice_app_twilio.py` | `uv sync --extra openai --extra telephony --extra openai-agents --group dev` | `OPENAI_API_KEY`, `TWILIO_STREAM_URL`, `TWILIO_AUTH_TOKEN`; optional `TWILIO_STREAM_TOKEN_SECRET`, `TRUST_PROXY_HEADERS`, `TWILIO_PUBLIC_TWIML_URL` | | [openai_agents_voice.py](openai_agents_voice.py) | First local mic/speaker bot with OpenAI Agents SDK. | `uv run python examples/openai_agents_voice.py` | `uv sync --extra quickstart --group dev` | `OPENAI_API_KEY` | | [pydantic_ai_voice.py](pydantic_ai_voice.py) | Single-agent PydanticAI voice bot. | `uv run python examples/pydantic_ai_voice.py` | `uv sync --extra quickstart --extra pydantic-ai --group dev` | `OPENAI_API_KEY` | | [pydantic_ai_workflow_voice.py](pydantic_ai_workflow_voice.py) | Workflow-level PydanticAI hand-off across turns. | `uv run python examples/pydantic_ai_workflow_voice.py` | `uv sync --extra quickstart --extra pydantic-ai --group dev` | `OPENAI_API_KEY` | -| [langchain_voice.py](langchain_voice.py) | LangChain LCEL runnable bridged into voice. | `uv run python examples/langchain_voice.py` | `uv sync --extra quickstart --extra langchain --group dev`; `langchain-openai` | `OPENAI_API_KEY` | +| [langchain_voice.py](langchain_voice.py) | LangChain 1.x LCEL runnable bridged into voice. | `uv run python examples/langchain_voice.py` | `uv sync --extra quickstart --extra langchain --group dev` | `OPENAI_API_KEY` | | [langgraph_voice.py](langgraph_voice.py) | LangGraph state graph bridged into voice. | `uv run python examples/langgraph_voice.py` | `uv sync --extra quickstart --extra langgraph --group dev`; `langchain-openai` | `OPENAI_API_KEY` | ## Agent Tools And Session Actions @@ -79,11 +78,11 @@ an additional third-party package to install in the same environment with | --- | --- | --- | --- | --- | | [function_tools_openai.py](function_tools_openai.py) | OpenAI Agents SDK function tools. | `uv run python examples/function_tools_openai.py` | `uv sync --extra quickstart --group dev` | `OPENAI_API_KEY` | | [function_tools_pydantic.py](function_tools_pydantic.py) | PydanticAI function tools. | `uv run python examples/function_tools_pydantic.py` | `uv sync --extra quickstart --extra pydantic-ai --group dev` | `OPENAI_API_KEY` | -| [function_tools_langchain.py](function_tools_langchain.py) | LangChain `AgentExecutor` tools. | `uv run python examples/function_tools_langchain.py` | `uv sync --extra quickstart --extra langchain --group dev`; `langchain<1`, `langchain-openai` | `OPENAI_API_KEY` | +| [function_tools_langchain.py](function_tools_langchain.py) | LangChain 0.3 `AgentExecutor` tools. | `uv run python examples/function_tools_langchain.py` | `uv sync --extra quickstart --extra langchain-v0 --group dev` | `OPENAI_API_KEY` | | [function_tools_langgraph.py](function_tools_langgraph.py) | LangGraph ReAct tools. | `uv run python examples/function_tools_langgraph.py` | `uv sync --extra quickstart --extra langgraph --group dev`; `langchain-openai` | `OPENAI_API_KEY` | | [session_actions_openai.py](session_actions_openai.py) | OpenAI tool that queues EasyCat session actions. | `uv run python examples/session_actions_openai.py` | `uv sync --extra quickstart --group dev` | `OPENAI_API_KEY` | | [session_actions_pydantic.py](session_actions_pydantic.py) | PydanticAI tool that queues session actions through deps. | `uv run python examples/session_actions_pydantic.py` | `uv sync --extra quickstart --extra pydantic-ai --group dev` | `OPENAI_API_KEY`, optional `PYDANTIC_AI_MODEL` | -| [session_actions_langchain.py](session_actions_langchain.py) | LangChain tool that can end the current session. | `uv run python examples/session_actions_langchain.py` | `uv sync --extra quickstart --extra langchain --group dev`; `langchain<1`, `langchain-openai` | `OPENAI_API_KEY` | +| [session_actions_langchain.py](session_actions_langchain.py) | LangChain 0.3 tool that can end the current session. | `uv run python examples/session_actions_langchain.py` | `uv sync --extra quickstart --extra langchain-v0 --group dev` | `OPENAI_API_KEY` | | [session_actions_langgraph.py](session_actions_langgraph.py) | LangGraph tool that can end the current session. | `uv run python examples/session_actions_langgraph.py` | `uv sync --extra quickstart --extra langgraph --group dev`; `langchain-openai` | `OPENAI_API_KEY` | | [agent_event_subscription.py](agent_event_subscription.py) | Subscribe to agent deltas and tool-call events from the session. | `uv run python examples/agent_event_subscription.py` | `uv sync --extra quickstart --group dev` | `OPENAI_API_KEY` | @@ -115,8 +114,9 @@ Support files: `ws_browser_example.py`. - [ws_supervisor_client.html](ws_supervisor_client.html): browser caller and supervisor client for `ws_supervisor_server.py`. -- [webrtc_static/webrtc_client.html](webrtc_static/webrtc_client.html): browser - client served by `webrtc_server.py`. +- [webrtc_static/webrtc_client.html](webrtc_static/webrtc_client.html): compact + browser client served by `webrtc_observability_server.py`; `webrtc_server.py` + uses the richer client bundled with the EasyCat package. - [webrtc_static/webrtc_observability.html](webrtc_static/webrtc_observability.html): combined WebRTC/debugger page served by `webrtc_observability_server.py`. - [webtransport_browser_client.html](webtransport_browser_client.html): browser diff --git a/examples/custom_transport.py b/examples/custom_transport.py index 26cf2f18e..b6d55fae4 100644 --- a/examples/custom_transport.py +++ b/examples/custom_transport.py @@ -25,28 +25,32 @@ from __future__ import annotations from collections.abc import AsyncIterator +from typing import Any from easycat import AudioChunk, AudioFormat, EasyConfig, require_env, run -from easycat.providers import Transport from easycat.transports import LocalTransport class CountingTransport: - """Wraps any ``Transport`` and counts the audio bytes flowing each way. + """Wraps a ``LocalTransport`` and counts the audio bytes flowing each way. Implements the ``Transport`` Protocol structurally — no base class. - The ``audio_format`` / ``clear_audio`` / ``default_echo_cancellation_enabled`` - members are optional transport capabilities, delegated so the wrapper - behaves exactly like the local transport it instruments. + Explicit methods instrument the core audio path; ``__getattr__`` delegates + every other optional capability so the wrapper preserves local playback, + delivery-reporting, event-bus, and AEC behavior. """ default_echo_cancellation_enabled = True - def __init__(self, inner: Transport) -> None: + def __init__(self, inner: LocalTransport) -> None: self._inner = inner self.bytes_in = 0 self.bytes_out = 0 + def __getattr__(self, name: str) -> Any: + """Delegate optional transport capabilities to the wrapped transport.""" + return getattr(self._inner, name) + async def connect(self) -> None: await self._inner.connect() diff --git a/examples/ec2_webrtc/deploy.sh b/examples/ec2_webrtc/deploy.sh index afa784443..3a29028ff 100755 --- a/examples/ec2_webrtc/deploy.sh +++ b/examples/ec2_webrtc/deploy.sh @@ -49,7 +49,10 @@ if [ -z "$EXTERNAL_IP" ]; then fi TURN_PASSWORD="${TURN_PASSWORD:-$(openssl rand -base64 24)}" -WEBRTC_SIGNALING_TOKEN="${WEBRTC_SIGNALING_TOKEN:-$(openssl rand -base64 32)}" +# Keep the generated browser bootstrap token URL-safe. URLSearchParams treats +# an unescaped "+" in an encoded value as a space, which corrupts standard Base64 +# tokens before the client forwards them in the Authorization header. +WEBRTC_SIGNALING_TOKEN="${WEBRTC_SIGNALING_TOKEN:-$(openssl rand -hex 32)}" OPENAI_API_KEY="${OPENAI_API_KEY:?Set OPENAI_API_KEY before running this script}" INSTALL_DIR="/opt/easycat" @@ -184,13 +187,15 @@ echo "" echo "=== Deployment complete ===" echo "" echo " Backend HTTP URL: http://$EXTERNAL_IP:8080/webrtc_client.html" -echo " Browser URL: https:///webrtc_client.html?token=" +echo " Browser URL: https:///webrtc_client.html#token=" +echo " Percent-encode the value first if you supplied a custom token." echo " Signaling URL: https:// (after TLS proxy)" echo " TURN server: turn:$EXTERNAL_IP:3478" echo " TURN user: easycat" echo " TURN password: $TURN_PASSWORD" -echo " Browser TURN auth remains hidden from /config by default." -echo " Set WEBRTC_EXPOSE_ICE_CREDENTIALS=1 only for trusted demos or short-lived TURN creds." +echo " Browser TURN entries are omitted from /config by default (server-side relay only)." +echo " Clients that require their own relay need short-lived TURN credentials." +echo " Set WEBRTC_EXPOSE_ICE_CREDENTIALS=1 only for trusted demos or short-lived credentials." echo "" echo " Check status: sudo systemctl status easycat-webrtc" echo " View logs: sudo journalctl -u easycat-webrtc -f" diff --git a/examples/function_tools_langchain.py b/examples/function_tools_langchain.py index 2cca8d1bb..64a283018 100644 --- a/examples/function_tools_langchain.py +++ b/examples/function_tools_langchain.py @@ -1,4 +1,4 @@ -"""Function-calling tools — LangChain (``@tool`` + ``AgentExecutor``). +"""Function-calling tools — LangChain 0.3 (``@tool`` + ``AgentExecutor``). Two tools (``get_weather`` and ``get_time``) wired through a ``tool_calling`` LangChain ``AgentExecutor``. The executor is itself a @@ -6,8 +6,7 @@ :class:`LangChainBridge`. For tools that drive the call (end, transfer, DTMF) see ``session_actions_langchain.py``. -Setup: export OPENAI_API_KEY=...; uv sync --extra quickstart --extra langchain --group dev - uv pip install "langchain<1" langchain-openai +Setup: export OPENAI_API_KEY=...; uv sync --extra quickstart --extra langchain-v0 --group dev uv run easycat doctor uv run easycat doctor --env-file .env # if keys live in .env uv run easycat doctor --env-file .env --json # for parseable checks @@ -17,8 +16,8 @@ LangChain 1.x removed ``create_tool_calling_agent`` (the recommended replacement, ``langchain.agents.create_agent``, returns a LangGraph ``CompiledStateGraph`` — covered by ``function_tools_langgraph.py``). -This example demonstrates the LangChain-bridge path so the version pin -keeps it runnable against the still-supported 0.3.x line. +This example demonstrates the LangChain-bridge path for the separately +installed and tested 0.3.x compatibility line. """ from datetime import datetime @@ -31,10 +30,9 @@ from langchain_openai import ChatOpenAI except ImportError as exc: raise SystemExit( - "LangChain (<1.0) is required. For an app, run: " - "uv add 'easycat[quickstart,langchain]' 'langchain<1' langchain-openai. " - "In this repo, run: uv sync --extra quickstart --extra langchain --group dev; " - 'uv pip install "langchain<1" langchain-openai' + "LangChain 0.3 is required. For an app, run: " + "`uv add 'easycat[quickstart,langchain-v0]'`. In this repo, run: " + "uv sync --extra quickstart --extra langchain-v0 --group dev" ) from exc from easycat import EasyConfig, run diff --git a/examples/langchain_voice.py b/examples/langchain_voice.py index 777d0a613..383a29d84 100644 --- a/examples/langchain_voice.py +++ b/examples/langchain_voice.py @@ -1,4 +1,4 @@ -"""Local voice bot demo using a LangChain LCEL chain. +"""Local voice bot demo using a LangChain 1.x LCEL chain. Wraps any LangChain ``Runnable`` (an LCEL chain, a ``RunnableWithMessageHistory``, a LangChain ``AgentExecutor``, etc.) in @@ -8,7 +8,6 @@ For stateful multi-node agent workflows see ``langgraph_voice.py``. Setup: export OPENAI_API_KEY=...; uv sync --extra quickstart --extra langchain --group dev - uv pip install langchain-openai uv run easycat doctor uv run easycat doctor --env-file .env # if keys live in .env uv run easycat doctor --env-file .env --json # for parseable checks @@ -21,9 +20,9 @@ from langchain_openai import ChatOpenAI except ImportError as exc: raise SystemExit( - "LangChain is required. For an app, run: " - "uv add 'easycat[quickstart,langchain]' langchain-openai. In this repo, run: " - "uv sync --extra quickstart --extra langchain --group dev; uv pip install langchain-openai" + "LangChain 1.x is required. For an app, run: " + "uv add 'easycat[quickstart,langchain]'. In this repo, run: " + "uv sync --extra quickstart --extra langchain --group dev" ) from exc from easycat import EasyConfig, run diff --git a/examples/noise_reduction_backends.py b/examples/noise_reduction_backends.py index 5cda1c4e3..2dc497a31 100644 --- a/examples/noise_reduction_backends.py +++ b/examples/noise_reduction_backends.py @@ -5,6 +5,7 @@ Setup: export OPENAI_API_KEY=...; uv sync --extra quickstart --extra rnnoise --group dev + uv pip install krisp_audio # required only for --backend krisp (commercial SDK) uv run easycat doctor uv run easycat doctor --env-file .env # if keys live in .env uv run easycat doctor --env-file .env --json # for parseable checks @@ -23,10 +24,14 @@ ) from exc from easycat import EasyConfig, run -from easycat.noise_reduction import NoiseReducerConfig, create_noise_reducer +from easycat.noise_reduction import ( + NoiseReducerBackend, + NoiseReducerConfig, + create_noise_reducer, +) -def main(backend: str) -> None: +def main(backend: NoiseReducerBackend) -> None: # Probe so you can see which class actually resolved before the session starts. # Close the probe afterwards: Krisp/RNNoise reducers hold native # resources (and Krisp may allow only one session at a time), so diff --git a/examples/session_actions_langchain.py b/examples/session_actions_langchain.py index 995e35935..c2d197730 100644 --- a/examples/session_actions_langchain.py +++ b/examples/session_actions_langchain.py @@ -1,4 +1,4 @@ -"""Agent-initiated session actions — LangChain. +"""Agent-initiated session actions — LangChain 0.3. Shows the simplest session action that works on every transport: ending the session after the current reply finishes. The ``end_call`` tool @@ -10,8 +10,7 @@ For telephony-specific actions (transfer, DTMF, SMS) see ``examples/twilio_app.py``. -Setup: export OPENAI_API_KEY=...; uv sync --extra quickstart --extra langchain --group dev - uv pip install "langchain<1" langchain-openai +Setup: export OPENAI_API_KEY=...; uv sync --extra quickstart --extra langchain-v0 --group dev uv run easycat doctor uv run easycat doctor --env-file .env # if keys live in .env uv run easycat doctor --env-file .env --json # for parseable checks @@ -20,8 +19,8 @@ LangChain 1.x removed ``create_tool_calling_agent`` (the recommended replacement, ``langchain.agents.create_agent``, returns a LangGraph -``CompiledStateGraph``). This example pins the still-supported 0.3.x -line so the LangChain bridge keeps a runnable demo. +``CompiledStateGraph``). This example exercises the separately installed and +tested 0.3.x compatibility line. """ try: @@ -31,10 +30,9 @@ from langchain_openai import ChatOpenAI except ImportError as exc: raise SystemExit( - "LangChain (<1.0) is required. For an app, run: " - "uv add 'easycat[quickstart,langchain]' 'langchain<1' langchain-openai. " - "In this repo, run: uv sync --extra quickstart --extra langchain --group dev; " - 'uv pip install "langchain<1" langchain-openai' + "LangChain 0.3 is required. For an app, run: " + "`uv add 'easycat[quickstart,langchain-v0]'`. In this repo, run: " + "uv sync --extra quickstart --extra langchain-v0 --group dev" ) from exc from easycat import EasyConfig, SessionActions, run diff --git a/examples/vad_backends.py b/examples/vad_backends.py index 8dceec164..9931fe2a2 100644 --- a/examples/vad_backends.py +++ b/examples/vad_backends.py @@ -12,9 +12,8 @@ Setup: export OPENAI_API_KEY="..." uv sync --extra quickstart --group dev # bundled Silero ONNX - uv sync --extra funasr-vad --group dev # bundled FunASR ONNX + uv sync --extra quickstart --extra funasr-vad --group dev # bundled FunASR ONNX uv sync --extra quickstart --extra ten-vad --group dev # separate TEN VAD license - uv sync --extra silero-vad --group dev # bundled Silero ONNX uv pip install krisp_audio # Krisp SDK uv run easycat doctor uv run easycat doctor --env-file .env # if keys live in .env @@ -35,12 +34,12 @@ Agent = None # type: ignore[assignment] from easycat import EasyConfig, run -from easycat.vad import VADConfig, create_vad +from easycat.vad import VADBackend, VADConfig, create_vad -BACKENDS = ("auto", "silero", "funasr", "ten", "krisp") +BACKENDS: tuple[VADBackend, ...] = ("auto", "silero", "funasr", "ten", "krisp") -def main(backend: str) -> None: +def main(backend: VADBackend) -> None: if Agent is None: raise SystemExit( "openai-agents is required. For an app, run: " diff --git a/examples/voice_app_twilio.py b/examples/voice_app_twilio.py index ab1db5f7e..a5390ccf4 100644 --- a/examples/voice_app_twilio.py +++ b/examples/voice_app_twilio.py @@ -8,6 +8,8 @@ Setup: export OPENAI_API_KEY=...; export TWILIO_STREAM_URL=wss://your-host:8766 export TWILIO_AUTH_TOKEN=... # signs/validates the POST /twiml webhook + # Behind a TLS proxy: export TRUST_PROXY_HEADERS=1 + # Or pin the signed URL: export TWILIO_PUBLIC_TWIML_URL=https://your-host/twiml uv sync --extra openai --extra telephony --extra openai-agents --group dev uv run easycat doctor uv run easycat doctor --env-file .env # if keys live in .env @@ -19,8 +21,9 @@ ``POST /twiml`` mints a media stream token on every request, so the webhook is authenticated by default: ``TWILIO_AUTH_TOKEN`` validates the ``X-Twilio-Signature`` header before a token is issued. Behind a TLS-terminating -proxy, also pass ``trust_proxy_headers=True`` so the signed public URL is -reconstructed from the forwarded headers. +proxy, set ``TRUST_PROXY_HEADERS=1`` so the signed public URL is reconstructed +from forwarded headers, or set ``TWILIO_PUBLIC_TWIML_URL`` to the exact public +route used in Twilio's request signature. For the lower-level FastAPI reference (outbound calls, status callbacks, SMS), see examples/twilio_app.py. @@ -36,6 +39,7 @@ ) from exc from easycat import EasyConfig, TelephonyConfig, VoiceApp, require_env +from easycat.transports import TwilioConnectionTransport def main() -> None: @@ -43,7 +47,7 @@ def main() -> None: stream_url = require_env("TWILIO_STREAM_URL") twilio_auth_token = require_env("TWILIO_AUTH_TOKEN") - def config_factory(transport: object) -> EasyConfig: + def config_factory(transport: TwilioConnectionTransport) -> EasyConfig: return EasyConfig.phone( transport=transport, agent=Agent(name="assistant", instructions="You are a helpful phone assistant."), diff --git a/examples/webrtc_observability_server.py b/examples/webrtc_observability_server.py index 18fa397ee..1d4a6d634 100644 --- a/examples/webrtc_observability_server.py +++ b/examples/webrtc_observability_server.py @@ -21,6 +21,9 @@ debugger UI below) http://localhost:8080/webrtc_client.html (just the bot) http://localhost:8090 (just the debugger) + +When ``WEBRTC_SIGNALING_TOKEN`` is set, use the ``#token=`` URLs printed at +startup and replace the placeholder with the URL-encoded token value. """ from __future__ import annotations @@ -88,16 +91,16 @@ def main() -> None: allow_remote=debugger_host not in ("127.0.0.1", "localhost"), ) - token_query = "&token=" if transport.auth_token else "" + token_fragment = "#token=" if transport.auth_token else "" combined_url = ( f"http://localhost:{transport.port}/webrtc_observability.html" - f"?debugger_port={debugger_port}{token_query}" + f"?debugger_port={debugger_port}{token_fragment}" ) - client_token_query = "?token=" if transport.auth_token else "" + client_token_fragment = "#token=" if transport.auth_token else "" print(f"WebRTC + debugger: {combined_url}") print( f"WebRTC client only: " - f"http://localhost:{transport.port}/webrtc_client.html{client_token_query}" + f"http://localhost:{transport.port}/webrtc_client.html{client_token_fragment}" ) print(f"Debugger only: http://{debugger_host}:{debugger_port}") diff --git a/examples/webrtc_server.py b/examples/webrtc_server.py index 85bef4413..343b1c317 100644 --- a/examples/webrtc_server.py +++ b/examples/webrtc_server.py @@ -23,15 +23,19 @@ TURN_SERVER_URL — Optional. TURN server URL (e.g. turn:1.2.3.4:3478). TURN_USERNAME — Optional. TURN server username. TURN_CREDENTIAL — Optional. TURN server credential. - WEBRTC_EXPOSE_ICE_CREDENTIALS — Optional. Set to 1 to return TURN - credentials from /config. Use only with - trusted demos or short-lived credentials. + WEBRTC_EXPOSE_ICE_CREDENTIALS — Optional. Set to 1 to return complete TURN + entries from /config. Use only with + trusted demos or short-lived credentials; + otherwise browser config is STUN-only and + TURN is server-side relay only. SIGNALING_HOST — Optional. Bind address (default 127.0.0.1). SIGNALING_PORT — Optional. Listen port (default 8080). WEBRTC_SIGNALING_TOKEN — Optional on localhost; required for public binds. WEBRTC_MAX_SESSIONS — Optional. Concurrent browser sessions (default 64). -Then open http://localhost:8080 in your browser. +Then open http://localhost:8080 in your browser. When +``WEBRTC_SIGNALING_TOKEN`` is set, use the printed ``#token=`` bootstrap URL and +replace its placeholder with the URL-encoded token value. The bundled client is same-origin with the signaling server. If you host a custom browser UI elsewhere, pass explicit cors_allowed_origins to WebRTCTransportConfig instead of relying on wildcard CORS. @@ -61,14 +65,15 @@ def config(transport): return EasyConfig.browser(transport=transport, agent=agent) transport = webrtc_transport_config_from_env() - token_hint = "?token=" if transport.auth_token else "" + token_hint = "#token=" if transport.auth_token else "" print(f"Open http://localhost:{transport.port}/webrtc_client.html{token_hint} in your browser") - if any(any("turn:" in u for u in s.urls) for s in transport.ice_servers): + ice_urls = (url for server in transport.ice_servers for url in server.urls) + if any(url.lower().startswith(("turn:", "turns:")) for url in ice_urls): print("TURN server: configured") if transport.expose_ice_credentials: - print("TURN auth: exposed via /config") + print("TURN browser: /config includes only entries with complete credentials") else: - print("TURN auth: hidden from /config") + print("TURN browser: omitted from /config (server-side relay only)") else: print("TURN server: not configured (STUN only — NAT traversal may fail)") diff --git a/examples/webrtc_static/webrtc_client.html b/examples/webrtc_static/webrtc_client.html index 05d3b6dc9..161eb2e63 100644 --- a/examples/webrtc_static/webrtc_client.html +++ b/examples/webrtc_static/webrtc_client.html @@ -3,6 +3,7 @@ + EasyCat WebRTC Voice Chat