o
    io                    @   s   d Z ddlZddlZddlZddl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 ddlmZ G dd	 d	eZdS )
z
ElevenLabs WebSocket Service
Integrates ElevenLabs Conversational AI WebSocket API for service_type 4
Handles real-time audio streaming between Mcube and ElevenLabs
    N)State)OptionalCallable)ABC)Config)Logc                   @   s,  e Zd ZdZd5dee dee fddZdee fdd	Zdefd
dZd6de	de
fddZdd Z			d7dee de	dee fddZdefddZd8dedede	fdd Zd!d" Zde	fd#d$Zd8d%ed&e	fd'd(Zd)eegdf fd*d+Zd%efd,d-Zd.ede	fd/d0Zde	fd1d2Zd3d4 ZdS )9ElevenLabsWebSocketServicez
    ElevenLabs WebSocket service for real-time conversational AI.
    Handles bidirectional audio streaming and message processing.
    Nagent_idapi_keyc                 C   s   t tdr
|p	tjnd| _|ptj| _| jstd| js!tdd| _d| _d| _	d| _
d| _d| _d| _d| _d| _d| _d| _d| _d| _d| _d| _d| _d| _d| _d| _d| _td| jdd	  d
 dS )z
        Initialize ElevenLabs WebSocket service.
        
        Args:
            agent_id: ElevenLabs agent ID (from config or bot_config)
            api_key: ElevenLabs API key (from config)
        ELEVENLABS_AGENT_IDNz@ELEVENLABS_AGENT_ID is required. Set it in config or bot_config.z1ELEVENLABS_API_KEY is required. Set it in config.Fr   {Gzt?u=   🎙️ ElevenLabs WebSocket service initialized - Agent ID:    ...)hasattrr   r   r	   ELEVENLABS_API_KEYr
   
ValueError	websocketkeep_alive_taskrunning	connectedlast_reconnect_timelast_audio_send_timemin_audio_intervalagent_audio_formatuser_audio_formatconversation_idon_audio_receivedon_transcript_receivedon_response_receivedon_vad_score_receivedon_interruption_receivedon_tool_request_receivedon_connection_closedon_automated_greeting_detectedon_tool_abandonedon_conversation_id_receivedr   info)selfr	   r
    r(   J/var/www/html/live_calls/homebook/services/elevenlabs_websocket_service.py__init__  s:     z#ElevenLabsWebSocketService.__init__returnc           
   
      s  | j sdS zddl}W n ty   td Y dS w zd| j }d| j i}|jddd}|jd	d
}|j||d4 I dH i}|j	||d4 I dH F}|j
dkrs| I dH }|	dW  d  I dH  W  d  I dH  W S td|j
  	 W d  I dH  W d  I dH  W dS 1 I dH sw   Y  W d  I dH  W dS 1 I dH sw   Y  W dS  ty }	 ztd|	  W Y d}	~	dS d}	~	ww )z-Get signed URL for private agent (if needed).Nr   z;aiohttp not installed. Install it with: pip install aiohttpzIhttps://api.elevenlabs.io/v1/convai/conversation/get-signed-url?agent_id=
xi-api-keyd      )limitlimit_per_hostg      $@)total)	connectortimeout)headers   
signed_urlzFailed to get signed URL: zError getting signed URL: )r
   aiohttpImportErrorr   errorr	   TCPConnectorClientTimeoutClientSessiongetstatusjson	Exception)
r'   r7   urlr4   r2   r3   sessionresponsedataer(   r(   r)   get_signed_url  s@   


2z)ElevenLabsWebSocketService.get_signed_urlc                 C   s&   | j rd| j d| j  S d| j S )z]Get WebSocket URL for ElevenLabs. Includes conversation_id if resuming existing conversation.z8wss://api.elevenlabs.io/v1/convai/conversation?agent_id=z&conversation_id=)r   r	   r'   r(   r(   r)   get_websocket_url  s   z,ElevenLabsWebSocketService.get_websocket_urlF   use_signed_urlmax_retriesc                    sv  |   }d| ji}| jrtd| jdd  d ntd d}d}td	|d	 D ]}zx|d	krT|d
|d
   }td| d| d|dd t|I dH  | jrgtd| d| d| d ntd|dd  d| d| d tjt	j
||dddd|dI dH | _d| _d| _td| d t|  | _W  dS  tjy   ||k rtd| d| d| d Y q,d| d| d}	t|	 d | _t|	 ty/ }
 zJ||k rd!dl}td"| d| d#|
  td$|   W Y d}
~
q,d!dl}d%| d&|
 }	t|	 td'|   d | _t|	|
