
    iP                       d Z ddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlmZmZ ddlmZ  ej        e          ZdedefdZdededefd	Zded
edefdZdedefdZdedz  fdZdZdedz  dedz  fdZdedz  defdZdedz  deeef         fdZ ddl!m"Z"m#Z# ddl$m$Z$ ddl%m&Z& ddl'm(Z(m)Z)m*Z*m+Z+m,Z,m-Z-m.Z. ddl/m0Z0 ddl%m&Z1 e
j2        3                    d e e1e4          5                                j6        d                              ddl7m8Z8m9Z9 ddl:m;Z;m<Z< ddl=m>Z> dZ?d[ded edefd!Z@d" ZA e>d#d$          ZBde&fd%ZCd&eDdefd'ZEd\d&eDd)edefd*ZFd]ded)ed+edefd,ZGd^d.edefd/ZH e>d0d1          ZIde&fd2ZJd_d&eDd)edefd4ZKd`ded)ed+edefd5ZL e>d6d7          ZMd8d9d:d:d;d<d=d>d?ZNde&fd@ZOd&eDdAedefdBZPd^d.edefdCZQ G dD dEe0          ZR G dF dGe0          ZSe" G dH dI                      ZTe" G dJ dK                      ZUdLdMdNe(eeTf         dOedPeTdQeddf
dRZVdSZWe,eTge-e*e                  f         ZX	 dZdTedUedVedz  dedz  fdWZY G dX dYe          ZZdS )az
Base platform adapter interface.

All platform adapters (Telegram, Discord, WhatsApp) inherit from this
and implement the required methods.
    N)ABCabstractmethod)urlsplitsreturnc                 L    t          |                     d                    dz  S )u  Count UTF-16 code units in *s*.

    Telegram's message-length limit (4 096) is measured in UTF-16 code units,
    **not** Unicode code-points.  Characters outside the Basic Multilingual
    Plane (emoji like 😀, CJK Extension B, musical symbols, …) are encoded as
    surrogate pairs and therefore consume **two** UTF-16 code units each, even
    though Python's ``len()`` counts them as one.

    Ported from nearai/ironclaw#2304 which discovered the same discrepancy in
    Rust's ``chars().count()``.
    z	utf-16-le   )lenencode)r   s    >/home/agentuser/.hermes/hermes-agent/gateway/platforms/base.py	utf16_lenr      s#     qxx$$%%**    limitc                     t          |           |k    r| S dt          |           }}||k     r4||z   dz   dz  }t          | d|                   |k    r|}n|dz
  }||k     4| d|         S )u   Return the longest prefix of *s* whose UTF-16 length ≤ *limit*.

    Unlike a plain ``s[:limit]``, this respects surrogate-pair boundaries so
    we never slice a multi-code-unit character in half.
    r      r	   N)r   r
   )r   r   lohimids        r   _prefix_within_utf16_limitr   '   s     ||uAB
r''Bw{q QttW&&BBqB r'' SbS6Mr   budgetc                      ||           |k    rt          |           S dt          |           }}||k     r0||z   dz   dz  } || d|                   |k    r|}n|dz
  }||k     0|S )a8  Return the largest codepoint offset *n* such that ``len_fn(s[:n]) <= budget``.

    Used by :meth:`BasePlatformAdapter.truncate_message` when *len_fn* measures
    length in units different from Python codepoints (e.g. UTF-16 code units).
    Falls back to binary search which is O(log n) calls to *len_fn*.
    r   r   r	   Nr
   )r   r   len_fnr   r   r   s         r   _custom_unit_to_cpr   :   s     vayyF1vvAB
r''Bw{q 6!DSD'??f$$BBqB r'' Ir   hostc                    	 t          j        |           }|j        rdS t          |dd          r|j        j        rdS dS # t
          $ r Y nw xY w	 t          j        | dt          j        t          j	                  }|D ],\  }}}}}t          j        |d                   }|j        s dS -dS # t          j
        t          f$ r Y dS w xY w)a  Return True if *host* would expose the server beyond loopback.

    Loopback addresses (127.0.0.1, ::1, IPv4-mapped ::ffff:127.0.0.1)
    are local-only.  Unspecified addresses (0.0.0.0, ::) bind all
    interfaces.  Hostnames are resolved; DNS failure fails closed.
    Fipv4_mappedNTr   )	ipaddress
ip_addressis_loopbackgetattrr   
ValueError_socketgetaddrinfo	AF_UNSPECSOCK_STREAMgaierrorOSError)r   addrresolved_family_type_proto
_canonnamesockaddrs           r   is_network_accessibler0   M   s   #D)) 	5 4-- 	$2B2N 	5t   &$)7+>
 

 =E 	 	8GUFJ'44D# ttug&   tts/   A  A   
AAAB- *B- -CCc                  2   t           j        dk    rdS 	 t          j        ddgddt          j                  } n# t
          $ r Y dS w xY wi }|                                 D ]\}|                                }d|v rB|                    d          \  }}}|                                ||                                <   ]d	D ]W\  }}}|	                    |          d
k    r8|	                    |          }	|	                    |          }
|	r|
r
d|	 d|
 c S XdS )zRead the macOS system HTTP(S) proxy via ``scutil --proxy``.

    Returns an ``http://host:port`` URL string if an HTTP or HTTPS proxy is
    enabled, otherwise *None*.  Falls back silently on non-macOS or on any
    subprocess error.
    darwinNscutilz--proxy   T)timeouttextstderrz : ))HTTPSEnable
HTTPSProxy	HTTPSPort)
HTTPEnable	HTTPProxyHTTPPort1zhttp://:)
sysplatform
subprocesscheck_outputDEVNULL	Exception
splitlinesstrip	partitionget)outpropslinekey_val
enable_keyhost_keyport_keyr   ports              r   _detect_macos_system_proxyrT   p   sU    |xt%y!14
@R
 
 
    tt E   - -zz||D==..//KCC!$E#))+++ / /&
Hh 99Z  C''99X&&D99X&&D / /........4s   $9 
AAplatform_env_varc                    | r7t           j                            |           pd                                }|r|S dD ];}t           j                            |          pd                                }|r|c S <t	                      S )uK  Return a proxy URL from env vars, or macOS system proxy.

    Check order:
      0. *platform_env_var* (e.g. ``DISCORD_PROXY``) — highest priority
      1. HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants)
      2. macOS system proxy via ``scutil --proxy`` (auto-detect)

    Returns *None* if no proxy is found.
     )HTTPS_PROXY
