o
    pwojh                     @   s*  d dl Z d dlZd dlZd dlmZmZmZ d dlmZ d dl	m
Z
 d dlmZ d dlmZ d dlmZ d dlmZ d d	lmZ d d
lmZ d dlmZmZ d dlmZmZ d dlmZmZ d dl m!Z! d dl"m#Z# d dl$m%Z% d dl&m'Z' e(e)Z*ee+e,ef ged f Z-dZ.dZ/G dd dZ0dS )    N)AnyCallable	Awaitable)	AriClient)log_ari_event)AriEventsListener)BotActionTrigger)create_metrics_tracker)MetricsTracker)	CallState)CallRegistry)CallStateMachinePROCESSED_CHANNELS)Settingsget_settings)CrmApiClientCrmApiError)create_crm_client)MediaManager)AgentSessionStore)BotSessionManagerg      $@g       @c                   @   s  e Zd Z				dTdedB dedB dedB dedB ddf
ddZd	edB d
e	ee
f dedB fddZdeddfddZdededdfddZdededdfddZdeddfddZdeddfddZdUddZdUddZdUdd Zd
e	ee
f ddfd!d"Zdeddfd#d$Zdeddfd%d&Zdeddfd'd(Zded)eddfd*d+ZdedB fd,d-Zdefd.d/ZdedB fd0d1Zd2ed3ed4ede	fd5d6Zde	fd7d8Zd9edefd:d;Z dedefd<d=Z!d9ede"fd>d?Z#de$e fd@dAZ%d9ededB fdBdCZ&d9ededB fdDdEZ'd9ed	eddfdFdGZ(d9ed	eddfdHdIZ)d9edefdJdKZ*e+de"fdLdMZ,e+de"fdNdOZ-e+de"fdPdQZ.de"fdRdSZ/dS )VCallServiceNsettings	broadcastcrmmetricsreturnc              
   C   s   |pt  | _t| j| _t | _t| j| _|d ur|nt| j| _	|d u | _
|d ur-|nt| j| _t| j| j| j| j| jd| _t| j| j| j| j| j| j| j| jd| _|| _t | _t| j| j| _i | _i | _t | _ d S )N)	on_update	on_actionr   )media_managercall_resolverget_agent_endpointget_registered_extensionon_bot_ready)!r   r   r   arir   registryr   r   r   r   	_owns_crmr	   r   r   _notify_on_bot_actionbot_managerr   resolve_call_for_eventr!   r"   _on_bot_readystate_machine
_broadcastr   agent_sessionr   _handle_ari_eventari_listener_pending_outbound_outbound_callsset_cleaned_calls)selfr   r   r   r    r6   services/call_service.py__init__    s<   