d}
~
ww d | _td%| d)(a  
        Connect to ElevenLabs WebSocket with timeout and retry logic.
        
        Args:
            use_signed_url: Whether to use signed URL (for private agents)
            max_retries: Maximum number of connection retry attempts (default: 3)
        r,   u'   🔁 Resuming ElevenLabs conversation: Nr   r   u)   🆕 Starting NEW ElevenLabs conversationg      4@g      ?      u"   🔄 Retrying connection (attempt /z) after z.1fz
s delay...z$Connecting to ElevenLabs WebSocket: z
 (attempt )P   z... (attempt 
   )additional_headersping_intervalping_timeoutclose_timeout)r3   Tu<   ✅ Connected to ElevenLabs WebSocket successfully (attempt u   ⚠️ Connection timeout (zs) on attempt z, will retry...uC   ❌ Failed to connect to ElevenLabs WebSocket: Connection timeout (z	s) after z	 attemptsFr   u#   ⚠️ Connection error on attempt z: Traceback: u4   ❌ Failed to connect to ElevenLabs WebSocket after z attempts: Full traceback: )rH   r
   r   r   r&   rangeasynciosleepwait_for
websocketsconnectr   r   r   create_task_keep_aliver   TimeoutErrorwarningr9   r@   	tracebackdebug
format_exc)r'   rJ   rK   rA   r4   connection_timeout
base_delayattemptdelay	error_msgrE   rb   r(   r(   r)   r]   	  sp   

  &




z"ElevenLabsWebSocketService.connectc                    sl  d}d}d}| j r4ztdI dH  | j sW dS | js/|d7 }||kr-td W dS W q|  sE|d7 }||krCtd W dS W qd}z|  I dH  t }t	t d dkrbt
d W n| tjjy } z.t|d	ru|jnd
}t|dr|jr|jnd}td| d| d d| _W Y d}~W dS d}~w ty } z5|d7 }|  r| j rtd| d| d|  ||kr|  std W Y d}~W dS W Y d}~W qd}~ww W nM tjy   Y dS  ty. } z6|d7 }| j r
td| d| d|  ||kr$| jr|  s$td W Y d}~dS W Y d}~qd}~ww | j sdS dS )zzSend keep-alive messages every 5 seconds using user_activity to prevent idle timeouts without interfering with bot speech.r      NrL   u3   ⚠️ Keep-alive: WebSocket is None, stopping taskuP   ⚠️ Keep-alive: connection not connected after multiple checks, stopping task   u5   💓 Keep-alive sent successfully (connection active)codeunknownreasonz	No reasonu8   ⚠️ Keep-alive: connection closed during send (Code: z
, Reason: rO   Fu!   ⚠️ Keep-alive error (attempt rN   ): u<   ❌ Keep-alive: too many consecutive failures, stopping tasku,   ⚠️ Keep-alive unexpected error (attempt )r   rY   rZ   r   r   ra   is_connectedsend_user_activitytimeintrc   r\   
exceptionsConnectionClosedr   rl   rn   r   r@   r9   CancelledError)r'   consecutive_failuresmax_consecutive_failureslast_success_timerE   
close_codeclose_reasonr(   r(   r)   r_   Z  sv   




z&ElevenLabsWebSocketService._keep_alivecustom_llm_extra_body	text_onlydynamic_variablesc                    s   ddi}dddddddd	did
}|r$d|vri |d< d|d d< ||d< |r.||d< |r4||d< |  |I dH  td dS )a  
        Send conversation initiation message.
        
        Args:
            custom_llm_extra_body: Custom LLM configuration
            text_only: Whether to use text-only mode (False for audio)
            dynamic_variables: Dynamic variables for the conversation
        type#conversation_initiation_client_data	ulaw_8000Tg      ?i,  )enable	thresholdprefix_padding_mssilence_duration_ms)output_audio_formatvadr   )agentttsr   r}   conversation_config_overrider|   r~   Nu/   📤 Sent conversation initiation to ElevenLabs)_send_messager   r&   )r'   r|   r}   r~   messageconversation_configr(   r(   r)   send_conversation_initiation  s0   z7ElevenLabsWebSocketService.send_conversation_initiation
audio_datac           	         s   | j r| js	dS zBddl}| }|| j }d}||k r/|| }t|I dH  | | _n|| _t|d}d|d}| j	|ddI dH  W dS  t
yU   Y dS w )	z
        Send audio chunk to ElevenLabs with rate limiting to prevent 1008 policy violations.
        
        Args:
            audio_data: Raw audio bytes (PCM format expected)
        Nr   r   zutf-8user_audio_chunk)r   r   Tsilent)r   r   rr   r   rY   rZ   base64	b64encodedecoder   r@   )	r'   r   rr   current_timetime_since_last_sendmin_interval