HTTP_PROXY	ALL_PROXYhttps_proxy
http_proxy	all_proxy)osenvironrI   rG   rT   )rU   valuerM   s      r   resolve_proxy_urlra      s       0117R>>@@ 	L:  $$*1133 	LLL	%'''r   	proxy_urlc                     | si S |                                                      d          rO	 ddlm} |                    | d          }d|iS # t
          $ r  t                              d|            i cY S w xY wd| iS )	u  Build kwargs for ``commands.Bot()`` / ``discord.Client()`` with proxy.

    Returns:
      - SOCKS URL  → ``{"connector": ProxyConnector(..., rdns=True)}``
      - HTTP URL   → ``{"proxy": url}``
      - *None*     → ``{}``

    ``rdns=True`` forces remote DNS resolution through the proxy — required
    by many SOCKS implementations (Shadowrocket, Clash) and essential for
    bypassing DNS pollution behind the GFW.
    socksr   ProxyConnectorTrdns	connectorV   aiohttp_socks not installed — SOCKS proxy %s ignored. Run: pip install aiohttp-socksproxylower
startswithaiohttp_socksrf   from_urlImportErrorloggerwarningrb   rf   ri   s      r   proxy_kwargs_for_botru      s      	##G,, 	444444&//	/EEI++ 	 	 	NN1  
 III	 Ys    A 'A87A8c                    | si i fS |                                                      d          rS	 ddlm} |                    | d          }d|ii fS # t
          $ r" t                              d|            i i fcY S w xY wi d| ifS )	u  Build kwargs for standalone ``aiohttp.ClientSession`` with proxy.

    Returns ``(session_kwargs, request_kwargs)`` where:
      - SOCKS → ``({"connector": ProxyConnector(...)}, {})``
      - HTTP  → ``({}, {"proxy": url})``
      - None  → ``({}, {})``

    Usage::

        sess_kw, req_kw = proxy_kwargs_for_aiohttp(proxy_url)
        async with aiohttp.ClientSession(**sess_kw) as session:
            async with session.get(url, **req_kw) as resp:
                ...
    rd   r   re   Trg   ri   rj   rk   rl   rt   s      r   proxy_kwargs_for_aiohttprw      s      2v##G,, 	444444&//	/EEI+R// 	 	 	NN1  
 r6MMM	 ###s   "A )A>=A>)	dataclassfield)datetime)Path)DictListOptionalAnyCallable	AwaitableTuple)Enumr	   )PlatformPlatformConfig)SessionSourcebuild_session_key)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.P   urlmax_lenc                    |dk    rdS | dS t          |           }|sdS 	 t          |          }n# t          $ r |d|         cY S w xY w|j        rs|j        rl|j                            dd          d         }|j         d| }|j        pd}|r1|dk    r+|                    dd          d         }|r| d	| n| d
}n|}n|}t          |          |k    r|S |dk    rd|z  S |d|dz
            dS )z?Return a URL string safe for logs (no query/fragment/userinfo).r   rW   N@r   z:///z/.../z/...r4   .z...)strr   rE   schemenetlocrsplitpathr
   )	r   r   rawparsedr   baser   basenamesafes	            r   safe_url_for_logr      sc   !||r
{r
c((C r#   8G8} }  %%c1--b1-,,F,,{ b 	DCKK{{3**2.H/7Jd+++++]]]DDDD
4yyG!||W}<GaK< %%%%s   1 AAc                    K   | j         rP| j        rKt          | j        j                  }ddlm}  ||          s#t          dt          |                     dS dS dS )a%  Re-validate each redirect target to prevent redirect-based SSRF.

    Without this, an attacker can host a public URL that 302-redirects to
    http://169.254.169.254/ and bypass the pre-flight is_safe_url() check.

    Must be async because httpx.AsyncClient awaits response event hooks.
    r   is_safe_urlz.Blocked redirect to private/internal address: N)is_redirectnext_requestr   r   tools.url_safetyr   r"   r   )responseredirect_urlr   s      r   _ssrf_redirect_guardr   "  s         5 80455000000{<(( 	aAQR^A_A_aa  	   	 	r   zcache/imagesimage_cachec                  H    t                               dd           t           S )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_dirr   @  !    $666r   datac                    t          |           dk     rdS | dd         dk    rdS | dd         dk    rdS | dd	         d
v rdS | dd         dk    rdS | dd         dk    r#t          |           dk    r| dd         dk    rdS dS )zDReturn True if *data* starts with a known image magic-byte sequence.   FN   s   PNG

Tr4   s      )s   GIF87as   GIF89ar	   s   BMs   RIFF   s   WEBPr   )r   s    r   _looks_like_imager   F  s    
4yy1}}uBQBx'''tBQBx?""tBQBx)))tBQBx5tBQBx7s4yyB4":3H3Ht5r   .jpgextc                 B   t          |           s5| dd                             dd          }t          d| d|d          t                      }d	t	          j                    j        dd
          | }||z  }|                    |            t          |          S )a  
    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.

    Raises:
        ValueError: If *data* does not look like a valid image (e.g. an HTML
            error page returned by the upstream server).
    Nr   zutf-8replace)errorsz$Refusing to cache non-image data as z (starts with: )img_r   )	r   decoder"   r   uuiduuid4hexwrite_bytesr   )r   r   snippet	cache_dirfilenamefilepaths         r   cache_image_from_bytesr   W  s     T"" 
ss)""79"==*3 * *$* * *
 
 	
 $%%I2djll&ss+2S22H8#Hx==r   retriesc                 `  K   ddl m}  ||           st          dt          |                      ddl}ddl}ddl}|                    t                    }d}|	                    dddt          gi          4 d{V 	 }	t          |d	z             D ]}
	 |	                    | d
dd           d{V }|                                 t          |j        |          c cddd          d{V  S # |j        |j        f$ r}|}t%          ||j                  r|j        j        dk     r |
|k     rMd|
d	z   z  }|                    d|
d	z   |t          |           ||            |j        |           d{V  Y d}~ d}~ww xY w	 ddd          d{V  n# 1 d{V swxY w Y   |)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.

    Raises:
        ValueError: If the URL targets a private/internal network (SSRF protection).
    r   r   &Blocked unsafe URL (SSRF protection): N      >@Tr   r5   follow_redirectsevent_hooksr   )Mozilla/5.0 (compatible; HermesAgent/1.0)zimage/*,*/*;q=0.8z
User-AgentAcceptheaders        ?z*Media cache retry %d/%d for %s (%.1fs): %s)r   r   r"   r   asynciohttpxlogging	getLogger__name__AsyncClientr   rangerI   raise_for_statusr   contentTimeoutExceptionHTTPStatusError
isinstancer   status_codedebugsleepr   r   r   r   r   r   _logging_loglast_excclientattemptr   excwaits                 r   cache_image_from_urlr   s  %     $ -,,,,,;s [YBRSVBWBWYYZZZNNNLLLh''DH  "6!78 !                   
Wq[)) 	 	G!'&Q"5  ", " "       ))+++-h.>DDDD                             *E,AB   c5#899 cl>VY\>\>\W$$'A+.DJJD!(--   ('----------HHHH!	                                                     B ND   FAC5 F5FA6F<FFFF
F'*F'   max_age_hoursc                 H   ddl }t                      }|                                 | dz  z
  }d}|                                D ]^}|                                rH|                                j        |k     r+	 |                                 |dz  }N# t          $ r Y Zw xY w_|S )zd
    Delete cached images older than *max_age_hours*.

    Returns the number of files removed.
    r   N  r   )timer   iterdiris_filestatst_mtimeunlinkr(   r   r   r   cutoffremovedfs         r   cleanup_image_cacher     s     KKK#%%IYY[[MD01FG    99;; 	16688,v55


1   N   7B
BBzcache/audioaudio_cachec                  H    t                               dd           t           S )zBReturn the audio cache directory, creating it if it doesn't exist.Tr   )AUDIO_CACHE_DIRr   r   r   r   get_audio_cache_dirr    r   r   .oggc                     t                      }dt          j                    j        dd          | }||z  }|                    |            t          |          S )a  
    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_Nr   )r  r   r   r   r   r   )r   r   r   r   r   s        r   cache_audio_from_bytesr    s]     $%%I4
("-4s44H8#Hx==r   c                 `  K   ddl m}  ||           st          dt          |                      ddl}ddl}ddl}|                    t                    }d}|	                    dddt          gi          4 d{V 	 }	t          |d	z             D ]}
	 |	                    | d
dd           d{V }|                                 t          |j        |          c cddd          d{V  S # |j        |j        f$ r}|}t%          ||j                  r|j        j        dk     r |
|k     rMd|
d	z   z  }|                    d|
d	z   |t          |           ||            |j        |           d{V  Y d}~ d}~ww xY w	 ddd          d{V  n# 1 d{V swxY w Y   |)aE  
    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.

    Raises:
        ValueError: If the URL targets a private/internal network (SSRF protection).
    r   r   r   Nr   Tr   r   r   r   zaudio/*,*/*;q=0.8r   r   r   r   z*Audio cache retry %d/%d for %s (%.1fs): %s)r   r   r"   r   r   r   r   r   r   r   r   r   rI   r   r  r   r   r   r   r   r   r   r   r   s                 r   cache_audio_from_urlr    r   r   zcache/documentsdocument_cachezapplication/pdfztext/markdownz
text/plainzapplication/zipzGapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentzAapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetzIapplication/vnd.openxmlformats-officedocument.presentationml.presentation)z.pdfz.mdz.txtz.logz.zipz.docxz.xlsxz.pptxc                  H    t                               dd           t           S )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99r   r   c                    t                      }|rt          |          j        nd}|                    dd                                          }|r|dv rd}dt          j                    j        dd          d| }||z  }|                                	                    |                                          st          d	|          |                    |            t          |          S )
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 rW   )r   z..doc_Nr   rN   zPath traversal rejected: )r  r{   namer   rG   r   r   r   resolveis_relative_tor"   r   r   )r   r   r   	safe_namecached_namer   s         r   cache_document_from_bytesr  E  s    " '((I'/?X##ZI!!&"--3355I 	[00	<)#2#.<<<<K;&H,,Y->->-@-@AA CAXAABBBx==r   c                 H   ddl }t                      }|                                 | dz  z
  }d}|                                D ]^}|                                rH|                                j        |k     r+	 |                                 |dz  }N# t          $ r Y Zw xY w_|S )zg
    Delete cached documents older than *max_age_hours*.

    Returns the number of files removed.
    r   Nr   r   )r   r  r   r   r   r   r   r(   r   s         r   cleanup_document_cacher  e  s     KKK&((IYY[[MD01FG    99;; 	16688,v55


1   Nr   c                   6    e Zd ZdZdZdZdZdZdZdZ	dZ
d	Zd
ZdS )MessageTypezTypes of incoming messages.r6   locationphotovideoaudiovoicer  stickercommandN)r   
__module____qualname____doc__TEXTLOCATIONPHOTOVIDEOAUDIOVOICEDOCUMENTSTICKERCOMMANDr   r   r   r  r  z  sA        %%DHEEEEHGGGGr   r  c                       e Zd ZdZdZdZdZdS )ProcessingOutcomez=Result classification for message-processing lifecycle hooks.successfailure	cancelledN)r   r"  r#  r$  SUCCESSFAILURE	CANCELLEDr   r   r   r/  r/    s#        GGGGIIIr   r/  c                      e Zd ZU dZeed<   ej        Zeed<   dZ	e
ed<   dZeed<   dZee         ed<    ee          Zee         ed	<    ee          Zee         ed
<   dZee         ed<   dZee         ed<   dZeeee         z           ed<   dZee         ed<   dZeed<    eej                  Zeed<   defdZdee         fdZdefdZdS )MessageEventzi
    Incoming message from a platform.
    
    Normalized representation that all adapters produce.
    r6   message_typeNsourceraw_message