zCallService.__init__
channel_ideventc                 C   s  | dpg }d}|D ]}t| d}|r!t|r!|} nq|r.| j|}|r.|S |rY| j |}|r:|S | j |}	|	rY| j|	 |rQ| j	|	| t
d| |	S |r| j D ]!\}
}||kr| j |
}	|	r| j|	 | j	|	| |	  S q`dS )uG   Busca la llamada por canal, call_id UUID en args o caché de salientes.argsNz'"z/Llamada %s re-sincronizada al registry (Stasis))getstrstripr   _looks_like_call_idr%   get_by_channelr2   addlink_channelloggerinfor1   items)r5   r9   r:   r;   call_idrawvalueby_chfoundcachedcidchr6   r6   r7   r*   F   sH   z"CallService.resolve_call_for_eventfnc                 C   s   || _ | j| j d S N)r-   r)   set_on_updater'   )r5   rN   r6   r6   r7   set_broadcastp   s   zCallService.set_broadcastcalltriggerc                    sv   |j pd  }|dkr| ||jI dH  dS |dkr0td|j | |jI dH  dS t	d|j |j dS )z=Acciones disparadas por frases del bot (TE TRANSFIERO, etc.). transferNupdate_planu.   Actualizando plan call=%s — cortando llamadau%   Acción de bot desconocida %r call=%s)
actionr>   lowertransfer_call_to_numbertargetrC   rD   rF   hangupwarning)r5   rR   rS   rW   r6   r6   r7   r(   t   s   zCallService._on_bot_actionnumberc              
      s  d dd |p	dD }|std|j dS |js|jr'td|j dS |jdv r.dS |js:td|j dS d	|_||_	d
|_
| |I dH  | j|}td|j|| | j|jI dH  |j  r|jrz| j|j I dH  W n ty } ztd|j| W Y d}~nd}~ww z| j I dH  W n ty } ztd|j| W Y d}~nd}~ww d|_d|_ |jv rЇ fdd|jD |_| j|jI dH  z+| jj|| jjd	|jdgdI dH }d	|_| j||d  td|j|d | W n. ty3 } z!d|_d|_d|_
td|j|| | |I dH  W Y d}~dS d}~ww d|_
| |I dH  dS )z?Detiene el bot, saca externalMedia y origina destino al puente.rT   c                 s   s$    | ]}|  s|d kr|V  qdS )+N)isdigit.0cr6   r6   r7   	<genexpr>   s   " z6CallService.transfer_call_to_number.<locals>.<genexpr>u)   Transferencia sin destino válido call=%sNz'Transferencia ya en curso/hecha call=%sendedfailedz Transferencia sin bridge call=%sTtransferringu!   Transferencia call=%s → %s (%s)z.No se pudo quitar media del puente call=%s: %sz&Hangup media post-transfer call=%s: %sFc                    s   g | ]}| kr|qS r6   r6   r`   media_chr6   r7   
<listcomp>   s    z7CallService.transfer_call_to_number.<locals>.<listcomp>agent	caller_id
use_stasisapp_argsidu6   Pata transferencia originada call=%s channel=%s → %serroru/   Fallo originar transferencia call=%s → %s: %stransferred)joinrC   rq   rF   transfer_in_progressagent_leg_originatedrD   status	bridge_idtransfer_targetagent_stater'   r   format_endpointr)   stop_for_callexternal_media_channel_idr$   remove_from_bridge	Exceptionr\   r[   debugexternal_media_attachedchannel_idsr   close_sessionoriginate_channeloutbound_caller_idr%   rB   )r5   rR   r]   rZ   endpointexcchannelr6   rh   r7   rY      s   


z#CallService.transfer_call_to_numberc                    s   t d j| jj| jj| jj tj| 	 d jd d  d}| j
j |dI d H  t fdd jD  jr@ jd nd }|rYtj|  j|d	 jd d  d |  I d H  d S )
NuB   Iniciando conversación bot call=%s providers stt=%s tts=%s llm=%szpoliza-   name)poliza_taskc                 3   s    | ]
}| j kr|V  qd S rO   )r|   )ra   rL   rR   r6   r7   rc      s    
z,CallService._on_bot_ready.<locals>.<genexpr>r   z
watch-bot-)rC   rD   rF   r   stt_providertts_providerllm_providerasynciocreate_task_load_poliza_for_callr)   start_for_callnextr   _watch_bot_channelr'   )r5   rR   r   customer_channelr6   r   r7   r+      s2   
zCallService._on_bot_readyc              
      s   |j pd }|std|j dS | jjr| jjs$t	d|j dS z| j
|I dH }W n tyK } ztd||j| W Y d}~dS d}~ww |sXtd||j dS | |_t	d|j||j|j|j dS )zEGET /polizas/{document_id} y guarda el resultado en call.poliza_data.rT   u7   Bot call=%s sin document_id: no se consulta póliza CRMNu4   CRM deshabilitado: se omite carga de póliza call=%sz Error CRM polizas/%s call=%s: %su,   Póliza no encontrada CRM polizas/%s call=%su?   Póliza cargada call=%s doc=%s poliza=%s producto=%s cliente=%s)document_idr>   rC   r\   rF   r   crm_api_enabledr   enabledrD   
get_polizar   rq   	to_publicpoliza_datapolizaproductonombre_completo)r5   rR   docr   r   r6   r6   r7   r     sN   
z!CallService._load_poliza_for_callc              
      s   z
