ccp-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccp_sdk-0.1.0/LICENSE +21 -0
- ccp_sdk-0.1.0/PKG-INFO +130 -0
- ccp_sdk-0.1.0/README.md +83 -0
- ccp_sdk-0.1.0/pyproject.toml +44 -0
- ccp_sdk-0.1.0/setup.cfg +4 -0
- ccp_sdk-0.1.0/src/ccp_sdk/__init__.py +11 -0
- ccp_sdk-0.1.0/src/ccp_sdk/client.py +119 -0
- ccp_sdk-0.1.0/src/ccp_sdk/errors.py +18 -0
- ccp_sdk-0.1.0/src/ccp_sdk/messages.py +70 -0
- ccp_sdk-0.1.0/src/ccp_sdk/py.typed +0 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/__init__.py +0 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/ccp-message.json +15 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/error.json +37 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/hello-ack.json +41 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/hello.json +72 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/session-end.json +21 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/session-ended.json +21 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/session-start.json +21 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/session-started.json +23 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/turn-result.json +41 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas/turn.json +31 -0
- ccp_sdk-0.1.0/src/ccp_sdk/schemas.py +67 -0
- ccp_sdk-0.1.0/src/ccp_sdk/validator.py +67 -0
- ccp_sdk-0.1.0/src/ccp_sdk.egg-info/PKG-INFO +130 -0
- ccp_sdk-0.1.0/src/ccp_sdk.egg-info/SOURCES.txt +26 -0
- ccp_sdk-0.1.0/src/ccp_sdk.egg-info/dependency_links.txt +1 -0
- ccp_sdk-0.1.0/src/ccp_sdk.egg-info/requires.txt +2 -0
- ccp_sdk-0.1.0/src/ccp_sdk.egg-info/top_level.txt +1 -0
ccp_sdk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonathan Hidalgo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ccp_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ccp-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reference Python SDK for Cortex Capsule Protocol (CCP)
|
|
5
|
+
Author: CCP contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Jonathan Hidalgo
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/<your-org>/<your-repo>
|
|
29
|
+
Project-URL: Repository, https://github.com/<your-org>/<your-repo>
|
|
30
|
+
Keywords: ccp,protocol,iot,robotics,gateway
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
41
|
+
Requires-Python: >=3.9
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: httpx>=0.27
|
|
45
|
+
Requires-Dist: jsonschema>=4.21
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# CCP
|
|
49
|
+
Cortex Capsule Protocol (CCP) — v0.1 Draft
|
|
50
|
+
|
|
51
|
+
CCP standardizes communication between devices (robots, microcontrollers, gateways) and Cortex Capsule servers (local or cloud brain runtime).
|
|
52
|
+
|
|
53
|
+
## Contents
|
|
54
|
+
- Core specification
|
|
55
|
+
- HTTP mapping
|
|
56
|
+
- JSON schemas
|
|
57
|
+
- Message examples
|
|
58
|
+
|
|
59
|
+
## Structure
|
|
60
|
+
- [docs/](docs/README.md)
|
|
61
|
+
- [schemas/](schemas)
|
|
62
|
+
- [examples/](examples)
|
|
63
|
+
|
|
64
|
+
## Get started
|
|
65
|
+
1. Read the core spec: [docs/spec/ccp-core.md](docs/spec/ccp-core.md)
|
|
66
|
+
2. Check HTTP mapping: [docs/spec/http-binding.md](docs/spec/http-binding.md)
|
|
67
|
+
3. See schemas: [docs/spec/message-schemas.md](docs/spec/message-schemas.md)
|
|
68
|
+
4. See examples: [examples/](examples)
|
|
69
|
+
|
|
70
|
+
## How to use CCP
|
|
71
|
+
- This repo is the **specification**, not a dependency to install.
|
|
72
|
+
- Projects should depend on a **CCP SDK** (language/runtime specific) that implements the protocol.
|
|
73
|
+
- If you need JSON schemas in production, the SDK should bundle them or reference a published schema package.
|
|
74
|
+
|
|
75
|
+
## Python SDK (reference)
|
|
76
|
+
|
|
77
|
+
This repository now includes a minimal reference Python SDK that you can install with pip.
|
|
78
|
+
|
|
79
|
+
### Install (local dev)
|
|
80
|
+
From the repo root:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python -m pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Install (from PyPI)
|
|
87
|
+
After you publish it:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python -m pip install ccp-sdk
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Usage
|
|
94
|
+
See a runnable example in [examples/python_client.py](examples/python_client.py).
|
|
95
|
+
|
|
96
|
+
If your ESP32 sends data to a Python gateway API, see: [examples/esp32_gateway_api/](examples/esp32_gateway_api/).
|
|
97
|
+
|
|
98
|
+
## ESP32 note
|
|
99
|
+
ESP32 (Arduino/ESP-IDF) does not run `pip` packages directly.
|
|
100
|
+
|
|
101
|
+
Typical architecture:
|
|
102
|
+
- ESP32 handles hardware (mic/speaker/LED/display) and sends events/audio to a gateway over Wi-Fi/serial.
|
|
103
|
+
- The gateway (Raspberry Pi / PC) runs the Python CCP SDK and talks to the CCP server over HTTP.
|
|
104
|
+
|
|
105
|
+
Practical mapping:
|
|
106
|
+
- Mic (I2S) -> capture audio -> upload to `/v1/media/audio` -> send `turn` with `audio_ref`
|
|
107
|
+
- Speaker (I2S) <- receive `turn_result.payload.reply.audio_ref` -> download audio -> play
|
|
108
|
+
- LED strip / display <- drive UI based on `turn_result.payload.state`, `safety`, or `actions`
|
|
109
|
+
|
|
110
|
+
## SDKs (planned)
|
|
111
|
+
- `ccp-python` — reference SDK for capsules/gateways
|
|
112
|
+
- `ccp-esp32` — thin device SDK for microcontrollers
|
|
113
|
+
|
|
114
|
+
## Version
|
|
115
|
+
CCP v0.1 (Draft)
|
|
116
|
+
|
|
117
|
+
## Roadmap
|
|
118
|
+
See [docs/roadmap.md](docs/roadmap.md).
|
|
119
|
+
|
|
120
|
+
## Community
|
|
121
|
+
Discord: invite link TBD.
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to propose changes and submit PRs.
|
|
125
|
+
|
|
126
|
+
## Code of Conduct
|
|
127
|
+
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
128
|
+
|
|
129
|
+
## Security
|
|
130
|
+
See [SECURITY.md](SECURITY.md).
|
ccp_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# CCP
|
|
2
|
+
Cortex Capsule Protocol (CCP) — v0.1 Draft
|
|
3
|
+
|
|
4
|
+
CCP standardizes communication between devices (robots, microcontrollers, gateways) and Cortex Capsule servers (local or cloud brain runtime).
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
- Core specification
|
|
8
|
+
- HTTP mapping
|
|
9
|
+
- JSON schemas
|
|
10
|
+
- Message examples
|
|
11
|
+
|
|
12
|
+
## Structure
|
|
13
|
+
- [docs/](docs/README.md)
|
|
14
|
+
- [schemas/](schemas)
|
|
15
|
+
- [examples/](examples)
|
|
16
|
+
|
|
17
|
+
## Get started
|
|
18
|
+
1. Read the core spec: [docs/spec/ccp-core.md](docs/spec/ccp-core.md)
|
|
19
|
+
2. Check HTTP mapping: [docs/spec/http-binding.md](docs/spec/http-binding.md)
|
|
20
|
+
3. See schemas: [docs/spec/message-schemas.md](docs/spec/message-schemas.md)
|
|
21
|
+
4. See examples: [examples/](examples)
|
|
22
|
+
|
|
23
|
+
## How to use CCP
|
|
24
|
+
- This repo is the **specification**, not a dependency to install.
|
|
25
|
+
- Projects should depend on a **CCP SDK** (language/runtime specific) that implements the protocol.
|
|
26
|
+
- If you need JSON schemas in production, the SDK should bundle them or reference a published schema package.
|
|
27
|
+
|
|
28
|
+
## Python SDK (reference)
|
|
29
|
+
|
|
30
|
+
This repository now includes a minimal reference Python SDK that you can install with pip.
|
|
31
|
+
|
|
32
|
+
### Install (local dev)
|
|
33
|
+
From the repo root:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Install (from PyPI)
|
|
40
|
+
After you publish it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python -m pip install ccp-sdk
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Usage
|
|
47
|
+
See a runnable example in [examples/python_client.py](examples/python_client.py).
|
|
48
|
+
|
|
49
|
+
If your ESP32 sends data to a Python gateway API, see: [examples/esp32_gateway_api/](examples/esp32_gateway_api/).
|
|
50
|
+
|
|
51
|
+
## ESP32 note
|
|
52
|
+
ESP32 (Arduino/ESP-IDF) does not run `pip` packages directly.
|
|
53
|
+
|
|
54
|
+
Typical architecture:
|
|
55
|
+
- ESP32 handles hardware (mic/speaker/LED/display) and sends events/audio to a gateway over Wi-Fi/serial.
|
|
56
|
+
- The gateway (Raspberry Pi / PC) runs the Python CCP SDK and talks to the CCP server over HTTP.
|
|
57
|
+
|
|
58
|
+
Practical mapping:
|
|
59
|
+
- Mic (I2S) -> capture audio -> upload to `/v1/media/audio` -> send `turn` with `audio_ref`
|
|
60
|
+
- Speaker (I2S) <- receive `turn_result.payload.reply.audio_ref` -> download audio -> play
|
|
61
|
+
- LED strip / display <- drive UI based on `turn_result.payload.state`, `safety`, or `actions`
|
|
62
|
+
|
|
63
|
+
## SDKs (planned)
|
|
64
|
+
- `ccp-python` — reference SDK for capsules/gateways
|
|
65
|
+
- `ccp-esp32` — thin device SDK for microcontrollers
|
|
66
|
+
|
|
67
|
+
## Version
|
|
68
|
+
CCP v0.1 (Draft)
|
|
69
|
+
|
|
70
|
+
## Roadmap
|
|
71
|
+
See [docs/roadmap.md](docs/roadmap.md).
|
|
72
|
+
|
|
73
|
+
## Community
|
|
74
|
+
Discord: invite link TBD.
|
|
75
|
+
|
|
76
|
+
## Contributing
|
|
77
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to propose changes and submit PRs.
|
|
78
|
+
|
|
79
|
+
## Code of Conduct
|
|
80
|
+
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
81
|
+
|
|
82
|
+
## Security
|
|
83
|
+
See [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ccp-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reference Python SDK for Cortex Capsule Protocol (CCP)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "CCP contributors" }
|
|
13
|
+
]
|
|
14
|
+
license = { file = "LICENSE" }
|
|
15
|
+
keywords = ["ccp", "protocol", "iot", "robotics", "gateway"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.27",
|
|
30
|
+
"jsonschema>=4.21",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/<your-org>/<your-repo>"
|
|
35
|
+
Repository = "https://github.com/<your-org>/<your-repo>"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
package-dir = {"" = "src"}
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
ccp_sdk = ["py.typed", "schemas/*.json"]
|
ccp_sdk-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .errors import CCPHTTPError
|
|
8
|
+
from .messages import hello as hello_msg
|
|
9
|
+
from .messages import session_end as session_end_msg
|
|
10
|
+
from .messages import session_start as session_start_msg
|
|
11
|
+
from .messages import turn_audio_ref as turn_audio_ref_msg
|
|
12
|
+
from .messages import turn_text as turn_text_msg
|
|
13
|
+
from .validator import validate_message
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CCPClient:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
base_url: str,
|
|
21
|
+
device_token: Optional[str] = None,
|
|
22
|
+
timeout_s: float = 10.0,
|
|
23
|
+
validate: bool = True,
|
|
24
|
+
):
|
|
25
|
+
self._base_url = base_url.rstrip("/")
|
|
26
|
+
self._device_token = device_token
|
|
27
|
+
self._timeout_s = timeout_s
|
|
28
|
+
self._validate = validate
|
|
29
|
+
self._client = httpx.Client(timeout=timeout_s)
|
|
30
|
+
|
|
31
|
+
def close(self) -> None:
|
|
32
|
+
self._client.close()
|
|
33
|
+
|
|
34
|
+
def __enter__(self) -> "CCPClient":
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
38
|
+
self.close()
|
|
39
|
+
|
|
40
|
+
def _headers(self) -> Dict[str, str]:
|
|
41
|
+
headers = {"Content-Type": "application/json"}
|
|
42
|
+
if self._device_token:
|
|
43
|
+
headers["Authorization"] = f"Device {self._device_token}"
|
|
44
|
+
return headers
|
|
45
|
+
|
|
46
|
+
def _post_ccp(self, path: str, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
47
|
+
if self._validate:
|
|
48
|
+
validate_message(message)
|
|
49
|
+
|
|
50
|
+
url = f"{self._base_url}{path}"
|
|
51
|
+
resp = self._client.post(url, headers=self._headers(), json=message)
|
|
52
|
+
if resp.status_code < 200 or resp.status_code >= 300:
|
|
53
|
+
body: Any
|
|
54
|
+
try:
|
|
55
|
+
body = resp.json()
|
|
56
|
+
except Exception:
|
|
57
|
+
body = resp.text
|
|
58
|
+
raise CCPHTTPError(resp.status_code, body)
|
|
59
|
+
|
|
60
|
+
data = resp.json()
|
|
61
|
+
if self._validate and isinstance(data, dict):
|
|
62
|
+
validate_message(data)
|
|
63
|
+
return data
|
|
64
|
+
|
|
65
|
+
def hello(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
device_id: str,
|
|
69
|
+
device_token: str,
|
|
70
|
+
firmware: str,
|
|
71
|
+
capabilities: Dict[str, Any],
|
|
72
|
+
locale: Optional[str] = None,
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
msg = hello_msg(
|
|
75
|
+
device_id=device_id,
|
|
76
|
+
device_token=device_token,
|
|
77
|
+
firmware=firmware,
|
|
78
|
+
capabilities=capabilities,
|
|
79
|
+
locale=locale,
|
|
80
|
+
)
|
|
81
|
+
return self._post_ccp("/ccp/hello", msg)
|
|
82
|
+
|
|
83
|
+
def session_start(self, *, device_id: str) -> Dict[str, Any]:
|
|
84
|
+
return self._post_ccp("/ccp/session/start", session_start_msg(device_id=device_id))
|
|
85
|
+
|
|
86
|
+
def turn_text(self, *, session_id: str, text: str) -> Dict[str, Any]:
|
|
87
|
+
return self._post_ccp("/ccp/turn", turn_text_msg(session_id=session_id, text=text))
|
|
88
|
+
|
|
89
|
+
def turn_audio_ref(self, *, session_id: str, audio_ref: str) -> Dict[str, Any]:
|
|
90
|
+
return self._post_ccp(
|
|
91
|
+
"/ccp/turn", turn_audio_ref_msg(session_id=session_id, audio_ref=audio_ref)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def session_end(self, *, session_id: str) -> Dict[str, Any]:
|
|
95
|
+
return self._post_ccp("/ccp/session/end", session_end_msg(session_id=session_id))
|
|
96
|
+
|
|
97
|
+
def upload_audio(self, *, audio_bytes: bytes, content_type: str = "audio/wav") -> str:
|
|
98
|
+
url = f"{self._base_url}/v1/media/audio"
|
|
99
|
+
headers = {k: v for k, v in self._headers().items() if k.lower() != "content-type"}
|
|
100
|
+
headers["Content-Type"] = content_type
|
|
101
|
+
resp = self._client.post(
|
|
102
|
+
url,
|
|
103
|
+
headers=headers,
|
|
104
|
+
content=audio_bytes,
|
|
105
|
+
)
|
|
106
|
+
if resp.status_code < 200 or resp.status_code >= 300:
|
|
107
|
+
body: Any
|
|
108
|
+
try:
|
|
109
|
+
body = resp.json()
|
|
110
|
+
except Exception:
|
|
111
|
+
body = resp.text
|
|
112
|
+
raise CCPHTTPError(resp.status_code, body)
|
|
113
|
+
|
|
114
|
+
data = resp.json()
|
|
115
|
+
audio_ref = data.get("audio_ref") if isinstance(data, dict) else None
|
|
116
|
+
if not isinstance(audio_ref, str) or not audio_ref:
|
|
117
|
+
raise CCPHTTPError(resp.status_code, data)
|
|
118
|
+
return audio_ref
|
|
119
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CCPError(Exception):
|
|
5
|
+
"""Base error for the CCP SDK."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CCPSchemaError(CCPError):
|
|
9
|
+
"""Raised when a message fails JSON Schema validation."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CCPHTTPError(CCPError):
|
|
13
|
+
"""Raised when an HTTP call fails (non-2xx or invalid payload)."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, status_code: int, body: object | None = None):
|
|
16
|
+
super().__init__(f"CCP HTTP error: {status_code}")
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.body = body
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def now_ts() -> str:
|
|
9
|
+
return datetime.now(timezone.utc).isoformat()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def envelope(
|
|
13
|
+
*,
|
|
14
|
+
type: str,
|
|
15
|
+
payload: Dict[str, Any],
|
|
16
|
+
ccp_version: str = "0.1",
|
|
17
|
+
id: Optional[str] = None,
|
|
18
|
+
ts: Optional[str] = None,
|
|
19
|
+
) -> Dict[str, Any]:
|
|
20
|
+
return {
|
|
21
|
+
"ccp_version": ccp_version,
|
|
22
|
+
"type": type,
|
|
23
|
+
"id": id or str(uuid4()),
|
|
24
|
+
"ts": ts or now_ts(),
|
|
25
|
+
"payload": payload,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def hello(
|
|
30
|
+
*,
|
|
31
|
+
device_id: str,
|
|
32
|
+
device_token: str,
|
|
33
|
+
firmware: str,
|
|
34
|
+
capabilities: Dict[str, Any],
|
|
35
|
+
locale: Optional[str] = None,
|
|
36
|
+
) -> Dict[str, Any]:
|
|
37
|
+
payload: Dict[str, Any] = {
|
|
38
|
+
"device_id": device_id,
|
|
39
|
+
"device_token": device_token,
|
|
40
|
+
"firmware": firmware,
|
|
41
|
+
"capabilities": capabilities,
|
|
42
|
+
}
|
|
43
|
+
if locale is not None:
|
|
44
|
+
payload["locale"] = locale
|
|
45
|
+
return envelope(type="hello", payload=payload)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def session_start(*, device_id: str) -> Dict[str, Any]:
|
|
49
|
+
return envelope(type="session_start", payload={"device_id": device_id})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def turn_text(*, session_id: str, text: str) -> Dict[str, Any]:
|
|
53
|
+
return envelope(
|
|
54
|
+
type="turn",
|
|
55
|
+
payload={"session_id": session_id, "input": {"kind": "text", "text": text}},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def turn_audio_ref(*, session_id: str, audio_ref: str) -> Dict[str, Any]:
|
|
60
|
+
return envelope(
|
|
61
|
+
type="turn",
|
|
62
|
+
payload={
|
|
63
|
+
"session_id": session_id,
|
|
64
|
+
"input": {"kind": "audio_ref", "audio_ref": audio_ref},
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def session_end(*, session_id: str) -> Dict[str, Any]:
|
|
70
|
+
return envelope(type="session_end", payload={"session_id": session_id})
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/ccp-message.json",
|
|
4
|
+
"title": "CCP Message Envelope",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["ccp_version", "type", "id", "payload"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"ccp_version": { "type": "string", "enum": ["0.1"] },
|
|
9
|
+
"type": { "type": "string" },
|
|
10
|
+
"id": { "type": "string", "minLength": 1 },
|
|
11
|
+
"ts": { "type": "string", "format": "date-time" },
|
|
12
|
+
"payload": { "type": "object" }
|
|
13
|
+
},
|
|
14
|
+
"additionalProperties": false
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/error.json",
|
|
4
|
+
"title": "CCP error",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "error" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["code", "message"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"code": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": [
|
|
17
|
+
"INVALID_REQUEST",
|
|
18
|
+
"UNAUTHORIZED",
|
|
19
|
+
"FORBIDDEN",
|
|
20
|
+
"NOT_FOUND",
|
|
21
|
+
"CONFLICT",
|
|
22
|
+
"RATE_LIMIT",
|
|
23
|
+
"SERVER_BUSY",
|
|
24
|
+
"MEDIA_TOO_LARGE",
|
|
25
|
+
"UNSUPPORTED_CAPABILITY"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"message": { "type": "string" },
|
|
29
|
+
"retry_after_ms": { "type": "integer", "minimum": 0 },
|
|
30
|
+
"request_id": { "type": "string" }
|
|
31
|
+
},
|
|
32
|
+
"additionalProperties": true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/hello-ack.json",
|
|
4
|
+
"title": "CCP hello_ack",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "hello_ack" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["accepted", "device_id"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"accepted": { "type": "boolean" },
|
|
15
|
+
"device_id": { "type": "string" },
|
|
16
|
+
"session_supported": { "type": "boolean" },
|
|
17
|
+
"negotiated": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"audio_codec": { "type": "string" },
|
|
21
|
+
"sample_rate": { "type": "integer" },
|
|
22
|
+
"max_audio_seconds": { "type": "number" },
|
|
23
|
+
"preferred_transport": { "type": "string" }
|
|
24
|
+
},
|
|
25
|
+
"additionalProperties": true
|
|
26
|
+
},
|
|
27
|
+
"limits": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"turns_per_min": { "type": "integer" },
|
|
31
|
+
"tts_seconds_per_min": { "type": "integer" }
|
|
32
|
+
},
|
|
33
|
+
"additionalProperties": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/hello.json",
|
|
4
|
+
"title": "CCP hello",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "hello" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["device_id", "device_token", "firmware", "capabilities"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"device_id": { "type": "string" },
|
|
15
|
+
"device_token": { "type": "string" },
|
|
16
|
+
"firmware": { "type": "string" },
|
|
17
|
+
"capabilities": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"audio_in": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"codecs": { "type": "array", "items": { "type": "string" } },
|
|
24
|
+
"sample_rates": { "type": "array", "items": { "type": "integer" } },
|
|
25
|
+
"channels": { "type": "integer", "minimum": 1 }
|
|
26
|
+
},
|
|
27
|
+
"additionalProperties": true
|
|
28
|
+
},
|
|
29
|
+
"audio_out": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"codecs": { "type": "array", "items": { "type": "string" } },
|
|
33
|
+
"sample_rates": { "type": "array", "items": { "type": "integer" } },
|
|
34
|
+
"channels": { "type": "integer", "minimum": 1 }
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": true
|
|
37
|
+
},
|
|
38
|
+
"io": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"button": { "type": "boolean" },
|
|
42
|
+
"led": { "type": "boolean" },
|
|
43
|
+
"screen": { "type": "boolean" }
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": true
|
|
46
|
+
},
|
|
47
|
+
"actions": { "type": "array", "items": { "type": "string" } },
|
|
48
|
+
"streaming": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": { "supported": { "type": "boolean" } },
|
|
51
|
+
"additionalProperties": true
|
|
52
|
+
},
|
|
53
|
+
"network": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": {
|
|
56
|
+
"type": { "type": "string" },
|
|
57
|
+
"metered": { "type": "boolean" }
|
|
58
|
+
},
|
|
59
|
+
"additionalProperties": true
|
|
60
|
+
},
|
|
61
|
+
"extensions": { "type": "array", "items": { "type": "string" } }
|
|
62
|
+
},
|
|
63
|
+
"additionalProperties": true
|
|
64
|
+
},
|
|
65
|
+
"locale": { "type": "string" }
|
|
66
|
+
},
|
|
67
|
+
"additionalProperties": true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/session-end.json",
|
|
4
|
+
"title": "CCP session_end",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "session_end" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["session_id"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"session_id": { "type": "string" }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/session-ended.json",
|
|
4
|
+
"title": "CCP session_ended",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "session_ended" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["session_id"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"session_id": { "type": "string" }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/session-start.json",
|
|
4
|
+
"title": "CCP session_start",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "session_start" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["device_id"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"device_id": { "type": "string" }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/session-started.json",
|
|
4
|
+
"title": "CCP session_started",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "session_started" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["session_id"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"session_id": { "type": "string" },
|
|
15
|
+
"active_capsule_id": { "type": "string" },
|
|
16
|
+
"kid_safe_applied": { "type": "boolean" }
|
|
17
|
+
},
|
|
18
|
+
"additionalProperties": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/turn-result.json",
|
|
4
|
+
"title": "CCP turn_result",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "turn_result" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["session_id"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"session_id": { "type": "string" },
|
|
15
|
+
"reply": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"text": { "type": "string" },
|
|
19
|
+
"audio_ref": { "type": "string" }
|
|
20
|
+
},
|
|
21
|
+
"additionalProperties": true
|
|
22
|
+
},
|
|
23
|
+
"state": { "type": "object", "additionalProperties": true },
|
|
24
|
+
"safety": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"blocked": { "type": "boolean" },
|
|
28
|
+
"kid_safe_applied": { "type": "boolean" },
|
|
29
|
+
"reason": { "type": ["string", "null"] }
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": true
|
|
32
|
+
},
|
|
33
|
+
"actions": { "type": "array", "items": { "type": "object" } },
|
|
34
|
+
"meta": { "type": "object", "additionalProperties": true }
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ccp.dev/schemas/turn.json",
|
|
4
|
+
"title": "CCP turn",
|
|
5
|
+
"allOf": [
|
|
6
|
+
{ "$ref": "ccp-message.json" },
|
|
7
|
+
{
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": { "const": "turn" },
|
|
10
|
+
"payload": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["session_id", "input"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"session_id": { "type": "string" },
|
|
15
|
+
"input": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": ["kind"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"kind": { "type": "string", "enum": ["text", "audio_ref"] },
|
|
20
|
+
"text": { "type": "string" },
|
|
21
|
+
"audio_ref": { "type": "string" }
|
|
22
|
+
},
|
|
23
|
+
"additionalProperties": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from jsonschema import Draft202012Validator
|
|
8
|
+
from jsonschema.validators import RefResolver
|
|
9
|
+
|
|
10
|
+
from .errors import CCPSchemaError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_MESSAGE_TYPE_TO_SCHEMA = {
|
|
14
|
+
"hello": "hello.json",
|
|
15
|
+
"hello_ack": "hello-ack.json",
|
|
16
|
+
"session_start": "session-start.json",
|
|
17
|
+
"session_started": "session-started.json",
|
|
18
|
+
"turn": "turn.json",
|
|
19
|
+
"turn_result": "turn-result.json",
|
|
20
|
+
"session_end": "session-end.json",
|
|
21
|
+
"session_ended": "session-ended.json",
|
|
22
|
+
"error": "error.json",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_all_schemas() -> Dict[str, Dict[str, Any]]:
|
|
27
|
+
schema_pkg = resources.files("ccp_sdk.schemas")
|
|
28
|
+
schemas: Dict[str, Dict[str, Any]] = {}
|
|
29
|
+
for entry in schema_pkg.iterdir():
|
|
30
|
+
if entry.name.endswith(".json"):
|
|
31
|
+
schemas[entry.name] = json.loads(entry.read_text(encoding="utf-8"))
|
|
32
|
+
return schemas
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_ALL_SCHEMAS = _load_all_schemas()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _make_resolver(schema: Dict[str, Any]) -> RefResolver:
|
|
39
|
+
store: Dict[str, Any] = {}
|
|
40
|
+
for filename, schema_obj in _ALL_SCHEMAS.items():
|
|
41
|
+
store[filename] = schema_obj
|
|
42
|
+
schema_id = schema_obj.get("$id")
|
|
43
|
+
if isinstance(schema_id, str):
|
|
44
|
+
store[schema_id] = schema_obj
|
|
45
|
+
|
|
46
|
+
return RefResolver.from_schema(schema, store=store)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_message(message: Dict[str, Any]) -> None:
|
|
50
|
+
msg_type = message.get("type")
|
|
51
|
+
if not isinstance(msg_type, str) or not msg_type:
|
|
52
|
+
raise CCPSchemaError("Missing or invalid message.type")
|
|
53
|
+
|
|
54
|
+
schema_filename = _MESSAGE_TYPE_TO_SCHEMA.get(msg_type)
|
|
55
|
+
if schema_filename is None:
|
|
56
|
+
raise CCPSchemaError(f"Unknown message type: {msg_type}")
|
|
57
|
+
|
|
58
|
+
schema = _ALL_SCHEMAS.get(schema_filename)
|
|
59
|
+
if schema is None:
|
|
60
|
+
raise CCPSchemaError(f"Schema not bundled: {schema_filename}")
|
|
61
|
+
|
|
62
|
+
validator = Draft202012Validator(schema, resolver=_make_resolver(schema))
|
|
63
|
+
errors = sorted(validator.iter_errors(message), key=lambda e: e.path)
|
|
64
|
+
if errors:
|
|
65
|
+
first = errors[0]
|
|
66
|
+
loc = "/".join(str(p) for p in first.path) or "<root>"
|
|
67
|
+
raise CCPSchemaError(f"Invalid CCP message at {loc}: {first.message}")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from jsonschema import Draft202012Validator
|
|
8
|
+
from jsonschema.validators import RefResolver
|
|
9
|
+
|
|
10
|
+
from .errors import CCPSchemaError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_MESSAGE_TYPE_TO_SCHEMA = {
|
|
14
|
+
"hello": "hello.json",
|
|
15
|
+
"hello_ack": "hello-ack.json",
|
|
16
|
+
"session_start": "session-start.json",
|
|
17
|
+
"session_started": "session-started.json",
|
|
18
|
+
"turn": "turn.json",
|
|
19
|
+
"turn_result": "turn-result.json",
|
|
20
|
+
"session_end": "session-end.json",
|
|
21
|
+
"session_ended": "session-ended.json",
|
|
22
|
+
"error": "error.json",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_all_schemas() -> Dict[str, Dict[str, Any]]:
|
|
27
|
+
schema_pkg = resources.files("ccp_sdk.schemas")
|
|
28
|
+
schemas: Dict[str, Dict[str, Any]] = {}
|
|
29
|
+
for entry in schema_pkg.iterdir():
|
|
30
|
+
if entry.name.endswith(".json"):
|
|
31
|
+
schemas[entry.name] = json.loads(entry.read_text(encoding="utf-8"))
|
|
32
|
+
return schemas
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_ALL_SCHEMAS = _load_all_schemas()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _make_resolver(schema: Dict[str, Any]) -> RefResolver:
|
|
39
|
+
store: Dict[str, Any] = {}
|
|
40
|
+
for filename, schema_obj in _ALL_SCHEMAS.items():
|
|
41
|
+
store[filename] = schema_obj
|
|
42
|
+
schema_id = schema_obj.get("$id")
|
|
43
|
+
if isinstance(schema_id, str):
|
|
44
|
+
store[schema_id] = schema_obj
|
|
45
|
+
|
|
46
|
+
return RefResolver.from_schema(schema, store=store)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_message(message: Dict[str, Any]) -> None:
|
|
50
|
+
msg_type = message.get("type")
|
|
51
|
+
if not isinstance(msg_type, str) or not msg_type:
|
|
52
|
+
raise CCPSchemaError("Missing or invalid message.type")
|
|
53
|
+
|
|
54
|
+
schema_filename = _MESSAGE_TYPE_TO_SCHEMA.get(msg_type)
|
|
55
|
+
if schema_filename is None:
|
|
56
|
+
raise CCPSchemaError(f"Unknown message type: {msg_type}")
|
|
57
|
+
|
|
58
|
+
schema = _ALL_SCHEMAS.get(schema_filename)
|
|
59
|
+
if schema is None:
|
|
60
|
+
raise CCPSchemaError(f"Schema not bundled: {schema_filename}")
|
|
61
|
+
|
|
62
|
+
validator = Draft202012Validator(schema, resolver=_make_resolver(schema))
|
|
63
|
+
errors = sorted(validator.iter_errors(message), key=lambda e: e.path)
|
|
64
|
+
if errors:
|
|
65
|
+
first = errors[0]
|
|
66
|
+
loc = "/".join(str(p) for p in first.path) or "<root>"
|
|
67
|
+
raise CCPSchemaError(f"Invalid CCP message at {loc}: {first.message}")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ccp-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reference Python SDK for Cortex Capsule Protocol (CCP)
|
|
5
|
+
Author: CCP contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Jonathan Hidalgo
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/<your-org>/<your-repo>
|
|
29
|
+
Project-URL: Repository, https://github.com/<your-org>/<your-repo>
|
|
30
|
+
Keywords: ccp,protocol,iot,robotics,gateway
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
41
|
+
Requires-Python: >=3.9
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: httpx>=0.27
|
|
45
|
+
Requires-Dist: jsonschema>=4.21
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# CCP
|
|
49
|
+
Cortex Capsule Protocol (CCP) — v0.1 Draft
|
|
50
|
+
|
|
51
|
+
CCP standardizes communication between devices (robots, microcontrollers, gateways) and Cortex Capsule servers (local or cloud brain runtime).
|
|
52
|
+
|
|
53
|
+
## Contents
|
|
54
|
+
- Core specification
|
|
55
|
+
- HTTP mapping
|
|
56
|
+
- JSON schemas
|
|
57
|
+
- Message examples
|
|
58
|
+
|
|
59
|
+
## Structure
|
|
60
|
+
- [docs/](docs/README.md)
|
|
61
|
+
- [schemas/](schemas)
|
|
62
|
+
- [examples/](examples)
|
|
63
|
+
|
|
64
|
+
## Get started
|
|
65
|
+
1. Read the core spec: [docs/spec/ccp-core.md](docs/spec/ccp-core.md)
|
|
66
|
+
2. Check HTTP mapping: [docs/spec/http-binding.md](docs/spec/http-binding.md)
|
|
67
|
+
3. See schemas: [docs/spec/message-schemas.md](docs/spec/message-schemas.md)
|
|
68
|
+
4. See examples: [examples/](examples)
|
|
69
|
+
|
|
70
|
+
## How to use CCP
|
|
71
|
+
- This repo is the **specification**, not a dependency to install.
|
|
72
|
+
- Projects should depend on a **CCP SDK** (language/runtime specific) that implements the protocol.
|
|
73
|
+
- If you need JSON schemas in production, the SDK should bundle them or reference a published schema package.
|
|
74
|
+
|
|
75
|
+
## Python SDK (reference)
|
|
76
|
+
|
|
77
|
+
This repository now includes a minimal reference Python SDK that you can install with pip.
|
|
78
|
+
|
|
79
|
+
### Install (local dev)
|
|
80
|
+
From the repo root:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python -m pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Install (from PyPI)
|
|
87
|
+
After you publish it:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python -m pip install ccp-sdk
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Usage
|
|
94
|
+
See a runnable example in [examples/python_client.py](examples/python_client.py).
|
|
95
|
+
|
|
96
|
+
If your ESP32 sends data to a Python gateway API, see: [examples/esp32_gateway_api/](examples/esp32_gateway_api/).
|
|
97
|
+
|
|
98
|
+
## ESP32 note
|
|
99
|
+
ESP32 (Arduino/ESP-IDF) does not run `pip` packages directly.
|
|
100
|
+
|
|
101
|
+
Typical architecture:
|
|
102
|
+
- ESP32 handles hardware (mic/speaker/LED/display) and sends events/audio to a gateway over Wi-Fi/serial.
|
|
103
|
+
- The gateway (Raspberry Pi / PC) runs the Python CCP SDK and talks to the CCP server over HTTP.
|
|
104
|
+
|
|
105
|
+
Practical mapping:
|
|
106
|
+
- Mic (I2S) -> capture audio -> upload to `/v1/media/audio` -> send `turn` with `audio_ref`
|
|
107
|
+
- Speaker (I2S) <- receive `turn_result.payload.reply.audio_ref` -> download audio -> play
|
|
108
|
+
- LED strip / display <- drive UI based on `turn_result.payload.state`, `safety`, or `actions`
|
|
109
|
+
|
|
110
|
+
## SDKs (planned)
|
|
111
|
+
- `ccp-python` — reference SDK for capsules/gateways
|
|
112
|
+
- `ccp-esp32` — thin device SDK for microcontrollers
|
|
113
|
+
|
|
114
|
+
## Version
|
|
115
|
+
CCP v0.1 (Draft)
|
|
116
|
+
|
|
117
|
+
## Roadmap
|
|
118
|
+
See [docs/roadmap.md](docs/roadmap.md).
|
|
119
|
+
|
|
120
|
+
## Community
|
|
121
|
+
Discord: invite link TBD.
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to propose changes and submit PRs.
|
|
125
|
+
|
|
126
|
+
## Code of Conduct
|
|
127
|
+
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
128
|
+
|
|
129
|
+
## Security
|
|
130
|
+
See [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/ccp_sdk/__init__.py
|
|
5
|
+
src/ccp_sdk/client.py
|
|
6
|
+
src/ccp_sdk/errors.py
|
|
7
|
+
src/ccp_sdk/messages.py
|
|
8
|
+
src/ccp_sdk/py.typed
|
|
9
|
+
src/ccp_sdk/schemas.py
|
|
10
|
+
src/ccp_sdk/validator.py
|
|
11
|
+
src/ccp_sdk.egg-info/PKG-INFO
|
|
12
|
+
src/ccp_sdk.egg-info/SOURCES.txt
|
|
13
|
+
src/ccp_sdk.egg-info/dependency_links.txt
|
|
14
|
+
src/ccp_sdk.egg-info/requires.txt
|
|
15
|
+
src/ccp_sdk.egg-info/top_level.txt
|
|
16
|
+
src/ccp_sdk/schemas/__init__.py
|
|
17
|
+
src/ccp_sdk/schemas/ccp-message.json
|
|
18
|
+
src/ccp_sdk/schemas/error.json
|
|
19
|
+
src/ccp_sdk/schemas/hello-ack.json
|
|
20
|
+
src/ccp_sdk/schemas/hello.json
|
|
21
|
+
src/ccp_sdk/schemas/session-end.json
|
|
22
|
+
src/ccp_sdk/schemas/session-ended.json
|
|
23
|
+
src/ccp_sdk/schemas/session-start.json
|
|
24
|
+
src/ccp_sdk/schemas/session-started.json
|
|
25
|
+
src/ccp_sdk/schemas/turn-result.json
|
|
26
|
+
src/ccp_sdk/schemas/turn.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ccp_sdk
|