message_id)default_factory
media_urlsmedia_typesreply_to_message_idreply_to_text
auto_skillchannel_promptFinternal	timestampr   c                 6    | j                             d          S )z8Check if this is a command message (e.g., /new, /reset).r   )r6   rn   selfs    r   
is_commandzMessageEvent.is_command  s    y##C(((r   c                    |                                  sdS | j                            d          }|r"|d         dd                                         nd}|r d|v r|                    dd          d         }|rd|v rdS |S )z2Extract command name if this is a command message.Nr   maxsplitr   r   r   )rH  r6   splitrm   )rG  partsr   s      r   get_commandzMessageEvent.get_command  s       	4	++&+5eAhqrrl  """ 	'3#::))C##A&C 	3#::4
r   c                     |                                  s| j        S | j                            d          }t          |          dk    r|d         ndS )z"Get the arguments after a command.r   rJ  rW   )rH  r6   rL  r
   )rG  rM  s     r   get_command_argszMessageEvent.get_command_args  sM       	9	++u::>>uQxxr1r   ) r   r"  r#  r$  r   __annotations__r  r%  r8  r9  r   r:  r   r;  r~   ry   listr=  r}   r>  r?  r@  rA  rB  rC  boolrz   nowrD  rH  rN  rP  r   r   r   r7  r7    s          III + 0L+000 !FM    K $J$$$ "E$777JS	777"U4888Kc888 *.#---#'M8C=''' -1JtCy)000 %)NHSM((( Hd  %===Ix===)D ) ) ) )Xc]    2# 2 2 2 2 2 2r   r7  c                   n    e Zd ZU dZeed<   dZee         ed<   dZ	ee         ed<   dZ
eed<   dZeed<   dS )	
SendResultzResult of sending a message.r0  Nr;  errorraw_responseF	retryable)r   r"  r#  r$  rS  rQ  r;  r~   r   rW  rX  r   rY  r   r   r   rV  rV    si         &&MMM $J$$$E8C=L#Itr   rV  F)
merge_textpending_messagessession_keyeventrZ  c                   |                      |          }|rt          |dd          t          j        k    }|j        t          j        k    }t          |j                  }t          |j                  }|rs|rq|j                            |j                   |j                            |j                   |j	        r*t                              |j	        |j	                  |_	        dS |s|r|r>|j                            |j                   |j                            |j                   |j	        r>|j	        r+t                              |j	        |j	                  |_	        n|j	        |_	        |s|rt          j        |_        dS |rat          |dd          t          j        k    rB|j        t          j        k    r-|j	        r$|j	        r|j	         d|j	         n|j	        |_	        dS || |<   dS )a  Store or merge a pending event for a session.

    Photo bursts/albums often arrive as multiple near-simultaneous PHOTO
    events. Merge those into the existing queued event so the next turn sees
    the whole burst.

    When ``merge_text`` is enabled, rapid follow-up TEXT events are appended
    instead of replacing the pending turn. This is used for Telegram bursty
    follow-ups so a multi-part user thought is not silently truncated to only
    the last queued fragment.
    r8  N
)rI   r!   r  r'  r8  rS  r=  extendr>  r6   BasePlatformAdapter_merge_captionr%  )	r[  r\  r]  rZ  existingexisting_is_photoincoming_is_photoexisting_has_mediaincoming_has_medias	            r   merge_pending_message_eventrh    s   $  ##K00H !#HndCC{GXX!.+2CC!("566!%"233 	!2 	&&u'7888 ''(9:::z ^ 3 B B8=RWR\ ] ]F 	!3 	! ?#**5+;<<<$++E,=>>>z /= /$7$F$Fx}V[V`$a$aHMM$)JHM  :$5 :(3(9%F 	.$77;;KKK"k&666z bDLM a8= @ @EJ @ @ @W\WaF$)[!!!r   )	connecterrorconnectionerrorconnectionresetconnectionrefusedconnecttimeoutnetworkzbroken piperemotedisconnectedeoferrorconfig_extra
channel_id	parent_idc                     |                      d          pi }t          |t                    sdS ||fD ]D}|s|                     |          }|t          |                                          }|r|c S EdS )a  Resolve a per-channel ephemeral prompt from platform config.

    Looks up ``channel_prompts`` in the adapter's ``config.extra`` dict.
    Prefers an exact match on *channel_id*; falls back to *parent_id*
    (useful for forum threads / child channels inheriting a parent prompt).

    Returns the prompt string, or None if no match is found.  Blank/whitespace-
    only prompts are treated as absent.
    channel_promptsN)rI   r   dictr   rG   )rq  rr  rs  promptsrM   prompts         r   resolve_channel_promptry  .  s     0117RGgt$$ tI&   	S!!>V""$$ 	MMM	4r   c                      e Zd ZdZdedefdZedefd            Z	ede