| j  I d H  W n ty# } ztd| W Y d }~nd }~ww t  | j I d H  | j	
  | j	js?td n| j	jsNtd|  I d H  tj|  dd d S )Nz!ARI cleanup on startup failed: %sz-No se pudo iniciar la tarea del WebSocket ARIu4   WebSocket ARI conectando… (HTTP ARI alcanzable=%s)zllm-warmup-startupr   )r$   cleanup_allr~   rC   r\   r   clearr   startupr0   start
task_aliverq   	connectedrD   ari_healthyr   r   _warmup_llm_background)r5   r   r6   r6   r7   r   .  s*   

zCallService.startupc                    s8   z| j  I d H  W d S  ty   td Y d S w )Nu   Warmup LLM en startup falló)r)   
warmup_llmr~   rC   	exceptionr5   r6   r6   r7   r   C  s   z"CallService._warmup_llm_backgroundc                    s`   | j  I d H  | j I d H  | j I d H  | j I d H  | jr.| j I d H  d S d S rO   )	r)   closer   shutdownr0   stopr$   r&   r   r   r6   r6   r7   r   I  s   zCallService.shutdownc                    s   | j |I dH }|r| | |r |jdv r | |I dH  |r*| |I dH  |ddkrR|dp7i d}| ||}|rT| | | |I dH  dS dS dS )z
        if self.settings.ari_debug:
            log_ari_event(
                event,
                full=self.settings.ari_debug_full_events,
            )
        Nrd   typeStasisStartr   rp   )r,   handle_event_touch_metricsrv   _cleanup_after_remote_endr'   r<   r*   )r5   r:   rR   r9   r6   r6   r7   r/   Q  s    

zCallService._handle_ari_eventc                 C   s,   | j | |jdv r| j |j dS dS )uB   Asegura CallSession en memoria y marca answerTime si ya contestó.answeredtalkingN)r   
start_callrv   mark_answeredrF   r5   rR   r6   r6   r7   r   h  s   
zCallService._touch_metricsc                    s*   | j r|  d| dI d H  d S d S )Ncall_update)r   rR   )r-   r   r   r6   r6   r7   r'   n  s   zCallService._notifyc                    s  |j }|| jv rdS | j| | j|I dH  |jpd}| jj||dI dH  t|j	}|j
r8||j
 |D ]'}z| j|I dH  W q: tya } ztd||| W Y d}~q:d}~ww | j|I dH  | j|d | j|d |jrz| j|jI dH  W n ty } ztd|j| W Y d}~nd}~ww d|_d|_
d|_d|_d|_| j| td| dS )z<Detiene bot, cuelga canales restantes y libera media/bridge.NcleanupreasonzHangup residual %s call=%s: %szBridge %s ya eliminado: %sFz9Cleanup completo call=%s (bot detenido, canales colgados))rF   r4   rA   r)   r{   disconnect_reasonr   finalize_and_persistr3   r   r|   r$   r[   r~   rC   r   r   r   r1   popr2   rw   delete_bridger   bot_media_readyry   r%   removerD   )r5   rR   rF   r   r   r9   r   r6   r6   r7   r   t  sP   


z%CallService._cleanup_after_remote_endr   c                    s^   |j dv rd S |r|js||_|d td|j| | |I d H  | |I d H  d S )Nrd   re   zColgado remoto call_id=%s (%s))rv   r   finalizerC   rD   rF   r   r'   )r5   rR   r   r6   r6   r7   _remote_hangup_detected  s   


z#CallService._remote_hangup_detectedc                 C   s2   | j jr| j jS | jj}|sd S | j jj|jdS )N	extension)r   agent_endpointr.   currentagent_endpoint_templateformatr   r5   rk   r6   r6   r7   r!     s   zCallService.get_agent_endpointc                 C   s(   | j j}|r|j d|j dS | jjS )Nz <>)r.   r   usernamer   r   r   r   r6   r6   r7   get_outbound_caller_id  s   z"CallService.get_outbound_caller_idc                 C   s   | j j}|r	|jS d S rO   )r.   r   r   r   r6   r6   r7   r"     s   z$CallService.get_registered_extensionr   r   passwordc              
      s  |  }|  }|r|r|std|  I d H }|s td| jjj|d}d}|dra|ddd }z| j	
