+
    iE                    F   R t ^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RIHtH	t	 ^ RI
Ht ]P                  ! ]4      t^ RIHtHt ^ RIHt ^ RIHt ^ RIHtHtHtHtHtHtHt ^ RIHt ^ RIt^ RIHt  ]PB                  PE                  ^ ]#! ] ! ]$4      PK                  4       PL                  ^,          4      4       ^ R	I'H(t(H)t) ^ R
I*H+t+H,t, ^ RI-H.t. ^ RI/H0t0 Rt1R@R R llt2]0! RR4      t3R R lt4RAR R llt5RBR R llt6RCR R llt7]0! RR4      t8R R lt9RDR R llt:RER  R! llt;]0! R"R#4      t<R$R%R&R'R(R)R*R+R,R-R.R/R0R1/t=R2 R3 lt>R4 R5 lt?RCR6 R7 llt@ ! R8 R9]4      tA] ! R: R;4      4       tB] ! R< R=4      4       tCRFtD]]B.]]]#,          ,          3,          tE ! R> R?]4      tFR# )Gz
Base platform adapter interface.

All platform adapters (Telegram, Discord, WhatsApp) inherit from this
and implement the required methods.
N)ABCabstractmethod)urlsplit)	dataclassfield)datetimePath)DictListOptionalAnyCallable	AwaitableTuple)Enum)PlatformPlatformConfig)SessionSourcebuild_session_key)get_hermes_home)get_hermes_dirzSecure secret entry is not supported over messaging. Load this skill in the local CLI to be prompted, or add the key to ~/.hermes/.env manually.c                <    V ^8  d   QhR\         R\        R\         /# )   urlmax_lenreturnstrint)formats   "3/home/ubuntu/hermes-agent/gateway/platforms/base.py__annotate__r"   (   s!     "& "&3 "& "&c "&    c                P   V^ 8:  d   R# V f   R# \        V 4      pV'       g   R#  \        V4      pTP                  '       d   TP                  '       d   TP                  P                  R^4      R
,          pTP                   RT 2pTP                  ;'       g    RpT'       d6   TR8w  d/   TP                  R^4      R
,          pT'       d   T RT 2MT R2pMTpMTp\        T4      T8:  d   T# T^8:  d
   RT,          # TRT^,
            R	2#   \         d	    TRT u # i ; i)z?Return a URL string safe for logs (no query/fragment/userinfo). N@z:///z/.../z/....z...)r   r   	Exceptionschemenetlocrsplitpathlen)	r   r   rawparsedr,   baser.   basenamesafes	   &&       r!   _safe_url_for_logr5   (   s   !|
{
c(C# }}}%%c1-b1--F8,{{  bDCK{{3*2.H/7dV5
+vT]DD
4yG!|W}<GaK !%%)  8G}s   D D%$D%zcache/imagesimage_cachec                $    V ^8  d   QhR\         /# r   r   r   )r    s   "r!   r"   r"   Z         T r#   c                 <    \         P                  RRR7       \         # )zBReturn the image cache directory, creating it if it doesn't exist.Tparentsexist_ok)IMAGE_CACHE_DIRmkdir r#   r!   get_image_cache_dirrA   Z       $6r#   c                <    V ^8  d   QhR\         R\        R\        /# r   dataextr   bytesr   )r    s   "r!   r"   r"   `   !       S c r#   c                    \        4       pR\        P                  ! 4       P                  R,           V 2pW#,          pVP	                  V 4       \        V4      # )z
Save raw image bytes to the cache and return the absolute file path.

Args:
    data: Raw image bytes.
    ext:  File extension including the dot (e.g. ".jpg", ".png").

Returns:
    Absolute path to the cached image file as a string.
img_N   N)rA   uuiduuid4hexwrite_bytesr   rE   rF   	cache_dirfilenamefilepaths   &&   r!   cache_image_from_bytesrV   `   sN     $%Idjjl&&s+,SE2H#Hx=r#   c                H    V ^8  d   QhR\         R\         R\        R\         /# r   r   rF   retriesr   r   )r    s   "r!   r"   r"   r   )     2 2C 2c 2S 2QT 2r#   c                b  "   ^ RI p^ RIp^ RIpVP                  \        4      pRpVP                  RRR7      ;_uu_4       GRj  xL
 p\        V^,           4       F[  p	 VP                  V RRRR/R	7      G Rj  xL
 p
V
P                  4        \        V
P                  V4      u uuRRR4      GRj  xL
  # 	  RRR4      GRj  xL
  Vh L LT L  TP                  TP                  3 d   pTp\        YP                  4      '       d   TP                  P                  R
8  d   h Y8  dY   RT	^,           ,          pTP!                  RT	^,           T\#        T 4      TT4       TP$                  ! T4      G Rj  xL 
   Rp?EK+  h Rp?ii ; i L  + GRj  xL 
 '       g   i     Th; i5i)a  
Download an image from a URL and save it to the local cache.

Retries on transient failures (timeouts, 429, 5xx) with exponential
backoff so a single slow CDN response doesn't lose the media.

Args:
    url: The HTTP/HTTPS URL to download from.
    ext: File extension including the dot (e.g. ".jpg", ".png").
    retries: Number of retry attempts on transient failures.

Returns:
    Absolute path to the cached image file as a string.
N      >@Ttimeoutfollow_redirects
User-Agent)Mozilla/5.0 (compatible; HermesAgent/1.0)Acceptzimage/*,*/*;q=0.8headers        ?z*Media cache retry %d/%d for %s (%.1fs): %s)asynciohttpxlogging	getLogger__name__AsyncClientrangegetraise_for_statusrV   contentTimeoutExceptionHTTPStatusError
isinstanceresponsestatus_codedebugr5   sleepr   rF   rY   rg   rh   _logging_loglast_excclientattemptrt   excwaits   &&&          r!   cache_image_from_urlr   r        h'DH   EEEWq[)G!'$&Q "5 ", "  ))+-h.>.>DD FEE) FE: N; F F **E,A,AB c#8#899cll>V>VY\>\$'A+.DJJD!)#. "-----! FEEE: N   AF/CF/	F C:C
;)C$F&F/3C4F/9F;F/FF/CF/F	0B
F	:E=;F	 FF	F	FF/F,	F
F,	$F,	&	F/c                0    V ^8  d   QhR\         R\         /# r   max_age_hoursr   r   )r    s   "r!   r"   r"      s      s C r#   c                Z   ^ RI p\        4       pVP                  4       V R,          ,
          p^ pVP                  4        FV  pVP                  4       '       g   K  VP	                  4       P
                  V8  g   K<   VP                  4        V^,          pKX  	  V#   \         d     Kj  i ; i)zX
Delete cached images older than *max_age_hours*.

Returns the number of files removed.
N  )timerA   iterdiris_filestatst_mtimeunlinkOSErrorr   r   rS   cutoffremovedfs   &     r!   cleanup_image_cacher      s     #%IYY[MD01FG 99;;1668,,v5
1	 ! N     <BB*)B*zcache/audioaudio_cachec                $    V ^8  d   QhR\         /# r8   r   )r    s   "r!   r"   r"      r9   r#   c                 <    \         P                  RRR7       \         # )zBReturn the audio cache directory, creating it if it doesn't exist.Tr;   )AUDIO_CACHE_DIRr?   r@   r#   r!   get_audio_cache_dirr      rB   r#   c                <    V ^8  d   QhR\         R\        R\        /# rD   rG   )r    s   "r!   r"   r"      rI   r#   c                    \        4       pR\        P                  ! 4       P                  R,           V 2pW#,          pVP	                  V 4       \        V4      # )z
Save raw audio bytes to the cache and return the absolute file path.

Args:
    data: Raw audio bytes.
    ext:  File extension including the dot (e.g. ".ogg", ".mp3").

Returns:
    Absolute path to the cached audio file as a string.
audio_rL   )r   rN   rO   rP   rQ   r   rR   s   &&   r!   cache_audio_from_bytesr      sN     $%I

((-.se4H#Hx=r#   c                H    V ^8  d   QhR\         R\         R\        R\         /# rX   r   )r    s   "r!   r"   r"      rZ   r#   c                b  "   ^ RI p^ RIp^ RIpVP                  \        4      pRpVP                  RRR7      ;_uu_4       GRj  xL
 p\        V^,           4       F[  p	 VP                  V RRRR/R	7      G Rj  xL
 p
V
P                  4        \        V
P                  V4      u uuRRR4      GRj  xL
  # 	  RRR4      GRj  xL
  Vh L LT L  TP                  TP                  3 d   pTp\        YP                  4      '       d   TP                  P                  R
8  d   h Y8  dY   RT	^,           ,          pTP!                  RT	^,           T\#        T 4      TT4       TP$                  ! T4      G Rj  xL 
   Rp?EK+  h Rp?ii ; i L  + GRj  xL 
 '       g   i     Th; i5i)a  
Download an audio file from a URL and save it to the local cache.

Retries on transient failures (timeouts, 429, 5xx) with exponential
backoff so a single slow CDN response doesn't lose the media.

Args:
    url: The HTTP/HTTPS URL to download from.
    ext: File extension including the dot (e.g. ".ogg", ".mp3").
    retries: Number of retry attempts on transient failures.

Returns:
    Absolute path to the cached audio file as a string.
Nr\   Tr]   r`   ra   rb   zaudio/*,*/*;q=0.8rc   re   rf   z*Audio cache retry %d/%d for %s (%.1fs): %s)rg   rh   ri   rj   rk   rl   rm   rn   ro   r   rp   rq   rr   rs   rt   ru   rv   r5   rw   rx   s   &&&          r!   cache_audio_from_urlr      r   r   zcache/documentsdocument_cachez.pdfzapplication/pdfz.mdztext/markdownz.txtz
text/plainz.zipzapplication/zipz.docxzGapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentz.xlsxzAapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetz.pptxzIapplication/vnd.openxmlformats-officedocument.presentationml.presentationc                $    V ^8  d   QhR\         /# r8   r   )r    s   "r!   r"   r"   '  s       r#   c                 <    \         P                  RRR7       \         # )zEReturn the document cache directory, creating it if it doesn't exist.Tr;   )DOCUMENT_CACHE_DIRr?   r@   r#   r!   get_document_cache_dirr   '  s    TD9r#   c                <    V ^8  d   QhR\         R\        R\        /# )r   rE   rT   r   rG   )r    s   "r!   r"   r"   -  s!      E S S r#   c                   \        4       pV'       d   \        V4      P                  MRpVP                  RR4      P	                  4       pV'       d   VR9   d   RpR\
        P                  ! 4       P                  R,           RV 2pW$,          pVP                  4       P                  VP                  4       4      '       g   \        RV: 24      hVP                  V 4       \        V4      # )	a  