e         fd            Zede
e         fd            Zedefd	            Zd
ed ged         dz  f         ddfdZdndZdndZdedededdfdZdndZdedededefdZdndZedefd            Zedefd            Zd
eddfdZd
e
eeegee         f                  ddfdZdeddfdZe defd            Z!e dnd             Z"e 	 	 dod!ed"ed#e
e         d$e
e#eef                  de$f
d%            Z%d!ed&ed"ede$fd'Z&dpd!eddfd(Z'd!eddfd)Z(	 	 	 dqd!ed*ed+e
e         d#e
e         d$e
e#eef                  de$fd,Z)	 	 	 dqd!ed-ed+e
e         d#e
e         d$e
e#eef                  de$fd.Z*e+d/edefd0            Z,e+d"ede-e.e-eef                  ef         fd1            Z/	 	 dod!ed2ed+e
e         d#e
e         de$f
d3Z0d!ed2ede$fd4Z1	 	 dod!ed5ed+e
e         d#e
e         de$f
d6Z2	 	 	 dqd!ed7ed+e
e         d8e
e         d#e
e         de$fd9Z3	 	 dod!ed:ed+e
e         d#e
e         de$f
d;Z4e+d"ede-e.e-eef                  ef         fd<            Z5e+d"ede-e.e         ef         fd=            Z6drd!ed?e7ddfd@Z8d!eddfdAZ9d!eddfdBZ:dCeddfdDZ;dCedEe<ddfdFZ=dGedHedIeddfdJZ>e+dKe
e         defdL            Z?e+dKe
e         defdM            Z@	 	 	 	 dsd!ed"ed#e
e         d$edOeAdPe7ddQfdRZBe+dSe
e         dTedefdU            ZCdCeddfdVZDe+de7fdW            ZEdCedXeddfdYZFdndZZGdXedefd[ZHdXede
e         fd\ZI	 	 	 	 	 	 	 	 dtd!ed^e
e         d_ed`e
e         dae
e         dbe
e         dce
e         dde
e         dee
e         deJfdfZKe d!ede#eef         fdg            ZLd"edefdhZMe+	 	 dud"edjeAdke
dl         de.e         fdm            ZNdS )vra  z
    Base class for platform adapters.
    
    Subclasses implement platform-specific logic for:
    - Connecting and authenticating
    - Receiving messages
    - Sending messages/responses
    - Handling media
    configrA   c                 F   || _         || _        d | _        d| _        d | _        d | _        d| _        d | _        i | _        i | _	        t                      | _        i | _        t                      | _        d | _        t                      | _        t                      | _        d S )NFT)r{  rA   _message_handler_running_fatal_error_code_fatal_error_message_fatal_error_retryable_fatal_error_handler_active_sessions_pending_messagesset_background_tasks_post_delivery_callbacks_expected_cancelled_tasks_busy_session_handler_auto_tts_disabled_chats_typing_paused)rG  r{  rA   s      r   __init__zBasePlatformAdapter.__init__W  s     :>0437!&*#im! ;=:< 58EE
 >@%<?EE&_c"-0UU% $'55r   r   c                     | j         d uS Nr  rF  s    r   has_fatal_errorz#BasePlatformAdapter.has_fatal_errorv  s    (44r   c                     | j         S r  r  rF  s    r   fatal_error_messagez'BasePlatformAdapter.fatal_error_messagez  s    ((r   c                     | j         S r  )r  rF  s    r   fatal_error_codez$BasePlatformAdapter.fatal_error_code~  s    %%r   c                     | j         S r  )r  rF  s    r   fatal_error_retryablez)BasePlatformAdapter.fatal_error_retryable  s    **r   handlerNc                     || _         d S r  )r  rG  r  s     r   set_fatal_error_handlerz+BasePlatformAdapter.set_fatal_error_handler  s    $+!!!r   c                     d| _         d | _        d | _        d| _        	 ddlm}  || j        j        dd d            d S # t          $ r Y d S w xY w)NTr   write_runtime_status	connectedrA   platform_state
error_codeerror_message	r~  r  r  r  gateway.statusr  rA   r`   rE   rG  r  s     r   _mark_connectedz#BasePlatformAdapter._mark_connected  s    !%$(!&*#	;;;;;;  $-*=kfjz~ 	 	 	DD	   ? 
AAc                     d| _         | j        rd S 	 ddlm}  || j        j        dd d            d S # t          $ r Y d S w xY w)NFr   r  disconnectedr  )r~  r  r  r  rA   r`   rE   r  s     r   _mark_disconnectedz&BasePlatformAdapter._mark_disconnected  s     	F	;;;;;;  $-*=nim  ~B  C  C  C  C  C  C 	 	 	DD	s   3 
A AcodemessagerY  c                    d| _         || _        || _        || _        	 ddlm}  || j        j        d||           d S # t          $ r Y d S w xY w)NFr   r  fatalr  r  )rG  r  r  rY  r  s        r   _set_fatal_errorz$BasePlatformAdapter._set_fatal_error  s    !%$+!&/#		;;;;;;  ,&%	       	 	 	DD	r  c                 r   K   | j         }|sd S  ||           }t          j        |          r
| d {V  d S d S r  )r  r   iscoroutine)rG  r  results      r   _notify_fatal_errorz'BasePlatformAdapter._notify_fatal_error  sZ      + 	Fv&& 	LLLLLLLLL	 	r   scopeidentityresource_descc                 f   ddl m} || _        || _         |||d| j        j        i          \  }}|rdS t          |t                    r|                    d          nd}| d|rd	| d
ndz   dz   }t          
                    d| j        |           |                     | d|d           dS )z@Acquire a scoped lock for this adapter. Returns True on success.r   )acquire_scoped_lockrA   metadataTpidNz already in usez (PID r   rW   z. Stop the other gateway first.z[%s] %s_lockF)rY  )r  r  _platform_lock_scope_platform_lock_identityrA   r`   r   rv  rI   rr   rW  r  r  )	rG  r  r  r  r  acquiredrc  	owner_pidr  s	            r   _acquire_platform_lockz*BasePlatformAdapter._acquire_platform_lock  s    666666$)!'/$008z4=3F&G
 
 
(  	4+5h+E+EOHLL'''4	---(19$	$$$$r;/0 	
 	Y	7333ooow%HHHur   c                 l    t          | dd          }|sdS ddlm}  || j        |           d| _        dS )z;Release the scoped lock acquired by _acquire_platform_lock.r  Nr   )release_scoped_lock)r!   r  r  r  r  )rG  r  r  s      r   _release_platform_lockz*BasePlatformAdapter._release_platform_lock  sW    4!:DAA 	F666666D5x@@@'+$$$r   c                 >    | j         j                                        S )z%Human-readable name for this adapter.)rA   r`   titlerF  s    r   r  zBasePlatformAdapter.name  s     }"((***r   c                     | j         S )z(Check if adapter is currently connected.)r~  rF  s    r   is_connectedz BasePlatformAdapter.is_connected  s     }r   c                     || _         dS )z
        Set the handler for incoming messages.
        
        The handler receives a MessageEvent and should return
        an optional response string.
        N)r}  r  s     r   set_message_handlerz'BasePlatformAdapter.set_message_handler  s     !(r   c                     || _         dS )zESet an optional handler for messages arriving during active sessions.N)r  r  s     r   set_busy_session_handlerz,BasePlatformAdapter.set_busy_session_handler  s    %,"""r   session_storec                     || _         dS )a  
        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)rG  r  s     r   set_session_storez%BasePlatformAdapter.set_session_store  s     ,r   c                 
   K   dS )z
        Connect to the platform and start receiving messages.
        
        Returns True if connection was successful.
        Nr   rF  s    r   connectzBasePlatformAdapter.connect         	r   c                 
   K   dS )zDisconnect from the platform.Nr   rF  s    r   
disconnectzBasePlatformAdapter.disconnect  s       	r   chat_idr   reply_tor  c                 
   K   dS )ar  
        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   )rG  r  r   r  r  s        r   sendzBasePlatformAdapter.send  s      ( 	r   r;  c                 (   K   t          dd          S )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)r0  rW  )rV  )rG  r  r;  r   s       r   edit_messagez BasePlatformAdapter.edit_message  s       %????r   c                 
   K   dS )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   )rG  r  r  s      r   send_typingzBasePlatformAdapter.send_typing%  r  r   c                 
   K   dS )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   rG  r  s     r   stop_typingzBasePlatformAdapter.stop_typing.  s       	r   	image_urlcaptionc                 X   K   |r| d| n|}|                      |||           d{V S )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.
        r_  r  r   r  Nr  )rG  r  r  r  r  r  r6   s          r   
send_imagezBasePlatformAdapter.send_image6  sO        -4B'((Y(((YYwxYPPPPPPPPPr   animation_urlc                 F   K   |                      |||||           d{V S )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.
        )r  r  r  r  r  N)r  )rG  r  r  r  r  r  s         r   send_animationz"BasePlatformAdapter.send_animationI  sX       __WW^iq  }E_  F  F  F  F  F  F  F  F  	Fr   r   c                     |                                                      d          d         }|                    d          S )z=Check if a URL points to an animated GIF (vs a static image).?r   .gif)rm   rL  endswith)r   rm   s     r   _is_animation_urlz%BasePlatformAdapter._is_animation_urlZ  s6     		!!#&&q)~~f%%%r   c                 \  	 g }| }d}t          j        ||           D ]^}|                    d          }|                    d          	t          	fddD                       r|                    	|f           _d}t          j        ||           D ].}|                    d          	|                    	df           /|red |D             fd	}t          j        |||          }t          j        |||          }t          j        d
d|                                          }||fS )a  
        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\)]+)\)r   r	   c              3      K   | ]A}                                                     |          p|                                 v V  Bd S r  )rm   r  ).0r   r   s     r   	<genexpr>z5BasePlatformAdapter.extract_images.<locals>.<genexpr>y  sc       m ms399;;'',,Bsyy{{0B m m m m m mr   ).pngr   .jpegr  .webpz	fal.mediazfal-cdnzreplicate.deliveryzA<img\s+src=["\']?(https?://[^\s"\'<>]+)["\']?\s*/?>\s*(?:</img>)?rW   c                     h | ]\  }}|S r   r   )r  r   rN   s      r   	<setcomp>z5BasePlatformAdapter.extract_images.<locals>.<setcomp>  s    777fc1c777r   c                     | j         dk    r|                     d          n|                     d          }|v rdn|                     d          S )Nr	   r   rW   r   )	lastindexgroup)matchr   extracted_urlss     r   _remove_if_extractedz@BasePlatformAdapter.extract_images.<locals>._remove_if_extracted  sJ    (-1(<(<ekk!nnn%++a.. N22rrAFr   \n{3,}

)refinditerr  anyappendsubrG   )
r   imagescleaned
md_patternr  alt_texthtml_patternr  r  r   s
           @@r   extract_imagesz"BasePlatformAdapter.extract_images`  s      =
