U
    Izi{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d/ee ee dddZee dddZedd	d
Zd0e	e
dddZdd Zd1ee e	ee dddZedddZd2eee	dddZdd Ze	dddZd3ee	dd d!Zeegdf d"d#d$Zed%d&d'Zee	d(d)d*Ze	dd+d,Zd-d. ZdS )4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s4td| jsBt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_timeZ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__  s6    z#ElevenLabsWebSocketService.__init__)returnc           
         s~  | j s
dS zddl}W n  tk
r6   td Y dS X zd| j }d| j i}|jddd}|jd	d
}|j||d4 I dH }|j	||d4 I dH }|j
dkr| I dH }|	dW  5 Q I dH R  W  5 Q I dH R  W S td|j
  W 5 Q I dH R  W 5 Q I dH R  W dS W 5 Q I dH R X W 5 Q I dH R X W n: tk
rx }	 ztd|	  W Y dS d}	~	X Y nX dS )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      )limitZlimit_per_hostg      $@)total)	connectortimeout)headers   Z
signed_urlzFailed to get signed URL: zError getting signed URL: )r
   aiohttpImportErrorr   errorr	   ZTCPConnectorZClientTimeoutZClientSessiongetstatusjson	Exception)
r&   r4   urlr2   r0   r1   sessionresponsedataer'   r'   r(   get_signed_url  s,    


0J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                    s  |   }d| ji}| jr6td| jdd  d n
td d}d}td	|d	 D ].}z|d	kr|d
|d
   }td| d| d|dd t|I dH  | jrtd| d| d| d n&td|dd  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k
r   ||k rtd| d| d| d Y qVn*d| d | d!}
t|
 d"| _t|
Y qV tk
r } z||k r.d#dl}td$| d| d%|  td&|   W Y NqVnFd#dl}d'| d(| }
t|
 td)|   d"| _t|
|W 5 d}~X Y qVX qVd"| _td'| d!dS )*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 c                 S   s   g | ]\}}||fqS r'   r'   ).0kvr'   r'   r(   
<listcomp>/  s     z6ElevenLabsWebSocketService.connect.<locals>.<listcomp>
   )extra_headersping_intervalping_timeoutclose_timeout)r1   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: )rB   r
   r   r   r%   rangeasynciosleepitemswait_for
websocketsconnectr   r   r   create_task_keep_aliver   TimeoutErrorwarningr6   r:   	tracebackdebug
format_exc)r&   rD   rE   r;   r2   connection_timeoutZ
base_delayattemptdelayZextra_headers_list	error_msgr?   ra   r'   r'   r(   r\   	  sl    

  &




z"ElevenLabsWebSocketService.connectc                    s  d}d}d}| j rztdI dH  | j s4W q| js^|d7 }||krZtd W qW q|  s|d7 }||krtd W qW qd}z:|  I dH  t }t	t d dkrt
d W n tjjk
rL } z`t|d	r|jnd
}t|dr|jr|jnd}td| d| d d| _W Y 
W qW 5 d}~X Y n tk
r } zn|d7 }|  r| j rtd| d| d|  ||kr|  std W Y W qW Y W qW 5 d}~X Y nX W q tjk
r   Y qY q tk
r } zh|d7 }| j r8td| d| d|  ||krh| jrT|  shtd W Y qW Y qW 5 d}~X Y qX qdS )zzSend keep-alive messages every 5 seconds using user_activity to prevent idle timeouts without interfering with bot speech.r      NrF   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: rI   Fu!   ⚠️ Keep-alive error (attempt rH   ): u<   ❌ Keep-alive: too many consecutive failures, stopping tasku,   ⚠️ Keep-alive unexpected error (attempt )r   rW   rX   r   r   r`   is_connectedsend_user_activitytimeintrb   r[   
exceptionsConnectionClosedr   rj   rl   r   r:   r6   CancelledError)r&   Zconsecutive_failuresZmax_consecutive_failuresZlast_success_timer?   
close_codeclose_reasonr'   r'   r(   r^   ^  sd    


 


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
}|rFd|kr:i |d< d|d d< ||d< |rZ||d< |rf||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
        typeZ#conversation_initiation_client_data	ulaw_8000Tg      ?i,  )enable	thresholdZprefix_padding_msZsilence_duration_ms)output_audio_formatZvadr~   )agentZttsr   rx   Zconversation_config_overriderw   ry   Nu/   📤 Sent conversation initiation to ElevenLabs)_send_messager   r%   )r&   rw   rx   ry   messageZconversation_configr'   r'   r(   send_conversation_initiation  s2      z7ElevenLabsWebSocketService.send_conversation_initiation)