Save raw document bytes to the cache and return the absolute file path.

The cached filename preserves the original human-readable name with a
unique prefix: ``doc_{uuid12}_{original_filename}``.

Args:
    data: Raw document bytes.
    filename: Original filename (e.g. "report.pdf").

Returns:
    Absolute path to the cached document file as a string.

Raises:
    ValueError: If the sanitized path escapes the cache directory.
document r%   doc_rL   _zPath traversal rejected: )r(   z..)r   r	   namereplacestriprN   rO   rP   resolveis_relative_to
ValueErrorrQ   r   )rE   rT   rS   	safe_namecached_namerU   s   &&    r!   cache_document_from_bytesr   -  s    " '(I'/X##ZI!!&"-335I	[0	))#./q<K&H,,Y->->-@AA4XLABBx=r#   c                0    V ^8  d   QhR\         R\         /# r   r   )r    s   "r!   r"   r"   M  s      # s r#   c                Z   ^ RI p\        4       pVP                  4       V R,          ,
          p^ pVP                  4        FV  pVP                  4       '       g   K  VP	                  4       P
                  V8  g   K<   VP                  4        V^,          pKX  	  V#   \         d     Kj  i ; i)z[
Delete cached documents older than *max_age_hours*.

Returns the number of files removed.
Nr   )r   r   r   r   r   r   r   r   r   s   &     r!   cleanup_document_cacher   M  s     &(IYY[MD01FG 99;;1668,,v5
1	 ! N  r   c                   >    ] tR tRtRtRtRtRtRtRt	Rt
R	tR
tRtRtR# )MessageTypeib  zTypes of incoming messages.textlocationphotovideoaudiovoicer   stickercommandr@   N)rk   
__module____qualname____firstlineno____doc__TEXTLOCATIONPHOTOVIDEOAUDIOVOICEDOCUMENTSTICKERCOMMAND__static_attributes__r@   r#   r!   r   r   b  s2    %DHEEEEHGGr#   r   c                      a  ] tR tRt o Rt]P                  tRtRt	Rt
]! ]R7      t]! ]R7      tRtRtRt]! ]P&                  R7      tV 3R lR ltV 3R lR ltV 3R	 lR
 ltV 3R ltRtV tR# )MessageEventio  zY
Incoming message from a platform.