[W55 	/ 	/E{{1~~H++a..C m m m mkm m m m m /sHo... \[w77 	% 	%E++a..CMM3)$$$$  	A77777NG G G G G fZ)=wGGGf\+?IIGfY88>>@@Gwr   
audio_pathc                 ^   K   d| }|r| d| }|                      |||           d{V S )a
  
        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  )rG  r  r  r  r  kwargsr6   s          r   
send_voicezBasePlatformAdapter.send_voice  sZ       +j** 	(''''DYYwxYPPPPPPPPPr   c                 2   K    | j         d||d| d{V S )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).
        )r  r  Nr   )r  )rG  r  r  r  s       r   play_ttszBasePlatformAdapter.play_tts  s9       %T_VWVVvVVVVVVVVVr   
video_pathc                 ^   K   d| }|r| d| }|                      |||           d{V S )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  )rG  r  r  r  r  r  r6   s          r   
send_videozBasePlatformAdapter.send_video  sZ       +j** 	(''''DYYwxYPPPPPPPPPr   	file_path	file_namec                 ^   K   d| }|r| d| }|                      |||           d{V S )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  )rG  r  r  r  r  r  r  r6   s           r   send_documentz!BasePlatformAdapter.send_document  sZ       )Y(( 	(''''DYYwxYPPPPPPPPPr   
image_pathc                 ^   K   d| }|r| d| }|                      |||           d{V S )a  
        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  )rG  r  r  r  r  r  r6   s          r   send_image_filez#BasePlatformAdapter.send_image_file  sZ       .-- 	(''''DYYwxYPPPPPPPPPr   c                    g }| }d| v }|                     dd          }t          j        d          }|                    |           D ]}|                    d                                          }t          |          dk    r8|d         |d         k    r&|d         dv r|d	d                                         }|                    d                              d
          }|r4|	                    t          j                            |          |f           |r>|                    d|          }t          j        dd|                                          }||fS )a  
        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]]rW   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   r	   r   r   z`"'r   z
`"',.;:)}]r  r  )r   r   compiler  r  rG   r
   lstriprstripr  r^   r   
expanduserr  )r   mediar  has_voice_tagmedia_patternr  r   s          r   extract_mediaz!BasePlatformAdapter.extract_media  sf     -7//"6;; 
 G
 
 #++G44 	H 	HE;;v&&,,..D4yyA~~$q'T"X"5"5$q'V:K:KAbDz''));;v&&--m<<D Hbg0066FGGG  	A#''G44GfY88>>@@Gg~r   c                    d}d                     d |D                       }t          j        d|z   dz   t          j                  }g t          j        d| t          j                  D ]=}                    |                                |                                f           >t          j        d|           D ]=}                    |                                |                                f           >dt          d	t          ffd
}g }|                    |           D ]} ||                                          r |                    d          }t          j                            |          }	t          j                            |	          r|                    ||	f           t!                      }
g }|D ]5\  }}	|	|
vr,|
                    |	           |                    ||	f           6d |D             }| }|rF|D ]\  }}|                    |d          }t          j        dd|                                          }||fS )aW  
        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).
        )
r  r   r  r  r  .mp4.mov.avi.mkv.webm|c              3   @   K   | ]}|                     d           V  dS )r   N)r  )r  es     r   r  z:BasePlatformAdapter.extract_local_files.<locals>.<genexpr>,  s,      EEaAHHSMMEEEEEEr   z/(?<![/:\w.])(?:~/|/)(?:[\w.\-]+/)*[\w.\-]+\.(?:z)\bz```[^\n]*\n.*?```z	`[^`\n]+`posr   c                 <     t           fdD                       S )Nc              3   >   K   | ]\  }}|cxk    o|k     nc V  d S r  r   )r  r   r-  r.  s      r   r  zLBasePlatformAdapter.extract_local_files.<locals>._in_code.<locals>.<genexpr>>  s;      ;;1qC||||!||||;;;;;;r   )r  )r.  
code_spanss   `r   _in_codez9BasePlatformAdapter.extract_local_files.<locals>._in_code=  s'    ;;;;
;;;;;;r   r   c                     g | ]\  }}|S r   r   )r  rN   expandeds      r   
<listcomp>z;BasePlatformAdapter.extract_local_files.<locals>.<listcomp>Q  s    444ka444r   rW   r  r  )joinr   r  
IGNORECASEr  DOTALLr  startendintrS  r  r^   r   r   isfiler  addr   r  rG   )r   _LOCAL_MEDIA_EXTSext_partpath_remr2  foundr  r   r4  seenuniquepathsr  _expr1  s                  @r   extract_local_filesz'BasePlatformAdapter.extract_local_files  st   "
 88EE3DEEEEE
 *>IFRM
 
 
17BIFF 	4 	4Aqwwyy!%%''23333\733 	4 	4Aqwwyy!%%''23333	<# 	<$ 	< 	< 	< 	< 	< 	< %%g.. 	. 	.Ex&& ++a..Cw))#..Hw~~h'' .c8_--- EE" 	/ 	/MCt##"""sHo...44V444 	A# 3 3	T!//#r22fY88>>@@Gg~r          @intervalc                 "  K   	 	 || j         vr|                     ||           d{V  t          j        |           d{V  A# t          j        $ r Y nw xY w	 t          | d          r-	 |                     |           d{V  n# t          $ r Y nw xY w| j                             |           dS # t          | d          r-	 |                     |           d{V  n# t          $ r Y nw xY w| j                             |           w xY w)u/  
        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.
        
        Skips send_typing when the chat is in ``_typing_paused`` (e.g. while
        the agent is waiting for dangerous-command approval).  This is critical
        for Slack's Assistant API where ``assistant_threads_setStatus`` disables
        the compose box — pausing lets the user type ``/approve`` or ``/deny``.
        Tr  Nr  )	r  r  r   r   CancelledErrorhasattrr  rE   discard)rG  r  rI  r  s       r   _keep_typingz BasePlatformAdapter._keep_typing[  s     	1.$"555**7X*FFFFFFFFFmH---------. % 	 	 	D	 t]++ **73333333333    D''00000 t]++ **73333333333    D''0000sZ   AA AB5 AB5 -B	 	
BB5DC#"D#
C0-D/C00Dc                 :    | j                             |           dS )u   Pause typing indicator for a chat (e.g. during approval waits).

        Thread-safe (CPython GIL) — can be called from the sync agent thread
        while ``_keep_typing`` runs on the async event loop.
        N)r  r=  r  s     r   pause_typing_for_chatz)BasePlatformAdapter.pause_typing_for_chatz  s!     	(((((r   c                 :    | j                             |           dS )z;Resume typing indicator for a chat after approval resolves.N)r  rM  r  s     r   resume_typing_for_chatz*BasePlatformAdapter.resume_typing_for_chat  s    ##G,,,,,r   r]  c                 
   K   dS )z.Hook called when background processing begins.Nr   )rG  r]  s     r   on_processing_startz'BasePlatformAdapter.on_processing_start  
        r   outcomec                 
   K   dS )z1Hook called when background processing completes.Nr   )rG  r]  rV  s      r   on_processing_completez*BasePlatformAdapter.on_processing_complete  rU  r   	hook_nameargsr  c                    K   t          | |d          }t          |          sdS 	  ||i | d{V  dS # t          $ r-}t                              d| j        ||           Y d}~dS d}~ww xY w)zARun a lifecycle hook without letting failures break message flow.Nz[%s] %s hook failed: %s)r!   callablerE   rr   rs   r  )rG  rY  rZ  r  hookr-  s         r   _run_processing_hookz(BasePlatformAdapter._run_processing_hook  s      tY--~~ 	F	O$'''''''''''' 	O 	O 	ONN4diANNNNNNNNN	Os   6 
A- "A((A-rW  c                 t    | sdS |                                  t          fdt          D                       S )zGReturn True if the error string looks like a transient network failure.Fc              3       K   | ]}|v V  	d S r  r   )r  patlowereds     r   r  z:BasePlatformAdapter._is_retryable_error.<locals>.<genexpr>  s'      GGc3'>GGGGGGr   )rm   r  _RETRYABLE_ERROR_PATTERNSrW  rb  s    @r   _is_retryable_errorz'BasePlatformAdapter._is_retryable_error  sC      	5++--GGGG-FGGGGGGr   c                 J    | sdS |                                  }d|v pd|v pd|v S )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)rm   rd  s     r   _is_timeout_errorz%BasePlatformAdapter._is_timeout_error  s>      	5++--g%^')A^^W^E^^r   r	   max_retries
base_delayrV  c           	        K   |                      ||||           d{V }|j        r|S |j        pd}|j        p|                     |          }	|	s|                     |          r|S |	rft          d|dz             D ]}
|d|
dz
  z  z  t          j        dd          z   }t          
                    d| j        |