sleep_timeaudio_base64r   r(   r(   r)   send_audio_chunk  s*   
z+ElevenLabsWebSocketService.send_audio_chunktool_call_idresultis_errorc                    s$   d|||d}|  |I dH  dS )zSend tool result to ElevenLabs.client_tool_result)r   r   r   r   N)r   )r'   r   r   r   r   r(   r(   r)   send_client_tool_result  s   z2ElevenLabsWebSocketService.send_client_tool_resultc                    s>   | j r| js	dS |  sdS ddi}| j|ddI dH  dS )z)Send user_activity message as keep-alive.Nr   user_activityTr   )r   r   rp   r   r'   r   r(   r(   r)   rq     s   z-ElevenLabsWebSocketService.send_user_activityc                    sV   | j r| js	dS zddtt  d}| |I d H  W dS  ty*   Y dS w )NF	interrupt
interrupt_r   event_idT)r   r   rs   rr   r   r@   r   r(   r(   r)   send_interruption   s   z,ElevenLabsWebSocketService.send_interruptionr   r   c              
      s  | j sd S zt|}| j |I d H  W d S  tjjyE } z d| _|s:t	d|j
 d|j  W Y d }~d S W Y d }~d S d }~w tjjym } zd| _|sbt	d|  W Y d }~d S W Y d }~d S d }~w ty } zRt|  t|j }t|ttfpd v p|dv }|rt fddd	D rd| _|st	d
|  W Y d }~d S W Y d }~d S W Y d }~d S W Y d }~d S d }~ww )NFu2   ⚠️ WebSocket connection closed while sending: z - u#   ⚠️ WebSocket in invalid state: r   )connectionerroroserrorc                 3       | ]}| v V  qd S Nr(   ).0phrase	error_strr(   r)   	<genexpr>K      z;ElevenLabsWebSocketService._send_message.<locals>.<genexpr>)zconnection closedzconnection lostznot connectedzwebsocket is closedzwebsocket is closingu"   ⚠️ Connection error detected: )r   r?   dumpssendr\   rt   ru   r   r   ra   rl   rn   InvalidStater@   strlowerr   __name__
isinstanceConnectionErrorOSErrorany)r'   r   r   message_jsonrE   
error_typeis_connection_errorr(   r   r)   r   -  sD   
(z(ElevenLabsWebSocketService._send_message
on_messagec                    s  | j stdz;zJ| j 2 zv3 dH W }z@t|}|dd}|dkrC|di }|d}|rAd|d	}| j|d
dI dH  W qt|rP||I dH  n|| W q ty } z$ddl}	t	
d|  t	
d|  t	
d|	   W Y d}~qd}~ww 6 | j rTzt| j dd}
t| j ddpd}|
dus|r
t	d|
 d| d d| _| |}|rt	d| d n	t	d| d | jr
zt| jr| |
pd||I dH  n	| |
pd|| W n ty	 } zt	
d|  W Y d}~nd}~ww W nH tyS } z;t	d|  d| _| jrIzt| jr6| dddI dH  n| ddd W n
 tyH   Y nw W Y d}~nd}~ww W n tjjy } zd| _t|drl|jnd}
t|d r{|jr{|jnd!}t	d|
 d| d | |}|rt	d| d n	t	d| d | jrzt| jr| |
||I dH  n| |
|| W n ty } zt	
d|  W Y d}~nd}~ww | jr| j st	d" W Y d}~n;d}~w ty# } zt	
d#|  ddl}	t	
d$|	   d| _W Y d}~nd}~ww W d| _t	d% dS W d| _t	d% dS W d| _t	d% dS d| _t	d% w )&z
        Receive messages from ElevenLabs WebSocket.
        
        Args:
            on_message: Callback function to handle received messages
        zWebSocket not connectedNr   rm   ping