Normalized representation that all adapters produce.
N)default_factoryc                    < V ^8  d   QhRS[ /# r8   bool)r    __classdict__s   "r!   r"   MessageEvent.__annotate__  s     ) )D )r#   c                8    V P                   P                  R4      # )z8Check if this is a command message (e.g., /new, /reset).r'   )r   
startswithselfs   &r!   
is_commandMessageEvent.is_command  s    yy##C((r#   c                0   < V ^8  d   QhRS[ S[,          /# r8   r   r   )r    r   s   "r!   r"   r     s     	 	Xc] 	r#   c                   V P                  4       '       g   R# V P                  P                  ^R7      pV'       d   V^ ,          R,          P                  4       MRpV'       d!   RV9   d   VP                  R^4      ^ ,          pV# )z2Extract command name if this is a command message.Nmaxsplit:   NNr&   )r   r   splitlower)r   partsr0   s   &  r!   get_commandMessageEvent.get_command  sc      		+&+eAhrl  "3#:))C#A&C
r#   c                    < V ^8  d   QhRS[ /# r8   r   )r    r   s   "r!   r"   r     s     2 2# 2r#   c                    V P                  4       '       g   V P                  # V P                  P                  ^R7      p\        V4      ^8  d
   V^,          # R# )z"Get the arguments after a command.r   r%   )r   r   r   r/   )r   r   s   & r!   get_command_argsMessageEvent.get_command_args  sF      99		+u:>uQx1r1r#   c                   < V ^8  d   Qh/ S[ ;R&   S[;R&   S[;R&   S[;R&   S[S[ ,          ;R&   S[S[ ,          ;R&   S[S[ ,          ;R&   S[S[ ,          ;R&   S[S[ ,          ;R	&   S[S[ ,          ;R
&   S[;R&   # )r   r   message_typesourceraw_message
message_id
media_urlsmedia_typesreply_to_message_idreply_to_text
auto_skill	timestamp)r   r   r   r   r   r   r   )r    r   s   "r!   r"   r   o  s      I  0        $! ( S	7) * c8+ 0 "#-1 2 C='3 8 $9 > =? r#   r@   )rk   r   r   r   r   r   r   r   r   r   r   r   listr   r   r   r   r   r   nowr   r   r   r   __annotate_func__r   __classdictcell__r   s   @r!   r   r   o  s      !, 0 0L !F K $J "$7J"48K *.#'M !%J  =I) )	 	2 2a  r#   r   c                   @   a  ] tR tRt o RtRtRtRtRtV 3R lt	Rt
V tR# )
SendResulti  zResult of sending a message.NFc                v   < V ^8  d   Qh/ S[ ;R&   S[S[,          ;R&   S[S[,          ;R&   S[;R&   S[ ;R&   # )r   successr   errorraw_response	retryable)r   r   r   r   )r    r   s   "r!   r"   SendResult.__annotate__  sM      M  $	 
 C=     r#   r@   )rk   r   r   r   r   r   r  r  r  r   r   r   r   s   @r!   r   r     s#     & $JELI  r#   r   c                   h  a  ] tR tRt o RtV 3R lR lt]V 3R lR l4       t]V 3R lR l4       t]V 3R	 lR
 l4       t	]V 3R lR l4       t
V 3R lR ltV 3R lR ltV 3R lR ltV 3R lR ltV 3R lR lt]V 3R lR l4       t]V 3R lR l4       tV 3R lR ltV 3R lR lt]V 3R lR  l4       t]V 3R! lR" l4       t]ReV 3R$ lR% ll4       tV 3R& lR' ltRfV 3R( lR) lltV 3R* lR+ ltRgV 3R, lR- lltRgV 3R. lR/ llt]V 3R0 lR1 l4       t]V 3R2 lR3 l4       tReV 3R4 lR5 llt V 3R6 lR7 lt!ReV 3R8 lR9 llt"RgV 3R: lR; llt#ReV 3R< lR= llt$]V 3R> lR? l4       t%]V 3R@ lRA l4       t&RhV 3RB lRC llt'V 3RD lRE lt(V 3RF lRG lt)V 3RH lRI lt*]V 3RJ lRK l4       t+]V 3RL lRM l4       t,RiV 3RN lRO llt-V 3RP lRQ lt.]V 3RR lRS l4       t/V 3RT lRU lt0V 3RV lRW lt1V 3RX lRY lt2V 3RZ lR[ lt3RjV 3R\ lR] llt4]V 3R^ lR_ l4       t5V 3R` lRa lt6]RkV 3Rb lRc ll4       t7Rdt8V t9R## )lBasePlatformAdapteri  z
Base class for platform adapters.

Subclasses implement platform-specific logic for:
- Connecting and authenticating
- Receiving messages
- Sending messages/responses
- Handling media
c                &   < V ^8  d   QhRS[ RS[/# )r   configplatform)r   r   )r    r   s   "r!   r"    BasePlatformAdapter.__annotate__  s     3 3~ 3 3r#   c                    Wn         W n        R V n        RV n        R V n        R V n        RV n        R V n        / V n        / V n	        \        4       V n        \        4       V n        R # )NFT)r
  r  _message_handler_running_fatal_error_code_fatal_error_message_fatal_error_retryable_fatal_error_handler_active_sessions_pending_messagesset_background_tasks_auto_tts_disabled_chats)r   r
  r  s   &&&r!   __init__BasePlatformAdapter.__init__  sc     :>0437!&*#im! ;=:< 58E-0U%r#   c                    < V ^8  d   QhRS[ /# r8   r   )r    r   s   "r!   r"   r    s     5 5 5r#   c                    V P                   R J# Nr  r   s   &r!   has_fatal_error#BasePlatformAdapter.has_fatal_error  s    ((44r#   c                0   < V ^8  d   QhRS[ S[,          /# r8   r   )r    r   s   "r!   r"   r    s     ) )Xc] )r#   c                    V P                   # r  r  r   s   &r!   fatal_error_message'BasePlatformAdapter.fatal_error_message  s    (((r#   c                0   < V ^8  d   QhRS[ S[,          /# r8   r   )r    r   s   "r!   r"   r    s     & &(3- &r#   c                    V P                   # r  )r  r   s   &r!   fatal_error_code$BasePlatformAdapter.fatal_error_code  s    %%%r#   c                    < V ^8  d   QhRS[ /# r8   r   )r    r   s   "r!   r"   r    s     + +t +r#   c                    V P                   # r  )r  r   s   &r!   fatal_error_retryable)BasePlatformAdapter.fatal_error_retryable  s    ***r#   c                V   < V ^8  d   QhRS[ R.S[R,          R,          3,          RR/# )r   handlerr  Nr   )r   r   )r    r   s   "r!   r"   r    s7     , ,x9N8OQZ[_Q`cgQg8g/h ,mq ,r#   c                    Wn         R # r  )r  r   r.  s   &&r!   set_fatal_error_handler+BasePlatformAdapter.set_fatal_error_handler  s    $+!r#   c                   < V ^8  d   QhRR/# r   r   Nr@   )r    r   s   "r!   r"   r    s     	 	 	r#   c                    R V n         RV n        RV n        R V n         ^ RIHp V! V P                  P                  RRRR7       R#   \         d     R# i ; i)TNwrite_runtime_status	connectedr  platform_state
error_codeerror_message	r  r  r  r  gateway.statusr7  r  valuer*   r   r7  s   & r!   _mark_connected#BasePlatformAdapter._mark_connected  sU    !%$(!&*#	; $--*=*=kfjz~ 		s   &A AAc                   < V ^8  d   QhRR/# r4  r@   )r    r   s   "r!   r"   r    s      D r#   c                    R V n         V P                  '       d   R#  ^ RIHp V! V P                  P
                  RRRR7       R#   \         d     R# i ; i)FNr6  disconnectedr9  )r  r  r>  r7  r  r?  r*   r@  s   & r!   _mark_disconnected&BasePlatformAdapter._mark_disconnected  sS    	; $--*=*=nim  ~B  C 		s   &A AAc                0   < V ^8  d   QhRS[ RS[ RS[RR/# )r   codemessager  r   Nr   r   )r    r   s   "r!   r"   r    s)      S 3 d t r#   c                   R V n         Wn        W n        W0n         ^ RIHp V! V P                  P                  RVVR7       R#   \         d     R# i ; i)Fr6  fatalr9  Nr=  )r   rI  rJ  r  r7  s   &&&$ r!   _set_fatal_error$BasePlatformAdapter._set_fatal_error  sU    !%$+!&/#		; ,,&%	  		s   &A AAc                   < V ^8  d   QhRR/# r4  r@   )r    r   s   "r!   r"   r  !  s      4 r#   c                   "   V P                   pV'       g   R # V! V 4      p\        P                  ! V4      '       d   VG R j  xL
  R # R #  L5ir  )r  rg   iscoroutine)r   r.  results   &  r!   _notify_fatal_error'BasePlatformAdapter._notify_fatal_error!  s?     ++v&&LL 's   :AAA	Ac                    < V ^8  d   QhRS[ /# r8   r   )r    r   s   "r!   r"   r  *  s     + +c +r#   c                J    V P                   P                  P                  4       # )z%Human-readable name for this adapter.)r  r?  titler   s   &r!   r   BasePlatformAdapter.name)  s     }}""((**r#   c                    < V ^8  d   QhRS[ /# r8   r   )r    r   s   "r!   r"   r  /  s      d r#   c                    V P                   # )z(Check if adapter is currently connected.)r  r   s   &r!   is_connected BasePlatformAdapter.is_connected.  s     }}r#   c                $   < V ^8  d   QhRS[ RR/# )r   r.  r   N)MessageHandler)r    r   s   "r!   r"   r  3  s     ( (> (d (r#   c                    Wn         R# )z|
Set the handler for incoming messages.

The handler receives a MessageEvent and should return
an optional response string.
N)r  r0  s   &&r!   set_message_handler'BasePlatformAdapter.set_message_handler3  s
     !(r#   c                $   < V ^8  d   QhRS[ RR/# )r   session_storer   N)r   )r    r   s   "r!   r"   r  <  s     , ,s ,t ,r#   c                    Wn         R# )z
Set the session store for checking active sessions.

Used by adapters that need to check if a thread/conversation
has an active session before processing messages (e.g., Slack
thread replies without explicit mentions).
N)_session_store)r   rd  s   &&r!   set_session_store%BasePlatformAdapter.set_session_store<  s
     ,r#   c                    < V ^8  d   QhRS[ /# r8   r   )r    r   s   "r!   r"   r  G  s      t r#   c                   "   R# 5i)zc
Connect to the platform and start receiving messages.

Returns True if connection was successful.
Nr@   r   s   &r!   connectBasePlatformAdapter.connectF  
      	   c                   < V ^8  d   QhRR/# r4  r@   )r    r   s   "r!   r"   r  P  s      $ r#   c                   "   R# 5i)zDisconnect from the platform.Nr@   r   s   &r!   
disconnectBasePlatformAdapter.disconnectO  s
      	rn  Nc                n   < V ^8  d   QhRS[ RS[ RS[S[ ,          RS[S[S[ S[3,          ,          RS[/# )r   chat_idrp   reply_tometadatar   r   r   r
   r   r   )r    r   s   "r!   r"   r  U  sN        3-	
 4S>* 
r#   c                   "   R# 5i)a  
Send a message to a chat.

Args:
    chat_id: The chat/channel ID to send to
    content: Message content (may be markdown)
    reply_to: Optional message ID to reply to
    metadata: Additional platform-specific options

Returns:
    SendResult with success status and message ID
Nr@   )r   rt  rp   ru  rv  s   &&&&&r!   sendBasePlatformAdapter.sendT  s
     ( 	rn  c                2   < V ^8  d   QhRS[ RS[ RS[ RS[/# )r   rt  r   rp   r   r   r   )r    r   s   "r!   r"   r  j  s9     @ @@ @ 	@
 
@r#   c                $   "   \        RRR7      # 5i)u   
Edit a previously sent message. Optional — platforms that don't
support editing return success=False and callers fall back to
sending a new message.
FzNot supported)r  r  )r   )r   rt  r   rp   s   &&&&r!   edit_message BasePlatformAdapter.edit_messagej  s      %??s   c                $   < V ^8  d   QhRS[ RR/# r   rt  r   Nr   )r    r   s   "r!   r"   r  w  s        r#   c                   "   R# 5i)z
Send a typing indicator.

Override in subclasses if the platform supports it.
metadata: optional dict with platform-specific context (e.g. thread_id for Slack).
Nr@   )r   rt  rv  s   &&&r!   send_typingBasePlatformAdapter.send_typingw  rm  rn  c                $   < V ^8  d   QhRS[ RR/# r  r   )r    r   s   "r!   r"   r    s        r#   c                   "   R# 5i)zStop a persistent typing indicator (if the platform uses one).

Override in subclasses that start background typing loops.
Default is a no-op for platforms with one-shot typing indicators.
Nr@   r   rt  s   &&r!   stop_typingBasePlatformAdapter.stop_typing  s
      	rn  c                   < V ^8  d   QhRS[ RS[ RS[S[ ,          RS[S[ ,          RS[S[S[ S[3,          ,          RS[/# )r   rt  	image_urlcaptionru  rv  r   rw  )r    r   s   "r!   r"   r    sd     Q QQ Q #	Q
 3-Q 4S>*Q 
Qr#   c                f   "   V'       d   V RV 2MTpV P                  WVR7      G Rj  xL
 #  L5i)z
Send an image natively via the platform API.

Override in subclasses to send images as proper attachments
instead of plain-text URLs. Default falls back to sending the
URL as a text message.

rt  rp   ru  Nry  )r   rt  r  r  ru  rv  r   s   &&&&&& r!   
send_imageBasePlatformAdapter.send_image  s4       -4'"YK(YYwxYPPPPs   (1/1c                   < V ^8  d   QhRS[ RS[ RS[S[ ,          RS[S[ ,          RS[S[S[ S[3,          ,          RS[/# )r   rt  animation_urlr  ru  rv  r   rw  )r    r   s   "r!   r"   r    sd     F FF F #	F
 3-F 4S>*F 
Fr#   c                F   "   V P                  WW4VR7      G Rj  xL
 #  L5i)z
Send an animated GIF natively via the platform API.

Override in subclasses to send GIFs as proper animations
(e.g., Telegram send_animation) so they auto-play inline.
Default falls back to send_image.
)rt  r  r  ru  rv  N)r  )r   rt  r  r  ru  rv  s   &&&&&&r!   send_animation"BasePlatformAdapter.send_animation  s4      __WW^  }E_  F  F  	F  Fs   !!c                &   < V ^8  d   QhRS[ RS[/# )r   r   r   rK  )r    r   s   "r!   r"   r    s     & &s &t &r#   c                p    V P                  4       P                  R4      ^ ,          pVP                  R4      # )z=Check if a URL points to an animated GIF (vs a static image).?.gif)r   r   endswith)r   r   s   & r!   _is_animation_url%BasePlatformAdapter._is_animation_url  s.     		!!#&q)~~f%%r#   c                b   < V ^8  d   QhRS[ RS[S[S[S[ S[ 3,          ,          S[ 3,          /# r   rp   r   r   r   r   )r    r   s   "r!   r"   r    s2     - - -d5c?.CS.H(I -r#   c                  aa
 . pT pRp\         P                  ! W04       Fz  pVP                  ^4      pVP                  ^4      o\        ;QJ d    V3R lR
 4       F  '       g   K   RM	  RM! V3R lR
 4       4      '       g   Kg  VP	                  SV34       K|  	  Rp\         P                  ! W`4       F'  pVP                  ^4      oVP	                  SR34       K)  	  V'       dq   V UUu0 uF  w  rxVkK	  	  uppo
V
3R lp	\         P
                  ! W9V4      p\         P
                  ! WiV4      p\         P
                  ! RR	V4      P                  4       pW3# u uppi )av  
Extract image URLs from markdown and HTML image tags in a response.

Finds patterns like:
- ![alt text](https://example.com/image.png)
- <img src="https://example.com/image.png">
- <img src="https://example.com/image.png"></img>

Args:
    content: The response text to scan.

Returns:
    Tuple of (list of (url, alt_text) pairs, cleaned content with image tags removed).
z$!\[([^\]]*)\]\((https?://[^\s\)]+)\)c              3      <"   T F?  pSP                  4       P                  V4      ;'       g    VSP                  4       9   x  KA  	  R # 5ir  )r   r  ).0rF   r   s   & r!   	<genexpr>5BasePlatformAdapter.extract_images.<locals>.<genexpr>  sB      mk HK399;'',BBsyy{0BBks
   *A
A
TFzA<img\s+src=["\']?(https?://[^\s"\'<>]+)["\']?\s*/?>\s*(?:</img>)?r%   c                    < V P                   ^8  d   V P                  ^4      MV P                  ^4      pVS9   d   R# V P                  ^ 4      # )r   r%   )	lastindexgroup)matchr   extracted_urlss   & r!   _remove_if_extracted@BasePlatformAdapter.extract_images.<locals>._remove_if_extracted  s?    (-1(<ekk!n%++a. N2rFAFr#   \n{3,}

).png.jpg.jpegr  .webpz	fal.mediazfal-cdnzreplicate.delivery)refinditerr  anyappendsubr   )rp   imagescleaned
md_patternr  alt_texthtml_patternr   r   r  r  s   &      `  @r!   extract_images"BasePlatformAdapter.extract_images  s+      =
[[5E{{1~H++a.Cs mkmsss mkm m msHo. 6 \[[7E++a.CMM3)$ 8
 067fcc7NG ffZwGGff\IGffY8>>@G 8s   -Ec          
      X   < V ^8  d   QhRS[ RS[ RS[S[ ,          RS[S[ ,          RS[/# )r   rt  
audio_pathr  ru  r   r   r   r   )r    r   s   "r!   r"   r    L     Q QQ Q #	Q
 3-Q 
Qr#   c                l   "   RV 2pV'       d   V RV 2pV P                  WVR7      G Rj  xL
 #  L5i)z
Send an audio file as a native voice message via the platform API.

Override in subclasses to send audio as voice bubbles (Telegram)
or file attachments (Discord). Default falls back to sending the
file path as text.
u   🔊 Audio: r  r  Nr  )r   rt  r  r  ru  kwargsr   s   &&&&&, r!   
send_voiceBasePlatformAdapter.send_voice  s?      j\*Yb'DYYwxYPPPP   +424c                ,   < V ^8  d   QhRS[ RS[ RS[/# )r   rt  r  r   r|  )r    r   s   "r!   r"   r    s.     W WW W
 
Wr#   c                J   "   V P                   ! RRVRV/VB G Rj  xL
 #  L5i)z
Play auto-TTS audio for voice replies.

Override in subclasses for invisible playback (e.g. Web UI).
Default falls back to send_voice (shows audio player).
rt  r  Nr@   )r  )r   rt  r  r  s   &&&,r!   play_ttsBasePlatformAdapter.play_tts  s)      __VWVVvVVVVs   #!#c          
      X   < V ^8  d   QhRS[ RS[ RS[S[ ,          RS[S[ ,          RS[/# )r   rt  
video_pathr  ru  r   r  )r    r   s   "r!   r"   r    sL     Q QQ Q #	Q
 3-Q 
Qr#   c                l   "   RV 2pV'       d   V RV 2pV P                  WVR7      G Rj  xL
 #  L5i)z
Send a video natively via the platform API.

Override in subclasses to send videos as inline playable media.
Default falls back to sending the file path as text.
u   🎬 Video: r  r  Nr  )r   rt  r  r  ru  r  r   s   &&&&&, r!   
send_videoBasePlatformAdapter.send_video  s?      j\*Yb'DYYwxYPPPPr  c                n   < V ^8  d   QhRS[ RS[ RS[S[ ,          RS[S[ ,          RS[S[ ,          RS[/# )r   rt  	file_pathr  	file_nameru  r   r  )r    r   s   "r!   r"   r    s[     Q QQ Q #	Q
 C=Q 3-Q 
Qr#   c                l   "   RV 2pV'       d   V RV 2pV P                  WVR7      G Rj  xL
 #  L5i)z
Send a document/file natively via the platform API.

Override in subclasses to send files as downloadable attachments.
Default falls back to sending the file path as text.
u   📎 File: r  r  Nr  )r   rt  r  r  r  ru  r  r   s   &&&&&&, r!   send_document!BasePlatformAdapter.send_document  s?      YK(Yb'DYYwxYPPPPr  c          
      X   < V ^8  d   QhRS[ RS[ RS[S[ ,          RS[S[ ,          RS[/# )r   rt  
image_pathr  ru  r   r  )r    r   s   "r!   r"   r  +  r  r#   c                l   "   RV 2pV'       d   V RV 2pV P                  WVR7      G Rj  xL
 #  L5i)z
Send a local image file natively via the platform API.

Unlike send_image() which takes a URL, this takes a local file path.
Override in subclasses for native photo attachments.
Default falls back to sending the file path as text.
u   🖼️ Image: r  r  Nr  )r   rt  r  r  ru  r  r   s   &&&&&, r!   send_image_file#BasePlatformAdapter.send_image_file+  s?      !-Yb'DYYwxYPPPPr  c                b   < V ^8  d   QhRS[ RS[S[S[S[ S[3,          ,          S[ 3,          /# r  )r   r   r   r   )r    r   s   "r!   r"   r  @  s3     ' 's 'uT%T	2B-CS-H'I 'r#   c                \   . pT pRV 9   pVP                  RR4      p\        P                  ! R4      pVP                  V 4       F  pVP	                  R4      P                  4       p\        V4      ^8  d7   V^ ,          VR	,          8X  d"   V^ ,          R9   d   V^R	 P                  4       pVP                  R4      P                  R4      pV'       g   K  VP                  Wc34       K  	  V'       d9   VP                  RV4      p\        P                  ! RRV4      P                  4       pW3# )
a6  
Extract MEDIA:<path> tags and [[audio_as_voice]] directives from response text.

The TTS tool returns responses like:
    [[audio_as_voice]]
    MEDIA:/path/to/audio.ogg

Args:
    content: The response text to scan.

Returns:
    Tuple of (list of (path, is_voice) pairs, cleaned content with tags removed).
[[audio_as_voice]]r%   z[`"']?MEDIA:\s*(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a)(?=[\s`"',;:)\]}]|$)|\S+)[`"']?r.   z`"'z
`"',.;:)}]r  r  r)   )r   r  compiler  r  r   r/   lstriprstripr  r  )rp   mediar  has_voice_tagmedia_patternr  r.   s   &      r!   extract_media!BasePlatformAdapter.extract_media?  s     -7//"6; 

 G
 #++G4E;;v&,,.D4yA~$q'T"X"5$q'V:KAbz'');;v&--m<Dtd23 5 #''G4GffY8>>@G~r#   c                L   < V ^8  d   QhRS[ RS[S[S[ ,          S[ 3,          /# r  r  )r    r   s   "r!   r"   r  j  s,     A AS AU49c>-B Ar#   c                  a RpRP                  R V 4       4      p\        P                  ! RV,           R,           \        P                  4      p. o\        P                  ! RV \        P
                  4       F2  pSP                  VP                  4       VP                  4       34       K4  	  \        P                  ! RV 4       F2  pSP                  VP                  4       VP                  4       34       K4  	  R V3R llp. pVP	                  V 4       F  pV! VP                  4       4      '       d   K!  VP                  ^ 4      p\        P                  P                  V4      p	\        P                  P                  V	4      '       g   Kx  VP                  W34       K  	  \        4       p
. pV F0  w  rW9  g   K  V
P                  V	4       VP                  W34       K2  	  V UU	u. uF  w  rV	NK	  	  ppp	T pV'       dD   V F  w  rVP!                  VR	4      pK  	  \        P"                  ! R
RV4      P%                  4       pW3# u up	pi )a  
Detect bare local file paths in response text for native media delivery.

Matches absolute paths (/...) and tilde paths (~/) ending in common
image or video extensions.  Validates each candidate with
``os.path.isfile()`` to avoid false positives from URLs or
non-existent paths.

Paths inside fenced code blocks (``` ... ```) and inline code
(`...`) are ignored so that code samples are never mutilated.

Returns:
    Tuple of (list of expanded file paths, cleaned text with the
    raw path strings removed).
|c              3   B   "   T F  qP                  R 4      x  K  	  R# 5i)r(   N)r  )r  es   & r!   r  :BasePlatformAdapter.extract_local_files.<locals>.<genexpr>~  s     E3DaHHSMM3Ds   z/(?<![/:\w.])(?:~/|/)(?:[\w.\-]+/)*[\w.\-]+\.(?:z)\bz```[^\n]*\n.*?```z	`[^`\n]+`c                0    V ^8  d   QhR\         R\        /# )r   posr   )r   r   )r    s   "r!   r"   =BasePlatformAdapter.extract_local_files.<locals>.__annotate__  s     	< 	<# 	<$ 	<r#   c                 z   <a  \         ;QJ d    V 3R  lS 4       F  '       g   K   R# 	  R# ! V 3R  lS 4       4      # )c              3   T   <"   T F  w  rVSu;8*  ;'       d    V8  Mu x  K  	  R # 5ir  r@   )r  sr  r  s   &  r!   r  LBasePlatformAdapter.extract_local_files.<locals>._in_code.<locals>.<genexpr>  s     ;
qC||!||
s   %(TF)r  )r  
code_spanss   fr!   _in_code9BasePlatformAdapter.extract_local_files.<locals>._in_code  s,    3;
;33;3;3;
;;;r#   r%   r  r  )
r  r  r  r  r  .mp4.mov.avi.mkv.webm)joinr  r  
IGNORECASEr  DOTALLr  startendr  osr.   
expanduserisfiler  addr   r  r   )rp   _LOCAL_MEDIA_EXTSext_partpath_remr  foundr  r0   expandedseenuniquer   pathsr  _expr  s   &               @r!   extract_local_files'BasePlatformAdapter.extract_local_filesi  s   "
 88E3DEE
 **>IFRMM
 
17BIIFAqwwy!%%'23 G\73Aqwwy!%%'23 4	< 	< %%g.E&&++a.Cww))#.Hww~~h''c_- / E"MC#"so. #
 .44VkaV4#	!//#r2 $ffY8>>@G~ 5s   +Ic                *   < V ^8  d   QhRS[ RS[RR/# )r   rt  intervalr   N)r   float)r    r   s   "r!   r"   r    s#      #  X\ r#   c                  "     V P                  WR7      G Rj  xL
  \        P                  ! V4      G Rj  xL
  K;   L$ L  \        P                   d     Mi ; i\	        T R4      '       d0    T P                  T4      G Rj  xL 
  R#   \         d     R# i ; iR#   \	        T R4      '       d.    T P                  T4      G Rj  xL 
  i   \         d     i i ; ii ; i5i)z
Continuously send typing indicator until cancelled.

Telegram/Discord typing status expires after ~5 seconds, so we refresh every 2
to recover quickly after progress messages interrupt it.
rv  Nr  )r  rg   rw   CancelledErrorhasattrr  r*   )r   rt  r  rv  s   &&&&r!   _keep_typing BasePlatformAdapter._keep_typing  s     	&&w&BBBmmH--- C-%% 		 t]++**7333   ,wt]++**7333   ,s   C%A ?A AA A AB! AB! C%1B BB C%BC%BC%!C"5C	C
CC"CC"CC""C%c                $   < V ^8  d   QhRS[ RR/# r   eventr   Nr   )r    r   s   "r!   r"   r    s     = =| = =r#   c                   "   R# 5i)z.Hook called when background processing begins.Nr@   )r   r  s   &&r!   on_processing_start'BasePlatformAdapter.on_processing_start       rn  c                *   < V ^8  d   QhRS[ RS[RR/# )r   r  r  r   N)r   r   )r    r   s   "r!   r"   r    s(     @ @, @ @RV @r#   c                   "   R# 5i)z1Hook called when background processing completes.Nr@   )r   r  r  s   &&&r!   on_processing_complete*BasePlatformAdapter.on_processing_complete  r"  rn  c                0   < V ^8  d   QhRS[ RS[RS[RR/# )r   	hook_nameargsr  r   N)r   r   )r    r   s   "r!   r"   r    s0     O OC O Os OW[ Or#   c                   "   \        WR4      p\        V4      '       g   R#  V! V/ VB G Rj  xL
  R#  L  \         d-   p\        P	                  RT P
                  Y4        Rp?R# Rp?ii ; i5i)zARun a lifecycle hook without letting failures break message flow.Nz[%s] %s hook failed: %s)getattrcallabler*   loggerwarningr   )r   r(  r)  r  hookr  s   &&*,  r!   _run_processing_hook(BasePlatformAdapter._run_processing_hook  s_     t-~~	O'''' 	ONN4diiNN	Os7    A17 57 A17 A.!A)#A1)A..A1c                6   < V ^8  d   QhRS[ S[,          RS[/# r   r  r   r   r   r   )r    r   s   "r!   r"   r    s#     H H8C= HT Hr#   c                   a V '       g   R# V P                  4       o\        ;QJ d#    V3R l\         4       F  '       g   K   R# 	  R# ! V3R l\         4       4      # )zGReturn True if the error string looks like a transient network failure.Fc              3   ,   <"   T F	  qS9   x  K  	  R # 5ir  r@   )r  patlowereds   & r!   r  :BasePlatformAdapter._is_retryable_error.<locals>.<genexpr>  s     G-Fc'>-Fs   T)r   r  _RETRYABLE_ERROR_PATTERNSr  r8  s   &@r!   _is_retryable_error'BasePlatformAdapter._is_retryable_error  sA     ++-sG-FGssGsGsG-FGGGr#   c                6   < V ^8  d   QhRS[ S[,          RS[/# r3  r4  )r    r   s   "r!   r"   r    s#     	_ 	_# 	_4 	_r#   c                t    V '       g   R# V P                  4       pRV9   ;'       g    RV9   ;'       g    RV9   # )u   Return True if the error string indicates a read/write timeout.

Timeout errors are NOT retryable and should NOT trigger plain-text
fallback — the request may have already been delivered.
Fz	timed outreadtimeoutwritetimeout)r   r;  s   & r!   _is_timeout_error%BasePlatformAdapter._is_timeout_error  s;     ++-g%^^')A^^^W^E^^r#   c                R   < V ^8  d   QhRS[ RS[ RS[S[ ,          RS[RS[RS[RR/# )	r   rt  rp   ru  rv  max_retries
base_delayr   r   )r   r   r   r   r  )r    r   s   "r!   r"   r    s^     P PP P 3-	P
 P P P 
Pr#   c           	       "   V P                  VVVVR7      G Rj  xL
 pVP                  '       d   V# VP                  ;'       g    RpVP                  ;'       g    V P	                  V4      p	V	'       g   V P                  V4      '       d   V# V	'       Edh   \        ^V^,           4       EF  p
V^V
^,
          ,          ,          \        P                  ! ^ ^4      ,           p\        P                  RV P                  WW4       \        P                  ! V4      G Rj  xL
  V P                  VVVVR7      G Rj  xL
 pVP                  '       d&   \        P                  RV P                  V
4       Vu # VP                  ;'       g    RpVP                  '       d   K  V P	                  V4      '       d   EK   MC	  \        P                  RV P                  WX4       Rp V P                  WW4R7      G Rj  xL
  V# \        P                  R	V P                  V4       V P                  VR
VR,           2VVR7      G Rj  xL
 pVP                  '       g,   \        P                  RV P                  VP                  4       V#  ELl ELl ELR L  \         d-   p\        P!                  RT P                  T4        Rp?T# Rp?ii ; i L5i)aH  
Send a message with automatic retry for transient network errors.

On permanent failures (e.g. formatting / permission errors) falls back
to a plain-text version before giving up. If all attempts fail due to
network errors, sends the user a brief delivery-failure notice so they
know to retry rather than waiting indefinitely.
rt  rp   ru  rv  Nr%   z7[%s] Send failed (attempt %d/%d, retrying in %.1fs): %sz[%s] Send succeeded on retry %dz4[%s] Failed to deliver response after %d retries: %su   ⚠️ Message delivery failed after multiple attempts. Please try again — your request was processed but the response could not be sent.z/[%s] Could not send delivery-failure notice: %su3   [%s] Send failed: %s — trying plain-text fallbackz+(Response formatting failed, plain text:)

:Ni  Nz"[%s] Fallback send also failed: %s)ry  r  r  r  r<  rB  rm   randomuniformr-  r.  r   rg   rw   infor*   rv   )r   rt  rp   ru  rv  rE  rF  rS  	error_str
is_networkr}   delaynotice
notify_errfallback_results   &&&&&&&        r!   _send_with_retry$BasePlatformAdapter._send_with_retry  sv    $ yy	 ! 
 
 >>>MLL&&B	%%LL)A)A))L
 d44Y??M: K!O4"aGaK&89FNN1a<PPMIIwU mmE***#yy##%%	  )    >>>KK A499gV!M"LL..B	(((D,D,DY,O,O% 5* SUYU^U^`kwm k))Gh)jjj  	LdiiYbc $		CGENCST	 !* !
 
 &&&LL=tyy/J_J_`}
4 +( k  kLL!RTXT]T]_ijjk
s   KJKKKK7K
KBKJ
K;J<K7K
K!K;'K#J 9J:J >AKKK.K
KKJ K	!K>KK		Kc                $   < V ^8  d   QhRS[ RR/# r  r  )r    r   s   "r!   r"   r  ?  s#     lC lC, lC4 lCr#   c                	  "   V P                   '       g   R# \        VP                  V P                  P                  P                  RR4      V P                  P                  P                  RR4      R7      pW P                  9   Ed<   VP                  4       pVR9   d   \        P                  RV P                  W24        VP                  P                  '       d   RVP                  P                  /MRpV P                  V4      G Rj  xL
 pV'       d<   V P                  VP                  P                  VVP                  VR	7      G Rj  xL
  R# VR8X  d   \        P                  RV P                  V4        VP                  P                  '       d   RVP                  P                  /MRpV P                  V4      G Rj  xL
 pV'       d<   V P                  VP                  P                  VVP                  VR	7      G Rj  xL
  R# VP"                  \$        P&                  8X  Ed@   \        P                  RV P                  V4       V P(                  P                  V4      pV'       d   VP"                  \$        P&                  8X  d   VP*                  P-                  VP*                  4       VP.                  P-                  VP.                  4       VP0                  '       do   VP0                  '       g   VP0                  Vn        R# VP0                  VP0                  9  d/   VP0                   RVP0                   2P3                  4       Vn        R# WP(                  V&   R# \        P                  RV P                  V4       WP(                  V&   V P                  V,          P5                  4        R# \6        P8                  ! 4       V P                  V&   \6        P:                  ! V P=                  W4      4      p V P>                  PA                  V4       \E        TR4      '       d(   TPG                  T P>                  PH                  4       R# R#  ELV EL  \         d/   p\        P!                  R
T P                  TRR7        Rp?R# Rp?ii ; i EL EL  \         d/   p\        P!                  RT P                  TRR7        Rp?R# Rp?ii ; i  \B         d     R# i ; i5i)z
Process an incoming message.

This method returns quickly by spawning background tasks.
This allows new messages to be processed even while an agent is running,
enabling interruption support.
Ngroup_sessions_per_userTthread_sessions_per_userF)rV  rW  zA[%s] Approval command '/%s' bypassing active-session guard for %s	thread_idrH  z![%s] Approval dispatch failed: %sexc_infostatusz9[%s] Status command bypassing active-session guard for %sz[%s] Status dispatch failed: %sz=[%s] Queuing photo follow-up for session %s without interruptr  uD   [%s] New message while session %s is active — triggering interruptadd_done_callback)approvedeny)%r  r   r   r
  extrarn   r  r   r-  rv   r   rX  rR  rt  r   r*   r  r   r   r   r  r   extendr   r   r   r  rg   Eventcreate_task_process_message_backgroundr  r  	TypeErrorr  r\  discard)	r   r  session_keycmd_thread_metart   r  existingtasks	   &&       r!   handle_message"BasePlatformAdapter.handle_message?  s     $$$'LL$(KK$5$5$9$9:SUY$Z%)[[%6%6%:%:;UW\%]
 /// ##%C))WIIscLQLLLbLbLbK1G1G#HhlL%)%:%:5%AAH"33$)LL$8$8$,%*%5%5%1	 4    
 hOII{aLQLLLbLbLbK1G1G#HhlL%)%:%:5%AAH"33$)LL$8$8$,%*%5%5%1	 4    
 !![%6%66\^b^g^gitu1155kB 5 59J9J J''..u/?/?@((//0A0ABzzz'}}},1JJHM
 	 #ZZx}}</7}}oT%**,N,T,T,VHM  ;@**;7 LL_aeajajlwx27"";/!!+.224 .5]]_k* ""4#C#CE#WX	""&&t,
 4,--""4#9#9#A#AB .U  B ! cLL!DdiiQR]aLbbc  B ! aLL!BDIIq[_L``aP  	 	s   B>S7A
Q' Q!Q' 6Q' Q$Q' *S7>R) .R) R#	R) 6R) R&R) A%S76A:S71S7DS7S% &;S7!Q' $Q' 'R 2#RS7R  S7#R) &R) )S"4#SS7S""S7%S40S73S44S7c                    < V ^8  d   QhRS[ /# r8   )r  )r    r   s   "r!   r"   r    s     @ @e @r#   c                 <   ^ RI p \        P                  ! RR4      P                  4       pVR8X  d   R# \	        \        P                  ! RR4      4      p\	        \        P                  ! RR4      4      pVR	8X  d   R
Rr2V P
                  ! VR,          VR,          4      # )a3  
Return a random delay in seconds for human-like response pacing.

Reads from env vars:
  HERMES_HUMAN_DELAY_MODE: "off" (default) | "natural" | "custom"
  HERMES_HUMAN_DELAY_MIN_MS: minimum delay in ms (default 800, custom mode)
  HERMES_HUMAN_DELAY_MAX_MS: maximum delay in ms (default 2500, custom mode)
NHERMES_HUMAN_DELAY_MODEoffg        HERMES_HUMAN_DELAY_MIN_MS800HERMES_HUMAN_DELAY_MAX_MS2500naturali   i	  g     @@)rI  r  getenvr   r   rJ  )rI  modemin_msmax_mss       r!   _get_human_delay$BasePlatformAdapter._get_human_delay  s     	yy2E:@@B5=RYY:EBCRYY:FCD9 $F~~fvov??r#   c                *   < V ^8  d   QhRS[ RS[RR/# )r   r  rf  r   N)r   r   )r    r   s   "r!   r"   r    s)     F7 F7| F7RU F7Z^ F7r#   c           	     x  a)a*"   Ro)Ro*V)V*3R lpV P                   P                  V4      ;'       g    \        P                  ! 4       pW@P                   V&   VP                  P
                  '       d   RVP                  P
                  /MRp\        P                  ! V P                  VP                  P                  VR7      4      p V P                  RV4      G Rj  xL
  V P                  V4      G Rj  xL
 pV'       g6   \        P                  RV P                  VP                  P                  4       V'       Ed   V P                  V4      w  rV P                  V4      w  rV