|||           t          j        |           d{V  |                      ||||           d{V }|j        r%t                              d| j        |
           |c S |j        pd}|j        s|                     |          s nt                              d	| j        ||           d
}	 |                      ||||           d{V  n8# t          $ r+}t                              d| j        |           Y d}~nd}~ww xY w|S t          
                    d| j        |           |                      |d|dd          ||           d{V }|j        s&t                              d| j        |j                   |S )ax  
        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.
        r  r   r  r  NrW   r   r	   r   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:)

i  z"[%s] Fallback send also failed: %s)r  r0  rW  rY  re  ri  r   randomuniformrr   rs   r  r   r   inforE   r   )rG  r  r   r  r  rj  rk  r  	error_str
is_networkr   delaynotice
notify_errfallback_results                  r   _send_with_retryz$BasePlatformAdapter._send_with_retry  s.     $ yy	 ! 
 
 
 
 
 
 
 
 > 	ML&B	%L)A)A))L)L
  	d44Y?? 	M  	 K!O44  "aGaK&89FN1a<P<PPMIwUI   mE*********#yy##%%	  )           > "KK A49gVVV!MMM"L.B	( D,D,DY,O,O E SUYU^`kmvwwwm k))GVhai)jjjjjjjjjj  k k kLL!RTXT]_ijjjjjjjjk 	LdiYbccc $		TGETENTT	 !* !
 !
 
 
 
 
 
 
 & 	aLL=ty/J_```s   8F 
G"!GGexisting_textnew_textc                     | s|S d |                      d          D             }|                                |vr|  d|                                 S | S )a`  Merge a new caption into existing text, avoiding duplicates.

        Uses line-by-line exact match (not substring) to prevent false positives
        where a shorter caption is silently dropped because it appears as a
        substring of a longer one (e.g. "Meeting" inside "Meeting agenda").
        Whitespace is normalised for comparison.
        c                 6    g | ]}|                                 S r   )rG   )r  cs     r   r5  z6BasePlatformAdapter._merge_caption.<locals>.<listcomp>  s     LLL1QWWYYLLLr   r  )rL  rG   )rx  ry  existing_captionss      r   rb  z"BasePlatformAdapter._merge_caption   so      	OLL0C0CF0K0KLLL>>#444#333399;;;r   c                   K   | j         sdS t          |j        | j        j                            dd          | j        j                            dd                    }|| j        v r|                                }|dv rt          	                    d| j
        ||           	 |j        j        rd	|j        j        ind}|                      |           d{V }|r.|                     |j        j        ||j        |
           d{V  n;# t          $ r.}t                              d| j
        ||d           Y d}~nd}~ww xY wdS | j        Z	 |                     ||           d{V rdS n:# t          $ r-}t                              d| j
        |d           Y d}~nd}~ww xY w|j        t&          j        k    r9t          	                    d| j
        |           t+          | j        ||           dS t          	                    d| j
        |           || j        |<   | j        |                                          dS t1          j                    | j        |<   t1          j        |                     ||                    }	 | j                            |           n# t<          $ r Y dS w xY wt?          |d          r@|                     | j        j!                   |                     | j"        j!                   dS dS )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)r  r  )
approvedenystatusstopnewreset
backgroundrestartqueueqz8[%s] Command '/%s' bypassing active-session guard for %s	thread_idrm  z&[%s] Command '/%s' dispatch failed: %sexc_infoz$[%s] Busy-session handler failed: %sz=[%s] Queuing photo follow-up for session %s without interruptuD   [%s] New message while session %s is active — triggering interruptadd_done_callback)#r}  r   r9  r{  extrarI   r  rN  rr   r   r  r  rw  r  r;  rE   rW  r  r8  r  r'  rh  r  r  r   Eventcreate_task_process_message_backgroundr  r=  	TypeErrorrL  r  rM  r  )rG  r]  r\  cmd_thread_metar   r-  tasks           r   handle_messagez"BasePlatformAdapter.handle_message  s      $ 	F'L$(K$5$9$9:SUY$Z$Z%)[%6%:%:;UW\%]%]
 
 
 $/// ##%%CrrrNIsK  mLQLLb#lK1G#H#HhlL%)%:%:5%A%AAAAAAAH "33$)L$8$,%*%5%1	 4          ! m m mLL!I49VY[\gkLllllllllm)5f!77{KKKKKKKK   f f fLL!GTU`dLeeeeeeeef ![%666\^b^gituuu+D,BKQVWWW LL_aeajlwxxx27D";/!+.22444F .5]__k* "4#C#CE;#W#WXX	"&&t,,,, 	 	 	 FF	 4,-- 	K""4#9#ABBB""4#A#IJJJJJ	K 	KsC   $A'D 
E$D??EE0 0
F':#F""F'J# #
J10J1c                  4   ddl } t          j        dd                                          }|dk    rdS t	          t          j        dd                    }t	          t          j        dd	                    }|d
k    rd\  }} | j        |dz  |dz            S )ac  
        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)
        r   NHERMES_HUMAN_DELAY_MODEoffg        HERMES_HUMAN_DELAY_MIN_MS800HERMES_HUMAN_DELAY_MAX_MS2500natural)i   i	  g     @@)rn  r^   getenvrm   r;  ro  )rn  modemin_msmax_mss       r   _get_human_delayz$BasePlatformAdapter._get_human_delayj  s     	y2E::@@BB5==3RY:EBBCCRY:FCCDD9&NFFv~fvov???r   r\  c           	        ,-K   d,d-,-fd}| j                             |          pt          j                    }|| j         |<   |j        j        rd|j        j        ind}t          j        |                     |j        j        |                    }	 | 	                    d|           d{V  | 
                    |           d{V }|r@|                                r,|| j        v r#t                              d| j        |           d}|s+t                              d| j        |j        j                   |r|                     |          \  }}|                     |          \  }	}
|
                    d	d
                                          }
t+          j        dd
|
                                          }
|	r<t                              d| j        t/          |	          t/          |                     |                     |
          \  }}
|r.t                              d| j        t/          |                     d}|j        t4          j        k    r|
r|s|j        j        | j        vr	 ddlm}m}  |            rddl }t+          j        dd
|
          dd                                         }|stC          d          t          j"        ||           d{V }|#                    |          }|                    d          }n8# tH          $ r+}t          %                    d| j        |           Y d}~nd}~ww xY w|rtM          |          '                                ry	 | (                    |j        j        ||           d{V  	 tS          j*        |           n:# tV          $ r Y n.w xY w# 	 tS          j*        |           w # tV          $ r Y w w xY wxY w|
rrt                              d| j        t/          |
          |j        j                   | ,                    |j        j        |
