o
    bfjm                  	   @   s  U d dl 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	 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 dd	lmZ dd
lmZ edZe  eddZeddZeddZedZ de!e"e"f fddZ#de"de"fddZ$eddZ%edd& Z'edddZ(edd Z)da*ej+dB e,d!< e)-d"dOd#d$Z.e)/d%de0e"ef fd&d'Z1d(ede2fd)d*Z3dPd,ed-e4de"fd.d/Z5d0e"d1e0e"ef de0e"ef fd2d3Z6e)/d4d(edefd5d6Z7e)/d7d(edefd8d9Z8e)9ed(edefd:d;Z:dOd<d=Z;ddd>d(ed?e4dB d@e4dB defdAdBZ<e)9e  dCdDe4dEe4d(edefdFdGZ=e)9e  dHdIe4d(edefdJdKZ>e)9ed(edefdLdMZ?e@dNkroe;  dS dS )Q    N)Any)urlparse)FastAPIRequest)JSONResponse   )load_agent_runtime_dotenv)'get_runtime_overrides_from_agents_table)get_default_mcube_call_config)configure_mcube_file_loggingzmcube.webhook	REDIS_URLzredis://localhost:6379/0MCUBE_WEBHOOK_PATHz/webhooks/mcubeMCUBE_OUTBOUND_PATHz/api/mcube/outbound-call/returnc                  C   s.   t   tdd } tdd }| |fS )z
    Re-read backend/.env on each outbound call so ngrok URL updates apply without
    restarting the webhook (stale MCUBE_PUBLIC_* was causing Mcube to open WSS to an old tunnel).
    MCUBE_PUBLIC_BASE_URL MCUBE_PUBLIC_WS_URL_BASE)r   osgetenvstrip)basewss r   Y/var/www/html/livekitdocker/backend/agent_runtime/src/mcube_integration/webhook_server.py_mcube_public_urls   s   r   urlc                 C   s>   | pd  }|s