P!                  RR	4      P#                  4       p
\$        P&                  ! R
R	V
4      P#                  4       p
V	'       d5   \        P)                  RV P                  \+        V	4      \+        V4      4       V P-                  V
4      w  rV'       d+   \        P)                  RV P                  \+        V4      4       RpVP.                  \0        P2                  8X  d   V
'       d   V'       g   VP                  P                  V P4                  9  d    ^ RIHpHp V! 4       '       d   ^ RIp\$        P&                  ! RR	V
4      R,          P#                  4       pV'       g   \?        R4      h\        P@                  ! VVR7      G Rj  xL
 pVPC                  V4      pVP                  R4      pV'       dh   \I        V4      PK                  4       '       dI    V PM                  VP                  P                  VVR7      G Rj  xL
   \N        PP                  ! V4       V
'       d   \        P)                  RV P                  \+        V
4      VP                  P                  4       V PU                  VP                  P                  V
VPV                  VR7      G Rj  xL
 pV! V4       V PY                  4       pV	'       d+   \        P)                  RV P                  \+        V	4      4       V	 EF7  w  ppV^ 8  d   \        PZ                  ! V4      G Rj  xL
   \        P)                  RV P                  \]        V4      V'       d
   VR,          MR	4       V P_                  V4      '       d=   T Pa                  VP                  P                  TV'       d   TMRVR7      G Rj  xL
 pM;T Pc                  VP                  P                  TV'       d   TMRVR7      G Rj  xL
 pVPd                  '       g/   \        Pg                  RV P                  VPf                  4       EK7  EK:  	  0 R0mp0 R1mp0 R2mpV EFi  w  ppV^ 8  d   \        PZ                  ! V4      G Rj  xL
   \I        V4      Ph                  Pk                  4       pVV9   d2   V Pm                  VP                  P                  VVR7      G Rj  xL
 p MVV9   d2   V Po                  VP                  P                  VVR 7      G Rj  xL
 p MhVV9   d2   V Pq                  VP                  P                  VVR!7      G Rj  xL
 p M0V Ps                  VP                  P                  VVR"7      G Rj  xL
 p V Pd                  '       g0   \        PG                  R#V P                  VV Pf                  4       EKi  EKl  	  V F  p"V^ 8  d   \        PZ                  ! V4      G Rj  xL
   \I        V"4      Ph                  Pk                  4       pVV9   d3   V Pq                  VP                  P                  V"VR!7      G Rj  xL
  K  VV9   d3   V Po                  VP                  P                  V"VR 7      G Rj  xL
  K  V Ps                  VP                  P                  V"VR"7      G Rj  xL
  K  	  S)'       d   S*M\u        V4      '       * p$V P                  R&VV$4      G Rj  xL
  W Pv                  9   Ed   V Pv                  Py                  V4      p%\        P                  R'V P                  4       W P                   9   d   V P                   V VP{                  4         VG Rj  xL
  V P                  V%V4      G Rj  xL
   VP{                  4         VG Rj  xL
   \        V R(4      '       d.   V P                  VP                  P                  4      G Rj  xL
  Y P                   9   d   T P                   T R# R#  TP{                  4         TG Rj  xL
   \        T R(4      '       d.   T P                  TP                  P                  4      G Rj  xL
  Y P                   9   d   T P                   T R# R#  EL EL EL  \D         d-   p\        PG                  RT P                  T4        Rp?ELRp?ii ; i EL  \R         d     ELi ; i   \N        PP                  ! T4       i   \R         d     i i ; i; i ELO EL ELN EL  \D         d0   p\        Pg                  RT P                  TRR7        Rp?EK?  Rp?ii ; i EL EL~ ELI EL EL  \D         d.   p!\        PG                  R$T P                  T!4        Rp!?!EK?  Rp!?!ii ; i EL EL\ EL& EL  \D         d/   p#\        Pg                  R%T P                  T"T#4        Rp#?#EK  Rp#?#ii ; i EL EL|  \        P|                   d     ELi ; i EL ELh  \        P|                   d     EL~i ; i ELG  \D         d     ELSi ; i  \        P|                   d    T P                  R&TR4      G Rj  xL 
  h \D         Ed
   p&T P                  R&TR4      G Rj  xL 
  \        Pg                  R)T P                  T&RR7        \        T&4      P                  p'\        T&4      '       d   \        T&4      R*,          MR+p(TP                  P
                  '       d   RTP                  P
                  /MRpT P                  TP                  P                  R,T' R-T( R.2TR/7      G Rj  xL 
   Rp&?&EL`  \D         d	      Rp&?&ELri ; iRp&?&ii ; i ELh  \        P|                   d     EL~i ; i ELG  \D         d     ELSi ; i  TP{                  4         TG Rj  xL 
  M  \        P|                   d     Mi ; i \        T R(4      '       d/   T P                  TP                  P                  4      G Rj  xL 
  M  \D         d     Mi ; iY P                   9   d   T P                   T i i ; i5i)3z4Background task that actually processes the message.Fc                 F   < V f   R # Ro\        V RR4      '       d   RoR # R # )NTr  F)r+  )rS  delivery_attempteddelivery_succeededs   &r!   _record_deliveryIBasePlatformAdapter._process_message_background.<locals>._record_delivery  s+    ~!%vy%00%)" 1r#   rX  Nr  r   z0[%s] Handler returned empty/None response for %sr  r%   zMEDIA:\s*\S+z<[%s] extract_images found %d image(s) in response (%d chars)z5[%s] extract_local_files found %d file(s) in response)text_to_speech_toolcheck_tts_requirementsz[*_`#\[\]()]:Ni  Nz!Empty text after markdown cleanup)r   r  z[%s] Auto-TTS failed: %s)rt  r  rv  z&[%s] Sending response (%d chars) to %srH  z1[%s] Extracted %d image(s) to send as attachmentsz[%s] Sending image: %s (alt=%s):N   N)rt  r  r  rv  )rt  r  r  rv  z[%s] Failed to send image: %sz[%s] Error sending image: %sTrY  )rt  r  rv  )rt  r  rv  )rt  r  rv  z"[%s] Failed to send media (%s): %sz[%s] Error sending media: %sz$[%s] Error sending local file %s: %sr%  z-[%s] Processing queued message from interruptr  z[%s] Error handling message: %s:Ni,  Nzno details availablezSorry, I encountered an error (z).
z2
Try again or use /reset to start a fresh session.)rt  rp   rv  >   .m4a.mp3.ogg.wav.opus>   .3gpr  r  r  r  r  >   r  r  r  r  r  )Fr  rn   rg   ra  r   rX  rb  r  rt  r0  r  r-  rv   r   r  r  r   r   r  r  rK  r/   r  r   r   r   r  tools.tts_toolr  r  jsonr   	to_threadloadsr*   r.  r	   existsr  r  remover   rR  r   rz  rw   r5   r  r  r  r  r  suffixr   r  r  r  r  r   r  popcancelr  rc  r  r  typerk   r   ry  )+r   r  rf  r  interrupt_event_thread_metadatatyping_taskrt   media_filesr  text_contentlocal_files	_tts_pathr  r  _jsonspeech_texttts_result_strtts_datatts_errrS  human_delayr  r  
img_resultimg_err_AUDIO_EXTS_VIDEO_EXTS_IMAGE_EXTS
media_pathis_voicerF   media_result	media_errr  file_errprocessing_okpending_eventr  
error_typeerror_detailr  r  s+   &&&                                      @@r!   rc  /BasePlatformAdapter._process_message_background  s     #"	* //33K@SSGMMO-<k* EJLLDZDZDZK)?)?@`d))$*;*;ELL<P<P[k*;*lmn	7++,A5III "22599H OQUQZQZ\a\h\h\p\pqx(,(:(:8(D% (,':':8'D$+334H"MSSU!vvor<HNNPKK ^`d`i`iknoukvx{  }E  yF  G -1,D,D\,R)KK WY]YbYbdghsdtu !	&&+*;*;;( +!LL008U8UUW^1330*,&&"l*STY*Z*`*`*bK#.&01T&U U3:3D3D 3+4 .N (-{{>'BH(0[(AI
 i!7!7!9!9