|j-        |           d{V } ||           | .                                }|	r.t                              d| j        t/          |	                     |	D ]:\  }}|dk    rt          j/        |           d{V  	 t                              d| j        ta          |          |r
|dd         nd
           | 1                    |          r.| 2                    |j        j        ||r|nd|           d{V }n-| 3                    |j        j        ||r|nd|           d{V }|j4        s&t          5                    d| j        |j5                   # tH          $ r.}t          5                    d| j        |d !           Y d}~4d}~ww xY wh d"}h d#}h d$}|D ]c\  }}|dk    rt          j/        |           d{V  	 tM          |          j6        7                                }||v r)| 8                    |j        j        ||           d{V } n||v r)| 9                    |j        j        ||%           d{V } nU||v r)| :                    |j        j        ||&           d{V } n(| ;                    |j        j        ||'           d{V } | j4        s't          %                    d(| j        || j5                   ,# tH          $ r,}!t          %                    d)| j        |!           Y d}!~!]d}!~!ww xY w|D ]}"|dk    rt          j/        |           d{V  	 tM          |"          j6        7                                }||v r)| :                    |j        j        |"|&           d{V  nU||v r)| 9                    |j        j        |"|%           d{V  n(| ;                    |j        j        |"|'           d{V  # tH          $ r,}#t          5                    d*| j        |"|#           Y d}#~#d}#~#ww xY w,r-nty          |           }$| 	                    d+||$rtz          j>        ntz          j?                   d{V  || j        v rz| j        @                    |          }%t                              d,| j                   || j         v r| j         |= |A                                 	 | d{V  n# t          jB        $ r Y nw xY w| C                    |%|           d{V  	 t          | d-i           @                    |d          }&t          |&          r	  |&             n# tH          $ r Y nw xY w|A                                 	 | d{V  n# t          jB        $ r Y nw xY w	 t          | d.          r%| G                    |j        j                   d{V  n# tH          $ r Y nw xY w|| j         v r
| j         |= dS dS n\# t          jB        $ rU t          jH                    }'tz          jI        }(|'	|'| jJ        vrtz          j?        }(| 	                    d+||(           d{V   tH          $ r})| 	                    d+|tz          j?                   d{V  t          5                    d/| j        |)d !           	 t          |)          jL        }*t          |)          rt          |)          dd0         nd1}+|j        j        rd|j        j        ind}| N                    |j        j        d2|* d3|+ d4|5           d{V  n# tH          $ r Y nw xY wY d})~)nd})~)ww xY wt          | d-i           @                    |d          }&t          |&          r	  |&             n# tH          $ r Y nw xY w|A                                 	 | d{V  n# t          jB        $ r Y nw xY w	 t          | d.          r%| G                    |j        j                   d{V  n# tH          $ r Y nw xY w|| j         v r
| j         |= dS dS # t          | d-i           @                    |d          }&t          |&          r	  |&             n# tH          $ r Y nw xY w|A                                 	 | d{V  n# t          jB        $ r Y nw xY w	 t          | d.          r%| G                    |j        j                   d{V  n# tH          $ r Y nw xY w|| j         v r| j         |= w xY w)6z4Background task that actually processes the message.Fc                 >    | d S dt          | dd          rdd S d S )NTr0  F)r!   )r  delivery_attempteddelivery_succeededs    r   _record_deliveryzIBasePlatformAdapter._process_message_background.<locals>._record_delivery  s=    ~!%vy%00 *%)"""* *r   r  Nr  rT  z:[%s] Suppressing stale response for interrupted session %sz0[%s] Handler returned empty/None response for %sr  rW   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 responser   )text_to_speech_toolcheck_tts_requirementsz[*_`#\[\]()]i  z!Empty text after markdown cleanup)r6   r  z[%s] Auto-TTS failed: %s)r  r  r  z&[%s] Sending response (%d chars) to %srm  z1[%s] Extracted %d image(s) to send as attachmentsz[%s] Sending image: %s (alt=%s)   )r  r  r  r  )r  r  r  r  z[%s] Failed to send image: %sz[%s] Error sending image: %sTr  >   .m4a.mp3.wav.opusr  >   .3gpr(  r)  r'  r&  r*  >   r  r   r  r  r  )r  r  r  )r  r  r  )r  r  r  z"[%s] Failed to send media (%s): %sz[%s] Error sending media: %sz$[%s] Error sending local file %s: %srX  z-[%s] Processing queued message from interruptr  r  z[%s] Error handling message: %si,  zno details availablezSorry, I encountered an error (z).
z2
Try again or use /reset to start a fresh session.)r  r   r  )Or  rI   r   r  r9  r  r  rN  r  r^  r}  is_setr  rr   rp  r  r   r$  r
  r   rG   r   r  r
   rG  r8  r  r*  r  tools.tts_toolr  r  jsonr"   	to_threadloadsrE   rs   r{   existsr  r^   remover(   rw  r;  r  r   r   r  r  r  r0  rW  suffixrm   r  r  r  r  rS  r/  r3  r4  popcancelrK  r  r!   r\  rL  r  current_taskr5  r  typer   r   r  ).rG  r]  r\  r  interrupt_event_thread_metadatatyping_taskr   media_filesr  text_contentlocal_files	_tts_pathr  r  _jsonspeech_texttts_result_strtts_datatts_errr  human_delayr  r  
