diff --git a/AGENTS.md b/AGENTS.md index e04d2bb..3ce831a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -410,9 +410,9 @@ jambonz.say(text="Connecting you now.").dial( ## SDK Architecture -The SDK auto-generates verb methods from `specs.json` (from `@jambonz/verb-specifications`). When the spec changes, the SDK automatically picks up new parameters: +The SDK auto-generates verb methods from JSON Schema files (from `@jambonz/schema`). When the schema changes, the SDK automatically picks up new parameters: -1. `specs.json` — bundled verb/component specifications (synced from upstream) +1. `schema/verbs/*.schema.json` — bundled verb schemas (synced from upstream) 2. `verb_registry.py` — maps spec entries to Python methods + synonyms 3. `verb_builder.py` — generates methods at import time from specs + registry 4. `WebhookResponse` and `Session` both extend `VerbBuilder` diff --git a/CLAUDE.md b/CLAUDE.md index d7c250b..b4d2d82 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ src/jambonz_sdk/ │ ├── verbs.py # All 26+ verb TypedDicts │ ├── rest.py # REST API request/response types │ └── session.py # Call session & WebSocket message types -├── verb_builder.py # VerbBuilder — methods auto-generated from specs.json +├── verb_builder.py # VerbBuilder — methods auto-generated from JSON Schema ├── verb_registry.py # Verb definitions: maps spec entries → Python methods ├── webhook/ │ ├── __init__.py @@ -48,12 +48,12 @@ src/jambonz_sdk/ - **Transport-agnostic verb building**: Same verb methods on both `WebhookResponse` and `Session` - **Fluent/chainable API**: All verb methods return `self` for method chaining - **TypedDict for verb schemas**: Type-safe verb construction matching JSON schemas exactly -- **Auto-generated verb methods**: VerbBuilder methods are generated at import time from `specs.json` + `verb_registry.py` — when the spec changes, the SDK automatically picks up new parameters +- **Auto-generated verb methods**: VerbBuilder methods are generated at import time from JSON Schema files (`@jambonz/schema`) + `verb_registry.py` — when the schema changes, the SDK automatically picks up new parameters - **aiohttp for both HTTP and WebSocket**: Single dependency for REST client and WS transport ## Verb System -The SDK supports all 26+ jambonz verbs. Verb methods on VerbBuilder are **auto-generated** from the shared `specs.json` (in `/Users/xhoaluu/jambonz/verb-specifications/specs.json`). +The SDK supports all 26+ jambonz verbs. Verb methods on VerbBuilder are **auto-generated** from JSON Schema files bundled from [`@jambonz/schema`](https://github.com/jambonz/schema). ### How verb generation works diff --git a/scripts/generate_stubs.py b/scripts/generate_stubs.py index e44e983..32cb859 100644 --- a/scripts/generate_stubs.py +++ b/scripts/generate_stubs.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -"""Generate verb_builder.pyi stub file from specs.json + verb_registry. +"""Generate verb_builder.pyi stub file from JSON Schema + verb_registry. This creates a .pyi type stub that IDEs (VS Code Pylance, PyCharm, mypy) read for static type checking and autocomplete. Run this after syncing -specs.json or updating verb_registry.py. +the schema or updating verb_registry.py. Usage: python scripts/generate_stubs.py @@ -19,10 +19,10 @@ from jambonz_sdk.verb_registry import VERB_DEFS -SPECS_PATH = SRC_DIR / "jambonz_sdk" / "specs.json" +SCHEMA_DIR = SRC_DIR / "jambonz_sdk" / "schema" / "verbs" STUB_PATH = SRC_DIR / "jambonz_sdk" / "verb_builder.pyi" -# Maps specs.json type strings to Python type annotation strings for .pyi +# Maps JSON Schema type strings to Python type annotation strings for .pyi TYPE_MAP = { "string": "str", "number": "int | float", @@ -33,7 +33,7 @@ def resolve_type(spec_type) -> str: - """Convert a specs.json type descriptor to a .pyi type string.""" + """Convert a JSON Schema type descriptor to a .pyi type string.""" if isinstance(spec_type, str): if spec_type.startswith("#"): return "dict[str, Any]" @@ -61,9 +61,37 @@ def resolve_type(spec_type) -> str: return "Any" +def _load_schemas() -> dict: + """Load verb JSON Schemas from the bundled schema directory.""" + schemas: dict = {} + for schema_file in sorted(SCHEMA_DIR.glob("*.schema.json")): + with schema_file.open() as f: + schema = json.load(f) + schema_id = schema.get("$id", "") + if schema_id: + spec_name = schema_id.rsplit("/", 1)[-1] + else: + spec_name = schema_file.stem.replace(".schema", "") + properties = {} + for prop_name, prop_def in schema.get("properties", {}).items(): + if prop_name == "verb": + continue + properties[prop_name] = prop_def + for entry in schema.get("allOf", []): + if "properties" in entry: + for prop_name, prop_def in entry["properties"].items(): + if prop_name == "verb": + continue + properties[prop_name] = prop_def + schemas[spec_name] = { + "properties": properties, + "required": schema.get("required", []), + } + return schemas + + def generate() -> str: - with SPECS_PATH.open() as f: - specs = json.load(f) + specs = _load_schemas() lines = [ '"""Auto-generated type stubs for VerbBuilder.', @@ -75,7 +103,6 @@ def generate() -> str: "", "from jambonz_sdk.types.verbs import AnyVerb", "", - "", "class VerbBuilder:", " _verbs: list[AnyVerb]", "", diff --git a/src/jambonz_sdk/verb_builder.pyi b/src/jambonz_sdk/verb_builder.pyi index 1e102cd..196fdc4 100644 --- a/src/jambonz_sdk/verb_builder.pyi +++ b/src/jambonz_sdk/verb_builder.pyi @@ -16,11 +16,11 @@ class VerbBuilder: def say( self, id: str = ..., - text: str | list[Any] = ..., + text: Any = ..., instructions: str = ..., stream: bool = ..., - loop: int | float | str = ..., - synthesizer: dict[str, Any] = ..., + loop: Any = ..., + synthesizer: Any = ..., earlyMedia: bool = ..., disableTtsCache: bool = ..., closeStreamOnEmpty: bool = ..., @@ -30,11 +30,11 @@ class VerbBuilder: Args: id: str - text: str | list[Any] + text: Any instructions: str stream: bool - loop: int | float | str - synthesizer: dict[str, Any] + loop: Any + synthesizer: Any earlyMedia: bool disableTtsCache: bool closeStreamOnEmpty: bool @@ -47,12 +47,12 @@ class VerbBuilder: def play( self, id: str = ..., - url: str | list[Any] = ..., - loop: int | float | str = ..., + url: Any = ..., + loop: Any = ..., earlyMedia: bool = ..., - seekOffset: int | float | str = ..., - timeoutSecs: int | float | str = ..., - actionHook: dict[str, Any] | str = ..., + seekOffset: Any = ..., + timeoutSecs: Any = ..., + actionHook: Any = ..., **kwargs: Any, ) -> Self: """Play an audio file from a URL. @@ -61,12 +61,12 @@ class VerbBuilder: Args: id: str - url: str | list[Any] (required) - loop: int | float | str + url: Any (required) + loop: Any earlyMedia: bool - seekOffset: int | float | str - timeoutSecs: int | float | str - actionHook: dict[str, Any] | str + seekOffset: Any + timeoutSecs: Any + actionHook: Any Returns: self for chaining. @@ -76,50 +76,50 @@ class VerbBuilder: def gather( self, id: str = ..., - actionHook: dict[str, Any] | str = ..., - finishOnKey: str = ..., + actionHook: Any = ..., input: list[Any] = ..., + finishOnKey: str = ..., numDigits: int | float = ..., minDigits: int | float = ..., maxDigits: int | float = ..., interDigitTimeout: int | float = ..., - partialResultHook: dict[str, Any] | str = ..., speechTimeout: int | float = ..., + timeout: int | float = ..., + partialResultHook: Any = ..., listenDuringPrompt: bool = ..., dtmfBargein: bool = ..., bargein: bool = ..., minBargeinWordCount: int | float = ..., - timeout: int | float = ..., - recognizer: dict[str, Any] = ..., - play: dict[str, Any] = ..., + recognizer: Any = ..., say: dict[str, Any] = ..., - fillerNoise: dict[str, Any] = ..., - actionHookDelayAction: dict[str, Any] = ..., + play: dict[str, Any] = ..., + fillerNoise: Any = ..., + actionHookDelayAction: Any = ..., **kwargs: Any, ) -> Self: """Collect speech (STT) and/or DTMF input. Args: id: str - actionHook: dict[str, Any] | str - finishOnKey: str + actionHook: Any input: list[Any] + finishOnKey: str numDigits: int | float minDigits: int | float maxDigits: int | float interDigitTimeout: int | float - partialResultHook: dict[str, Any] | str speechTimeout: int | float + timeout: int | float + partialResultHook: Any listenDuringPrompt: bool dtmfBargein: bool bargein: bool minBargeinWordCount: int | float - timeout: int | float - recognizer: dict[str, Any] - play: dict[str, Any] + recognizer: Any say: dict[str, Any] - fillerNoise: dict[str, Any] - actionHookDelayAction: dict[str, Any] + play: dict[str, Any] + fillerNoise: Any + actionHookDelayAction: Any Returns: self for chaining. @@ -128,17 +128,6 @@ class VerbBuilder: def openai_s2s( self, - id: str = ..., - vendor: str = ..., - model: str = ..., - auth: dict[str, Any] = ..., - connectOptions: dict[str, Any] = ..., - mcpServers: list[Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., - toolHook: dict[str, Any] | str = ..., - events: list[Any] = ..., - llmOptions: dict[str, Any] = ..., **kwargs: Any, ) -> Self: """Connect caller to OpenAI for real-time voice conversation. @@ -146,17 +135,6 @@ class VerbBuilder: Required: llmOptions, vendor Args: - id: str - vendor: str (required) - model: str - auth: dict[str, Any] - connectOptions: dict[str, Any] - mcpServers: list[Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str - toolHook: dict[str, Any] | str - events: list[Any] - llmOptions: dict[str, Any] (required) Returns: self for chaining. @@ -165,17 +143,6 @@ class VerbBuilder: def google_s2s( self, - id: str = ..., - vendor: str = ..., - model: str = ..., - auth: dict[str, Any] = ..., - connectOptions: dict[str, Any] = ..., - mcpServers: list[Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., - toolHook: dict[str, Any] | str = ..., - events: list[Any] = ..., - llmOptions: dict[str, Any] = ..., **kwargs: Any, ) -> Self: """Connect caller to Google for real-time voice conversation. @@ -183,17 +150,6 @@ class VerbBuilder: Required: llmOptions, vendor Args: - id: str - vendor: str (required) - model: str - auth: dict[str, Any] - connectOptions: dict[str, Any] - mcpServers: list[Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str - toolHook: dict[str, Any] | str - events: list[Any] - llmOptions: dict[str, Any] (required) Returns: self for chaining. @@ -202,17 +158,6 @@ class VerbBuilder: def deepgram_s2s( self, - id: str = ..., - vendor: str = ..., - model: str = ..., - auth: dict[str, Any] = ..., - connectOptions: dict[str, Any] = ..., - mcpServers: list[Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., - toolHook: dict[str, Any] | str = ..., - events: list[Any] = ..., - llmOptions: dict[str, Any] = ..., **kwargs: Any, ) -> Self: """Connect caller to Deepgram for real-time voice conversation. @@ -220,17 +165,6 @@ class VerbBuilder: Required: llmOptions, vendor Args: - id: str - vendor: str (required) - model: str - auth: dict[str, Any] - connectOptions: dict[str, Any] - mcpServers: list[Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str - toolHook: dict[str, Any] | str - events: list[Any] - llmOptions: dict[str, Any] (required) Returns: self for chaining. @@ -239,17 +173,6 @@ class VerbBuilder: def elevenlabs_s2s( self, - id: str = ..., - vendor: str = ..., - model: str = ..., - auth: dict[str, Any] = ..., - connectOptions: dict[str, Any] = ..., - mcpServers: list[Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., - toolHook: dict[str, Any] | str = ..., - events: list[Any] = ..., - llmOptions: dict[str, Any] = ..., **kwargs: Any, ) -> Self: """Connect caller to ElevenLabs Conversational AI agent. @@ -257,17 +180,6 @@ class VerbBuilder: Required: llmOptions, vendor Args: - id: str - vendor: str (required) - model: str - auth: dict[str, Any] - connectOptions: dict[str, Any] - mcpServers: list[Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str - toolHook: dict[str, Any] | str - events: list[Any] - llmOptions: dict[str, Any] (required) Returns: self for chaining. @@ -276,17 +188,6 @@ class VerbBuilder: def ultravox_s2s( self, - id: str = ..., - vendor: str = ..., - model: str = ..., - auth: dict[str, Any] = ..., - connectOptions: dict[str, Any] = ..., - mcpServers: list[Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., - toolHook: dict[str, Any] | str = ..., - events: list[Any] = ..., - llmOptions: dict[str, Any] = ..., **kwargs: Any, ) -> Self: """Connect caller to Ultravox for real-time voice conversation. @@ -294,17 +195,6 @@ class VerbBuilder: Required: llmOptions, vendor Args: - id: str - vendor: str (required) - model: str - auth: dict[str, Any] - connectOptions: dict[str, Any] - mcpServers: list[Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str - toolHook: dict[str, Any] | str - events: list[Any] - llmOptions: dict[str, Any] (required) Returns: self for chaining. @@ -313,17 +203,6 @@ class VerbBuilder: def s2s( self, - id: str = ..., - vendor: str = ..., - model: str = ..., - auth: dict[str, Any] = ..., - connectOptions: dict[str, Any] = ..., - mcpServers: list[Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., - toolHook: dict[str, Any] | str = ..., - events: list[Any] = ..., - llmOptions: dict[str, Any] = ..., **kwargs: Any, ) -> Self: """Generic S2S verb (use when vendor is determined at runtime). @@ -331,17 +210,6 @@ class VerbBuilder: Required: llmOptions, vendor Args: - id: str - vendor: str (required) - model: str - auth: dict[str, Any] - connectOptions: dict[str, Any] - mcpServers: list[Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str - toolHook: dict[str, Any] | str - events: list[Any] - llmOptions: dict[str, Any] (required) Returns: self for chaining. @@ -350,17 +218,6 @@ class VerbBuilder: def llm( self, - id: str = ..., - vendor: str = ..., - model: str = ..., - auth: dict[str, Any] = ..., - connectOptions: dict[str, Any] = ..., - mcpServers: list[Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., - toolHook: dict[str, Any] | str = ..., - events: list[Any] = ..., - llmOptions: dict[str, Any] = ..., **kwargs: Any, ) -> Self: """Legacy LLM verb (prefer s2s or vendor-specific shortcuts). @@ -368,17 +225,6 @@ class VerbBuilder: Required: llmOptions, vendor Args: - id: str - vendor: str (required) - model: str - auth: dict[str, Any] - connectOptions: dict[str, Any] - mcpServers: list[Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str - toolHook: dict[str, Any] | str - events: list[Any] - llmOptions: dict[str, Any] (required) Returns: self for chaining. @@ -388,15 +234,15 @@ class VerbBuilder: def dialogflow( self, id: str = ..., - credentials: dict[str, Any] | str = ..., + credentials: Any = ..., project: str = ..., agent: str = ..., environment: str = ..., region: str = ..., model: str = ..., lang: str = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., + actionHook: Any = ..., + eventHook: Any = ..., events: list[Any] = ..., welcomeEvent: str = ..., welcomeEventParams: dict[str, Any] = ..., @@ -404,7 +250,7 @@ class VerbBuilder: noInputEvent: str = ..., passDtmfAsTextInput: bool = ..., thinkingMusic: str = ..., - tts: dict[str, Any] = ..., + tts: Any = ..., bargein: bool = ..., queryInput: dict[str, Any] = ..., **kwargs: Any, @@ -415,15 +261,15 @@ class VerbBuilder: Args: id: str - credentials: dict[str, Any] | str (required) + credentials: Any (required) project: str (required) agent: str environment: str region: str model: str lang: str (required) - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str + actionHook: Any + eventHook: Any events: list[Any] welcomeEvent: str welcomeEventParams: dict[str, Any] @@ -431,7 +277,7 @@ class VerbBuilder: noInputEvent: str passDtmfAsTextInput: bool thinkingMusic: str - tts: dict[str, Any] + tts: Any bargein: bool queryInput: dict[str, Any] @@ -443,19 +289,19 @@ class VerbBuilder: def pipeline( self, id: str = ..., - stt: dict[str, Any] = ..., - tts: dict[str, Any] = ..., - llm: dict[str, Any] = ..., - turnDetection: str | dict[str, Any] = ..., + stt: Any = ..., + tts: Any = ..., + turnDetection: Any = ..., bargeIn: dict[str, Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., - toolHook: dict[str, Any] | str = ..., + noResponseTimeout: int | float = ..., + llm: dict[str, Any] = ..., + actionHook: Any = ..., + eventHook: Any = ..., + toolHook: Any = ..., greeting: bool = ..., earlyGeneration: bool = ..., - noiseIsolation: str | dict[str, Any] = ..., + noiseIsolation: Any = ..., mcpServers: list[Any] = ..., - noResponseTimeout: int | float = ..., **kwargs: Any, ) -> Self: """Integrated STT → LLM → TTS voice AI pipeline. @@ -464,19 +310,19 @@ class VerbBuilder: Args: id: str - stt: dict[str, Any] - tts: dict[str, Any] - llm: dict[str, Any] (required) - turnDetection: str | dict[str, Any] + stt: Any + tts: Any + turnDetection: Any bargeIn: dict[str, Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str - toolHook: dict[str, Any] | str + noResponseTimeout: int | float + llm: dict[str, Any] (required) + actionHook: Any + eventHook: Any + toolHook: Any greeting: bool earlyGeneration: bool - noiseIsolation: str | dict[str, Any] + noiseIsolation: Any mcpServers: list[Any] - noResponseTimeout: int | float Returns: self for chaining. @@ -486,21 +332,20 @@ class VerbBuilder: def listen( self, id: str = ..., - actionHook: dict[str, Any] | str = ..., - auth: dict[str, Any] = ..., + url: str = ..., + actionHook: Any = ..., + wsAuth: Any = ..., + mixType: str = ..., + metadata: dict[str, Any] = ..., + sampleRate: int | float = ..., finishOnKey: str = ..., maxLength: int | float = ..., - metadata: dict[str, Any] = ..., - mixType: str = ..., passDtmf: bool = ..., playBeep: bool = ..., disableBidirectionalAudio: bool = ..., - bidirectionalAudio: dict[str, Any] = ..., - sampleRate: int | float = ..., + bidirectionalAudio: Any = ..., timeout: int | float = ..., - transcribe: dict[str, Any] = ..., - url: str = ..., - wsAuth: dict[str, Any] = ..., + transcribe: Any = ..., earlyMedia: bool = ..., channel: int | float = ..., **kwargs: Any, @@ -511,21 +356,20 @@ class VerbBuilder: Args: id: str - actionHook: dict[str, Any] | str - auth: dict[str, Any] + url: str (required) + actionHook: Any + wsAuth: Any + mixType: str + metadata: dict[str, Any] + sampleRate: int | float finishOnKey: str maxLength: int | float - metadata: dict[str, Any] - mixType: str passDtmf: bool playBeep: bool disableBidirectionalAudio: bool - bidirectionalAudio: dict[str, Any] - sampleRate: int | float + bidirectionalAudio: Any timeout: int | float - transcribe: dict[str, Any] - url: str (required) - wsAuth: dict[str, Any] + transcribe: Any earlyMedia: bool channel: int | float @@ -537,21 +381,20 @@ class VerbBuilder: def stream( self, id: str = ..., - actionHook: dict[str, Any] | str = ..., - auth: dict[str, Any] = ..., + url: str = ..., + actionHook: Any = ..., + wsAuth: Any = ..., + mixType: str = ..., + metadata: dict[str, Any] = ..., + sampleRate: int | float = ..., finishOnKey: str = ..., maxLength: int | float = ..., - metadata: dict[str, Any] = ..., - mixType: str = ..., passDtmf: bool = ..., playBeep: bool = ..., disableBidirectionalAudio: bool = ..., - bidirectionalAudio: dict[str, Any] = ..., - sampleRate: int | float = ..., + bidirectionalAudio: Any = ..., timeout: int | float = ..., - transcribe: dict[str, Any] = ..., - url: str = ..., - wsAuth: dict[str, Any] = ..., + transcribe: Any = ..., earlyMedia: bool = ..., channel: int | float = ..., **kwargs: Any, @@ -562,21 +405,20 @@ class VerbBuilder: Args: id: str - actionHook: dict[str, Any] | str - auth: dict[str, Any] + url: str (required) + actionHook: Any + wsAuth: Any + mixType: str + metadata: dict[str, Any] + sampleRate: int | float finishOnKey: str maxLength: int | float - metadata: dict[str, Any] - mixType: str passDtmf: bool playBeep: bool disableBidirectionalAudio: bool - bidirectionalAudio: dict[str, Any] - sampleRate: int | float + bidirectionalAudio: Any timeout: int | float - transcribe: dict[str, Any] - url: str (required) - wsAuth: dict[str, Any] + transcribe: Any earlyMedia: bool channel: int | float @@ -588,9 +430,10 @@ class VerbBuilder: def transcribe( self, id: str = ..., + enable: bool = ..., transcriptionHook: str = ..., translationHook: str = ..., - recognizer: dict[str, Any] = ..., + recognizer: Any = ..., earlyMedia: bool = ..., channel: int | float = ..., **kwargs: Any, @@ -599,9 +442,10 @@ class VerbBuilder: Args: id: str + enable: bool transcriptionHook: str translationHook: str - recognizer: dict[str, Any] + recognizer: Any earlyMedia: bool channel: int | float @@ -613,28 +457,28 @@ class VerbBuilder: def dial( self, id: str = ..., - actionHook: dict[str, Any] | str = ..., - onHoldHook: dict[str, Any] | str = ..., + target: list[Any] = ..., + actionHook: Any = ..., + onHoldHook: Any = ..., answerOnBridge: bool = ..., callerId: str = ..., callerName: str = ..., - confirmHook: dict[str, Any] | str = ..., - referHook: dict[str, Any] | str = ..., + confirmHook: Any = ..., + referHook: Any = ..., dialMusic: str = ..., - dtmfCapture: dict[str, Any] = ..., - dtmfHook: dict[str, Any] | str = ..., + dtmfCapture: Any = ..., + dtmfHook: Any = ..., headers: dict[str, Any] = ..., anchorMedia: bool = ..., exitMediaPath: bool = ..., - boostAudioSignal: int | float | str = ..., + boostAudioSignal: Any = ..., listen: dict[str, Any] = ..., stream: dict[str, Any] = ..., - target: list[Any] = ..., + transcribe: dict[str, Any] = ..., timeLimit: int | float = ..., timeout: int | float = ..., proxy: str = ..., - transcribe: dict[str, Any] = ..., - amd: dict[str, Any] = ..., + amd: Any = ..., dub: list[Any] = ..., tag: dict[str, Any] = ..., forwardPAI: bool = ..., @@ -646,28 +490,28 @@ class VerbBuilder: Args: id: str - actionHook: dict[str, Any] | str - onHoldHook: dict[str, Any] | str + target: list[Any] (required) + actionHook: Any + onHoldHook: Any answerOnBridge: bool callerId: str callerName: str - confirmHook: dict[str, Any] | str - referHook: dict[str, Any] | str + confirmHook: Any + referHook: Any dialMusic: str - dtmfCapture: dict[str, Any] - dtmfHook: dict[str, Any] | str + dtmfCapture: Any + dtmfHook: Any headers: dict[str, Any] anchorMedia: bool exitMediaPath: bool - boostAudioSignal: int | float | str + boostAudioSignal: Any listen: dict[str, Any] stream: dict[str, Any] - target: list[Any] (required) + transcribe: dict[str, Any] timeLimit: int | float timeout: int | float proxy: str - transcribe: dict[str, Any] - amd: dict[str, Any] + amd: Any dub: list[Any] tag: dict[str, Any] forwardPAI: bool @@ -689,11 +533,11 @@ class VerbBuilder: endConferenceDuration: int | float = ..., maxParticipants: int | float = ..., joinMuted: bool = ..., - actionHook: dict[str, Any] | str = ..., - waitHook: dict[str, Any] | str = ..., + actionHook: Any = ..., + waitHook: Any = ..., statusEvents: list[Any] = ..., - statusHook: dict[str, Any] | str = ..., - enterHook: dict[str, Any] | str = ..., + statusHook: Any = ..., + enterHook: Any = ..., record: dict[str, Any] = ..., listen: dict[str, Any] = ..., distributeDtmf: bool = ..., @@ -714,11 +558,11 @@ class VerbBuilder: endConferenceDuration: int | float maxParticipants: int | float joinMuted: bool - actionHook: dict[str, Any] | str - waitHook: dict[str, Any] | str + actionHook: Any + waitHook: Any statusEvents: list[Any] - statusHook: dict[str, Any] | str - enterHook: dict[str, Any] | str + statusHook: Any + enterHook: Any record: dict[str, Any] listen: dict[str, Any] distributeDtmf: bool @@ -732,10 +576,9 @@ class VerbBuilder: self, id: str = ..., name: str = ..., - actionHook: dict[str, Any] | str = ..., - waitHook: dict[str, Any] | str = ..., + actionHook: Any = ..., + waitHook: Any = ..., priority: int | float = ..., - _: dict[str, Any] = ..., **kwargs: Any, ) -> Self: """Place caller into a named call queue. @@ -745,10 +588,9 @@ class VerbBuilder: Args: id: str name: str (required) - actionHook: dict[str, Any] | str - waitHook: dict[str, Any] | str + actionHook: Any + waitHook: Any priority: int | float - _: dict[str, Any] Returns: self for chaining. @@ -759,7 +601,7 @@ class VerbBuilder: self, id: str = ..., name: str = ..., - actionHook: dict[str, Any] | str = ..., + actionHook: Any = ..., timeout: int | float = ..., beep: bool = ..., callSid: str = ..., @@ -772,7 +614,7 @@ class VerbBuilder: Args: id: str name: str (required) - actionHook: dict[str, Any] | str + actionHook: Any timeout: int | float beep: bool callSid: str @@ -802,8 +644,8 @@ class VerbBuilder: def redirect( self, id: str = ..., - actionHook: dict[str, Any] | str = ..., - statusHook: dict[str, Any] | str = ..., + actionHook: Any = ..., + statusHook: Any = ..., **kwargs: Any, ) -> Self: """Transfer control to a different webhook URL. @@ -812,8 +654,8 @@ class VerbBuilder: Args: id: str - actionHook: dict[str, Any] | str (required) - statusHook: dict[str, Any] | str + actionHook: Any (required) + statusHook: Any Returns: self for chaining. @@ -868,7 +710,7 @@ class VerbBuilder: method: str = ..., body: str = ..., headers: dict[str, Any] = ..., - actionHook: dict[str, Any] | str = ..., + actionHook: Any = ..., **kwargs: Any, ) -> Self: """Send a SIP request within the current dialog. @@ -880,7 +722,7 @@ class VerbBuilder: method: str (required) body: str headers: dict[str, Any] - actionHook: dict[str, Any] | str + actionHook: Any Returns: self for chaining. @@ -894,8 +736,8 @@ class VerbBuilder: referredBy: str = ..., referredByDisplayName: str = ..., headers: dict[str, Any] = ..., - actionHook: dict[str, Any] | str = ..., - eventHook: dict[str, Any] | str = ..., + actionHook: Any = ..., + eventHook: Any = ..., **kwargs: Any, ) -> Self: """Send a SIP REFER for call transfer. @@ -908,8 +750,8 @@ class VerbBuilder: referredBy: str referredByDisplayName: str headers: dict[str, Any] - actionHook: dict[str, Any] | str - eventHook: dict[str, Any] | str + actionHook: Any + eventHook: Any Returns: self for chaining. @@ -919,25 +761,25 @@ class VerbBuilder: def config( self, id: str = ..., - synthesizer: dict[str, Any] = ..., - recognizer: dict[str, Any] = ..., + synthesizer: Any = ..., + recognizer: Any = ..., bargeIn: dict[str, Any] = ..., ttsStream: dict[str, Any] = ..., record: dict[str, Any] = ..., listen: dict[str, Any] = ..., stream: dict[str, Any] = ..., transcribe: dict[str, Any] = ..., - amd: dict[str, Any] = ..., - fillerNoise: dict[str, Any] = ..., + amd: Any = ..., + fillerNoise: Any = ..., + vad: Any = ..., notifyEvents: bool = ..., notifySttLatency: bool = ..., - reset: str | list[Any] = ..., + reset: Any = ..., onHoldMusic: str = ..., - actionHookDelayAction: dict[str, Any] = ..., - sipRequestWithinDialogHook: dict[str, Any] | str = ..., - boostAudioSignal: int | float | str = ..., - vad: dict[str, Any] = ..., - referHook: dict[str, Any] | str = ..., + actionHookDelayAction: Any = ..., + sipRequestWithinDialogHook: Any = ..., + boostAudioSignal: Any = ..., + referHook: Any = ..., earlyMedia: bool = ..., autoStreamTts: bool = ..., disableTtsCache: bool = ..., @@ -950,25 +792,25 @@ class VerbBuilder: Args: id: str - synthesizer: dict[str, Any] - recognizer: dict[str, Any] + synthesizer: Any + recognizer: Any bargeIn: dict[str, Any] ttsStream: dict[str, Any] record: dict[str, Any] listen: dict[str, Any] stream: dict[str, Any] transcribe: dict[str, Any] - amd: dict[str, Any] - fillerNoise: dict[str, Any] + amd: Any + fillerNoise: Any + vad: Any notifyEvents: bool notifySttLatency: bool - reset: str | list[Any] + reset: Any onHoldMusic: str - actionHookDelayAction: dict[str, Any] - sipRequestWithinDialogHook: dict[str, Any] | str - boostAudioSignal: int | float | str - vad: dict[str, Any] - referHook: dict[str, Any] | str + actionHookDelayAction: Any + sipRequestWithinDialogHook: Any + boostAudioSignal: Any + referHook: Any earlyMedia: bool autoStreamTts: bool disableTtsCache: bool @@ -1027,9 +869,9 @@ class VerbBuilder: action: str = ..., track: str = ..., play: str = ..., - say: str | dict[str, Any] = ..., + say: Any = ..., loop: bool = ..., - gain: int | float | str = ..., + gain: Any = ..., **kwargs: Any, ) -> Self: """Manage audio dubbing tracks. @@ -1041,9 +883,9 @@ class VerbBuilder: action: str (required) track: str (required) play: str - say: str | dict[str, Any] + say: Any loop: bool - gain: int | float | str + gain: Any Returns: self for chaining. @@ -1053,14 +895,14 @@ class VerbBuilder: def message( self, id: str = ..., - carrier: str = ..., - account_sid: str = ..., - message_sid: str = ..., to: str = ..., from_: str = ..., text: str = ..., - media: str | list[Any] = ..., - actionHook: dict[str, Any] | str = ..., + media: Any = ..., + carrier: str = ..., + account_sid: str = ..., + message_sid: str = ..., + actionHook: Any = ..., **kwargs: Any, ) -> Self: """Send SMS/MMS message. @@ -1069,14 +911,14 @@ class VerbBuilder: Args: id: str - carrier: str - account_sid: str - message_sid: str to: str (required) from_: str (required) text: str - media: str | list[Any] - actionHook: dict[str, Any] | str + media: Any + carrier: str + account_sid: str + message_sid: str + actionHook: Any Returns: self for chaining. diff --git a/src/jambonz_sdk/verb_registry.py b/src/jambonz_sdk/verb_registry.py index 3dd4e8e..ce5ddd0 100644 --- a/src/jambonz_sdk/verb_registry.py +++ b/src/jambonz_sdk/verb_registry.py @@ -1,6 +1,6 @@ """Verb registry — the single source of truth for mapping spec entries to SDK methods. -This module defines which entries in ``specs.json`` are top-level verbs +This module defines which entries in the JSON Schema files are top-level verbs (as opposed to nested component types), their Python method names, docstrings, and any synonym/alias transforms. @@ -19,7 +19,7 @@ class VerbDef: """Definition of a single verb method on VerbBuilder. Attributes: - spec_name: The key in specs.json (e.g., ``"say"``, ``"sip:decline"``). + spec_name: The schema identifier (e.g., ``"say"``, ``"sip:decline"``). method_name: The Python method name (e.g., ``"say"``, ``"sip_decline"``). json_verb: The ``verb`` value in the output JSON. Defaults to ``spec_name``. doc: One-line docstring for the generated method. diff --git a/tests/unit/test_verb_builder.py b/tests/unit/test_verb_builder.py index d45851c..2c9e44f 100644 --- a/tests/unit/test_verb_builder.py +++ b/tests/unit/test_verb_builder.py @@ -2,13 +2,13 @@ These tests validate that: 1. Every verb in the registry has a corresponding method on VerbBuilder -2. Every method produces JSON output matching the specs.json contract +2. Every method produces JSON output matching the JSON Schema contract 3. Verb synonyms and injected properties work correctly 4. The builder's chaining and reset behavior is correct -5. Property names in output match specs.json exactly (camelCase preserved) +5. Property names in output match JSON Schema exactly (camelCase preserved) 6. The 'from' → 'from_' Python mapping works for the message verb -Tests are driven by specs.json — if a new property is added to a verb spec, +Tests are driven by JSON Schema — if a new property is added to a verb schema, these tests verify the SDK can pass it through correctly. """ @@ -59,10 +59,10 @@ def test_method_produces_correct_verb_name(self, verb_def): assert verbs[0]["verb"] == verb_def.json_verb -# ── Spec-driven: output properties must match specs.json ─────────── +# ── Spec-driven: output properties must match JSON Schema ───────── class TestVerbOutputMatchesSpec: - """For each verb, passing a property defined in specs.json must + """For each verb, passing a property defined in the JSON Schema must appear in the output JSON with the exact same key name.""" @pytest.mark.parametrize( @@ -312,7 +312,7 @@ def test_listen_with_bidirectional_audio(self): # ── Helpers ───────────────────────────────────────────────────────── def _dummy_value(spec_type): - """Generate a dummy value matching a specs.json type descriptor.""" + """Generate a dummy value matching a JSON Schema type descriptor.""" if isinstance(spec_type, str): if spec_type.startswith("#"): return {}