
    ًxjH              
      ~
   U d Z ddlmZ 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 ddlmZmZmZ  ee      j-                         j.                  Z ed	      Zej.                  j5                  d
d
        ej6                  d      Z eeddd      Zej=                  e        ej>                  ej@                        Z!e!j=                  e        ejD                  ejF                  ee!g        ejH                  d      Z%	 ddl&m'Z'  e'd        e'd        e'edz          ejp                  dd      ja                         Z9 ejp                  dd      ja                         Z: ejp                  dd      ja                         Z; ejp                  dd       ejp                  d d       ejp                  d!d       ejp                  d"d       ejp                  d#d       ejp                  d$d       ejp                  d%d      d&Z< e= ejp                  d'd(            Z> e= ejp                  d)d*            Z? e= ejp                  d+d,            Z@ ejp                  d-d.      j                         d.k(  ZBe9se%j                  d/       ed0z  ZDi ZEd1eFd2<   eDjY                         r<	  ej                  eDj                  d            ZEe%j                  d3 eJeE             ne%j                  d5       dd6lNmOZO dd7lPmQZQmRZRmSZSmTZTmUZUmVZVmWZWmXZX  eeY      ZZ e
       Z[d8eFd9<    ej                         Z]d:Z^d;Z_dWd<Z`i Zad=eFd><    ej                         ZbdXd?Zc	 	 	 	 dYd@Zd	 	 	 	 	 	 dZdAZeddBd[dCZfd\dDZgeZj                  dEdFgG      dH        ZieZj                  dIdJgG      dK        ZjeZj                  dLdFgG      dM        ZkeYdNk(  rg el ejp                  dOdP            Zm ejp                  dQdR      Zne%j                  dSeneme> ejp                  dT             eZj                  enemdUd
V       yy# e($ r dd e)edz        fD ]  Z*ejV                  jY                  e*      s e-e*d      5 Z.e.D ]w  Z/e/ja                         Z1e1re1je                  d      sde1vr+e1jg                  dd      \  Z4Z5ejl                  jo                  e4ja                         e5ja                                y 	 ddd       # 1 sw Y   xY w Y w xY w# eK$ rZLe%j                  d4eL       Y dZL[L/dZL[Lww xY w)]u  
TORQ SDR Bruno — webhook Flask porta 8920.

Fluxo:
  1. GHL manda POST /webhook (mensagem inbound WhatsApp)
  2. Responde 200 imediatamente
  3. Idempotencia via message_id (dedup em memoria)
  4. Debounce: acumula msgs do mesmo contato durante DEBOUNCE_SECONDS
  5. Busca historico da conversa via GHL API + contexto do contato
  6. Chama Bruno (Claude Sonnet 4.6 com prompt caching)
  7. Parseia resposta -> texto + meta (campos, stage, notify)
  8. Envia texto pro lead via GHL (com typing delay + blocos [[BREAK]])
  9. Aplica meta: atualiza custom fields + move opportunity + notifica Renato

Log estruturado em /opt/mia/logs/sdr_bruno.log
    )annotationsN)OrderedDict)RotatingFileHandler)Path)Any)Flaskjsonifyrequestz/opt/mia/logs/sdr_bruno.logT)parentsexist_okz1%(asctime)s [%(levelname)s] %(name)s: %(message)si      utf-8)maxBytesbackupCountencoding)levelhandlerstorq_sdr)load_dotenvz/opt/mia/.envz/opt/mia-bot/.envztorq_bruno.envr   #=   LINKIA_TOKEN LINKIA_LOCATION_IDPIPELINE_IDSTAGE_NOVO_LEADSTAGE_QUALIFICADOSTAGE_TEST_DRIVESTAGE_PROPOSTASTAGE_PERDIDOSTAGE_GANHAMOSSTAGE_PERDEMOS)	novo_leadqualificadotest_drive_agendadopropostaperdidoganhamosperdemosSDR_DEBOUNCE_SECONDSz6.0SDR_TYPING_DELAY_SECONDSz3.0SDR_BLOCK_DELAY_SECONDSz4.0NOTIFY_RENATOtrueu<   LINKIA_TOKEN ausente — webhook nao vai conseguir responderzcustom_fields.jsonzdict[str, str]CUSTOM_FIELD_ID_MAPz,carregado custom_fields.json com %d entradasz,nao consegui carregar custom_fields.json: %su=   custom_fields.json nao existe — rode setup_custom_fields.py)gerar_resposta)send_whatsapp_messageget_conversation_messagesget_contactupdate_contact_custom_fieldsfind_opportunity_for_contactcreate_opportunitymove_opportunity_stagefetch_last_inbound_messagezOrderedDict[str, float]_seen_msg_idsi  i  c                    | syt        j                          }t        5  t        r|t        t	        t        j                                     z
  t        kD  st        t              t        kD  rdt        j                  d       t        rH|t        t	        t        j                                     z
  t        kD  rMt        t              t        kD  rd| t        v r
	 d d d        y|t        | <   	 d d d        y# 1 sw Y   y xY w)NF)lastT)
time
_seen_lockr;   nextitervaluesDEDUP_TTL_Slen	DEDUP_MAXpopitem)msg_idnows     @/opt/mia/workspace/clientes/torq-brasil/sdr_bruno_webhook/app.py_seen_alreadyrJ      s    
))+C	 tD1E1E1G,H'I!IK!W$'$6$B!!u!- tD1E1E1G,H'I!IK!W$'$6$B]"  !$f  s   BC48C4	C4!
C44C=zdict[str, dict]_pendingc           	        t         5  t        j                  | d      }ddd       sy|d   }|d   }|d   }dj                  |      }t        j                  d| t        |      |dd        i }t        | t              }|r|j                  dg       xs g }t        j                         D 	
ci c]  \  }	}
|
|	
 }}	}
|D ]N  }|j                  d	      }|j                  |      }|s(|j                  d
      xs |j                  d      ||<   P g }|rt        |t        d      }t        |      D ]h  }|j                  d      xs dj                         }|s)|j                  d      xs dj                         }|dk(  rdnd}|j!                  ||d       j 	 t#        | ||||xs d      \  }}|st        j)                  d|        yt        j                  d| |dd |       |j+                  d      D cg c]#  }|j                         s|j                         % }}|s|g}t-        j.                  t0               t3        |      D ]`  \  }}|dkD  rt-        j.                  t4               t7        | |t        |      \  }}}t        j                  d|dz   t        |      |||        b t9        | ||       y# 1 sw Y   xY wc c}
}	w # t$        $ r }t        j'                  d|       Y d}~yd}~ww xY wc c}w )z2Processa msgs acumuladas do contato apos debounce.Nmessagesconversation_idcontact_meta
z+[processando] contact=%s msgs=%d preview=%r   customFieldsidvalue
fieldValue   )limitbodyr   	directioninbounduser	assistant)rolecontent)
contact_idmensagem_leadrO   custom_fieldshistorico_ghlzerro chamando Bruno: %sz%Bruno retornou texto vazio contact=%sz#[bruno] contact=%s texto=%r meta=%sz	[[BREAK]]r   )r_   messagetokenrN   z0[enviado] bloco %d/%d ok=%s status=%s contact=%sr   )_pending_lockrK   popjoinloginforD   r5   r   getr1   itemsr4   reversedstriplowerappendr2   	Exception	exceptionwarningsplitr>   sleepTYPING_DELAY_SECONDS	enumerateBLOCK_DELAY_SECONDSr3   _aplicar_meta)r_   datamessages_txtrN   rO   mensagemra   contatocf_listkv	id_to_keycffidfkeyrb   ghl_msgsmrX   rY   r]   textometaebblocosiblocookstatuss                                 rI   _fire_contactr      s   	 .||J-.
#L,-O'Lyy&HHH:\*HTcN< %'M*l3G++nb17R&9&?&?&ABdaQTB	B 	NB&&,C==%D&(ffWo&M9Md#		N +-M,_lRTU(# 	BAEE&M'R..0D{+1r88:I&)36D  $4!@A	B
$!"%''/4
t ;ZHHH2td, "'[!9GAQWWYaggiGFGJJ#$f% 
=5q5JJ*+0!+	
FD 	CQFR	=
= *dL1_. .( C:  /3 Hs5   K .KK <K?K? K
	K<K77K<c           	        |sy |j                  d      xs i }|rzt        rt|j                         D ci c]  \  }}|t        v s|| }}}|rFt        | |t        t              }t
        j                  d| |t        |j                                      |j                  d      }|rt        j                  |      rt        |   }	t        | t        t        t              }