d|I d H }|d u}W n ty` }	 ztd	||	 W Y d }	~	nd }	~	ww | j|||}
td
|||| d|
 ||| jjd| j dS )Nz/username, extension y password son obligatorioszARI no disponibler   FzPJSIP//   PJSIPz$No se pudo verificar endpoint %s: %sz8Agente registrado ext=%s user=%s endpoint=%s verified=%sT)r   rk   r   endpoint_verifiedwebrtc_enabledinbound_readysip)r>   
ValueErrorr   RuntimeErrorr   r   r   
startswithsplitr$   get_endpointr~   rC   r\   r.   registerrD   r   r   get_sip_config)r5   r   r   r   ari_okr   r   resourcerD   r   rk   r6   r6   r7   register_agent  sN   
zCallService.register_agentc                 C   s@   | j j}|  }|d u|r| nd || jj|d u| j dS )N)r   rk   r   r   r   r   )r.   r   r!   r   r   r   r   )r5   rk   r   r6   r6   r7   get_agent_status  s   zCallService.get_agent_statusrF   c              
      st   |  |}|st|z| j|I d H  W n ty* } ztt||d }~ww | | | |I d H  |S rO   )get_callKeyErrorr,   answer_inbound_callr   r=   r   r'   r5   rF   rR   r   r6   r6   r7   answer_inbound  s   

zCallService.answer_inboundc              
      sZ  | j |}tdd|d}| j| || j|j< | j|j | j	
| td|jt| j  z9| jj||  d|jdgdI d H }|d }| j|| || j|j< tj| |j|d	|jd d
  d W n5 ty } z)td| d|_|jpd| |_|d | j	j||jdI d H  W Y d }~nd }~ww | |I d H  |S )Noutboundringing)	directionrv   r]   z4Llamada saliente registrada call_id=%s (registry=%d)Tcustomerrl   rp   zwatch-outbound-r   r   zOriginate failed: %srf   zoriginate failed: r   )r   rz   r   r%   rA   r2   rF   r4   discardr   r   rC   rD   lenlist_allr$   r   r   rB   r1   r   r   _watch_outbound_channelr~   rq   rv   r   r   r   r'   )r5   r]   r   rR   r   r9   r   r6   r6   r7   start_outbound  sT   

	zCallService.start_outboundc                    s  | j |}|sdS t|jD ]&}z| j|I d H  W q ty6 } ztd|| W Y d }~qd }~ww |j	rg|j	|jvrgz| j|j	I d H  W n tyf } ztd|j	| W Y d }~nd }~ww |
d |jsrd|_| |I d H  | |I d H  dS )NFzHangup channel %s failed: %sz"Hangup externalMedia %s failed: %sre   z
hangup APIT)r%   r<   listr   r$   r[   r~   rC   r\   r|   r   r   r   r'   )r5   rF   rR   r9   r   r6   r6   r7   r[   -  s8   
zCallService.hangupc                 C   s
   | j  S rO   )r%   r   r   r6   r6   r7   
list_callsH  s   
zCallService.list_callsc                 C   s   | j |p| j|S rO   )r%   r<   r2   )r5   rF   r6   r6   r7   r   K  s   zCallService.get_callc              
      s  |  |}|r|jdkr|S |jr|jr|S | j|p&|jr%|jd nd}|s+|S z| j|I dH }W n, t	yb } z t
d|| |jsM|jdv rV| |dI dH  |W  Y d}~S d}~ww |ddkrl|S d	|d
g|d}| j|I dH }|r| |I dH  |S |S )uN   Si el WS ARI falla, configura puente/media vía HTTP cuando el canal está Up.r   r   Nzsync_outbound_media %s: %sr   zcanal no existe en ARIstateUpr   r   )r   r;   r   )r   r   r   rw   r1   r<   r   r$   get_channelr~   rC   r   outbound_stasis_setuprv   r   r,   r   r'   )r5   rF   rR   r9   rM   r   r:   updatedr6   r6   r7   sync_outbound_mediaN  s>   
zCallService.sync_outbound_mediac              
      s  d}t dD ]}tdI dH  | |}|r|jdv r  dS z| j|I dH }W n tyH   |s7|jrD| 	|d| dI dH  Y  dS w |
d}|d	kru|setd
|| | |I dH  d}| |}|rt|jrt|jrtd}q|r|dv r| 	|d| I dH   dS qdS )zHPolling ARI: setup al contestar y detectar colgado remoto sin WebSocket.FiX  g      ?Nrd   zcanal z cerrado en Asteriskr   r   u.   Canal %s Up — setup por polling (call_id=%s)T)DownRingingu   canal cliente pasó a )ranger   sleepr   rv   r$   r   r~   r   r   r<   rC   rD   r   r   rw   )r5   rF   r9   
setup_done_rR   rM   r   r6   r6   r7   r   o  sJ   






z#CallService._watch_outbound_channelc              	      s   	 | j rtnt}t|I dH  | |}|r|jdv rdS | j|s'dS z| j	
|I dH  W n tyI   | |d| dI dH  Y dS w q)u   Fallback de colgado remoto si el WS ARI pierde StasisEnd.

        Con WS sano: poll cada 10s (StasisEnd es el camino normal).
        Con WS caído o sin eventos recientes: poll cada 2s.
        TNrd   zcanal bot cliente z cerrado en Asterisk (watchdog))ari_ws_healthy_BOT_WATCH_INTERVAL_WS_OK_BOT_WATCH_INTERVAL_WS_DOWNr   r   r   rv   r)   
is_runningr$   r   r~   r   )r5   rF   r9   intervalrR   r6   r6   r7   r     s,   


zCallService._watch_bot_channelc              
      s   | j |p| j|}|st|| j |s| j | | jjs%|S | j|I dH  |j	s\z| j
|| j| j I dH  W |S  ty[ } ztd|| W Y d}~|S d}~ww |S )u@   Sesión WebRTC lista y reintento de externalMedia si hace falta.Nz#externalMedia pendiente para %s: %s)r%   r<   r2   r   rA   r   r   r   prepare_sessionr   attach_external_mediar$   r~   rC   r\   r   r6   r6   r7   ensure_webrtc_ready  s2   
	zCallService.ensure_webrtc_readyc                 C   s   | j jS rO   )r0   r   r   r6   r6   r7   ari_connected  s   zCallService.ari_connectedc                 C   s    | j j}|d uot | dk S )Ng     F@)r0   last_event_attime	monotonic)r5   lastr6   r6   r7   ari_events_recent  s   zCallService.ari_events_recentc                 C   s   | j jp| jS rO   )r0   r   r  r   r6   r6   r7   r     s   zCallService.ari_ws_healthyc                    s   | j  I d H S rO   )r$   health_checkr   r6   r6   r7   r     s   zCallService.ari_healthy)NNNN)r   N)0__name__
__module____qualname__r   BroadcastFnr   r
   r8   r=   dictr   r   r*   rQ   r   r(   rY   r+   r   r   r   r   r/   r   r'   r   r   r!   r   r"   r   r   r   r   boolr[   r   r   r   r   r   r   r  propertyr  r  r   r   r6   r6   r6   r7   r      s    
&

*

c
'

.

,+!%r   )1r   loggingr	  typingr   r   r   
ari.clientr   ari.debug_logr   
ari.eventsr   bot_actionsr   call_metricsr	   call_metrics.trackerr
   calls.modelsr   calls.registryr   calls.state_machiner   r   configr   r   
crm.clientr   r   crm.factoryr   media.managerr   services.agent_sessionr   services.bot_sessionr   	getLoggerr  rC   r  r=   r  r   r  r   r6   r6   r6   r7   <module>   s0    
