Add UserTurnCompletionLLMServiceMixin (#3518)

* Added UserTurnCompletionLLMServiceMixin class

* Added 22-filter-incomplete-turns.py foundational example

* Removed old 22 natural conversation foundational examples

* Added test_user_turn_completion_mixin.py
This commit is contained in:
Mark Backman
2026-01-30 14:57:15 -05:00
committed by GitHub
parent 569ea9849a
commit 63a23246d5
15 changed files with 881 additions and 2067 deletions

View File

@@ -47,6 +47,7 @@ from pipecat.frames.frames import (
LLMThoughtEndFrame,
LLMThoughtStartFrame,
LLMThoughtTextFrame,
LLMUpdateSettingsFrame,
SpeechControlParamsFrame,
StartFrame,
TextFrame,
@@ -69,6 +70,7 @@ from pipecat.turns.user_idle_controller import UserIdleController
from pipecat.turns.user_mute import BaseUserMuteStrategy
from pipecat.turns.user_start import BaseUserTurnStartStrategy, UserTurnStartedParams
from pipecat.turns.user_stop import BaseUserTurnStopStrategy, UserTurnStoppedParams
from pipecat.turns.user_turn_completion_mixin import UserTurnCompletionConfig
from pipecat.turns.user_turn_controller import UserTurnController
from pipecat.turns.user_turn_strategies import ExternalUserTurnStrategies, UserTurnStrategies
from pipecat.utils.string import TextPartForConcatenation, concatenate_aggregated_text
@@ -89,6 +91,13 @@ class LLMUserAggregatorParams:
has been idle (not speaking) for this duration. Set to None to disable
idle detection.
vad_analyzer: Voice Activity Detection analyzer instance.
filter_incomplete_user_turns: Whether to filter out incomplete user turns.
When enabled, the LLM outputs a turn completion marker at the start of
each response: ✓ (complete), ○ (incomplete short), or ◐ (incomplete long).
Incomplete responses are suppressed and timeouts trigger re-prompting.
user_turn_completion_config: Configuration for turn completion behavior including
custom instructions, timeouts, and prompts. Only used when
filter_incomplete_user_turns is True.
"""
user_turn_strategies: Optional[UserTurnStrategies] = None
@@ -96,6 +105,8 @@ class LLMUserAggregatorParams:
user_turn_stop_timeout: float = 5.0
user_idle_timeout: Optional[float] = None
vad_analyzer: Optional[VADAnalyzer] = None
filter_incomplete_user_turns: bool = False
user_turn_completion_config: Optional[UserTurnCompletionConfig] = None
@dataclass
@@ -488,6 +499,24 @@ class LLMUserAggregator(LLMContextAggregator):
for s in self._params.user_mute_strategies:
await s.setup(self.task_manager)
# Enable incomplete turn filtering on the LLM if configured
if self._params.filter_incomplete_user_turns:
# Get config or use defaults
config = self._params.user_turn_completion_config or UserTurnCompletionConfig()
# Enable the feature on the LLM with config
await self.push_frame(
LLMUpdateSettingsFrame(
settings={
"filter_incomplete_user_turns": True,
"user_turn_completion_config": config,
}
)
)
# Auto-inject turn completion instructions into context
self._context.add_message({"role": "system", "content": config.completion_instructions})
async def _stop(self, frame: EndFrame):
await self._maybe_emit_user_turn_stopped(on_session_end=True)
await self._cleanup()
@@ -1145,13 +1174,40 @@ class LLMAssistantAggregator(LLMContextAggregator):
async def _trigger_assistant_turn_stopped(self):
aggregation = await self.push_aggregation()
if aggregation:
# Strip turn completion markers from the transcript
content = self._maybe_strip_turn_completion_markers(aggregation)
message = AssistantTurnStoppedMessage(
content=aggregation, timestamp=self._assistant_turn_start_timestamp
content=content, timestamp=self._assistant_turn_start_timestamp
)
await self._call_event_handler("on_assistant_turn_stopped", message)
self._assistant_turn_start_timestamp = ""
def _maybe_strip_turn_completion_markers(self, text: str) -> str:
"""Strip turn completion markers from assistant transcript.
These markers (✓, ○, ◐) are used internally for turn completion
detection and shouldn't appear in the final transcript.
"""
from pipecat.turns.user_turn_completion_mixin import (
USER_TURN_COMPLETE_MARKER,
USER_TURN_INCOMPLETE_LONG_MARKER,
USER_TURN_INCOMPLETE_SHORT_MARKER,
)
marker_found = False
for marker in (
USER_TURN_COMPLETE_MARKER,
USER_TURN_INCOMPLETE_SHORT_MARKER,
USER_TURN_INCOMPLETE_LONG_MARKER,
):
if marker in text:
text = text.replace(marker, "")
marker_found = True
# Only strip whitespace if we removed a marker
return text.strip() if marker_found else text
class LLMContextAggregatorPair:
"""Pair of LLM context aggregators for updating context with user and assistant messages."""

View File

@@ -439,7 +439,7 @@ class AnthropicLLMService(LLMService):
if event.type == "content_block_delta":
if hasattr(event.delta, "text"):
await self.push_frame(LLMTextFrame(event.delta.text))
await self._push_llm_text(event.delta.text)
completion_tokens_estimate += self._estimate_tokens(event.delta.text)
elif hasattr(event.delta, "partial_json") and tool_use_block:
json_accumulator += event.delta.partial_json

View File

@@ -1107,7 +1107,7 @@ class AWSBedrockLLMService(LLMService):
if "contentBlockDelta" in event:
delta = event["contentBlockDelta"]["delta"]
if "text" in delta:
await self.push_frame(LLMTextFrame(delta["text"]))
await self._push_llm_text(delta["text"])
completion_tokens_estimate += self._estimate_tokens(delta["text"])
elif "toolUse" in delta and "input" in delta["toolUse"]:
# Handle partial JSON for tool use

View File

@@ -1023,7 +1023,7 @@ class GoogleLLMService(LLMService):
await self.push_frame(LLMThoughtEndFrame())
else:
accumulated_text += part.text
await self.push_frame(LLMTextFrame(part.text))
await self._push_llm_text(part.text)
elif part.function_call:
function_call = part.function_call
function_call_id = function_call.id or str(uuid.uuid4())

View File

@@ -56,6 +56,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.turns.user_turn_completion_mixin import UserTurnCompletionLLMServiceMixin
# Type alias for a callable that handles LLM function calls.
FunctionCallHandler = Callable[["FunctionCallParams"], Awaitable[None]]
@@ -142,7 +143,7 @@ class FunctionCallRunnerItem:
run_llm: Optional[bool] = None
class LLMService(AIService):
class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
"""Base class for all LLM services.
Handles function calling registration and execution with support for both
@@ -186,6 +187,7 @@ class LLMService(AIService):
super().__init__(**kwargs)
self._run_in_parallel = run_in_parallel
self._function_call_timeout_secs = function_call_timeout_secs
self._filter_incomplete_user_turns: bool = False
self._start_callbacks = {}
self._adapter = self.adapter_class()
self._functions: Dict[Optional[str], FunctionCallRegistryItem] = {}
@@ -295,6 +297,35 @@ class LLMService(AIService):
if not self._run_in_parallel:
await self._cancel_sequential_runner_task()
async def _update_settings(self, settings: Mapping[str, Any]):
"""Update LLM service settings.
Handles turn completion settings specially since they are not model
parameters and should not be passed to the underlying LLM API.
Args:
settings: Dictionary of settings to update.
"""
# Turn completion settings to extract (not model parameters)
turn_completion_keys = {"filter_incomplete_user_turns", "user_turn_completion_config"}
# Handle turn completion settings
if "filter_incomplete_user_turns" in settings:
self._filter_incomplete_user_turns = settings["filter_incomplete_user_turns"]
logger.info(
f"{self}: Incomplete turn filtering {'enabled' if self._filter_incomplete_user_turns else 'disabled'}"
)
# Configure the mixin with config object
if self._filter_incomplete_user_turns and "user_turn_completion_config" in settings:
self.set_user_turn_completion_config(settings["user_turn_completion_config"])
# Remove turn completion settings before passing to parent
settings = {k: v for k, v in settings.items() if k not in turn_completion_keys}
# Let the parent handle remaining model parameters
await super()._update_settings(settings)
async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process a frame.
@@ -322,6 +353,20 @@ class LLMService(AIService):
await super().push_frame(frame, direction)
async def _push_llm_text(self, text: str):
"""Push LLM text, using turn completion detection if enabled.
This helper method simplifies text pushing in LLM implementations by
handling the conditional logic for turn completion internally.
Args:
text: The text content from the LLM to push.
"""
if self._filter_incomplete_user_turns:
await self._push_turn_text(text)
else:
await self.push_frame(LLMTextFrame(text))
async def _handle_interruptions(self, _: InterruptionFrame):
for function_name, entry in self._functions.items():
if entry.cancel_on_interruption:

View File

@@ -422,7 +422,7 @@ class BaseOpenAILLMService(LLMService):
# Keep iterating through the response to collect all the argument fragments
arguments += tool_call.function.arguments
elif chunk.choices[0].delta.content:
await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content))
await self._push_llm_text(chunk.choices[0].delta.content)
# When gpt-4o-audio / gpt-4o-mini-audio is used for llm or stt+llm
# we need to get LLMTextFrame for the transcript