!"mm$)LL$8$8'0%5 ,   !IIi0
  KK H$))UXYeUfhmhthth|h|}#'#8#8 % 4 4 ,!&!1!1!1	 $9 $ F %V, #335 KK SUYU^U^`cdj`kl+1'Ix"Q%mmK888h= II-i8-5HSM2	  11)<</3/B/B(-(<(<.74<$)9	 0C 0 *J 04(-(<(<*34<$)9	 0? 0 *J  *111"LL)H$))U_UeUef  23 ,2> HOH,7(J"Q%mmK888]":.55;;=+-15(-(<(<+5)9 2A 2 ,L
 !K/15(-(<(<+5)9 2A 2 ,L
 !K/151E1E(-(<(<+5)9 2F 2 ,L 261C1C(-(<(<*4)9 2D 2 ,L  ,333"NN+OQUQZQZ\_amasast  4= -8H "-I"Q%mmK888m"9o44::<+-"&"6"6(-(<(<+4)9 #7 #  
 !K/"&//(-(<(<+4)9 #2 #   #'"4"4(-(<(<*3)9 #5 #  % "-6 3E.dS[nJ\M++,De][[[ 444 $ 6 6 : :; GLdiiX"7"77--k:""$%%% 66}kRRR4  !!!
4//**5<<+?+?@@@ 333))+6 4i 5N  !!!
4//**5<<+?+?@@@ 333))+6 4Y J :P.
 % W'A499gVVW  ' ! !!IIi0& ! !  9** % h%CTYYPWbfggh 9,,,, % ]'EtyyR[\\] 9
 % m%KTYYXackllm
 \ &--  S: "))  A E %% 	++,DeUSSS 	++,DeUSSSLL:DIIqSWLX!!W--
/21vvs1vd|;QLQLLLbLbLbK1G1G#Hhl ii!LL009*T'. )LL .       #	. "))  A   !!!)) 4//**5<<+?+?@@@  333))+6 4s  Cv:n gn 6g7n =n Dn n %n g 8g &g 9g:&g  n (n +h. 3h4h. 9h n A5n i#n 1An in 
Ai'(i'i'i!-i'i'i$i'&+i'=n j$n Aj3)j'*7j3!j*"7j3j-0j3
j0j3!,j3-n :k.;n  Ak:k1k:n 2k:k4k:n +k: k7k:n (n 9l6:A=n 8l< =l9>l< n mn v:.m 3m4m 9:m= 3m:4m= 8!v:t v:+s 0s1s 6:s: 0s71s: 5!v:n n g h*!hn hn h. h+'n *h++n .i0iiiiiin n !i'$i''j!2#jn j!!n 'j3*j3-j30j33k+>!k&n &k++n 1k:4k:7k::l3"l.'n .l33n 9l< <mn mn m m73v:6m77v::m= =nv:nv:+s:n=;sss"o%#(sAr:"Ar:.r1/r:4t :sst ssst s s40v:3s44v:7s: :t	v:t		v:v7t*#t&$t*)v7*u?v7uv7:v vvv7vv7v"v77v:c                   < V ^8  d   QhRR/# r4  r@   )r    r   s   "r!   r"   r    s     & &t &r#   c                  "   V P                    Uu. uF  qP                  4       '       d   K  VNK  	  ppV F  pVP                  4        K  	  V'       d   \        P                  ! VRR/ G Rj  xL
  V P                   P                  4        V P                  P                  4        V P                  P                  4        R# u upi  LY5i)zCancel any in-flight background message-processing tasks.

Used during gateway shutdown/replacement so active sessions from the old
process do not keep running after adapters are being torn down.
return_exceptionsTN)r  doner  rg   gatherclearr  r  )r   rj  taskss   &  r!   cancel_background_tasks+BasePlatformAdapter.cancel_background_tasks  s      #'"8"8L"8$		"8LDKKM ..%@4@@@$$&$$&##% M As'   CCC"CC/C	0ACc                &   < V ^8  d   QhRS[ RS[/# r   rf  r   rK  )r    r   s   "r!   r"   r    s     d d d dr#   c                p    WP                   9   ;'       d"    V P                   V,          P                  4       # )z3Check if there's a pending interrupt for a session.)r  is_setr   rf  s   &&r!   has_pending_interrupt)BasePlatformAdapter.has_pending_interrupt  s-    333cc8M8Mk8Z8a8a8ccr#   c                6   < V ^8  d   QhRS[ RS[S[,          /# r  )r   r   r   )r    r   s   "r!   r"   r    s      = =s =x7M =r#   c                :    V P                   P                  VR4      # )z0Get and clear any pending message for a session.N)r  r  r  s   &&r!   get_pending_message'BasePlatformAdapter.get_pending_message  s    %%))+t<<r#   c                   < V ^8  d   QhRS[ RS[S[ ,          RS[ RS[S[ ,          RS[S[ ,          RS[S[ ,          RS[S[ ,          RS[S[ ,          R	S[S[ ,          R
S[/
# )r   rt  	chat_name	chat_typeuser_id	user_namerX  
chat_topicuser_id_altchat_id_altr   )r   r   r   )r    r   s   "r!   r"   r    s     
 

 C=
 	

 #
 C=
 C=
 SM
 c]
 c]
 

r#   c
                   Ve   VP                  4       '       g   Rp\        V P                  \        V4      TTV'       d   \        V4      MRTV'       d   \        V4      MRV'       d   VP                  4       MRVV	R7
      # )z2Helper to build a SessionSource for this platform.N)
r  rt  r  r  r  r  rX  r  r  r  )r   r   r  r   )
r   rt  r  r  r  r  rX  r  r  r  s
   &&&&&&&&&&r!   build_source BasePlatformAdapter.build_source  sm     !**:*:*<*<J]]L$+CL(1c)nt-7z'')T##
 	
