
    qiF                         d Z ddlZddlmZ ddlmZmZ ddlmZ ddl	m
Z
mZmZmZmZmZ ddlmZ dZd	Zd
ZdZdZdZe G d d             Z G d d      Zy)aD  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.
    N)	dataclass)LiteralOptional)logger)FrameInterruptionFrameLLMFullResponseEndFrameLLMMessagesAppendFrameLLMRunFrameLLMTextFrame)FrameDirectionu   ✓u   ○u   ◐u  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.u  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.u  
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 (◐)?c                       e Zd ZU dZdZee   ed<   dZe	ed<   dZ
e	ed<   dZee   ed<   dZee   ed	<   ed
efd       Zed
efd       Zed
efd       Zy)UserTurnCompletionConfigu!  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.
    Ninstructionsg      @incomplete_short_timeoutg      $@incomplete_long_timeoutincomplete_short_promptincomplete_long_promptreturnc                 *    | j                   xs t        S )z7Turn completion instructions, using default if not set.)r   !USER_TURN_COMPLETION_INSTRUCTIONSselfs    Z/opt/pipecat/venv/lib/python3.12/site-packages/pipecat/turns/user_turn_completion_mixin.pycompletion_instructionsz0UserTurnCompletionConfig.completion_instructions   s       E$EE    c                 *    | j                   xs t        S )z2Short incomplete prompt, using default if not set.)r   DEFAULT_INCOMPLETE_SHORT_PROMPTr   s    r   short_promptz%UserTurnCompletionConfig.short_prompt   s     ++N/NNr   c                 *    | j                   xs t        S )z1Long incomplete prompt, using default if not set.)r   DEFAULT_INCOMPLETE_LONG_PROMPTr   s    r   long_promptz$UserTurnCompletionConfig.long_prompt   s     **L.LLr   )__name__
__module____qualname____doc__r   r   str__annotations__r   floatr   r   r   propertyr   r   r"    r   r   r   r      s    	 #'L(3-&&)e)%)U)-1Xc]1,0HSM0F F F Oc O O MS M Mr   r   c                        e Zd ZdZ fdZdefdZded   fdZd Z	ded   d	e
fd
Zd Zdedef fdZej                   fdedef fdZdefdZ xZS )!UserTurnCompletionLLMServiceMixinu  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.
    c                     t        |   |i | d| _        d| _        d| _        t               | _        d| _        d| _        y)zInitialize the turn completion mixin.

        Args:
            *args: Positional arguments passed to parent class.
            **kwargs: Keyword arguments passed to parent class.
         FN)	super__init___turn_text_buffer_turn_suppressed_turn_complete_foundr   _user_turn_completion_config_incomplete_timeout_task_incomplete_type)r   argskwargs	__class__s      r   r1   z*UserTurnCompletionLLMServiceMixin.__init__   sO     	$)&)!# !&$)! -E,F)@D%DHr   configc                     || _         y)zuSet the turn completion configuration.

        Args:
            config: The turn completion configuration.
        N)r5   )r   r;   s     r   set_user_turn_completion_configzAUserTurnCompletionLLMServiceMixin.set_user_turn_completion_config   s     -3)r   incomplete_type)shortlongc                 >  K   | j                          d{    || _        |dk(  r| j                  j                  }n| j                  j                  }t        j                  d| d| d       | j                  | j                  ||      d|       | _	        y7 w)zStart a timeout task for incomplete turn handling.

        Args:
            incomplete_type: Either "short" or "long" to determine timeout duration.
        Nr?   z	Starting z incomplete timeout (zs)_incomplete_timeout_)
_cancel_incomplete_timeoutr7   r5   r   r   r   debugcreate_task_incomplete_timeout_handlerr6   )r   r>   timeouts      r   _start_incomplete_timeoutz;UserTurnCompletionLLMServiceMixin._start_incomplete_timeout   s      --/// /g%77PPG77OOGy 11FwirRS(,(8(8,,_gF"?"34)
% 	0s   BBBBc                    K   | j                   rR| j                   j                         s8t        j                  d       | j	                  | j                          d{    d| _         d| _        y7 w)z+Cancel any pending incomplete timeout task.zCancelling incomplete timeoutN)r6   doner   rD   cancel_taskr7   r   s    r   rC   z<UserTurnCompletionLLMServiceMixin._cancel_incomplete_timeout  s]     ((1N1N1S1S1ULL89""4#@#@AAA(,% $ Bs   AA2A0A2rG   c                   K   	 t        j                  |       d{    t        j                  d| d       | j	                          d{    d| _        d| _        |dk(  r| j                  j                  }n| j                  j                  }| j                  t        d|dg             d{    | j                  t                      d{    y7 7 7 *7 # t         j                  $ r Y yw xY ww)zHandle incomplete timeout expiration.

        Args:
            incomplete_type: Either "short" or "long".
            timeout: The timeout duration in seconds.
        NzIncomplete z timeout expired, prompting LLMr?   system)rolecontent)messages)asynciosleepr   info_turn_resetr6   r7   r5   r   r"   