dS z
t|jpd W S  ty   Y dS w Nr   )r   r   netloclower	Exception)r   ur   r   r   _url_netloc(   s   r"   MCUBE_WS_PATH_PREFIXz/bid/websocketMCUBE_WS_BUSINESS_IDr   AGENT_BACKEND_BASE_URLzhttp://localhost:8000zMCube Webhook Receiver)title_redisstartupc                      s.   t jtddat I d H  tdt d S )NF)decode_responsesz$mcube webhook: connected to redis %s)redis_asyncfrom_urlr   r'   pingloginfor   r   r   r   _startup<   s   r/   z/healthc                      s
   ddiS )Nstatusokr   r   r   r   r   healthE   s   r2   requestc                 C   s2   t dd }|sdS | jdd }||kS )ue   
    Protect /api/mcube/inspect/* — set MCUBE_INSPECT_KEY and send header X-Mcube-Inspect-Key.
    MCUBE_INSPECT_KEYr   FzX-Mcube-Inspect-Key)r   r   r   headersget)r3   expectedgotr   r   r   _inspect_authorizedJ   s
   r9     val	max_charsc                 C   sX   | d u rdS t | dddd}|dd }t||kr"|S |d |d  d S )Nr   \n
z
 r   u   …)strreplacer   len)r;   r<   sr   r   r   _preview_cfg_fieldU   s   rD   call_idcfgc                 C   s   | d}t|tr|dd}| dpd }| | d| d| d| d	| d
| d| d| d| d| dt|dt|ddS )Nsystem_promptr=   r>   first_messager   business_idbot_idagent_id
agent_nameuser_idagent_email	llm_modelllm_providerstt_providertts_providerr:   )rE   rI   rJ   rK   rL   rM   rN   rO   rP   rQ   rR   system_prompt_previewfirst_message_preview)r6   
isinstancer@   rA   r   rD   )rE   rF   spfmr   r   r   _summarize_stored_config_   s$   

rX   z/api/mcube/inspect/activec                    s  t | stddddddS t}|dusJ ttdd	}td
td|}g }d}z~|jdd2 zt3 dH W }||kr> nj|d
7 }t	|t
tfrP|jdddnt|}z6||I dH }|si||dd W q2t	|t
tfrw|jdddnt|}t|}	||	d< ||	 W q2 ty }
 z||t|
d W Y d}
~
q2d}
~
ww 6 W n" ty }
 ztd tdt|
dddW  Y d}
~
S d}
~
ww tdt||dS )z
    Live PSTN legs where MCube sent `start` and we loaded Redis config (mcube_ws_active:*).
    Requires X-Mcube-Inspect-Key matching MCUBE_INSPECT_KEY.
    Funauthorized2Set MCUBE_INSPECT_KEY and send X-Mcube-Inspect-Keyr1   errorhint  status_codeNMCUBE_INSPECT_MAX_KEYS200r     r   zmcube_ws_active:*matchutf-8rA   errorsempty)	redis_keyr\   rj   zinspect active failedr1   r\     T)r1   countactive)r9   r   r'   intr   r   maxmin	scan_iterrU   bytes	bytearraydecoder@   r6   appendjsonloadsr    r-   	exceptionrB   )r3   redismax_krowsnkeyksrawraw_strpayloader   r   r   inspect_active_mcube_sessionsu   sN   
$$
 
"r   z /api/mcube/inspect/redis-configsc                    s  t | stddddddS t}|dusJ ttdd	}td
td|}g }d}z|jdd2 z3 dH W }||kr> n|d
7 }t	|t
tfrP|jdddnt|}|dd
d }zL||I dH }|sr|||dd W q2t	|t
tfr|jdddnt|}	t|	}
t	|
ts|||dd W q2t||
}||d< || W q2 ty } z|||t|d W Y d}~q2d}~ww 6 W n" ty } ztd tdt|dddW  Y d}~S d}~ww tdt||dS )u   
    Recent `mcube_call_config:*` blobs written at outbound-call time (TTL ~6h). Useful to see which
    bot/prompt was stored before MCube connects — compare with /inspect/active when a call is up.
    FrY   rZ   r[   r^   r_   Nra   rb   r   rc   r   zmcube_call_config:*rd   rf   rA   rg   mcube_call_config:ri   )rj   rE   r\   
not_a_dictrj   zinspect redis-configs failedrk   rl   T)r1   rm   stored_configs)r9   r   r'   ro   r   r   rp   rq   rr   rU   rs   rt   ru   r@   splitr6   rv   rw   rx   dictrX   r    r-   ry   rB   )r3   rz   r{   r|   r}   r~   r   suffixr   r   rF   rowr   r   r   r   inspect_redis_call_configs   sX   
$$


"
"r   c              
      s  z|   I d H }|dp|dp|d}t|dd }|dd}|dd	}|s:td
ddddW S t}|d usBJ d| d| }|j|ddddI d H }|s`tdddW S |jd| t|trp|dn|ddI d H  |jd| t|t	t
frt|dnt|dddI d H  |jd| t|dddI d H  |dv }	|	r|jd| dddI d H  tddiW S  ty }
 ztd  td
d!t|
d"d#dW  Y d }
~
S d }
~
ww )$NrE   callIdcallIDr0   r   durationr   answered_byhumanFzmissing call_idrk     r_   zmcube_webhook_processed::r   T   )nxex)r1   skippedzmcube_call_status:rf   iQ r   zmcube_call_duration:zmcube_call_answered_by:>   	no-answerbusyfailedblocked	completed	voicemailnot_answeredzmcube_call_ended:`T  r1   zmcube webhook failedmcube_webhook_failed)r1   r\   detailrl   )rw   r6   r@   r   r   r'   setrU   encodero   floatr    r-   ry   )r3   r   call_sidr0   r   r   rz   idem_keyalreadyterminalr   r   r   r   mcube_webhook   sT   (
r   c                  C   sN   t jt jd td dd l} tdd}ttdd}| jt	||d d S )	N)levelwebhookr   MCUBE_WEBHOOK_HOSTz0.0.0.0MCUBE_WEBHOOK_PORT8002)hostport)
loggingbasicConfigINFOr   uvicornr   r   ro   runAPP)r   r   r   r   r   r   main   s   r   path_business_idpath_bot_idr   r   c          :   
      s>
  ddl m} |  I dH }t|dp|dp|dpd |dp1|d	p1|d
}|dp;|d}|durB|}|durH|}ttdddtdt	tt
f ffdd dt
dt
dt	tt
f ffdd}t|dp{tdd }s|ds|dr|ddd dd}	z|	d}
|	|
d   W n	 ty   Y nw |dp|dpd }|std d!d"d#d$S dt	tt
f f fd%d&}t }t| |||I dH \}}td't | d( t|pi t|pi  t|d)p|d*p| }|}|d+}|d,}|d-p)|d-}|d.p4|d.}|d/vr>t|nt|pGtd0d }|d/vrTt|nt|p]td1d2 }t|d3p|d4p|d5p|d6p|d7p| jd3p| jd7ptd3dptd8d }tdusJ t }d9t
dt
fd:d;}t	|}| D ]}|||||||||< q|d/vrezt|}W n ty   d}Y nw |duretjt||d<p|d=|d>p|d?|d@p|dA|dp|ddBI dH }| D ]\}} | r,| ||< q!dCD ]4}!|||!||!||!t|t	rJ||!nd}"|"durct|"tr_|" dkrc|"||!< q0|dD}#t|#trv|# dEdF}#||#|dDp|dD |dD< ||dG|dH|dGpd |dG< dID ]}|||||p|| ||< qdJt
dt!fdKdL}$|$|r||d< |$|r||d< dMD ]}||}%|%durt|%tr|% dkr|%||< q|d@durt|d@pd dkrt|d@ |dA< |dAdur3t|dAp!d dkr3t|dA |dA< |r9|sXtj"dN| t#|$dOdPdQI dH  tdR|ddSdTdRdUS ddVl%m&}& |&|dW}'t' \}(})t(|(}*t(|)}+|dXp}|dYp}d },|,r|*rt(|,|*krt)dZt(|,|* d},|,p|(r|(*d t+ nd}-|)r|)*dnd}.t,pt|d[p|d\pd }/|d]p|d^pd }0|0r|+rt(|0|+krt)d_t(|0|+ d}0|0 }1|1s|.r|/rt|/- r|. d|/ d`| }1n	|. t. d| }1|1rF|pd }2|2r+|2dar.|1}nt(|2}3|+rF|3rF|3|+krFt)db|3|+ |1}t#|$dO}4tj"dN| |4dPdQI dH  tdc|t | d(  z|'j/||||||-pud|1pydddI dH }5W n& ty }6 zt0de| td df|t|6dgdhd$W  Y d}6~6S d}6~6ww tj"dN| |4dPdQI dH  |5j1rtj"dN|5j1 |4dPdQI dH  t|5j2pd3 }7|5j1pd}8dR||8|5j2|-|1|di}9|8du r|7rdj|7vrdk|9dl< t|9S |8du rdm|9dl< t|9S |7rdj|7vrdn|7vrdo|5j2dp|9dl< t|9S )qa  
    Minimal endpoint to kick off an outbound MCube click-to-call.

    This integrates with MCube's Restmcube-api/outbound-calls endpoint which expects:
    - HTTP header: Authorization
    - JSON body keys: custnumber, exenumber, gid, refurl, refid (as per your doc)

    URL forms (path overrides business_id / bot_id in JSON when set):
    - POST /api/mcube/outbound-call/{business_id}/{bot_id}
    - POST /api/mcube/outbound-call/{bot_id} (legacy: bot in path, business_id in body)
    - POST /api/mcube/outbound-call (all identifiers in body)

    Body examples supported:
    - { "to": "+1555..." , "exenumber": "8700...", "gid": "1", "call_id": "optional" }
    - or use env defaults for exenumber/gid/auth.
    r   )uuid4NrL   	agentNameagentr   rI   
businessIdbidrJ   botIdAGENT_BACKEND_FETCH_TIMEOUT_Sz15.0namer   c              
      s>  | si S t  d|  d}tj d}zztj|d4 I d H b}||4 I d H A}|jdkrMtd| |j i W  d   I d H  W  d   I d H  W S | I d H W  d   I d H  W  d   I d H  W S 1 I d H sow   Y  W d   I d H  W d S 1 I d H sw   Y  W d S  t	y   t
d|  i  Y S w )Nz/api/agents/z/config/totaltimeout   z<mcube outbound: agent config fetch failed agent=%s status=%sz3mcube outbound: agent config fetch errored agent=%s)r%   aiohttpClientTimeoutClientSessionr6   r0   r-   warningrw   r    ry   )r   r   r   sessionrespfetch_timeout_sr   r   _fetch_agent_mcube_config5  s4   
	2
z9_outbound_call_handler.<locals>._fetch_agent_mcube_configbusiness_id_val
bot_id_valc              
      s  | dv s	|dv ri S z
t | }t |}W n ty    i  Y S w t d| d| d}tj d}z{tj|d4 I d H c}||4 I d H B}|jdkrmt	d|||j i W  d   I d H  W  d   I d H  W S |
 I d H W  d   I d H  W  d   I d H  W 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   td	|| i  Y S w )
Nr   z/api/agents/cluster/bots/r   z/mcube-config/r   r   r   zJmcube outbound: cluster bot config fetch failed bid=%s bot_id=%s status=%szAmcube outbound: cluster bot config fetch errored bid=%s bot_id=%s)ro   r    r%   r   r   r   r6   r0   r-   r   rw   ry   )r   r   bid_intbot_intr   r   r   r   r   r   r   _fetch_cluster_bot_mcube_configI  sB   

2z?_outbound_call_handler.<locals>._fetch_cluster_bot_mcube_configrefurlMCUBE_REFURLhttp://https://?r   r   
custnumbertoFz$missing 'to' (or 'custnumber') valuerk   r   r_   c                      s,   rt   dv ri S  I d H S )N)defaultr   )r@   r   r   r   )r   rL   r   r   _agent_cfg_wrappedx  s   z2_outbound_call_handler.<locals>._agent_cfg_wrappedzNmcube outbound: django config fetch done ms=%.0f agent_keys=%s cluster_keys=%sg     @@refidrE   	exenumbergidmcube_exenumber	mcube_gidr   MCUBE_EXENUMBER	MCUBE_GID1HTTP_AUTHORIZATIONhttp_authorizationhttpAuthorizationauthorizationAuthorizationMCUBE_HTTP_AUTHORIZATIONvalsc                  W   s6   | D ]}|d u r	qt |tr| dkrq|  S d S r   rU   r@   r   )r   vr   r   r   _pick  s   z%_outbound_call_handler.<locals>._pickrK   agentIdrM   userIdemailrN   )rK   rM   r   r   )message_inboundmessage_outboundplatform_settingsconversation_behaviorrG   r=   r>   rH   agent_first_message)rO   rP   rQ   stt_language_codestt_model_idrR   	tts_modeltts_voice_idtts_encodingtts_chunk_mstts_gainplayback_pace_factorcheckpoint_everyr;   c                 S   s*   | d u rdS t | tr|  dkrdS dS )NFr   Tr   )r;   r   r   r   _cfg_nonempty  s
   z-_outbound_call_handler.<locals>._cfg_nonempty)rK   rM   rL   r   rf   r   r   Tnot_initiatedz)MCube auth token and/or exenumber not set)r1   rE   mcube_call_sidr0   r   stored_config)MCubeProvider)r   callback_urlcallbackUrlzZmcube outbound: ignoring stale client callback_url host=%r (MCUBE_PUBLIC_BASE_URL host=%r)mcube_ws_business_idmcubeWsBusinessIdwebsocket_urlwebsocketUrlz^mcube outbound: ignoring stale client websocket_url host=%r (MCUBE_PUBLIC_WS_URL_BASE host=%r)z/websocket/)r   r   zPmcube outbound: ignoring stale refurl host=%r (MCUBE_PUBLIC_WS_URL_BASE host=%r)zHmcube outbound: calling MCube initiate refid=%s prep_since_fetch_ms=%.0f)r   r   r   r   r   r	  r  z/mcube outbound: initiate_call failed call_id=%smcube_initiate_call_failed)r1   r\   rE   r   i  )r1   rE   r  r0   r	  r  mcube_refurlsucczMCube did not return a call id and status does not look successful; check mcube outbound logs (full response body) and the destination number.r   z{MCube did not return a callid in the response; outbound may not have been queued. See mcube.provider logs for the raw body.successzMCube status was z9; confirm in MCube dashboard whether the call was placed.)4uuidr   rw   r@   r6   r   r   r   r   r   r   
startswithr   indexr    r   timeperf_counterasynciogatherr-   r.   rB   r5   r'   r
   keysro   	to_threadr	   itemsrU   rA   boolr   dumpsr   providers.mcube_providerr  r   r"   r   rstripr   r$   isdigitr#   initiate_callry   r   r0   r   ):r3   r   r   r   bodyrI   rJ   r   r   partsidxcustnumber_inr   outbound_prep_t0	agent_cfgcluster_bot_cfgr   r   exenumber_bodygid_bodyexenumber_dbgid_dbr   r   r   defaultsr   call_configkr   biabkbv	extra_keyevbody_system_promptr  r   r  providerpub_basepub_wssexpected_http_hostexpected_wss_hostbody_cbr	  ws_basebid_wsbody_wsr  ruru_hostcfg_blobresultr   st	mcube_sidr   r   )r   rL   r   r   _outbound_call_handler	  s  
"


"









&

$
	(


$..

 "" 			
	rF  z/{business_id}/{bot_id}rI   rJ   c                    s   t || |dI d H S )Nr   rF  )rI   rJ   r3   r   r   r   outbound_call_scoped  s   rH  z/{legacy_path_bot_id}legacy_path_bot_idc                    s   t || dI d H S )N)r   rG  )rI  r3   r   r   r   outbound_call_legacy_path_bot  s   rJ  c                    s   t | I d H S )NrG  )r3   r   r   r   outbound_call  s   rK  __main__)r   N)r:   )Ar  r   rw   r   r  r   typingr   urllib.parser   redis.asyncior*   fastapir   r   fastapi.responsesr   env_loadr   business_id_agentsr	   mcube_defaultsr
   service_logr   	getLoggerr-   r   r   r   r   r   MCUBE_OUTBOUND_PATH_BASEtupler@   r   r"   r#   r   r$   r%   r   r'   Redis__annotations__on_eventr/   r6   r   r2   r  r9   ro   rD   rX   r   r   postr   r   rF  rH  rJ  rK  __name__r   r   r   r   <module>   s   
 


&
',
4
   +