r#   c                <   < V ^8  d   QhRS[ RS[S[ S[3,          /# )r   rt  r   )r   r
   r   )r    r   s   "r!   r"   r    s#      3 4S> r#   c                   "   R# 5i)zw
Get information about a chat/channel.

Returns dict with at least:
- name: Chat name
- type: "dm", "group", "channel"
Nr@   r  s   &&r!   get_chat_info!BasePlatformAdapter.get_chat_info  s
      	rn  c                &   < V ^8  d   QhRS[ RS[ /# r  r   )r    r   s   "r!   r"   r  	  s     	 	c 	c 	r#   c                    V# )z
Format a message for this platform.

Override in subclasses to handle platform-specific formatting
(e.g., Telegram MarkdownV2, Discord markdown).

Default implementation returns content as-is.
r@   )r   rp   s   &&r!   format_message"BasePlatformAdapter.format_message	  s	     r#   c                <   < V ^8  d   QhRS[ RS[RS[S[ ,          /# )r   rp   
max_lengthr   )r   r   r   )r    r   s   "r!   r"   r    s+     m m# m3 m$s) mr#   c           
        \        V 4      V8:  d   V .# ^
pRp. pT pRpV'       Edf   Ve   RV R2MRpW,
          \        V4      ,
          \        V4      ,
          pV^8  d
   V^,          p\        V4      \        V4      ,           W,
          8:  d   VP                  Wu,           4       EMVRV p	V	P                  R4      p