push_framer
   r   CancelledError)r   r>   rG   prompts       r   rF   z=UserTurnCompletionLLMServiceMixin._incomplete_timeout_handler
  s     	--((( KK+o%66UVW""$$$,0D)$(D! ')::GG::FF //&(v1V0WX   //+-000% ) % 1%% 		so   C<C# C0C# CA&C# 4C5 C# C!C# C<C# C# C# !C# #C96C<8C99C<c                   K   | j                   xs | j                  }|sP| j                  rDt        j                  |  d       | j                  t        | j                               d{    d| _        d| _         d| _        y7 w)ar  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).
        u   : filter_incomplete_user_turns is enabled but LLM response did not contain turn completion markers (✓/○/◐). Pushing text anyway. The system prompt may be missing turn completion instructions.Nr/   F)r3   r4   r2   r   warningrU   r   )r   marker_founds     r   rT   z-UserTurnCompletionLLMServiceMixin._turn_reset,  s      ,,I0I0I 6 6NN& Q Q
 //,t/E/E"FGGG!# %$)!	 Hs   A(B*B+Bframe	directionc                    K   t        |t              r0| j                          d{    | j                          d{    t        |   ||       d{    y7 57 7 	w)zProcess frames, handling turn completion state resets.

        Args:
            frame: The frame to process.
            direction: The direction of frame processing.
        N)
isinstancer   rC   rT   r0   process_framer   r[   r\   r:   s      r   r_   z/UserTurnCompletionLLMServiceMixin.process_frameD  s^      e./11333""$$$ g#E9555	 4$ 	6s2   $A#AA#A A#A!A#A#!A#c                    K   t        |t              r| j                          d{    t        |   ||       d{    y7 7 w)a  Push a frame downstream, resetting turn state at end of each LLM response.

        ``LLMFullResponseEndFrame`` is generated by the LLM service itself (pushed,
        not received), so it must be handled here rather than in ``process_frame``.

        Args:
            frame: The frame to push downstream.
            direction: The direction of frame flow. Defaults to downstream.
        N)r^   r	   rT   r0   rU   r`   s      r   rU   z,UserTurnCompletionLLMServiceMixin.push_frameS  sD      e45""$$$g 	222 %2s    $A	AA	A A	A	textc                 :  K   | j                   ry| j                  r#| j                  t        |             d{    y| xj                  |z  c_        d}t
        | j                  v rd}nt        | j                  v rd}|r|dk(  rt
        nt        }t        j                  d|j                          d| d       d| _         t        | j                        }d|_
        | j                  |       d{    d| _        | j                  |       d{    yt        | j                  v rt        j                  d	t         d
       | j                  j                  t              }|t        t              z   }| j                  d| }t        |      }d|_
        | j                  |       d{    | j                  |d }|r:|j                  d      r|dd }|r"| j                  t        |             d{    d| _        d| _        yy7 7 7 7 h7 w)uh  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.
        Nr?   r@   zINCOMPLETE z (z) detected, suppressing textTr/   z
COMPLETE (z!) detected, pushing buffered text    )r3   r4   rU   r   r2   !USER_TURN_INCOMPLETE_SHORT_MARKER USER_TURN_INCOMPLETE_LONG_MARKERr   rD   upperskip_ttsrH   USER_TURN_COMPLETE_MARKERindexlen
startswith)	r   rb   r>   markerr[   
marker_pos
marker_endmarker_textremaining_texts	            r   _push_turn_textz1UserTurnCompletionLLMServiceMixin._push_turn_textb  s     "    $$//,t"4555 	$&
 ?C,0F0FF%O-1G1GG$O #g- 25 
 LLo3356b@\] %)D! !!7!78E!EN//%(((%'D"00AAA %(>(>>LL:&?%@@abc //556OPJ#c*C&DDJ 00*=K -E!EN//%((( "33JK@N!,,S1%3AB%7N!//,~*FGGG &(D"(,D%5 ?K 6< ) B  ) HsZ   7HHB9H3H4HHBH.H/A
H9H:HHHHH)r#   r$   r%   r&   r1   r   r=   r   rH   rC   r)   rF   rT   r   r   r_   
DOWNSTREAMrU   r'   rs   __classcell__)r:   s   @r   r-   r-      s    4I(36N 3
w?W 
,% &7 BG D*06 6> 6 JXIbIb 3e 3 3U# Ur   r-   )r&   rQ   dataclassesr   typingr   r   logurur   pipecat.frames.framesr   r   r	   r
   r   r   "pipecat.processors.frame_processorr   rj   rf   rg   r   r!   r   r   r-   r+   r   r   <module>r{      s     ! $   > " $) !#(  ## "# M%\ !` M M MDB Br   