ping_eventr   pongr   Tr   r   u   ❌ Error processing message: zMessage data: rW   rz   r{   zConnection closed cleanlyu6   ⚠️ ElevenLabs WebSocket connection closed - Code: z, Reason: ''Fu2   🛑 Call ended legitimately (ElevenLabs reason: 'z') - reconnections disabledu#   ⚠️ Non-terminal close reason: 'z' - reconnection may be allowedi  )   ❌ Error in connection_closed callback: z+Could not get close reason from WebSocket: z"Connection closed (reason unknown)rl   rn   zNo reason provideduW   ⚠️ Connection closed while keep-alive was active - may indicate server-side closureu   ❌ Error receiving messages: rV   z&ElevenLabs receive_messages task ended)r   r@   r?   loadsr=   r   rY   iscoroutinefunctionrb   r   r9   rd   getattrra   r   _is_terminal_call_end_reasonr&   rc   r"   r\   rt   ru   r   rl   rn   r   doner   )r'   r   r   rD   msg_typer   r   pong_msgrE   rb   rz   r{   is_terminal_reasoncallback_errorr(   r(   r)   receive_messagesX  s   



 


z+ElevenLabsWebSocketService.receive_messagesc           "         s	  | dd}|dkrn| di }| d| _| jr^td| jd d  d | jr^zt| jr<| | jI d H  n| | j W n ty] } ztd	|  W Y d }~nd }~ww | d
d| _	| dd| _
d S |dkr| di }td|  | jrt| jr| |I d H  d S | | d S d S |dkr| di  dd}td|  | jrt| jr| |I d H  d S | | d S d S |dkr| di  dd}|rt|}| jrt| jr| |I d H  d S | | d S d S d S |dkr0| di  dd}| jr.t| jr'| |I d H  d S | | d S d S |dkrI| di }	|	 dd}
|	 dd}d S |dkr)| ddpVd}| }d |v std!|v std"|v std#|v std$|v rtd%|d d&  d' ntd(|d d)  d' |rd*|v r!d+d l}|d,||j|jB }|s|d-||j|jB }|r#|d. }td/|  |  g d0}t fd1d2|D r%td3 | jr'zt| jr| j|d4d5I d H  n
| j|d4d5 W d S W d S  ty } ztd6|  W Y d }~d S d }~ww d S d S d S d S d S |d7krV| d8i  d7d9}| jrTt| jrM| |I d H  d S | | d S d S |d:kr| d:i }| dd}
| dd}| d;d}| dd<}| d=i }td>|
 d?| d@| dA| dB	 tdC|  tdD|  | jrt| jr| |