W^,          8  d   V	P                  R4      p
V
^8  d   Tp
VRV
 pVP                  R4      VP                  R4      ,
          pV^,          ^8X  d   VP                  R4      pV^ 8  d*   W^,
          ,          R	8X  d   VP                  R^ V4      pK0  V^ 8  dA   VP                  R^ V4      pVP                  R^ V4      p\	        W4      pW^,          8  d   Tp
VRV
 pWZR P                  4       pVV,           pVRJpT;'       g    RpVP                  R4       Ft  pVP                  4       pVP                  R4      '       g   K,  V'       d   R
pRpK:  RpVR,          P                  4       pV'       d   VP                  4       ^ ,          MRpKv  	  V'       d   VV,          pTpMRpVP                  V4       EKn  \        V4      ^8  d;   \        V4      p\        V4       UUu. uF  w  ppV RV^,            RV R2NK  	  pppV# u uppi )a  
Split a long message into chunks, preserving code block boundaries.

When a split falls inside a triple-backtick code block, the fence is
closed at the end of the current chunk and reopened (with the original
language tag) at the start of the next chunk.  Multi-chunk responses
receive indicators like ``(1/3)``.

Args:
    content: The full message content
    max_length: Maximum length per chunk (platform-specific)

Returns:
    List of message chunks
