Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ htmlcov/
.coverage
*.egg
mcp.json
.vscode/
26 changes: 13 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ The SDK supports all 26+ jambonz verbs. Verb methods on VerbBuilder are **auto-g
### How verb generation works

1. `verb_registry.py` defines which spec entries are verbs, their Python method names, JSON verb names, and any synonym transforms
2. `verb_builder.py` loads `specs.json` at import time and generates a method for each registry entry
3. Each generated method has typed parameters, docstrings, and required-field documentation — all derived from the spec
2. `verb_builder.py` loads JSON Schema files from `schema/verbs/` at import time and generates a method for each registry entry
3. Each generated method has typed parameters, docstrings, and required-field documentation — all derived from the schema
4. To add a new verb: add one `VerbDef` entry in `verb_registry.py` — no other changes needed

### Verb List
Expand All @@ -80,24 +80,24 @@ Utility: `config`, `tag`, `dtmf`, `dub`, `message`, `alert`, `answer`, `leave`

SIP verbs use underscores: `sip_decline()`, `sip_request()`, `sip_refer()` (maps to `sip:decline`, `sip:request`, `sip:refer` in JSON).

## specs.json Management
## JSON Schema Management

The SDK bundles `specs.json` from `@jambonz/verb-specifications` (npm package / GitHub repo).
The file lives at `src/jambonz_sdk/specs.json` and is included in the wheel.
The SDK bundles JSON Schema files from `@jambonz/schema` (npm package / GitHub repo).
Schema files live at `src/jambonz_sdk/schema/` and are included in the wheel.

To update when the upstream spec changes:
To update when the upstream schema changes:
```bash
# From local sibling clone (default)
python scripts/sync_specs.py
# Download the pinned version
python scripts/sync_schema.py

# From GitHub main branch
python scripts/sync_specs.py --github
# Download a specific version
python scripts/sync_schema.py v0.1.1

# From a specific file
python scripts/sync_specs.py /path/to/specs.json
# Copy from a local directory
python scripts/sync_schema.py --local /path/to/schema
```

Source: https://github.com/jambonz/verb-specifications
Source: https://github.com/jambonz/schema

## AI Agent Support

Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ classifiers = [
]
dependencies = [
"aiohttp>=3.9",
"jsonschema>=4.20",
"referencing>=0.31",
"typing_extensions>=4.0; python_version < '3.11'",
]