audio_datac           	         s   | j r| jsdS zddl}| }|| j }d}||k r\|| }t|I dH  | | _n|| _t|d}d|d}| j	|ddI dH  W n t
k
r   Y nX dS )	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)rz   r   Tsilent)r   r   rp   r   rW   rX   base64	b64encodedecoder   r:   )	r&   r   rp   current_timeZtime_since_last_sendZmin_intervalZ
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.Zclient_tool_result)rz   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.Nrz   Zuser_activityTr   )r   r   rn   r   r&   r   r'   r'   r(   ro     s     z-ElevenLabsWebSocketService.send_user_activityc                    sZ   | j r| jsdS z.ddtt  d}| |I d H  W dS  tk
rT   Y dS X d S )NFZ	interruptZ
interrupt_rz   event_idT)r   r   rq   rp   r   r:   r   r'   r'   r(   send_interruption$  s    z,ElevenLabsWebSocketService.send_interruption)r   r   c              
      sN  | j s
d S z"t|}| j |I d H  W n tjjk
rv } z(d| _|sft	d|j
 d|j  W 5 d }~X Y n tjjk
r } zd| _|st	d|  W 5 d }~X Y n tk
rH } zvt|  t|j }t|ttfpd kp|dk}|r8t fddd	D r8d| _|s8t	d
|  W 5 d }~X Y nX d S )NFu2   ⚠️ WebSocket connection closed while sending: z - u#   ⚠️ WebSocket in invalid state: r   )ZconnectionerrorZoserrorc                 3   s   | ]}| kV  qd S Nr'   )rK   phrase	error_strr'   r(   	<genexpr>O  s     z;ElevenLabsWebSocketService._send_message.<locals>.<genexpr>)zconnection closedzconnection lostznot connectedzwebsocket is closedzwebsocket is closingu"   ⚠️ Connection error detected: )r   r9   dumpssendr[   rr   rs   r   r   r`   rj   rl   InvalidStater:   strlowerrz   __name__
isinstanceConnectionErrorOSErrorany)r&   r   r   Zmessage_jsonr?   
error_typeZis_connection_errorr'   r   r(   r   1  s2    
* z(ElevenLabsWebSocketService._send_message)
on_messagec                    sl  | j stdzDz| j 2 z3 dH W }zt|}|dd}|dkr|di }|d	}|rd
|d}| j|ddI dH  W qt	
|r||I dH  n|| W q tk
r } z@ddl}	td|  td|  td|	   W 5 d}~X Y qX q6 | j rz
t| j dd}
t| j ddp:d}|
dk	sL|r td|
 d| d d| _| |}|rtd| d ntd| d | jr zBt	
| jr| |
pd||I dH  n| |
pd|| W n4 tk
r } ztd|  W 5 d}~X Y nX W n tk
r } zptd|  d| _| jrz6t	
| jrz| dddI dH  n| ddd W n tk
r   Y nX W 5 d}~X Y nX W n tjjk
r } zd| _t|d r|jnd}
t|d!r|jr|jnd"}td|
 d| d | |}|rHtd| d ntd| d | jrz6t	
| jr| |
||I dH  n| |
|| W n4 tk
r } ztd|  W 5 d}~X Y nX | jr| j std# W 5 d}~X Y nV tk
rP } z6td$|  ddl}	td%|	   d| _W 5 d}~X Y nX W 5 d| _td X dS )&z
        Receive messages from ElevenLabs WebSocket.
        
        Args:
            on_message: Callback function to handle received messages
        zWebSocket not connectedFz&ElevenLabs receive_messages task endedNrz   rk   ping