||I d H  d S | |
|| d S d S |dEkr| dFi }| dd}| dGi }tdH| dI|  d S |dJkr#| dKi }| dLd}| dMd}| dd}|rtdN| dO| dP| dQ d S tdN| dB d S |dRkr| dRi }| dd}| dSd}| dTdU}|rG| nd}dV|v pVdW|v pVdX|v }|rtdY| dI|ri|d d) ndZ  | jrzt| jr| ||I d H  n	| || W d S W d S  ty } ztd[|  W Y d }~d S d }~ww d S td\| d]| dI|r|d d^ ndZ  d S |d_ks|d`krZ| dai p| dbi }| dcdp| ddpt|}tdd|  |rT|  g de}t fdfd2|D rVtdg | jrXzt| jr+| j|d4d5I d H  n
| j|d4d5 W d S W d S  tyS } ztd6|  W Y d }~d S d }~ww d S d S d S |dhkr| dhi }t| } tdi|  |  | rtdj | j!rzt| j!r| !dh| d4I d H  n
| !dh| d4 W d S W d S  ty } ztdk|  W Y d }~d S d }~ww d S d S g dl}!||!vrtdm|  d S d S )nNr   rm    conversation_initiation_metadata&conversation_initiation_metadata_eventr   u   📝 Received conversation_id: r   z... (will reuse on reconnect)u0   ❌ Error in conversation_id_received callback: agent_output_audio_format
mulaw_8000user_input_audio_formatpcm_8000user_transcriptuser_transcription_eventu   🎤 [User Transcript]: agent_responseagent_response_event u   🤖 [Agent Response]: audioaudio_eventaudio_base_64interruptioninterruption_eventr   client_tool_call	tool_namer   contextual_updatetextzagent changed the languagezchanged the language tozthe user is speaking inzuser is speaking inzin flag tamilu%   📝 [Contextual Update - Language]:    r   u   📝 [Contextual Update]: r5   zautomated greeting detectedr   z.automated greeting detected:\s*['\"](.*?)['\"]z#automated greeting detected:\s*(.+)rL   u:   📞 [Automated Greeting Detected via Contextual Update]: )	voicemailleave a messagerecord your messagenot availableat the toneplease recordafter the tonefinished recordingperson you are trying to reach%call has been forwarded to voice mailforwarded to voice mailzyour call has been forwardedc                 3   r   r   r(   r   patterngreeting_lowerr(   r)   r   :  r   z<ElevenLabsWebSocketService.handle_message.<locals>.<genexpr>uE   🛑 Voicemail detected via contextual update - marking call as endedT)is_voicemailu3   ❌ Error in automated_greeting_detected callback: 	vad_scorevad_score_eventg        agent_tool_request	tool_typezN/A
parametersu   🔧 [Agent Tool Request]: z (id: z, type: z, event_id: rO   u2      📋 Tool Parameters received from ElevenLabs: u      📦 Full tool request: agent_response_metadataagent_response_metadata_eventmetadatau*   📊 [Agent Response Metadata] (event_id: ro   agent_response_correctionagent_response_correction_eventcorrected_agent_responseoriginal_agent_responseu,   🔧 [Agent Response Correction] (event_id: z): 'z' -> 'r   agent_tool_responser   r   F	abandonedztool execution was abandonedzfailed to end the callu1   ⚠️ [Tool Execution Abandoned] (tool_call_id: z	No resultu&   ❌ Error in tool_abandoned callback: u*   🔧 [Agent Tool Response] (tool_call_id: z, is_error: r-   automated_greeting_detectedautomated_greeting!automated_greeting_detected_eventautomated_greeting_eventgreeting_textu$   📞 [Automated Greeting Detected]: )r   r   r   r   r   r   r   r   r   r   r   c                 3   r   r   r(   r   r   r(   r)   r     r   uF   🛑 Voicemail detected via automated greeting - marking call as endedr9   u   ❌ [Error from ElevenLabs]: u?   🛑 Error indicates terminal call end - reconnections disabledr   )r   r   agent_chat_response_partu   📨 [Unknown Message Type]: )"r=   r   r   r&   r%   rY   r   r@   r9   r   r   r   r   r   	b64decoder   r    r   rc   research
IGNORECASEDOTALLgroupstripra   r   r#   r   r!   r$   r   r   r"   )"r'   r   r   metadata_eventr   r   r   audio_bytesr   	tool_callr   r   
text_lowerr	  matchr  voicemail_patternsscoretool_requestr   tool_paramsr   correction_eventcorrected_textoriginal_texttool_responser   r   result_loweris_abandonedgreeting_eventr9   r   known_unhandled_typesr(   r   r)   handle_message  s  








	
$

"

(2	$

	

z)ElevenLabsWebSocketService.handle_messager{   c                 C   s`   |sdS |  }d|v r|dd}t|dkr|d  }g d}|D ]	}||v r- dS q$dS )a  
        Check if the WebSocket close reason indicates a terminal call end.
        Terminal reasons mean the call legitimately ended and should not reconnect.
        
        Args:
            close_reason: The close reason string from ElevenLabs (may include "How the call ended: " prefix)
            
        Returns:
            True if this is a terminal call end reason (should not reconnect)
        Fzhow the call ended:rL   )zconnection closed cleanlyzclient ended callzclient endedzuser ended callzuser hung upzuser disconnectedzagent ended callzagent endedzagent ended the callzcall exceeded maximum durationzmaximum durationzcall ended due to silencesilencezvoicemail detectedr   z
call endedzcall terminatedzconversation endedzsession endedT)r   splitlenr  )r'   r{   reason_lowerpartsterminal_patternsr   r(   r(   r)   r     s   !z7ElevenLabsWebSocketService._is_terminal_call_end_reasonc              
   C   s   | j sdS z7t| j dr9t| j jdr'| j jj}|dk}|| jkr$|| _|W S | j jtjk}|| jkr6|| _|W S W | jS  tyH   | j Y S  ty\ } z	| jW  Y d}~S d}~ww )z>Check if WebSocket is connected with improved state detection.FstatenameOPENN)	r   r   r'  r(  r   r   r)  AttributeErrorr@   )r'   
state_nameis_openrE   r(   r(   r)   rp     s,   



z'ElevenLabsWebSocketService.is_connectedc                    s   d| _ d| _| jr9| j s9| j  z z| jI d H  W n tjy'   Y n	 ty/   Y nw W d | _nd | _w | jrRz| j	 I d H  W d S  tyQ   Y d S w d S )NF)
r   r   r   r   cancelrY   rv   r@   r   closerG   r(   r(   r)   r.  1  s*   
z ElevenLabsWebSocketService.close)NN)FrI   )NFN)F)r   
__module____qualname____doc__r   r   r*   rF   rH   boolrs   r]   r_   dictr   bytesr   r   rq   r   r   r   r   r   r   rp   r.  r(   r(   r(   r)   r     s:    +
QR
5(
+t  
?r   )r1  rY   r?   r   rr   r\   websockets.protocolr   typingr   r   abcr   configr   services.log_utilsr   r   r(   r(   r(   r)   <module>   s&          #