|
sv|j                  d      xs d}t        | t        t        j                  d      xs |	| d	t        t        
      }
t
        j                  d| |
r|
j                  d      nd        |
rG|
j                  d      r6t        |
d   |	t        t              }t
        j                  d| |
d   ||       |j                  d      rt        rt!        | |||       y y y c c}}w )Ncampos)r_   fieldsrd   field_id_mapz&[cf_update] contact=%s ok=%s fields=%sstage)r_   pipeline_idrd   location_id
first_nameLeadr%   z (SDR Bruno))r_   r   stage_idnamerd   r   z[opp_created] contact=%s opp=%srS   )opportunity_idr   r   rd   z-[stage_move] contact=%s opp=%s stage=%s ok=%snotificar_renato)rj   r1   rk   r6   r   rh   ri   listkeys	STAGE_MAPr7   r   r   r8   r9   r/   _notificar_renato)r_   r   rO   r   r~   r   validr   	stage_keyr   oppnomes               rI   rx   rx      s    XXh%2F%"(,,.M$!QA9L4LAMM-%"0	B HH=T%**,%79 !IY]]9-Y'*!#*	
 ##L1;VD$%'"{3?xv\*".C HH6#4A 3774='"4y!'"	B HHDTIr; xx"#*lD&A )6#] Ns   G