ping_eventr   pongr   Tr   r   u   ❌ Error processing message: zMessage data: rU   ru   rv   zConnection closed cleanlyu6   ⚠️ ElevenLabs WebSocket connection closed - Code: z, Reason: ''u2   🛑 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)rj   rl   zNo reason provideduW   ⚠️ Connection closed while keep-alive was active - may indicate server-side closureu   ❌ Error receiving messages: rT   )r   r:   r   r   r%   r9   loadsr7   r   rW   iscoroutinefunctionra   r6   rc   getattrr`   r   _is_terminal_call_end_reasonrb   r!   r[   rr   rs   r   rj   rl   r   done)r&   r   r   r>   msg_typer   r   Zpong_msgr?   ra   ru   rv   is_terminal_reasoncallback_errorr'   r'   r(   receive_messages\  s    



*
&
"z+ElevenLabsWebSocketService.receive_messages)r   c           "         sb	  | dd}|dkr| di }| d| _| jrtd| jd d  d | jrz0t| jrv| | jI d H  n| | j W n2 tk
r } ztd	|  W 5 d }~X Y nX | d
d| _	| dd| _
n|dkr4| di }td|  | j	r^t| jr&| |I d H  n
| | n*|dkr| di  dd}td|  | j	r^t| jr| |I d H  n
| | n|dkr| di  dd}|	r^t|}| j	r^t| jr| |I d H  n
| | nb|dkrP| di  dd}| j	r^t| jrB| |I d H  n
| | n|dkr| di }	|	 dd}
|	 dd}n|dkr:| ddpd}| }d |ksd!|ksd"|ksd#|ksd$|krtd%|d d&  d' ntd(|d d)  d' |	r^d*|k	r^d+d l}|d,||j|jB }|sX|d-||j|jB }|	r^|d. }td/|  |  d0d1d2d3d4d5d6d7d8d9d:d;g}t fd<d=|D 	r^td> | j	r^z6t| jr| j|d?d@I d H  n| j|d?d@ W n4 tk
r4 } ztdA|  W 5 d }~X Y nX n$|dBkr| dCi  dBdD}| j	r^t| jr| |I d H  n
| | n|dEkrb| dEi }| dd}
| dd}| dFd}| ddG}| dHi }tdI|
 dJ| dK| dL| dM	 tdN|  tdO|  | j	r^t| jrP| |
||I d H  n| |
|| n|dPkr| dQi }| dd}| dRi }tdS| dT|  n|dUkr | dVi }| dWd}| dXd}| dd}|r
tdY| dZ| d[| d\ ntdY| dM n>|d]krZ| d]i }| dd}| d^d}| d_d`}|rh| nd}da|kpdb|kpdc|k}|r(tdd| dT|r|d d) nde  | jrVz2t| jr| ||I d H  n| || W n4 tk
r$ } ztdf|  W 5 d }~X Y nX n.tdg| dh| dT|rL|d di nde  n|djksn|dkkr|| dli p| dmi }| dndp| ddpt|}tdo|  |	r^|  d0d1d2d3d4d5d6d7d8d9d:g}t fdpd=|D 	r^tdq | j	r^z6t| jr4| j|d?d@I d H  n| j|d?d@ W n4 tk
rx } ztdA|  W 5 d }~X Y nX n|drk	r:| dri }t| } tds|  |  | 	r^tdt | j!	r^z6t| j!r| !dr| d?I d H  n| !dr| d? W n4 tk
	r6 } ztdu|  W 5 d }~X Y nX n$dvdwdxg}!||!k	r^tdy|  d S )zNrz   rk   Z conversation_initiation_metadataZ&conversation_initiation_metadata_eventr   u   📝 Received conversation_id: r   z... (will reuse on reconnect)u0   ❌ Error in conversation_id_received callback: Zagent_output_audio_format
