diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 77af50f15..3a6e64def 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -59,7 +59,7 @@ from pipecat.processors.aggregators.llm_response import ( from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.processors.frame_processor import FrameDirection from pipecat.services.ai_service import AIService -from pipecat.services.settings import ServiceSettings +from pipecat.services.settings import LLMSettings, ServiceSettings, is_given from pipecat.turns.user_turn_completion_mixin import UserTurnCompletionLLMServiceMixin from pipecat.utils.context.llm_context_summarization import ( LLMContextSummarizationUtil, @@ -311,6 +311,29 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService): await self._cancel_sequential_runner_task() await self._cancel_summary_task() + async def _update_settings_from_typed(self, update: LLMSettings) -> set[str]: + """Apply a typed settings update, handling turn-completion fields. + + Args: + update: A typed LLM settings delta. + + Returns: + Set of field names whose values actually changed. + """ + changed = await super()._update_settings_from_typed(update) + + if "filter_incomplete_user_turns" in changed: + self._filter_incomplete_user_turns = self._settings.filter_incomplete_user_turns + logger.info( + f"{self}: Incomplete turn filtering " + f"{'enabled' if self._filter_incomplete_user_turns else 'disabled'}" + ) + + if "user_turn_completion_config" in changed and self._filter_incomplete_user_turns: + self.set_user_turn_completion_config(self._settings.user_turn_completion_config) + + return changed + async def _update_settings(self, settings: Mapping[str, Any]): """Update LLM service settings. diff --git a/src/pipecat/services/settings.py b/src/pipecat/services/settings.py index fbec5cdf8..718996984 100644 --- a/src/pipecat/services/settings.py +++ b/src/pipecat/services/settings.py @@ -31,10 +31,13 @@ from __future__ import annotations import copy from dataclasses import dataclass, field, fields -from typing import Any, ClassVar, Dict, Mapping, Optional, Set, Type, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Mapping, Optional, Set, Type, TypeVar from loguru import logger +if TYPE_CHECKING: + from pipecat.turns.user_turn_completion_mixin import UserTurnCompletionConfig + # --------------------------------------------------------------------------- # NOT_GIVEN sentinel # --------------------------------------------------------------------------- @@ -258,6 +261,13 @@ class LLMSettings(ServiceSettings): frequency_penalty: Frequency penalty. presence_penalty: Presence penalty. seed: Random seed for reproducibility. + filter_incomplete_user_turns: Enable LLM-based turn completion detection + to suppress bot responses when the user was cut off mid-thought. + See ``examples/foundational/22-filter-incomplete-turns.py`` and + ``UserTurnCompletionLLMServiceMixin``. + user_turn_completion_config: Configuration for turn completion behavior + when ``filter_incomplete_user_turns`` is enabled. Controls timeouts + and prompts for incomplete turns. """ temperature: Any = field(default_factory=lambda: NOT_GIVEN) @@ -267,6 +277,10 @@ class LLMSettings(ServiceSettings): frequency_penalty: Any = field(default_factory=lambda: NOT_GIVEN) presence_penalty: Any = field(default_factory=lambda: NOT_GIVEN) seed: Any = field(default_factory=lambda: NOT_GIVEN) + filter_incomplete_user_turns: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + user_turn_completion_config: UserTurnCompletionConfig | _NotGiven = field( + default_factory=lambda: NOT_GIVEN + ) @dataclass