G
c                   t        d      }|j                         st        j                  d       y|j	                  d      xs |j	                  d      xs d}|j	                  d      xs d}|j	                  d	d
      }|j	                  dd
      }|j	                  dd
      }	|j	                  dd
      }
d| d| d| d| d|	 d|
 d|  }	 |t        t        j                         dz         dz  }|j                  t        j                  d|id      d       t        j                  d|j                         y# t        $ r }t        j                  d|       Y d}~yd}~ww xY w)z>Cria mensagem no outbox da Mia -> chega no Telegram do Renato.z/opt/mia-bot/outboxu+   outbox nao existe — nao notifiquei RenatoNr   sdr__nome_leadzsem nomephonez	sem phonesdr__cidader   sdr__perfilsdr__modelo_interesse_principalr   z'[Bruno SDR TORQ] Lead pra voce, chefe:
z (z
)
cidade: z | perfil: z | modelo: z
stage: z
contact_id: g    eAz.jsontextFensure_asciir   r   z![notify_renato] enviado outbox=%szfalha ao notificar Renato: %s)r   existsrh   rr   rj   intr>   
write_textjsondumpsri   r   rp   rq   )r_   rO   r   r   outboxr   r   cidadeperfilmodelor   r   fnamer   s                 rI   r   r   (  sj    '(F==?ABL)WVZZ8H-IWZDW%4EZZr*FZZr*FZZ92>FHHWb!E 3&5' (+fX[ A !l		$ 
:C		c 123599VUO%H") 	 	+4ejjA :5q99:s   A0E   	E)	E$$E)defaultc                   |D ]b  }d|v rF| }d}|j                  d      D ]  }t        |t              r
||v r||   }d} n |sA|d dg fvsI|c S || v sR| |   d dg fvs]| |   c S  |S )N.TFr   )rs   
isinstancedict)dr   r   r~   curr   parts          rI   
_get_firstr   K  s     !8CB c4(TS[d)CB c$B/
!V!T2rN2Q4K N    c                   t        | ddd      t        | ddd      t        | dd      t        | d	d
ddd      t        | dddd      t        | ddd      t        | dd      t        | dd      t        | ddd      t        | ddd      d 
S )!NrS   	messageIdz
message.id	contactIdr_   z
contact.idconversationIdrN   rX   rc   messageBodyr   zmessage.body	firstNamer   	full_namezcontact.firstNamelastName	last_namezcontact.lastNamer   zcontact.phonerY   rZ   r   typemessageTypeWhatsApp
locationIdr   zlocation.id)

message_idr_   rN   r{   r   r   r   rY   r   r   )r   )payloads    rI   extrair_payloadr   ]  s     $\J +|!-/%g/?&79w	=%~7 +|!,.AC[ 24GWo>iH7FM:N!'<"/1 r   z/healthGET)methodsc                     t        ddt        t        j                  dd            t	        t
              t        t              t        t        j                  dd      d      S )NTtorq_sdr_brunoPORT8920BRUNO_MODELzclaude-sonnet-4-6)r   serviceportlinkia_token_okcustom_fields_loaded
debounce_smodel)	r	   r   osgetenvboolr   rD   r1   DEBOUNCE_SECONDS r   rI   healthr   v  sP    #BIIff-.- #$7 8&=*=>  r   z/webhookPOSTc                 F   	 t        j                  dd      xs i } t        j                  dt        j                  | d	      d d
        t        |       }|d   r@t        t        |d               r)t        j                  d|d          t        ddd      dfS |d   s>t        j	                  dt        j                  | d	      d d        t        ddd      dfS t        |d         j                         dk(  r%t        j                  d       t        ddd      dfS t        |d         }|d   rt        |d         nd }|d   s|j                  d      xs t        }|s&t        j                  d|       t        ddd      dfS t        j                  d||       t!        ||t"              \  }}|s&t        j                  d|       t        ddd      dfS ||d<   |r|s|}t        j                  d |||d d!        t        |d         j%                         }|s&t        j                  d"|       t        dd#d      dfS |j                  d$      |j                  d%      |j                  d&      ||d'}	t&        5  |t(        v rLt(        |   }