mulaw_8000Zuser_input_audio_formatZpcm_8000user_transcriptZuser_transcription_eventu   🎤 [User Transcript]: Zagent_responseZagent_response_event u   🤖 [Agent Response]: ZaudioZaudio_eventZaudio_base_64ZinterruptionZinterruption_eventr   Zclient_tool_call	tool_namer   Z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]: r3   zautomated greeting detectedr   z.automated greeting detected:\s*['\"](.*?)['\"]z#automated greeting detected:\s*(.+)rF   u:   📞 [Automated Greeting Detected via Contextual Update]: 	voicemailzleave a messagezrecord your messageznot availablezat the tonezplease recordzafter the tonezfinished recordingzperson you are trying to reachz%call has been forwarded to voice mailzforwarded to voice mailzyour call has been forwardedc                 3   s   | ]}| kV  qd S r   r'   rK   patternZgreeting_lowerr'   r(   r   >  s     z<ElevenLabsWebSocketService.handle_message.<locals>.<genexpr>uE   🛑 Voicemail detected via contextual update - marking call as endedT)is_voicemailu3   ❌ Error in automated_greeting_detected callback: Z	vad_scoreZvad_score_eventg        Zagent_tool_request	tool_typezN/A
parametersu   🔧 [Agent Tool Request]: z (id: z, type: z, event_id: rI   u2      📋 Tool Parameters received from ElevenLabs: u      📦 Full tool request: Zagent_response_metadataZagent_response_metadata_eventmetadatau*   📊 [Agent Response Metadata] (event_id: rm   Zagent_response_correctionZagent_response_correction_eventZcorrected_agent_responseZoriginal_agent_responseu,   🔧 [Agent Response Correction] (event_id: z): 'z' -> 'r   Zagent_tool_responser   r   FZ	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,   Zautomated_greeting_detectedZautomated_greetingZ!automated_greeting_detected_eventZautomated_greeting_eventgreeting_textu$   📞 [Automated Greeting Detected]: c                 3   s   | ]}| kV  qd S r   r'   r   r   r'   r(   r     s     uF   🛑 Voicemail detected via automated greeting - marking call as endedr6   u   ❌ [Error from ElevenLabs]: u?   🛑 Error indicates terminal call end - reconnections disabledr   r   r   Zagent_chat_response_partu   📨 [Unknown Message Type]: )"r7   r   r   r%   r$   rW   r   r:   r6   r   r   r   r   r   	b64decoder   r   r   rb   research
IGNORECASEDOTALLgroupstripr`   r   r"   r   r    r#   r   r   r!   )"r&   r   r   Zmetadata_eventr   r   r   audio_bytesr   Z	tool_callr   r   Z
text_lowerr   matchr   voicemail_patternsscoretool_requestr   tool_paramsr   Zcorrection_eventZcorrected_textZoriginal_textZtool_responser   r   Zresult_lowerZis_abandonedZgreeting_eventr6   r   Zknown_unhandled_typesr'   r   r(   handle_message  sv   "







&

$

 

($2$
$

$

z)ElevenLabsWebSocketService.handle_message)rv   r*   c                 C   s   |sdS |  }d|kr<|dd}t|dkr<|d  }dddddd	d
dddddddddddg}|D ]}||krh dS qhd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:rF   z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 silenceZsilencezvoicemail detectedr   z
call endedzcall terminatedzconversation endedzsession endedT)r   splitlenr   )r&   rv   Zreason_lowerpartsZterminal_patternsr   r'   r'   r(   r     s<    z7ElevenLabsWebSocketService._is_terminal_call_end_reasonc              
   C   s   | j s
dS zjt| j drrt| j jdrN| j jj}|dk}|| jkrH|| _|W S | j jtjk}|| jkrl|| _|W S W nD tk
r   | j Y S  tk
r } z| j W Y S d}~X Y nX | jS )z>Check if WebSocket is connected with improved state detection.FstatenameOPENN)	r   r   r   r   r   r   r   AttributeErrorr:   )r&   Z
state_nameZis_openr?   r'   r'   r(   rn     s&    




z'ElevenLabsWebSocketService.is_connectedc                    s   d| _ d| _| jrn| j sn| j  z>z| jI d H  W n( tjk
rN   Y n tk
r`   Y nX W 5 d | _X | jrz| j	 I d H  W n tk
r   Y nX d S )NF)
r   r   r   r   cancelrW   rt   r:   r   closerA   r'   r'   r(   r   3  s     

z ElevenLabsWebSocketService.close)NN)FrC   )NFN)F)F)r   
__module____qualname____doc__r   r   r)   r@   rB   boolrq   r\   r^   dictr   bytesr   r   ro   r   r   r   r   r   r   rn   r   r'   r'   r'   r(   r     s4   +
UR   5(
+t  
=r   )r   rW   r9   r   rp   r[   Zwebsockets.protocolr   typingr   r   abcr   configr   services.log_utilsr   r   r'   r'   r'   r(   <module>  s   