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
1 change: 1 addition & 0 deletions livekit-agents/livekit/agents/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ async def capture_frame(self, frame: rtc.AudioFrame) -> None:

if not self._pushed_duration:
self._capture_start = time.monotonic()
self.on_playback_started(created_at=time.time())

self._pushed_duration += frame.duration
with self._audio_lock:
Expand Down
18 changes: 9 additions & 9 deletions livekit-agents/livekit/agents/inference/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from .bargein import (
BargeinDetector,
BargeinError,
BargeinEvent,
BargeinEventType,
from .interruption import (
AdaptiveInterruptionDetector,
InterruptionDetectionError,
InterruptionEvent,
InterruptionEventType,
)
from .llm import LLM, LLMModels, LLMStream
from .stt import STT, STTModels
Expand All @@ -16,8 +16,8 @@
"STTModels",
"TTSModels",
"LLMModels",
"BargeinDetector",
"BargeinEvent",
"BargeinError",
"BargeinEventType",
"AdaptiveInterruptionDetector",
"InterruptionEvent",
"InterruptionDetectionError",
"InterruptionEventType",
]

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions livekit-agents/livekit/agents/telemetry/trace_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@
# Platform-specific attributes
ATTR_LANGFUSE_COMPLETION_START_TIME = "langfuse.observation.completion_start_time"

# Bargein attributes
ATTR_IS_BARGEIN = "lk.is_bargein"
ATTR_BARGEIN_PROBABILITY = "lk.bargein.probability"
ATTR_BARGEIN_TOTAL_DURATION = "lk.bargein.total_duration"
ATTR_BARGEIN_PREDICTION_DURATION = "lk.bargein.prediction_duration"
ATTR_BARGEIN_DETECTION_DELAY = "lk.bargein.detection_delay"
# Adaptive Interruption attributes
ATTR_IS_INTERRUPTION = "lk.is_interruption"
ATTR_INTERRUPTION_PROBABILITY = "lk.interruption.probability"
ATTR_INTERRUPTION_TOTAL_DURATION = "lk.interruption.total_duration"
ATTR_INTERRUPTION_PREDICTION_DURATION = "lk.interruption.prediction_duration"
ATTR_INTERRUPTION_DETECTION_DELAY = "lk.interruption.detection_delay"
12 changes: 11 additions & 1 deletion livekit-agents/livekit/agents/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from dataclasses import dataclass
from typing import Literal, TypeVar, Union
from typing import Any, Literal, TypeVar, Union

from pydantic import GetCoreSchemaHandler
from pydantic_core import CoreSchema, core_schema
from typing_extensions import TypeAlias

ATTRIBUTE_TRANSCRIPTION_SEGMENT_ID = "lk.segment_id"
Expand Down Expand Up @@ -39,12 +41,20 @@ class FlushSentinel:


class NotGiven:
__slots__ = ()

def __bool__(self) -> Literal[False]:
return False

def __repr__(self) -> str:
return "NOT_GIVEN"

@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
return core_schema.is_instance_schema(cls)


NotGivenOr: TypeAlias = Union[_T, NotGiven]
NOT_GIVEN = NotGiven()
Expand Down
49 changes: 49 additions & 0 deletions livekit-agents/livekit/agents/utils/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import functools
import inspect
from collections import defaultdict
from typing import Any, Callable

from ..log import logger
from ..types import NOT_GIVEN
from .misc import is_given


def deprecate_params(mapping: dict[str, str]) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""
Args:
mapping: {old_param: suggestion}