|
j                  d(      r|
d(   j+                          |
d)   j-                  |       ||
d<   |	|
d*<   n|gd ||	d+t(        |<   t/        j0                  t2        t4        |f,      }d|_        |t(        |   d(<   |j9                          d d d        t        ddt2        d-      dfS # t        $ rB}t        j	                  d|t         j
                  d d        t        ddd      dfcY d }~S d }~ww xY w# 1 sw Y   lxY w).NTF)forcesilentzpayload nao-JSON: %s raw=%si  invalid_json)r   errorz[webhook] recebido: %sr   i   r   u*   [dedup] msg %s ja processada — ignorando	duplicate)r   skippedrQ   r_   zsem contact_id: %si,  missing_contact_idrY   outboundu#   outbound — ignorando (evita loop)rN   r{   r   uB   sem mensagem no payload e sem location_id — ignorando contact=%sno_message_no_locationuL   [fallback_api] payload sem msg — buscando ultima inbound contact=%s loc=%s)r_   r   rd   u6   sem msg no payload nem na API — ignorando contact=%sno_message_apiz:[fallback_api] contact=%s conv=%s recuperou msg preview=%rx   u2   mensagem vazia apos strip — ignorando contact=%sempty_messager   r   r   )r   r   r   r_   rN   timerrM   rO   )rM   r   rN   rO   )args)r   queuedr   )r
   get_jsonrp   rh   rr   ry   r	   ri   r   r   r   rJ   strrn   rj   r   r:   r   rm   re   rK   cancelro   	threadingTimerr   r   daemonstart)r   r   dadosr_   rN   loc_idrX   conv_idr{   rO   existingr   s               rI   webhookr    s   D""e<B
 HH%ZZe4Tc:< G$E \}S|1D-EF=u\?RSd{;<cAA(JJwU;DSA	Ce.BCDcII
5$$&*467dz:;S@@U<()J7<=N7Oc% 123UYO =)?-?HHY!$3KLMsRR_V	%2!v\
g HHM!$3CDEsJJ j?%OM_d4Cj	: 5$%++-HEzRd?@#EEii-YY{+7# *L 
 !
+H||G$!((*Z ''1*9H&''3H^$ &J#2 ,	$HZ   0-zmT(-W%%( $$"24 569: :c  D11gll4C6HIen=>CCDz s*   M	 B(N		N7N	NNN /c                 $    t        ddddd      S )NzTORQ SDR BrunozGET /healthzPOST /webhook)r   r  )r   	endpoints)r	   r   r   rI   indexr    s"    ##&
  r   __main__r   r   HOSTz0.0.0.0z0subindo Flask em %s:%s (debounce=%.1fs model=%s)r   F)hostr   debugthreaded)rG   r   returnr   )r_   r   r  None)r_   r   r   dict[str, Any]rO   r  r  r  )
r_   r   rO   r  r   r  r   r  r  r  )r   r   r   r   r   r   r  r   )r   r   r  r   )p__doc__
__future__r   r   loggingr   sysr   r>   collectionsr   logging.handlersr   pathlibr   typingr   flaskr   r	   r
   __file__resolveparentBASE_DIRLOG_FILEmkdir	Formatterfmtfile_handlersetFormatterStreamHandlerstdoutstream_handlerbasicConfigINFO	getLoggerrh   dotenvr   ImportErrorr   envfilepathr   openfrawrm   line
startswithrs   r~   r   environ
setdefaultr   r   r   r   r   floatr   ru   rw   rn   r/   r   CF_MAP_PATHr1   __annotations__loads	read_textri   rD   rp   r   rr   bruno_agentr2   
ghl_clientr3   r4   r5   r6   r7   r8   r9   r:   __name__appr;   Lockr?   rC   rE   rJ   rK   re   r   rx   r   r   r   router   r  r  r   r   r	  runr   r   rI   <module>r?     sc  " #   	 
   # 0   ) )>!!#**
 -.   dT  2gKL"86F/07D   # &&&szz2   C     ',,,1O Pg
#
<" #$++, ryy,224RYY3R8>>@ biir*002 ,b12990"5$299%7<		*B/ryy"-		*B/		*B/	 #95AB YRYY'A5IJ IBII&?GH 		/6288:fDIILM --&( ^ (G(djj)>)>)>)PQ?()	+
 KKOP '	 	 	 Ho
 *5& 6Y^^
	$ / 	 Q2h8B .8B378Bv:*:4B:GK:F 48 $2 9ug&	 '	 :x(U: )U:p 3  ! zyryy()D299VY'DHH?4)9299]+CEGGDG= [  <#%8#3346 
<ww~~g&'G, 	< <yy{ts3s$zz#q)1

%%aggi;<	< 	< 	<	
<<X  GBAFFGsC   !Q <:T <T?A=T=	TTTTT<T77T<