View File

@@ -0,0 +1,428 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Mixin for adding turn completion detection to LLM services.
This mixin enables LLM services to detect and process turn completion markers
(COMPLETE/INCOMPLETE) in LLM responses, allowing for smarter conversation flow
where the LLM can indicate whether the user's input was complete or if they
were interrupted mid-thought.
"""
import asyncio
from dataclasses import dataclass
from typing import Literal, Optional
from loguru import logger
from pipecat.frames.frames import (
Frame,
InterruptionFrame,
LLMFullResponseEndFrame,
LLMMessagesAppendFrame,
LLMRunFrame,
LLMTextFrame,
)
from pipecat.processors.frame_processor import FrameDirection
# Turn completion markers
USER_TURN_COMPLETE_MARKER = ""
USER_TURN_INCOMPLETE_SHORT_MARKER = "" # Short wait - user likely continues soon
USER_TURN_INCOMPLETE_LONG_MARKER = "" # Long wait - user needs more time
# Default prompts for incomplete timeouts
DEFAULT_INCOMPLETE_SHORT_PROMPT = """The user paused briefly. Generate a brief, natural prompt to encourage them to continue.
IMPORTANT: You MUST respond with ✓ followed by your message. Do NOT output ○ or ◐ - the user has already been given time to continue.
Your response should:
- Be contextually relevant to what was just discussed
- Sound natural and conversational
- Be very concise (1 sentence max)
- Gently prompt them to continue
Example format: ✓ Go ahead, I'm listening.
Generate your ✓ response now."""
DEFAULT_INCOMPLETE_LONG_PROMPT = """The user has been quiet for a while. Generate a friendly check-in message.
IMPORTANT: You MUST respond with ✓ followed by your message. Do NOT output ○ or ◐ - the user has already been given plenty of time.
Your response should:
- Acknowledge they might be thinking or busy
- Offer to help or continue when ready
- Be warm and understanding
- Be brief (1 sentence)
Example format: ✓ No rush! Let me know when you're ready to continue.
Generate your ✓ response now."""
# System prompt instructions for turn completion that can be appended to any base prompt
USER_TURN_COMPLETION_INSTRUCTIONS = """
CRITICAL INSTRUCTION - MANDATORY RESPONSE FORMAT:
Every single response MUST begin with a turn completion indicator. This is not optional.
TURN COMPLETION DECISION FRAMEWORK:
Ask yourself: "Has the user provided enough information for me to give a meaningful, substantive response?"
Mark as COMPLETE (✓) when:
- The user has answered your question with actual content
- The user has made a complete request or statement
- The user has provided all necessary information for you to respond meaningfully
- The conversation can naturally progress to your substantive response
Mark as INCOMPLETE SHORT (○) when the user will likely continue soon:
- The user was clearly cut off mid-sentence or mid-word
- The user is in the middle of a thought that got interrupted
- Brief technical interruption (they'll resume in a few seconds)
Mark as INCOMPLETE LONG (◐) when the user needs more time:
- The user explicitly asks for time: "let me think", "give me a minute", "hold on"
- The user is clearly pondering or deliberating: "hmm", "well...", "that's a good question"
- The user acknowledged but hasn't answered yet: "That's interesting..."
- The response feels like a preamble before the actual answer
RESPOND in one of these three formats:
1. If COMPLETE: `✓` followed by a space and your full substantive response
2. If INCOMPLETE SHORT: ONLY the character `○` (user will continue in a few seconds)
3. If INCOMPLETE LONG: ONLY the character `◐` (user needs more time to think)
KEY INSIGHT: Grammatically complete ≠ conversationally complete
- "That's a really good question." is grammatically complete but conversationally incomplete (use ◐)
- "I'd go to Japan because I love" is mid-sentence (use ○)
EXAMPLES:
You ask: "Where would you travel?"
User: "I'd go to Japan because I love"
→ `○`
(Cut off mid-sentence - they'll continue in seconds)
You ask: "Where would you travel?"
User: "That's a good question. Let me think..."
→ `◐`
(User is deliberating - give them time)
You ask: "Where would you travel?"
User: "Hmm, hold on a second."
→ `◐`
(User explicitly asked for time)
You ask: "Where would you travel?"
User: "I'd go to Japan because I love the culture."
→ `✓ Japan is a wonderful choice! The blend of ancient traditions and modern innovation is truly unique. Have you been before?`
(Complete answer - give full response)
User: "I need help with"
→ `○`
(Cut off mid-request - they'll finish soon)
User: "Well, let me think about that for a moment."
→ `◐`
(User needs time to think)
User: "Can you help me book a flight to New York next week?"
→ `✓ I'd be happy to help you with that! Let me gather some information...`
(Complete request - provide full response)
User: "Give me a minute to gather my thoughts."
→ `◐`
(User explicitly asked for time)
FORMAT REQUIREMENTS:
- ALWAYS use single-character indicators: `✓` (complete), `○` (short wait), or `◐` (long wait)
- For COMPLETE: `✓` followed by a space and your full response
- For INCOMPLETE: ONLY the single character (`○` or `◐`) with absolutely nothing else
- Your turn indicator must be the very first character in your response
Remember: Focus on conversational completeness and how long the user might need. Was it a mid-sentence cutoff (○) or do they need time to think (◐)?"""
@dataclass
class UserTurnCompletionConfig:
"""Configuration for turn completion behavior.
Attributes:
instructions: Custom instructions for turn completion. If not provided,
uses default USER_TURN_COMPLETION_INSTRUCTIONS.
incomplete_short_timeout: Seconds to wait after short incomplete (○) before prompting.
incomplete_long_timeout: Seconds to wait after long incomplete (◐) before prompting.
incomplete_short_prompt: Custom prompt when short timeout expires.
incomplete_long_prompt: Custom prompt when long timeout expires.
"""
instructions: Optional[str] = None
incomplete_short_timeout: float = 5.0
incomplete_long_timeout: float = 10.0
incomplete_short_prompt: Optional[str] = None
incomplete_long_prompt: Optional[str] = None
@property
def completion_instructions(self) -> str:
"""Turn completion instructions, using default if not set."""
return self.instructions or USER_TURN_COMPLETION_INSTRUCTIONS
@property
def short_prompt(self) -> str:
"""Short incomplete prompt, using default if not set."""
return self.incomplete_short_prompt or DEFAULT_INCOMPLETE_SHORT_PROMPT
@property
def long_prompt(self) -> str:
"""Long incomplete prompt, using default if not set."""
return self.incomplete_long_prompt or DEFAULT_INCOMPLETE_LONG_PROMPT
class UserTurnCompletionLLMServiceMixin:
"""Mixin that adds turn completion detection to LLM services.
This mixin provides methods to push LLM text with turn completion detection.
It processes turn completion markers to enable smarter conversation flow:
- ✓ (COMPLETE): Push response normally
- ○ (INCOMPLETE SHORT): Suppress response, wait ~5s, then prompt
- ◐ (INCOMPLETE LONG): Suppress response, wait ~15s, then prompt
When incomplete timeouts expire, the mixin automatically prompts the LLM
with a contextual follow-up message to re-engage the user.
Usage:
The LLM service controls when to use turn completion by calling
_push_turn_text instead of push_frame:
# With turn completion:
if self._filter_incomplete_user_turns:
await self._push_turn_text(chunk.text)
else:
await self.push_frame(LLMTextFrame(chunk.text))
The mixin requires that the base class has a `push_frame` method compatible
with FrameProcessor's signature.
"""
def __init__(self, *args, **kwargs):
"""Initialize the turn completion mixin.
Args:
*args: Positional arguments passed to parent class.
**kwargs: Keyword arguments passed to parent class.
"""
super().__init__(*args, **kwargs)
self._turn_text_buffer = ""
# Safety mechanism: True when incomplete is detected. While the prompt
# instructs the LLM to output ONLY the marker for incomplete turns, this flag
# ensures graceful degradation if the LLM disobeys and outputs additional text.
self._turn_suppressed = False
self._turn_complete_found = False # True when ✓ (COMPLETE) is detected
# Timeout handling
self._user_turn_completion_config = UserTurnCompletionConfig()
self._incomplete_timeout_task: Optional[asyncio.Task] = None
self._incomplete_type: Optional[Literal["short", "long"]] = None
def set_user_turn_completion_config(self, config: UserTurnCompletionConfig):
"""Set the turn completion configuration.
Args:
config: The turn completion configuration.
"""
self._user_turn_completion_config = config
async def _start_incomplete_timeout(self, incomplete_type: Literal["short", "long"]):
"""Start a timeout task for incomplete turn handling.
Args:
incomplete_type: Either "short" or "long" to determine timeout duration.
"""
# Cancel any existing timeout
await self._cancel_incomplete_timeout()
self._incomplete_type = incomplete_type
if incomplete_type == "short":
timeout = self._user_turn_completion_config.incomplete_short_timeout
else:
timeout = self._user_turn_completion_config.incomplete_long_timeout
logger.debug(f"Starting {incomplete_type} incomplete timeout ({timeout}s)")
self._incomplete_timeout_task = self.create_task(
self._incomplete_timeout_handler(incomplete_type, timeout),
f"_incomplete_timeout_{incomplete_type}",
)
async def _cancel_incomplete_timeout(self):
"""Cancel any pending incomplete timeout task."""
if self._incomplete_timeout_task and not self._incomplete_timeout_task.done():
logger.debug("Cancelling incomplete timeout")
await self.cancel_task(self._incomplete_timeout_task)
self._incomplete_timeout_task = None
self._incomplete_type = None
async def _incomplete_timeout_handler(
self, incomplete_type: Literal["short", "long"], timeout: float
):
"""Handle incomplete timeout expiration.
Args:
incomplete_type: Either "short" or "long".
timeout: The timeout duration in seconds.
"""
try:
await asyncio.sleep(timeout)
# Timeout expired - reset state before prompting LLM
logger.info(f"Incomplete {incomplete_type} timeout expired, prompting LLM")
await self._turn_reset()
self._incomplete_timeout_task = None
self._incomplete_type = None
# Get the appropriate prompt
if incomplete_type == "short":
prompt = self._user_turn_completion_config.short_prompt
else:
prompt = self._user_turn_completion_config.long_prompt
# Push through pipeline to trigger LLM response
await self.push_frame(
LLMMessagesAppendFrame(messages=[{"role": "system", "content": prompt}])
)
await self.push_frame(LLMRunFrame())
except asyncio.CancelledError:
# Timeout was cancelled (user spoke or interruption)
pass
async def _turn_reset(self):
"""Reset turn completion state between responses.
Call this at the end of each LLM response to clear buffered text and reset state.
If no marker was found, pushes the buffered text to avoid losing content.
Note: This does NOT cancel pending incomplete timeouts. Timeouts are only
cancelled on InterruptionFrame (when user speaks).
"""
# Check if no marker was found in this response
marker_found = self._turn_suppressed or self._turn_complete_found
if not marker_found and self._turn_text_buffer:
# Graceful degradation: push the buffered text so it's not lost
logger.warning(
f"{self}: filter_incomplete_user_turns is enabled but LLM response did not "
f"contain turn completion markers (✓/○/◐). Pushing text anyway. "
"The system prompt may be missing turn completion instructions."
)
await self.push_frame(LLMTextFrame(self._turn_text_buffer))
self._turn_text_buffer = ""
self._turn_suppressed = False
self._turn_complete_found = False
async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process frames, handling turn completion state resets.
Args:
frame: The frame to process.
direction: The direction of frame processing.
"""
# Handle interruptions by cancelling timeout and resetting state
if isinstance(frame, InterruptionFrame):
await self._cancel_incomplete_timeout()
await self._turn_reset()
# Reset turn state at end of LLM response (but don't cancel timeout -
# incomplete timeouts should continue running)
elif isinstance(frame, LLMFullResponseEndFrame):
await self._turn_reset()
# Pass frame to parent
await super().process_frame(frame, direction)
async def _push_turn_text(self, text: str):
"""Push LLM text with turn completion detection.
This method should be used instead of `push_frame(LLMTextFrame(text))` when
turn completion is enabled. It will:
1. Detect turn markers (✓, ○, or ◐)
2. When ○ (SHORT) is found: suppress text, start short timeout
3. When ◐ (LONG) is found: suppress text, start long timeout
4. When ✓ (COMPLETE) is found: push all text with marker marked as skip_tts
5. After marker detected: all subsequent text flows through immediately
Args:
text: The text content from the LLM to push.
"""
# If we've already detected incomplete, suppress all remaining text.
# This is a safety mechanism in case the LLM disobeys the prompt and outputs
# additional text after the marker (e.g., "○ Please continue...").
if self._turn_suppressed:
return
# If ✓ (COMPLETE) was already found, push text immediately without buffering
if self._turn_complete_found:
await self.push_frame(LLMTextFrame(text))
return
# Add text to buffer
self._turn_text_buffer += text
# Check for incomplete markers (○ short, ◐ long)
# These indicate the user was cut off or needs time - we suppress the bot's
# response and start a timeout to re-prompt later.
incomplete_type: Optional[Literal["short", "long"]] = None
if USER_TURN_INCOMPLETE_SHORT_MARKER in self._turn_text_buffer:
incomplete_type = "short"
elif USER_TURN_INCOMPLETE_LONG_MARKER in self._turn_text_buffer:
incomplete_type = "long"
if incomplete_type:
marker = (
USER_TURN_INCOMPLETE_SHORT_MARKER
if incomplete_type == "short"
else USER_TURN_INCOMPLETE_LONG_MARKER
)
logger.debug(
f"INCOMPLETE {incomplete_type.upper()} ({marker}) detected, suppressing text"
)
self._turn_suppressed = True
# Push the marker with skip_tts=True so it's added to context (maintains
# conversation continuity per prompt instructions) but not spoken by TTS
frame = LLMTextFrame(self._turn_text_buffer)
frame.skip_tts = True
await self.push_frame(frame)
self._turn_text_buffer = ""
await self._start_incomplete_timeout(incomplete_type)
return
# Check for ✓ (COMPLETE) marker - user's turn was complete, respond normally
if USER_TURN_COMPLETE_MARKER in self._turn_text_buffer:
logger.debug(f"COMPLETE ({USER_TURN_COMPLETE_MARKER}) detected, pushing buffered text")
# Split buffer at the marker to handle cases where marker and text
# arrive in the same chunk (e.g., "✓ Hello!" from some LLMs)
marker_pos = self._turn_text_buffer.index(USER_TURN_COMPLETE_MARKER)
marker_end = marker_pos + len(USER_TURN_COMPLETE_MARKER)
# Push the marker with skip_tts=True - adds to context but not spoken
marker_text = self._turn_text_buffer[:marker_end]
frame = LLMTextFrame(marker_text)
frame.skip_tts = True
await self.push_frame(frame)
# Push remaining text after marker as normal speech
remaining_text = self._turn_text_buffer[marker_end:]
if remaining_text:
# Strip leading space after marker if present (✓ Hello -> Hello)
if remaining_text.startswith(" "):
remaining_text = remaining_text[1:]
if remaining_text:
await self.push_frame(LLMTextFrame(remaining_text))
# Mark complete - all subsequent text flows through immediately
self._turn_text_buffer = ""
self._turn_complete_found = True
return