img_resultimg_err_AUDIO_EXTS_VIDEO_EXTS_IMAGE_EXTS
media_pathis_voicer   media_result	media_errr  file_errprocessing_okpending_event_post_cbr  rV  r-  
error_typeerror_detailr  r  s.                                               @@r   r  z/BasePlatformAdapter._process_message_background  sr      #"	* 	* 	* 	* 	* 	* /33K@@SGMOO-<k* EJLDZdK)?@@`d)$*;*;EL<P[k*;*l*lmmM	7++,A5IIIIIIIII "22599999999H 
 #**,,
   4#999PI  
   rOQUQZ\a\h\pqqq fm(,(:(:8(D(D%X (,':':8'D'D$+334H"MMSSUU!vor<HHNNPP GKK ^`d`iknoukvkvx{  }E  yF  yF  G  G  G -1,D,D\,R,R)\ vKK WY]Ybdghsdtdtuuu !	&+*;;;( < + < "L08UUUW^^^^^^^^1133 	B0000*,&"l*S*STYUYTY*Z*`*`*b*bK#. V&01T&U&U U3:3D 3+4 4 4 . . . . . .N (-{{>'B'BH(0[(A(AI$ W W W'A49gVVVVVVVVW  !i!7!7!9!9 !
!"mm$)L$8'0%5 ,         !Ii0000& ! ! ! D!!Ii0000& ! ! ! D!   -KK H$)UXYeUfUfhmhth|}}}#'#8#8 % 4 ,!&!1!1	 $9 $ $      F %$V,,, #3355  mKK SUYU^`cdj`k`klll+1 h h'Ix"Q%mK888888888h= I,Y77-5=HSbSMM2	    11)<< /3/B/B(-(<.74<(F$)9	 0C 0 0 * * * * * *JJ 04(-(<*34<(F$)9	 0? 0 0 * * * * * *J  *1 g"LL)H$)U_Uefff$ h h h%CTYPWbfggggggggh HGGOOOHHH,7 !] !](J"Q%mK888888888]":..5;;==+--15(-(<+5)9 2A 2 2 , , , , , ,LL
 !K//15(-(<+5)9 2A 2 2 , , , , , ,LL
 !K//151E1E(-(<+5)9 2F 2 2 , , , , , ,LL 261C1C(-(<*4)9 2D 2 2 , , , , , ,L  ,3 u"NN+OQUQZ\_amasttt$ ] ] ]'EtyR[\\\\\\\\] "- m mI"Q%mK888888888m"9oo4::<<+--"&"6"6(-(<+4)9 #7 # #        
 !K//"&//(-(<+4)9 #2 # #         #'"4"4(-(<*3)9 #5 # #       
 % m m m%KTYXackllllllllm 3E\..dS[nnJ\M++(-:Y!))@Q@Y         d444 $ 6 : :; G GLdiXXX$"777-k:""$$$%%%%%%%%%-   D 66}kRRRRRRRRR> t%?DDHHVZ[[H!! HJJJJ    D    !!!!!!!!!)   4// A**5<+?@@@@@@@@@    d333)+666 43A 5 % 	 	 	"/11L'1G#|4;Y'Y'Y+3++,DeWUUUUUUUUU 	 	 	++,DeM^MfgggggggggLL:DIqSWLXXX!!WW-
/21vvQs1vvdsd||;QLQLLb#lK1G#H#Hhl ii!L0L* L L'L L L .                #	, t%?DDHHVZ[[H!! HJJJJ    D    !!!!!!!!!)   4// A**5<+?@@@@@@@@@    d333)+666 43) t%?DDHHVZ[[H!! HJJJJ    D    !!!!!!!!!)   4// A**5<+?@@@@@@@@@    d333)+66666s\  Gh& 4BL h& 
M!M=h& M&h& .(N< N, +h& ,
N96h& 8N99h& <O#>OO#
O O#O  O##C#h& CV!h& !
W+#Wh& W7h& D\h& 
] !]h& ](h& 5B(`h& 
a("a
h& aB5h& 
d h& d%"h& $d%%h& :
f 
ff*f3 3gg	5g? ?
hh$q! &A,m?A
m:Bm%$m:%
m2/m:1m22m:5q! :m??q! 7
o 
oo'o0 0pp5p< <
q	q	!5t>
r"!t>"
r/,t>.r//t>st>s"t>!s""t>&5tt>
t)&t>(t))t>c                   K   d | j         D             }|D ]0}| j                            |           |                                 1|rt	          j        |ddi d{V  | j                                          | j                                         | j                                         | j                                         dS )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.
        c                 :    g | ]}|                                 |S r   )done)r  r  s     r   r5  z?BasePlatformAdapter.cancel_background_tasks.<locals>.<listcomp>  s%    LLL$		LLLLr   return_exceptionsTN)	r  r  r=  r  r   gatherclearr  r  )rG  tasksr  s      r   cancel_background_tasksz+BasePlatformAdapter.cancel_background_tasks  s       ML$"8LLL 	 	D*..t444KKMMMM 	A.%@4@@@@@@@@@$$&&&&,,...$$&&&##%%%%%r   c                 R    || j         v o| j         |                                         S )z3Check if there's a pending interrupt for a session.)r  r  rG  r\  s     r   has_pending_interruptz)BasePlatformAdapter.has_pending_interrupt  s)    d33c8Mk8Z8a8a8c8ccr   c                 8    | j                             |d          S )z0Get and clear any pending message for a session.N)r  r  r  s     r   get_pending_messagez'BasePlatformAdapter.get_pending_message  s    %))+t<<<r   dm	chat_name	chat_typeuser_id	user_namer  
chat_topicuser_id_altchat_id_altc
                     ||                                 sd}t          | j        t          |          |||rt          |          nd||rt          |          nd|r|                                 nd||	
  
        S )z2Helper to build a SessionSource for this platform.N)
rA   r  r  r  r  r  r  r  r  r  )rG   r   rA   r   )
rG  r  r  r  r  r  r  r  r  r  s
             r   build_sourcez BasePlatformAdapter.build_source  s     !**:*:*<*<!J]LL$+5CLLL(1;c)nnnt-7Az'')))T##
 
 
 	
r   c                 
   K   dS )z
        Get information about a chat/channel.
        
        Returns dict with at least:
        - name: Chat name
        - type: "dm", "group", "channel"
        Nr   r  s     r   get_chat_infoz!BasePlatformAdapter.get_chat_info  s       	r   c                     |S )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   )rG  r   s     r   format_messagez"BasePlatformAdapter.format_message  s	     r      
max_lengthr   zCallable[[str], int]c                    |pt           } ||           |k    r| gS d}d}g }| }d}|r|d| dnd}	||z
   ||	          z
   ||          z
  }
|
dk     r|dz  }
 ||	           ||          z   ||z
  k    r|                    |	|z              n |t           urt          ||
|          }n|
}|d|         }|                    d          }||dz  k     r|                    d	          }|dk     r|}|d|         }|                    d
          |                    d          z
  }|dz  dk    r|                    d
          }|dk    r;||dz
           dk    r,|                    d
d|          }|dk    r||dz
           dk    ,|dk    rI|                    d	d|          }|                    dd|          }t          ||          }||dz  k    r|}|d|         }||d                                         }|	|z   }|du}|pd}|                    d          D ]n}|                                }|	                    d          rC|rd}d}2d}|dd                                         }|r|                                d         nd}o|r||z  }|}nd}|                    |           |t          |          dk    r*t          |          fdt          |          D             }|S )a9  
        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)
            len_fn: Optional length function for measuring string length.
                     Defaults to ``len`` (Unicode code-points).  Pass
                     ``utf16_len`` for platforms that measure message
                     length in UTF-16 code units (e.g. Telegram).

        Returns:
            List of message chunks
        
   z
```Nz```r_  rW   r   r	    `z\`r   \r   FTr4   c                 2    g | ]\  }}| d |dz    d dS )z (r   r   r   r   )r  ichunktotals      r   r5  z8BasePlatformAdapter.truncate_message.<locals>.<listcomp>p  sG       19E5,,AE,,E,,,  r   )r
   r  r   rfindcountmaxr  rL  rG   rn   	enumerate)r   r  r   _lenINDICATOR_RESERVEFENCE_CLOSEchunks	remaining
carry_langprefixheadroom	_cp_limitregionsplit_at	candidatebacktick_countlast_bt
safe_splitnl_split
chunk_body
full_chunkin_codelangrL   strippedtagr  s                             @r   truncate_messagez$BasePlatformAdapter.truncate_message  s   2 }4==J&&9	 %)
 S	& .8-C):))))F "$55VDttKGXGXXH!||%? tF||dd9oo->O1OOOfy0111 3.y(DII		$	z	z*F||D))H)q.((!<<,,!||$ ")8),I&__S11IOOE4J4JJN!Q&&#//#..kki!&<&D&D'ooc1g>>G kki!&<&D&DQ;;!*a!A!AJ(tQ@@H!$Z!:!:J!IN22#-"9H9-J!()),3355I*,J !,G#D"((.. 	= 	=::<<&&u-- = ="'!"&&qrrl002214<syy{{1~~" "k)
!

!
MM*%%%g  S	&l v;;??KKE   =Fv=N=N  F r   )r   N)NNr  )NNN)rH  N)NNr	   rH  )Nr  NNNNNN)r  N)Or   r"  r#  r$  r   r   r  propertyrS  r  r~   r   r  r  r  r   r   r  r  r  r  r  r  r  r  r  MessageHandlerr  r7  r  r   r  r   r  r  r|   rV  r  r  r  r  r  r  staticmethodr  r   r}   r
  r  r  r  r  r  r$  rG  floatrN  rP  rR  rT  r/  rX  r^  re  ri  r;  rw  rb  r  r  r  r  r  r  r   r  r  r  r  r   r   r   ra  ra  L  s7        )~ ) ) ) ) )> 5 5 5 5 X5 )Xc] ) ) ) X) &(3- & & & X& +t + + + X+,x9N8OQZ[_Q`cgQg8g/h ,mq , , , ,	 	 	 	   S 3 d t        C 3 s W[    (, , , , +c + + + X+ d    X(> (d ( ( ( (-<QTBUW`aeWfBf9g0h -mq - - - -,s ,t , , , , t    ^    ^ 
 #'-1   3-	
 4S>* 
   ^*@@ @ 	@
 
@ @ @ @            "&"&-1Q QQ Q #	Q
 3-Q 4S>*Q 
Q Q Q Q. "&"&-1F FF F #	F
 3-F 4S>*F 
F F F F" &s &t & & & \&
 - -d5c?.CS.H(I - - - \-f "&"&Q QQ Q #	Q
 3-Q 
Q Q Q Q(WW W
 
W W W W$ "&"&Q QQ Q #	Q
 3-Q 
Q Q Q Q. "&#'"&Q QQ Q #	Q
 C=Q 3-Q 
Q Q Q Q0 "&"&Q QQ Q #	Q
 3-Q 
Q Q Q Q( 's 'uT%T	2B-CS-H'I ' ' ' \'R AS AU49c>-B A A A \AF1 1# 1 1X\ 1 1 1 1>)S )T ) ) ) )-c -d - - - -=| = = = = =@, @IZ @_c @ @ @ @OC O Os OW[ O O O O H8C= HT H H H \H 	_# 	_4 	_ 	_ 	_ \	_ #'P PP P 3-	P
 P P P 
P P P Pd hsm s s    \XK, XK4 XK XK XK XKt @e @ @ @ \@(e7| e7RU e7Z^ e7 e7 e7 e7N	& & & &"d d d d d d=s =x7M = = = = $(!%#'#'$(%)%)
 

 C=
 	

 #
 C=
 C=
 SM
 c]
 c]
 

 
 
 
: 3 4S>    ^	c 	c 	 	 	 	  37A AAA /0A 
c	A A A \A A Ar   ra  r  )r   )r   )r   r	   )r   )r  )r  r	   )[r$  r   r   r   r^   rn  r   socketr#   rB   r@   r   abcr   r   urllib.parser   r   r   rr   r   r;  r   r   r   rS  r0   rT   ra   rv  ru   tuplerw   dataclassesrx   ry   rz   pathlibr{   typingr|   r}   r~   r   r   r   r   enumr   _Pathr   insert__file__r  r   gateway.configr   r   gateway.sessionr   r   hermes_constantsr   *GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGEr   r   r   r   bytesr   r   r   r   r  r  r  r  r  SUPPORTED_DOCUMENT_TYPESr  r  r  r  r/  r7  rV  rh  rc  r  ry  ra  r   r   r   <module>r0     s0          				  				         



  # # # # # # # # ! ! ! ! ! !		8	$	$+ + + + + +# c c    &# s s    &           F!C$J ! ! ! !H( (d
 (cDj ( ( ( (, C$J  4        <$d
 $uT4Z7H $ $ $ $B ) ( ( ( ( ( ( (             H H H H H H H H H H H H H H H H H H       ! ! ! ! ! ! 33uuX..008;<< = = = 3 3 3 3 3 3 3 3 < < < < < < < < + + + + + +b +"& "&# "& "&S "& "& "& "&J  6 !.??T    E d    "  S c    8= =C =c =S =QT = = = =@ s C    8 !.>>T      S c    $= =C =c =S =QT = = = =N $^$57GHH  VPX	 	     E S S    @ # s    *
 
 
 
 
$ 
 
 
        @2 @2 @2 @2 @2 @2 @2 @2F         6* 6* 6*3,-6*6* 6*
 6* 
6* 6* 6* 6*@
  <.)HSM*BBC !  Tz 	4Z	   <h h h h h# h h h h hr   