z
```Nz```r  r%    `z\`\FT:   NNz (r'   ))
r/   r  rfindcountmaxr  r   r   r   	enumerate)rp   r  INDICATOR_RESERVEFENCE_CLOSEchunks	remaining
carry_langprefixheadroomregionsplit_at	candidatebacktick_countlast_bt
safe_splitnl_split
chunk_body
full_chunkin_codelanglinestrippedtagtotalichunks   &&                        r!   truncate_message$BasePlatformAdapter.truncate_message  s   " w<:%9	 %)
i .8-Cs:,b)F "5FCc+FVVH!|%? 6{S^+z/MMf01 y)F||D)Ha-'!<<,!|# ")8,I&__S1IOOE4JJN!Q&#//#.ki!&<&D'ooc1g>GQ;!*a!AJ(tQ@H!$Z!:J!M1#-"9H-J!),335I*,J !,G##D"((.::<&&u--"'!"&&rl00214syy{1~" / k)
!
!
MM*% v;?KE=Fv=N=NE5'AE7!E7!,=N   	s   5K)r  r  r  r  r  r  r  r  r  r  rf  r
  r  )NNr  )NNN)       @N)NNr   r  )NdmNNNNNN)i   ):rk   r   r   r   r   r  propertyr  r#  r'  r+  r1  rA  rF  rN  rT  r   r\  ra  rg  r   rk  rq  ry  r~  r  r  r  r  staticmethodr  r  r  r  r  r  r  r  r  r  r   r%  r0  r<  rB  rR  rk  rz  rc  r  r  r  r  r  r  r  r   r   r   s   @r!   r  r    s    3 3* 5 5 ) ) & & + +, ,	 	     + +  ( (, ,       *@ @  Q Q&F F" & &
 - -^Q Q(W WQ Q&Q Q(Q Q( ' 'R A AF 8= =@ @O O H H 	_ 	_P PdlC lC\ @ @(F7 F7P& &d d= =
 
:  	 	 m m mr#   r  )P   )r  )r  r   )   )r  )r  r   )	connecterrorconnectionerrorconnectionresetconnectionrefusedconnecttimeoutnetworkzbroken piperemotedisconnectedeoferror)Gr   rg   ri   r  rI  r  rN   abcr   r   urllib.parser   rj   rk   r-  dataclassesr   r   r   pathlibr	   typingr
   r   r   r   r   r   r   enumr   sys_Pathr.   insertr   __file__r   r<   gateway.configr   r   gateway.sessionr   r   hermes_cli.configr   hermes_constantsr   *GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGEr5   r>   rA   rV   r   r   r   r   r   r   r   SUPPORTED_DOCUMENT_TYPESr   r   r   r   r   r   r:  r_  r  r@   r#   r!   <module>r     s     	  	  # !			8	$ (   H H H  
 ! 3uX..088;< = 3 < - +b +"&^ !?$2j8 !>$2x $$57GH  	?
L
VPX @*
$ 
 42 42 42n    
  <.)HSM*BBCy# yr#   