agentship-voice 0.0.3__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.
- agentship_voice-0.0.3/.gitignore +27 -0
- agentship_voice-0.0.3/PKG-INFO +29 -0
- agentship_voice-0.0.3/README.md +13 -0
- agentship_voice-0.0.3/pyproject.toml +32 -0
- agentship_voice-0.0.3/src/agentship_voice/__init__.py +25 -0
- agentship_voice-0.0.3/src/agentship_voice/adapters/__init__.py +40 -0
- agentship_voice-0.0.3/src/agentship_voice/adapters/base.py +62 -0
- agentship_voice-0.0.3/src/agentship_voice/adapters/livekit_adapter.py +149 -0
- agentship_voice-0.0.3/src/agentship_voice/adapters/pipecat_adapter.py +375 -0
- agentship_voice-0.0.3/src/agentship_voice/browser.py +67 -0
- agentship_voice-0.0.3/src/agentship_voice/factories.py +307 -0
- agentship_voice-0.0.3/src/agentship_voice/pipeline.py +90 -0
- agentship_voice-0.0.3/src/agentship_voice/session.py +139 -0
- agentship_voice-0.0.3/src/agentship_voice/trace.py +67 -0
- agentship_voice-0.0.3/src/agentship_voice/turn.py +327 -0
- agentship_voice-0.0.3/tests/test_adapters.py +28 -0
- agentship_voice-0.0.3/tests/test_end_to_end.py +142 -0
- agentship_voice-0.0.3/tests/test_factories.py +209 -0
- agentship_voice-0.0.3/tests/test_interruption.py +127 -0
- agentship_voice-0.0.3/tests/test_livekit_adapter.py +183 -0
- agentship_voice-0.0.3/tests/test_observability.py +182 -0
- agentship_voice-0.0.3/tests/test_pipecat_adapter.py +310 -0
- agentship_voice-0.0.3/tests/test_session.py +80 -0
- agentship_voice-0.0.3/tests/test_turn.py +226 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
|
|
16
|
+
# Env / secrets — never commit
|
|
17
|
+
.env
|
|
18
|
+
.env.*
|
|
19
|
+
!.env.example
|
|
20
|
+
|
|
21
|
+
# Editor / OS
|
|
22
|
+
.DS_Store
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
|
|
26
|
+
# Docs build
|
|
27
|
+
site/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentship-voice
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: AgentShip voice — run the same agent over a live audio stream. A cascaded pipeline (transport/VAD → STT → agent → TTS) with barge-in, hosted in either Pipecat or LiveKit Agents. The agent is the one the REST service runs; only the hosting differs.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: agentship-core==0.0.3
|
|
8
|
+
Provides-Extra: all
|
|
9
|
+
Requires-Dist: livekit-agents<2,>=1.8; extra == 'all'
|
|
10
|
+
Requires-Dist: pipecat-ai<2,>=1.8; extra == 'all'
|
|
11
|
+
Provides-Extra: livekit
|
|
12
|
+
Requires-Dist: livekit-agents<2,>=1.8; extra == 'livekit'
|
|
13
|
+
Provides-Extra: pipecat
|
|
14
|
+
Requires-Dist: pipecat-ai<2,>=1.8; extra == 'pipecat'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# agentship-voice
|
|
18
|
+
|
|
19
|
+
Run the same AgentShip agent over a live audio stream.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install "agentship-voice[pipecat]" # or [livekit]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The agent is the one the REST service runs — same spec, memory, tools and tracing. A voice
|
|
26
|
+
framework supplies the ears (transport, VAD, STT) and the mouth (TTS); this package supplies
|
|
27
|
+
the agent in the shape that framework expects.
|
|
28
|
+
|
|
29
|
+
See [`docs/capabilities/voice.md`](../../docs/capabilities/voice.md).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# agentship-voice
|
|
2
|
+
|
|
3
|
+
Run the same AgentShip agent over a live audio stream.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install "agentship-voice[pipecat]" # or [livekit]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The agent is the one the REST service runs — same spec, memory, tools and tracing. A voice
|
|
10
|
+
framework supplies the ears (transport, VAD, STT) and the mouth (TTS); this package supplies
|
|
11
|
+
the agent in the shape that framework expects.
|
|
12
|
+
|
|
13
|
+
See [`docs/capabilities/voice.md`](../../docs/capabilities/voice.md).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentship-voice"
|
|
7
|
+
version = "0.0.3"
|
|
8
|
+
description = "AgentShip voice — run the same agent over a live audio stream. A cascaded pipeline (transport/VAD → STT → agent → TTS) with barge-in, hosted in either Pipecat or LiveKit Agents. The agent is the one the REST service runs; only the hosting differs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
# The base install carries NO voice framework. `VoiceTurn` is pure asyncio over
|
|
13
|
+
# agentship-core, so the seam, the config surface and their tests run keyless in CI;
|
|
14
|
+
# a framework arrives only with its extra. Same shape as the observability package,
|
|
15
|
+
# where the vendor SDK is never a hard dependency of the contract.
|
|
16
|
+
dependencies = ["agentship-core==0.0.3"]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
# Pipecat hosts the agent as a FrameProcessor in a frame pipeline.
|
|
20
|
+
pipecat = ["pipecat-ai>=1.8,<2"]
|
|
21
|
+
# LiveKit hosts the agent as an llm.LLM inside an AgentSession — a different shape
|
|
22
|
+
# for the same agent, which is exactly why the seam below is text-in/text-out and
|
|
23
|
+
# not a pipeline node. See adapters/base.py.
|
|
24
|
+
livekit = ["livekit-agents>=1.8,<2"]
|
|
25
|
+
all = ["pipecat-ai>=1.8,<2", "livekit-agents>=1.8,<2"]
|
|
26
|
+
|
|
27
|
+
[project.entry-points."agentship.voice_frameworks"]
|
|
28
|
+
pipecat = "agentship_voice.adapters.pipecat_adapter:PipecatAdapter"
|
|
29
|
+
livekit = "agentship_voice.adapters.livekit_adapter:LiveKitAdapter"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/agentship_voice"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Run the same AgentShip agent over a live audio stream.
|
|
2
|
+
|
|
3
|
+
The agent is the one the REST service runs — same spec, same memory, same tools, same tracing.
|
|
4
|
+
Only the hosting differs: a voice framework supplies the ears (transport, VAD, STT) and the
|
|
5
|
+
mouth (TTS), and this package supplies the agent in the shape that framework expects.
|
|
6
|
+
|
|
7
|
+
The public surface is small on purpose:
|
|
8
|
+
|
|
9
|
+
* :class:`~agentship_voice.turn.VoiceTurn` — text in, spoken text out. The whole vendor-free
|
|
10
|
+
seam, and the only thing both frameworks could agree on.
|
|
11
|
+
* :class:`~agentship_voice.trace.LatencyTrace` — where a turn's time went, emitted every turn.
|
|
12
|
+
* :class:`~agentship.spec.VoiceSpec` — the ``voice:`` block. It lives in the **kernel**, beside
|
|
13
|
+
``ObservabilitySpec``, because the kernel owns every authoring surface and imports no vendor;
|
|
14
|
+
the provider *names* in it are validated here, in :mod:`agentship_voice.factories`, which is
|
|
15
|
+
the half that knows what it can actually build.
|
|
16
|
+
|
|
17
|
+
Adapters live in :mod:`agentship_voice.adapters` and each needs its own extra installed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from agentship.spec import VoiceSpec
|
|
21
|
+
|
|
22
|
+
from .trace import LatencyTrace
|
|
23
|
+
from .turn import VoiceTurn
|
|
24
|
+
|
|
25
|
+
__all__ = ["LatencyTrace", "VoiceSpec", "VoiceTurn"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Framework adapters. Each imports its framework lazily, so importing this package is free.
|
|
2
|
+
|
|
3
|
+
An adapter is only meaningful with its extra installed (``agentship-voice[pipecat]`` or
|
|
4
|
+
``[livekit]``); the contract they implement lives in :mod:`agentship_voice.adapters.base`.
|
|
5
|
+
|
|
6
|
+
Adapters are discovered through the ``agentship.voice_frameworks`` entry-point group, the same
|
|
7
|
+
mechanism engines use — so a third party ships a class and one packaging line, and
|
|
8
|
+
``voice.framework: theirs`` works with no edit here. Our own two are registered the same way
|
|
9
|
+
rather than special-cased, because a plugin system its own author bypasses is one that quietly
|
|
10
|
+
stops working for everyone else.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from agentship.errors import CapabilityError
|
|
14
|
+
from agentship.registry import Registry
|
|
15
|
+
|
|
16
|
+
from .base import VoiceAdapter
|
|
17
|
+
|
|
18
|
+
__all__ = ["VOICE_FRAMEWORKS", "VoiceAdapter", "get_adapter"]
|
|
19
|
+
|
|
20
|
+
#: Voice framework adapters by ``voice.framework`` name.
|
|
21
|
+
VOICE_FRAMEWORKS: Registry[type[VoiceAdapter]] = Registry(
|
|
22
|
+
"agentship.voice_frameworks", label="voice framework"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_adapter(name: str) -> VoiceAdapter:
|
|
27
|
+
"""Return the adapter for ``name``, or raise naming the frameworks that exist.
|
|
28
|
+
|
|
29
|
+
An unknown framework fails here rather than deep inside a session, and the message lists the
|
|
30
|
+
real choices instead of leaving the author to guess the spelling.
|
|
31
|
+
|
|
32
|
+
Loading is lazy by construction: an entry point is only imported when its name is asked for,
|
|
33
|
+
so a stack with neither framework installed can still read this package's config and docs —
|
|
34
|
+
and a broken third-party adapter cannot stop ours from loading.
|
|
35
|
+
"""
|
|
36
|
+
adapter_class = VOICE_FRAMEWORKS.get(name)
|
|
37
|
+
if adapter_class is None:
|
|
38
|
+
known = ", ".join(VOICE_FRAMEWORKS.names()) or "none installed"
|
|
39
|
+
raise CapabilityError(f"unknown voice framework {name!r} — available: {known}")
|
|
40
|
+
return adapter_class()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The contract a voice framework must satisfy to host an AgentShip agent.
|
|
2
|
+
|
|
3
|
+
Deliberately narrow. An adapter is asked for two things:
|
|
4
|
+
|
|
5
|
+
1. **host the agent** — wrap a :class:`~agentship_voice.turn.VoiceTurn` in whatever shape its
|
|
6
|
+
framework requires (a Pipecat ``FrameProcessor``, a LiveKit ``llm.LLM``);
|
|
7
|
+
2. **run a session** — assemble transport, VAD, STT and TTS around it and serve until stopped.
|
|
8
|
+
|
|
9
|
+
Everything else a voice stack does — endpointing, turn detection, barge-in, jitter buffers,
|
|
10
|
+
audio codecs — belongs to the framework and is not restated here. Restating it would mean
|
|
11
|
+
owning it, and the point of two adapters is that neither framework's model leaks into ours.
|
|
12
|
+
|
|
13
|
+
**Why this ABC has two implementations from the start.** The engine ABC deliberately waits
|
|
14
|
+
for its second engine before freezing (ADR: the shape of an abstraction is a guess until a
|
|
15
|
+
second thing fits into it). Voice does not get that luxury cheaply: Pipecat and LiveKit
|
|
16
|
+
disagree about something fundamental — where the agent sits — so an ABC drawn against either
|
|
17
|
+
one alone would encode that framework's assumptions. Writing both at once is what proved the
|
|
18
|
+
seam had to be text-in/text-out rather than a pipeline node.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
|
|
25
|
+
from agentship.spec import VoiceSpec
|
|
26
|
+
|
|
27
|
+
from ..turn import VoiceTurn
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class VoiceAdapter(ABC):
|
|
31
|
+
"""Host an AgentShip agent inside one voice framework."""
|
|
32
|
+
|
|
33
|
+
#: Name used in ``voice.framework`` to select this adapter.
|
|
34
|
+
name: str
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def host(self, turn: VoiceTurn) -> object:
|
|
38
|
+
"""Return ``turn`` wrapped in this framework's own agent shape.
|
|
39
|
+
|
|
40
|
+
The return type is deliberately ``object``: a Pipecat ``FrameProcessor`` and a
|
|
41
|
+
LiveKit ``llm.LLM`` share no base class, and inventing a common one would mean
|
|
42
|
+
adding a layer neither framework asked for. Callers hand the result straight back
|
|
43
|
+
to the framework that produced its type.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
async def run(self, turn: VoiceTurn, config: VoiceSpec) -> None:
|
|
48
|
+
"""Assemble the pipeline around ``turn`` and serve until cancelled.
|
|
49
|
+
|
|
50
|
+
Returns only when the session ends. Cancellation is the normal way to stop, so an
|
|
51
|
+
implementation must leave the transport and provider connections closed on the way
|
|
52
|
+
out rather than relying on process exit.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def missing_dependency(self) -> str | None:
|
|
57
|
+
"""Return an actionable message when this framework is not installed, else ``None``.
|
|
58
|
+
|
|
59
|
+
Checked before a session is assembled so an absent extra is one clear line naming the
|
|
60
|
+
install, not an ``ImportError`` from three frames inside the framework. This mirrors
|
|
61
|
+
``agentship doctor``: a capability that cannot run should say so up front.
|
|
62
|
+
"""
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Host an AgentShip agent as the LLM inside a LiveKit ``AgentSession``.
|
|
2
|
+
|
|
3
|
+
LiveKit does not have a pipeline node to drop an agent into. It has an ``AgentSession`` with
|
|
4
|
+
slots — ``stt=``, ``vad=``, ``llm=``, ``tts=`` — and the agent goes in the ``llm`` slot. So
|
|
5
|
+
what we implement is ``llm.LLM``: given a chat context, return a stream of ``ChatChunk``.
|
|
6
|
+
|
|
7
|
+
That is a genuinely different shape from Pipecat's frame processor, which is why the seam
|
|
8
|
+
these two share had to be smaller than a pipeline node (see :mod:`agentship_voice.turn`).
|
|
9
|
+
|
|
10
|
+
**Who witnesses what was spoken.** On Pipecat we add a processor after TTS, because nothing
|
|
11
|
+
else reports it. LiveKit already does this work: it emits ``conversation_item_added`` with a
|
|
12
|
+
``ChatMessage`` that carries ``interrupted`` and content **already truncated to what was
|
|
13
|
+
actually spoken**. So here the adapter subscribes rather than measures — using the
|
|
14
|
+
framework's own answer instead of a second, worse one of our own.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
from agentship.spec import VoiceSpec
|
|
23
|
+
|
|
24
|
+
from ..turn import VoiceTurn
|
|
25
|
+
from .base import VoiceAdapter
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("agentship.voice")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_llm(turn: VoiceTurn):
|
|
31
|
+
"""Return an ``llm.LLM`` subclass bound to ``turn``.
|
|
32
|
+
|
|
33
|
+
Built inside a function for the same reason as the Pipecat processors: importing this
|
|
34
|
+
package must not require the framework, so the base classes are resolved on use.
|
|
35
|
+
"""
|
|
36
|
+
from livekit.agents import llm
|
|
37
|
+
|
|
38
|
+
class _AgentShipStream(llm.LLMStream):
|
|
39
|
+
"""Streams one AgentShip turn into LiveKit's chunk channel."""
|
|
40
|
+
|
|
41
|
+
async def _run(self) -> None:
|
|
42
|
+
"""Run the agent for the newest user message and emit its reply as chunks.
|
|
43
|
+
|
|
44
|
+
LiveKit hands the whole chat context, but AgentShip keeps its own conversation
|
|
45
|
+
through the session id — the same short-term memory the REST path uses. So only
|
|
46
|
+
the latest user message is taken; replaying LiveKit's history as well would give
|
|
47
|
+
the agent the conversation twice.
|
|
48
|
+
"""
|
|
49
|
+
spoken_to = _latest_user_text(self._chat_ctx)
|
|
50
|
+
if not spoken_to:
|
|
51
|
+
return # nothing was said; emit no chunks rather than an empty assistant turn
|
|
52
|
+
|
|
53
|
+
async for chunk in turn.say(spoken_to):
|
|
54
|
+
self._event_ch.send_nowait(
|
|
55
|
+
llm.ChatChunk(
|
|
56
|
+
id=self._llm._next_chunk_id(),
|
|
57
|
+
delta=llm.ChoiceDelta(role="assistant", content=chunk),
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
class AgentShipLLM(llm.LLM):
|
|
62
|
+
"""An AgentShip agent in the shape LiveKit's ``AgentSession`` expects."""
|
|
63
|
+
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
"""Hold a counter so each emitted chunk carries a distinct id."""
|
|
66
|
+
super().__init__()
|
|
67
|
+
self._chunks = 0
|
|
68
|
+
|
|
69
|
+
def _next_chunk_id(self) -> str:
|
|
70
|
+
"""Return a per-stream unique chunk id."""
|
|
71
|
+
self._chunks += 1
|
|
72
|
+
return f"agentship-{self._chunks}"
|
|
73
|
+
|
|
74
|
+
def chat(self, *, chat_ctx, tools=None, conn_options=None, **kwargs):
|
|
75
|
+
"""Return the stream that runs one AgentShip turn.
|
|
76
|
+
|
|
77
|
+
Tool arguments are accepted and ignored: tools belong to the agent's own spec and
|
|
78
|
+
run inside the engine, so letting LiveKit also inject them would give the model
|
|
79
|
+
two competing tool sets for one turn.
|
|
80
|
+
"""
|
|
81
|
+
from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS
|
|
82
|
+
|
|
83
|
+
return _AgentShipStream(
|
|
84
|
+
self,
|
|
85
|
+
chat_ctx=chat_ctx,
|
|
86
|
+
tools=tools or [],
|
|
87
|
+
conn_options=conn_options or DEFAULT_API_CONNECT_OPTIONS,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return AgentShipLLM
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _latest_user_text(chat_ctx) -> str:
|
|
94
|
+
"""Return the text of the newest user message in ``chat_ctx``, or ``""``."""
|
|
95
|
+
for item in reversed(getattr(chat_ctx, "items", [])):
|
|
96
|
+
if getattr(item, "role", None) == "user":
|
|
97
|
+
return (getattr(item, "text_content", None) or "").strip()
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class LiveKitAdapter(VoiceAdapter):
|
|
102
|
+
"""Run an AgentShip agent inside a LiveKit ``AgentSession``."""
|
|
103
|
+
|
|
104
|
+
name = "livekit"
|
|
105
|
+
|
|
106
|
+
def missing_dependency(self) -> str | None:
|
|
107
|
+
"""Return an install line when LiveKit Agents is absent, else ``None``."""
|
|
108
|
+
try:
|
|
109
|
+
import livekit.agents # noqa: F401
|
|
110
|
+
except ImportError:
|
|
111
|
+
return (
|
|
112
|
+
"voice.framework is 'livekit' but livekit-agents is not installed — "
|
|
113
|
+
'pip install "agentship-voice[livekit]"'
|
|
114
|
+
)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def host(self, turn: VoiceTurn) -> object:
|
|
118
|
+
"""Return the ``llm.LLM`` to pass as ``AgentSession(llm=...)``."""
|
|
119
|
+
return _build_llm(turn)()
|
|
120
|
+
|
|
121
|
+
def witness(self, session, turn: VoiceTurn) -> None:
|
|
122
|
+
"""Subscribe to the session so what LiveKit actually spoke reaches ``turn``.
|
|
123
|
+
|
|
124
|
+
LiveKit truncates an interrupted assistant message to the words that played, so this
|
|
125
|
+
confirms against the framework's own record rather than a second measurement of ours.
|
|
126
|
+
Call once, after the session is created and before it starts.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def _on_item(event) -> None:
|
|
130
|
+
"""Record an assistant message as spoken, and correct history if it was cut off."""
|
|
131
|
+
item = getattr(event, "item", None)
|
|
132
|
+
if item is None or getattr(item, "role", None) != "assistant":
|
|
133
|
+
return
|
|
134
|
+
text = getattr(item, "text_content", None) or ""
|
|
135
|
+
if text:
|
|
136
|
+
turn.confirm_spoken(text)
|
|
137
|
+
if getattr(item, "interrupted", False):
|
|
138
|
+
# LiveKit says so itself, so there is nothing to infer. Scheduled rather than
|
|
139
|
+
# awaited because this is a synchronous event callback: blocking it would stall
|
|
140
|
+
# the session's own event loop while we write to a checkpoint store.
|
|
141
|
+
asyncio.create_task(turn.interrupted()) # noqa: RUF006 — fire-and-forget by design
|
|
142
|
+
|
|
143
|
+
session.on("conversation_item_added", _on_item)
|
|
144
|
+
|
|
145
|
+
async def run(self, turn: VoiceTurn, config: VoiceSpec) -> None:
|
|
146
|
+
"""Assemble a room session around ``turn`` and serve until cancelled."""
|
|
147
|
+
raise NotImplementedError(
|
|
148
|
+
"LiveKitAdapter.run lands with the transport task — host() and witness() work now."
|
|
149
|
+
)
|