diff --git a/CLAUDE.md b/CLAUDE.md index c392c55..eb2f24e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,6 +149,35 @@ Raises: - Prefer region comments for grouping methods in files that already use them. - End edited Markdown and YAML files with a trailing newline. +## What this package actually ships (and what it does not) + +This client installs **`ondewo/csi` only**. It contains **no** `ondewo/nlu`, `ondewo/s2t` or +`ondewo/t2s` `*_pb2` modules — `pyproject.toml` declares `ondewo-nlu-client==7.0.2`, +`ondewo-s2t-client` and `ondewo-t2s-client`, and the csi protos reference those packages' +descriptors at import time. Verified against the installed `.dist-info/RECORD`: this dist claims +zero files under `ondewo/nlu/`. + +That matters when someone reasons about descriptor-pool collisions. The sibling client +`ondewo-vtsi-client-python` **does** vendor foreign protos (55 files under `ondewo/nlu`, plus +`ondewo/{s2t,t2s,sip,qa}`), so *it* has to be regenerated in lockstep with the service clients. This +one does not — bump it only when the `ondewo/csi` surface itself changes. Do not "helpfully" add +vendored nlu protos here; it would create exactly the duplicate-file-in-pool crash the current +layout avoids. + +Consumers pin this repo by git rev, not by PyPI version: `ondewo-vtsi` and `ondewo-csi` both pin +rev **`23b568fa`** (the `CALL_ENDED` control-status regen) in their `pyproject.toml`. Never rebase or +force-push a commit that a pin references. + +## Jenkins — never trigger a multibranch scan or branch indexing + +**NEVER trigger a Jenkins multibranch scan or branch indexing.** Do not call a multibranch/folder job's +`build`, `scan`, or reindex endpoints, click "Scan Repository Now" / "Build Now" on a folder, run +`p4 scan`, or use any API/CLI that reindexes branches or scans the repository. A scan/reindex runs across +**every** branch, consumes CI resources, and can kick off unintended builds and deploys. + +If a branch is not building — it was not discovered, or its job is marked `buildable: false` / orphaned — +**report it and stop**. Let the user or a Jenkins admin adjust branch-discovery/config or rename the branch +to the convention. Never force a build by scanning or reindexing. ## Release gotchas (hard-won this session) These bit us during the 6.14.0 release. Keep them in mind when releasing. diff --git a/Makefile b/Makefile index b4bbda5..5817784 100644 --- a/Makefile +++ b/Makefile @@ -143,7 +143,7 @@ create_async_services: ## Create async services for all synchronous services cp "$$file" "$$dir/async_$$filename"; \ done; \ for file in "$$dir"/async_*.py; do \ - perl -i -pe 'unless(/def stub/){ s/^([[:space:]]*)def /$$1async def /g; s/self\.stub/await self.stub/g; s/\(BaseServicesInterface\)/(AsyncBaseServicesInterface)/g; s/base_services_interface/async_base_services_interface/g; s/import BaseServicesInterface/import AsyncBaseServicesInterface/g; }' \ + perl -i -pe 'unless(/def stub/){ s/^([[:space:]]*)def /$$1async def /g; s/self\.stub/await self.stub/g; s/\(BaseServicesInterface\)/(AsyncBaseServicesInterface)/g; s/base_services_interface/async_base_services_interface/g; s/import BaseServicesInterface/import AsyncBaseServicesInterface/g; s/core\.services_interface/core.async_services_interface/g; s/import ServicesInterface/import AsyncServicesInterface/g; s/\((?SetControlStatus that playback of the buffered audio remainder has resumed""" + PLAYBACK_DONE: _ControlStatus.ValueType # 7 + """Status reported by the client to the server via SetControlStatus that playback of an utterance has completed""" + CALL_ENDED: _ControlStatus.ValueType # 8 + """Status reported by ondewo-sip to the server via SetControlStatus that the phone call has ENDED and the server must reset ALL per-call state in place (persistent-listener model). It is a command, not a state: it is never pushed onto the control stream""" class ControlStatus(_ControlStatus, metaclass=_ControlStatusEnumTypeWrapper): """Control status.""" @@ -60,6 +72,18 @@ EMERGENCY_STOP: ControlStatus.ValueType # 1 """Status that control stream needs to stop immediately""" VAD_START_OF_SPEECH: ControlStatus.ValueType # 2 """Status that voice activity detection detected start of speech""" +BARGE_IN: ControlStatus.ValueType # 3 +"""Status that the caller started speaking while the bot was speaking (barge-in): playback pauses and the un-played audio remainder is buffered""" +RESUME_PLAYBACK: ControlStatus.ValueType # 4 +"""Status sent by the server to the client to resume playback of the buffered audio remainder after a false interruption (no committed transcription final arrived within the false interruption timeout)""" +DISCARD_REMAINDER: ControlStatus.ValueType # 5 +"""Status sent by the server to the client to discard the buffered audio remainder because the interruption committed as a real turn""" +PLAYBACK_RESUMED: ControlStatus.ValueType # 6 +"""Status reported by the client to the server via SetControlStatus that playback of the buffered audio remainder has resumed""" +PLAYBACK_DONE: ControlStatus.ValueType # 7 +"""Status reported by the client to the server via SetControlStatus that playback of an utterance has completed""" +CALL_ENDED: ControlStatus.ValueType # 8 +"""Status reported by ondewo-sip to the server via SetControlStatus that the phone call has ENDED and the server must reset ALL per-call state in place (persistent-listener model). It is a command, not a state: it is never pushed onto the control stream""" global___ControlStatus = ControlStatus class _ControlMessageServiceName: @@ -1153,6 +1177,18 @@ class S2sStreamResponse(google.protobuf.message.Message): DETECT_INTENT_RESPONSE_FIELD_NUMBER: builtins.int SYNTHESIZE_RESPONSE_FIELD_NUMBER: builtins.int SIP_TRIGGER_FIELD_NUMBER: builtins.int + UTTERANCE_ID_FIELD_NUMBER: builtins.int + CHUNK_INDEX_FIELD_NUMBER: builtins.int + LAST_CHUNK_FIELD_NUMBER: builtins.int + TURN_EPOCH_FIELD_NUMBER: builtins.int + utterance_id: builtins.str + """Optional. Identifier of the utterance this response belongs to. All chunks of one synthesized utterance share the same utterance_id""" + chunk_index: builtins.int + """Optional. Zero-based index of this chunk within the utterance identified by utterance_id""" + last_chunk: builtins.bool + """Optional. If true, this is the last chunk of the utterance identified by utterance_id""" + turn_epoch: builtins.int + """Optional. Monotonic turn epoch of the conversation turn that produced this response. Used by consumers to fence stale audio of an interrupted turn""" @property def detect_intent_response(self) -> ondewo.nlu.session_pb2.DetectIntentResponse: """full NLU detect intent response""" @@ -1171,9 +1207,13 @@ class S2sStreamResponse(google.protobuf.message.Message): detect_intent_response: ondewo.nlu.session_pb2.DetectIntentResponse | None = ..., synthesize_response: ondewo.t2s.text_to_speech_pb2.SynthesizeResponse | None = ..., sip_trigger: global___SipTrigger | None = ..., + utterance_id: builtins.str = ..., + chunk_index: builtins.int = ..., + last_chunk: builtins.bool = ..., + turn_epoch: builtins.int = ..., ) -> None: ... def HasField(self, field_name: typing.Literal["detect_intent_response", b"detect_intent_response", "response", b"response", "sip_trigger", b"sip_trigger", "synthesize_response", b"synthesize_response"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["detect_intent_response", b"detect_intent_response", "response", b"response", "sip_trigger", b"sip_trigger", "synthesize_response", b"synthesize_response"]) -> None: ... + def ClearField(self, field_name: typing.Literal["chunk_index", b"chunk_index", "detect_intent_response", b"detect_intent_response", "last_chunk", b"last_chunk", "response", b"response", "sip_trigger", b"sip_trigger", "synthesize_response", b"synthesize_response", "turn_epoch", b"turn_epoch", "utterance_id", b"utterance_id"]) -> None: ... def WhichOneof(self, oneof_group: typing.Literal["response", b"response"]) -> typing.Literal["detect_intent_response", "synthesize_response", "sip_trigger"] | None: ... global___S2sStreamResponse = S2sStreamResponse @@ -1298,14 +1338,18 @@ class ControlStreamResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor CONTROL_STATUS_FIELD_NUMBER: builtins.int + EPOCH_FIELD_NUMBER: builtins.int control_status: global___ControlStatus.ValueType """Control status""" + epoch: builtins.int + """Monotonic barge-in epoch/sequence number so control status transitions are correlatable with the S2sStreamResponse turn_epoch and a second barge-in during a resumed remainder can never be coalesced away""" def __init__( self, *, control_status: global___ControlStatus.ValueType = ..., + epoch: builtins.int = ..., ) -> None: ... - def ClearField(self, field_name: typing.Literal["control_status", b"control_status"]) -> None: ... + def ClearField(self, field_name: typing.Literal["control_status", b"control_status", "epoch", b"epoch"]) -> None: ... global___ControlStreamResponse = ControlStreamResponse diff --git a/pyproject.toml b/pyproject.toml index 42e345c..8d7c61d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "loguru", "numpy", "ondewo-client-utils>=3.2.0", - "ondewo-nlu-client", + "ondewo-nlu-client==7.0.2", "ondewo-s2t-client", "ondewo-t2s-client", "requests", @@ -54,6 +54,7 @@ dev = [ "python-dotenv", "setuptools", "typing_extensions", + "types-requests", "pytest-cov", "pytest-asyncio", ] diff --git a/tests/unit/client/test_keycloak.py b/tests/unit/client/test_keycloak.py index 23c7d8b..bd57bd1 100644 --- a/tests/unit/client/test_keycloak.py +++ b/tests/unit/client/test_keycloak.py @@ -152,7 +152,7 @@ def _token_body(access_token: str, refresh_token: str, expires_in: int) -> Dict[ def _build_provider( transport: FakeTransport, - token_expiration_in_s: int | None = None, + token_expiration_in_s: Optional[int] = None, ) -> KeycloakTokenProvider: """Construct a `KeycloakTokenProvider` wired to the fake transport and shared test fixtures. @@ -1055,7 +1055,7 @@ def _build_background_provider( transport: FakeTransport, clock: Dict[str, float], stop_event: ScriptedEvent, - token_expiration_in_s: int | None = None, + token_expiration_in_s: Optional[int] = None, ) -> KeycloakTokenProvider: """Construct a provider wired to an injected clock + scripted event, background thread off. diff --git a/uv.lock b/uv.lock index 24add85..e4a9e85 100644 --- a/uv.lock +++ b/uv.lock @@ -3975,6 +3975,8 @@ dev = [ { name = "setuptools", version = "83.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "soundfile", version = "0.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "soundfile", version = "0.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-requests", version = "2.33.0.20260712", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions" }, ] @@ -3993,7 +3995,7 @@ requires-dist = [ { name = "mypy-protobuf", marker = "extra == 'dev'" }, { name = "numpy" }, { name = "ondewo-client-utils", specifier = ">=3.2.0" }, - { name = "ondewo-nlu-client" }, + { name = "ondewo-nlu-client", specifier = "==7.0.2" }, { name = "ondewo-s2t-client" }, { name = "ondewo-t2s-client" }, { name = "pre-commit", marker = "extra == 'dev'" }, @@ -4008,39 +4010,30 @@ requires-dist = [ { name = "setuptools" }, { name = "setuptools", marker = "extra == 'dev'" }, { name = "soundfile", marker = "extra == 'dev'", specifier = ">=0.10.3.post1" }, + { name = "types-requests", marker = "extra == 'dev'" }, { name = "typing-extensions", marker = "extra == 'dev'" }, ] provides-extras = ["dev"] [[package]] name = "ondewo-nlu-client" -version = "6.14.0" +version = "7.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dataclasses-json" }, { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "google-api-core", version = "2.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "grpcio", version = "1.81.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "grpcio-reflection", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "grpcio-reflection", version = "1.81.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "grpcio-tools", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "grpcio-tools", version = "1.81.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "loguru" }, { name = "ondewo-client-utils" }, { name = "polling" }, { name = "protobuf" }, - { name = "python-dateutil" }, - { name = "regex", version = "2026.1.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "regex", version = "2026.6.28", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "setuptools", version = "82.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "setuptools", version = "83.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/b5/3351e021c0421689d95a3e872b1b6c637e98d29a469bf5b578e7a168ef7d/ondewo_nlu_client-6.14.0.tar.gz", hash = "sha256:62ff5b02e829bd946822c7cdd6cf323f26438ed2d97a7aaa5554805588226fb3", size = 382977, upload-time = "2026-07-07T16:19:26.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/e2/7472b521a5a13ef2e1f5e83e3d471e6264ed5620f5bac8419b0d18c8f024/ondewo_nlu_client-7.0.2.tar.gz", hash = "sha256:0056d0e0ee9d91d101dfade051297a1662dc1247e4fe1f8a240d47ae51258578", size = 411455, upload-time = "2026-08-08T14:03:39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/80/3c9d2bafde077152a7cc0efab22bd23a6da44cf397942c3ff873742bc2f1/ondewo_nlu_client-6.14.0-py2.py3-none-any.whl", hash = "sha256:8b2172a7059f81d3a4c5d310f8bfe53d1347c7828f17a21bbbf31b47fa75cc19", size = 441118, upload-time = "2026-07-07T16:19:25.079Z" }, + { url = "https://files.pythonhosted.org/packages/0f/dc/e632cedddf6e4011c819ea044ffbd53094920f0cc0f9793a60b84d431fa8/ondewo_nlu_client-7.0.2-py3-none-any.whl", hash = "sha256:0d0e58e78d5e496ec1d15266e1b3af38d9b0a9d08618faee1cd1635d5fbdd60b", size = 461789, upload-time = "2026-08-08T14:03:35.379Z" }, ] [[package]] @@ -6283,6 +6276,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/1f/ec5caf72c2e3b688ca3927e0979a04ddad19e1afc4bf1c199bd743e0f419/types_protobuf-7.34.1.20260518-py3-none-any.whl", hash = "sha256:a0a5337413347166439c0e07cbc26c6164d091401c6f01b1dfd8cdb966c4dd8f", size = 85992, upload-time = "2026-05-18T06:01:45.696Z" }, ] +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260712" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0"