[project.urls]
Expand All @@ -46,7 +49,7 @@ dev = [
packages = ["src/jambonz_sdk"]

[tool.hatch.build]
include = ["src/jambonz_sdk/**/*.py", "src/jambonz_sdk/**/*.pyi", "src/jambonz_sdk/specs.json"]
include = ["src/jambonz_sdk/**/*.py", "src/jambonz_sdk/**/*.pyi", "src/jambonz_sdk/schema/**/*.json"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
126 changes: 126 additions & 0 deletions scripts/sync_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Sync JSON Schema files from the @jambonz/schema repo.

Downloads verb, component, and callback schemas and bundles them into this
package. Run this whenever the upstream schema changes.

Usage:
# Download the pinned version (SCHEMA_VERSION below)
python scripts/sync_schema.py

# Download a specific version tag
python scripts/sync_schema.py v0.1.1

# Copy from a local directory instead
python scripts/sync_schema.py --local /path/to/schema
"""

import json
import shutil
import sys
import urllib.request
from pathlib import Path

# ── Pin the schema version here ──────────────────────────────────────
SCHEMA_VERSION = "v0.1.1"
# ────────────────────────────────────────────────────────────────────

DEST = Path(__file__).resolve().parent.parent / "src" / "jambonz_sdk" / "schema"
GITHUB_RAW = "https://raw.githubusercontent.com/jambonz/schema/{version}"

SUBDIRS = ["verbs", "components", "callbacks"]


def download_file(url: str, dest: Path) -> None:
urllib.request.urlretrieve(url, dest)


def sync_from_github(version: str) -> None:
base_url = GITHUB_RAW.format(version=version)

# Ensure destination exists
DEST.mkdir(parents=True, exist_ok=True)

# Download root schema
root_schema = "jambonz-app.schema.json"
print(f"Downloading {root_schema}...")
download_file(f"{base_url}/{root_schema}", DEST / root_schema)

# Download each subdirectory's index and files
total = 0
for subdir in SUBDIRS:
subdir_path = DEST / subdir
subdir_path.mkdir(exist_ok=True)

# GitHub doesn't have a directory listing API on raw, so we fetch
# the known file list from the root schema's $ref entries or use
# the GitHub API
api_url = (
f"https://api.github.com/repos/jambonz/schema/contents/{subdir}"
f"?ref={version}"
)
print(f"Fetching {subdir}/ file list...")
req = urllib.request.Request(api_url, headers={"Accept": "application/json"})
with urllib.request.urlopen(req) as resp:
files = json.loads(resp.read())

schema_files = [f["name"] for f in files if f["name"].endswith(".schema.json")]
for fname in schema_files:
download_file(f"{base_url}/{subdir}/{fname}", subdir_path / fname)
total += 1

print(f"Downloaded {total} schema files + root schema → {DEST} (version {version})")


def sync_from_local(src: Path) -> None:
if not src.is_dir():
print(f"Error: {src} is not a directory")
sys.exit(1)

# Clean and recreate destination
if DEST.exists():
shutil.rmtree(DEST)
DEST.mkdir(parents=True, exist_ok=True)

# Copy root schema
root_schema = src / "jambonz-app.schema.json"
if root_schema.exists():
shutil.copy2(root_schema, DEST / "jambonz-app.schema.json")

# Copy subdirectories
total = 0
for subdir in SUBDIRS:
src_dir = src / subdir
if src_dir.is_dir():
dest_dir = DEST / subdir
dest_dir.mkdir(exist_ok=True)
for f in src_dir.glob("*.schema.json"):
shutil.copy2(f, dest_dir / f.name)
total += 1

print(f"Copied {total} schema files + root schema from {src} → {DEST}")


def main() -> None:
if len(sys.argv) > 1:
arg = sys.argv[1]
if arg == "--local":
if len(sys.argv) < 3:
print("Usage: python scripts/sync_schema.py --local /path/to/schema")
sys.exit(1)
sync_from_local(Path(sys.argv[2]))
elif arg.startswith("v"):
sync_from_github(arg)
else:
print(f"Unknown argument: {arg}")
print("Usage:")
print(" python scripts/sync_schema.py # download pinned version")
print(" python scripts/sync_schema.py v0.1.1 # download specific version")
print(" python scripts/sync_schema.py --local /path/to/schema")
sys.exit(1)
else:
sync_from_github(SCHEMA_VERSION)


if __name__ == "__main__":
main()
78 changes: 0 additions & 78 deletions scripts/sync_specs.py

This file was deleted.

2 changes: 2 additions & 0 deletions src/jambonz_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ def handle_session(session):

# Re-export main classes for convenience
from jambonz_sdk.client import JambonzClient
from jambonz_sdk.validator import JambonzValidator
from jambonz_sdk.verb_builder import VerbBuilder
from jambonz_sdk.webhook import WebhookResponse

__all__ = [
"JambonzClient",
"JambonzValidator",
"VerbBuilder",
"WebhookResponse",
"__version__",
Expand Down
50 changes: 50 additions & 0 deletions src/jambonz_sdk/schema/callbacks/amd.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://jambonz.org/schema/callbacks/amd",
"title": "AMD ActionHook Payload",
"description": "Payload sent to the AMD actionHook when an answering machine detection event occurs. Multiple events may fire during a single call (e.g. amd_machine_detected followed by amd_machine_stopped_speaking or amd_tone_detected).",
"allOf": [{ "$ref": "base" }],
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The AMD event type. IMPORTANT: This field is 'type', NOT 'amd_type'.",
"enum": [
"amd_human_detected",
"amd_machine_detected",
"amd_no_speech_detected",
"amd_decision_timeout",
"amd_machine_stopped_speaking",
"amd_tone_detected",
"amd_tone_timeout",
"amd_error",
"amd_stopped"
]
},
"reason": {
"type": "string",
"description": "Reason for the detection result (e.g. 'short greeting', 'long greeting', 'hint', 'digit count'). Present on amd_human_detected and amd_machine_detected events."
},
"greeting": {
"type": "string",
"description": "The transcribed greeting text. Present on amd_human_detected and amd_machine_detected events."
},
"hint": {
"type": "string",
"description": "The voicemail hint that matched, if detection was triggered by hint matching."
},
"language": {
"type": "string",
"description": "Language code from the transcription (e.g. 'en-US'). Present on amd_human_detected and amd_machine_detected events."
},
"frequency": {
"type": "number",
"description": "Frequency of the detected beep in Hz. Present on amd_tone_detected events."
},
"variance": {
"type": "number",
"description": "Frequency variance of the detected beep. Present on amd_tone_detected events."
}
},
"required": ["type"]
}
29 changes: 29 additions & 0 deletions src/jambonz_sdk/schema/callbacks/base.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://jambonz.org/schema/callbacks/base",
"title": "ActionHook Base Payload",
"description": "Common fields present in every actionHook callback payload. All verb-specific callback schemas extend this base.",
"type": "object",
"properties": {
"call_sid": { "type": "string", "description": "Unique identifier for this call." },
"account_sid": { "type": "string", "description": "Account identifier." },
"application_sid": { "type": "string", "description": "Application identifier." },
"direction": { "type": "string", "enum": ["inbound", "outbound"], "description": "Call direction." },
"from": { "type": "string", "description": "Caller phone number or SIP URI." },
"to": { "type": "string", "description": "Called phone number or SIP URI." },
"call_id": { "type": "string", "description": "SIP Call-ID." },
"sbc_callid": { "type": "string", "description": "SBC-level Call-ID." },
"call_status": {
"type": "string",
"enum": ["trying", "ringing", "early-media", "in-progress", "completed", "failed", "busy", "no-answer", "queued"],
"description": "Current call state."
},
"sip_status": { "type": "integer", "description": "SIP response code (e.g. 200, 486)." },
"sip_reason": { "type": "string", "description": "SIP reason phrase (e.g. 'OK', 'Busy Here')." },
"trace_id": { "type": "string", "description": "Distributed tracing identifier for correlating logs across jambonz components." },
"originating_sip_ip": { "type": "string", "description": "IP address of the originating SIP trunk." },
"originating_sip_trunk_name": { "type": "string", "description": "Name of the originating SIP trunk as configured in jambonz." },
"api_base_url": { "type": "string", "description": "jambonz REST API base URL. Use this for mid-call control via the REST API." }
},
"additionalProperties": true
}
Loading
Loading