Example:
>>> @deprecate_params({
... "old_param": "Use new_param instead",
... })
... def my_function(old_param: NotGivenOr[int] = NOT_GIVEN, new_param: int = 0):
... print(old_param)
>>> my_function(old_param=1)
WARNING: old_param is deprecated. Use new_param instead
1
>>> my_function(new_param=1) # no warning
"""

def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
signature = inspect.signature(fn)

@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
bound = signature.bind_partial(*args, **kwargs)
by_suggestion: defaultdict[str, list[str]] = defaultdict(list)
for name, suggestion in mapping.items():
if is_given(bound.arguments.get(name, NOT_GIVEN)):
by_suggestion[suggestion].append(name)

for suggestion, names in by_suggestion.items():
params = ", ".join(names)
logger.warning(
f"{params} {'are' if len(names) > 1 else 'is'} deprecated. {suggestion}",
stacklevel=2,
)
return fn(*args, **kwargs)

return wrapper

return decorator
57 changes: 42 additions & 15 deletions livekit-agents/livekit/agents/voice/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import time
from collections.abc import AsyncGenerator, AsyncIterable, Coroutine, Generator
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar

from livekit import rtc

Expand All @@ -15,9 +15,10 @@
from ..types import NOT_GIVEN, FlushSentinel, NotGivenOr
from ..utils import is_given, misc
from .speech_handle import SpeechHandle
from .turn import TurnHandlingConfig

if TYPE_CHECKING:
from ..inference import BargeinDetector, LLMModels, STTModels, TTSModels
from ..inference import LLMModels, STTModels, TTSModels
from ..llm import mcp
from .agent_activity import AgentActivity
from .agent_session import AgentSession
Expand All @@ -39,29 +40,41 @@ def __init__(
id: str | None = None,
chat_ctx: NotGivenOr[llm.ChatContext | None] = NOT_GIVEN,
tools: list[llm.Tool | llm.Toolset] | None = None,
turn_detection: NotGivenOr[TurnDetectionMode | None] = NOT_GIVEN,
stt: NotGivenOr[stt.STT | STTModels | str | None] = NOT_GIVEN,
vad: NotGivenOr[vad.VAD | None] = NOT_GIVEN,
bargein_detection: NotGivenOr[BargeinDetector | bool] = NOT_GIVEN,
turn_handling: NotGivenOr[TurnHandlingConfig] = NOT_GIVEN,
llm: NotGivenOr[llm.LLM | llm.RealtimeModel | LLMModels | str | None] = NOT_GIVEN,
tts: NotGivenOr[tts.TTS | TTSModels | str | None] = NOT_GIVEN,
mcp_servers: NotGivenOr[list[mcp.MCPServer] | None] = NOT_GIVEN,
allow_interruptions: NotGivenOr[bool] = NOT_GIVEN,
min_consecutive_speech_delay: NotGivenOr[float] = NOT_GIVEN,
use_tts_aligned_transcript: NotGivenOr[bool] = NOT_GIVEN,
# deprecated
turn_detection: NotGivenOr[TurnDetectionMode | None] = NOT_GIVEN,
min_endpointing_delay: NotGivenOr[float] = NOT_GIVEN,
max_endpointing_delay: NotGivenOr[float] = NOT_GIVEN,
allow_interruptions: NotGivenOr[bool] = NOT_GIVEN,
) -> None:
tools = tools or []
if type(self) is Agent:
self._id = "default_agent"
else:
self._id = id or misc.camel_to_snake_case(type(self).__name__)

turn_handling = (
TurnHandlingConfig.migrate(
min_endpointing_delay=min_endpointing_delay,
max_endpointing_delay=max_endpointing_delay,
turn_detection=turn_detection,
allow_interruptions=allow_interruptions,
)
if not is_given(turn_handling)
else turn_handling
)

self._instructions = instructions
self._tools = tools.copy() + find_function_tools(self)
self._chat_ctx = chat_ctx.copy(tools=self._tools) if chat_ctx else ChatContext.empty()
self._turn_detection = turn_detection
self._turn_detection = turn_handling.turn_detection

if isinstance(stt, str):
stt = inference.STT.from_model_string(stt)
Expand All @@ -76,12 +89,15 @@ def __init__(
self._llm = llm
self._tts = tts
self._vad = vad
self._allow_interruptions = allow_interruptions

self._interruption_detection = turn_handling.interruption_cfg.mode
self._allow_interruptions: NotGivenOr[bool] = NOT_GIVEN
if is_given(turn_handling.interruption_cfg.mode):
self._allow_interruptions = bool(turn_handling.interruption_cfg.mode)
self._min_consecutive_speech_delay = min_consecutive_speech_delay
self._use_tts_aligned_transcript = use_tts_aligned_transcript
self._min_endpointing_delay = min_endpointing_delay
self._max_endpointing_delay = max_endpointing_delay
self._bargein_detection: NotGivenOr[inference.BargeinDetector | bool] = bargein_detection

if isinstance(mcp_servers, list) and len(mcp_servers) == 0:
mcp_servers = None # treat empty list as None (but keep NOT_GIVEN)
Expand Down Expand Up @@ -127,6 +143,10 @@ def chat_ctx(self) -> llm.ChatContext:
"""
return _ReadOnlyChatContext(self._chat_ctx.items)

@property
def interruption_detection(self) -> NotGivenOr[Literal["adaptive", "vad", False]]:
return self._interruption_detection

async def update_instructions(self, instructions: str) -> None:
"""
Updates the agent's instructions.
Expand Down Expand Up @@ -654,32 +674,39 @@ def __init__(
instructions: str,
chat_ctx: NotGivenOr[llm.ChatContext] = NOT_GIVEN,
tools: list[llm.Tool | llm.Toolset] | None = None,
turn_detection: NotGivenOr[TurnDetectionMode | None] = NOT_GIVEN,
stt: NotGivenOr[stt.STT | None] = NOT_GIVEN,
vad: NotGivenOr[vad.VAD | None] = NOT_GIVEN,
bargein_detection: NotGivenOr[BargeinDetector | bool] = NOT_GIVEN,
turn_handling: NotGivenOr[TurnHandlingConfig] = NOT_GIVEN,
llm: NotGivenOr[llm.LLM | llm.RealtimeModel | None] = NOT_GIVEN,
tts: NotGivenOr[tts.TTS | None] = NOT_GIVEN,
mcp_servers: NotGivenOr[list[mcp.MCPServer] | None] = NOT_GIVEN,
# deprecated
turn_detection: NotGivenOr[TurnDetectionMode | None] = NOT_GIVEN,
allow_interruptions: NotGivenOr[bool] = NOT_GIVEN,
min_endpointing_delay: NotGivenOr[float] = NOT_GIVEN,
max_endpointing_delay: NotGivenOr[float] = NOT_GIVEN,
) -> None:
tools = tools or []
turn_handling = (
TurnHandlingConfig.migrate(
turn_detection=turn_detection,
allow_interruptions=allow_interruptions,
min_endpointing_delay=min_endpointing_delay,
max_endpointing_delay=max_endpointing_delay,
)
if not is_given(turn_handling)
else turn_handling
)
super().__init__(
instructions=instructions,
chat_ctx=chat_ctx,
tools=tools,
turn_detection=turn_detection,
stt=stt,
vad=vad,
bargein_detection=bargein_detection,
llm=llm,
tts=tts,
mcp_servers=mcp_servers,
allow_interruptions=allow_interruptions,
min_endpointing_delay=min_endpointing_delay,
max_endpointing_delay=max_endpointing_delay,
turn_handling=turn_handling,
)

self.__started = False
Expand Down
Loading