
    Jj                     
   U 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mZmZm	Z	m
Z
mZ ddlZerddlmZ ddlmZmZmZmZmZmZ ddlmZmZmZ ddlmZmZ ddlmZ dZe
e	         ed	<   dZ e
e	         ed
<   dZ!e
e	         ed<   dZ"e
e	         ed<   dZ#e
e	         ed<   ddl$m%Z% ddl&m'Z'm(Z)m*Z+m,Z, ddl-m.Z.m/Z/m0Z0 ddl1m2Z2m3Z3m4Z4 ddl5Z5 ej6        e7          Z8de9de9fdZ:de9de;fdZ<de=fdZ> e?h d          Z@de9fdZAde9fdZBd ZCde9fdZDde9fdZEde9fdZFde9de9fd ZGde9de;fd!ZHde;fd"ZIdeJe9         fd#ZKd$ZLd%ZM e%d&d'(          ZNdeOfd)ZPd*e9de9fd+ZQd,e9d-e9de
e9         fd.ZRd-e9d,e9d/eOdeSe9e;f         fd0ZTdd1ZUdd3e9d4eOde9fd5ZV	 	 dd6ee9         d7e9d/e
eO         de9fd8ZWde;fd9ZXe7d:k    r	  eYd;            eYd<            eX            ZZ e            Z[ e; ej\        d=d>          ]                                          Z^ e; ej\        d?d>          ]                                          Z_eZr eD            Z` eYd@e`            e`dAk    r eYdB           ne`dCk    r eYdD           ne`dEk    r eYdF           ne`dGk    r eYdH e:dI                      ne`dJk    r eYdK           ne`dLk    r eYdM           ne_rB eYdN ej\        d?          ]                                a                    dO                      nTe^r eYdP           nFe[r eYdQ e                        n- eYdR           n! eYdS            eYdT e                        eZs e5jb        dU            eYdV            eYdW eP             dX           eNjc        r0 eYdYeNjd                     eYdZeNje         d[eNjd         d\           n eYd]            eYd^            eYd_            eYd`            eYd>            eYda            eYdb            eYd>            eYdc            eYdd            eYde            eYdf            eYdg            eYdh            eYdi            eYdj            eYdk           ddllfmgZgmhZh dmdndodpdqdrdsdtdUdud2dvdwd3gdxdyZidzd{dod|d}dpid~d2ddsddddd6gdxdyZj egjk        dmdeid eX eK            dd            egjk        dzdejd eX eK            ddd	  	         dS )ae  
Standalone Web Tools Module

This module provides generic web tools that work with multiple backend providers.
Backend is selected during ``hermes tools`` setup (web.backend in config.yaml).
When available, Hermes can route Firecrawl calls through a Nous-hosted tool-gateway
for Nous Subscribers only.

Available tools:
- web_search_tool: Search the web for information
- web_extract_tool: Extract content from specific web pages

Backend compatibility:
- Exa: https://exa.ai (search, extract)
- Firecrawl: https://docs.firecrawl.dev/introduction (search, extract; direct or derived firecrawl-gateway.<domain> for Nous Subscribers)
- Parallel: https://docs.parallel.ai (search, extract)
- Tavily: https://tavily.com (search, extract)

LLM Processing:
- Uses OpenRouter API with Gemini 3 Flash Preview for intelligent content extraction
- Extracts key excerpts and creates markdown summaries to reduce token usage

Debug Mode:
- Set WEB_TOOLS_DEBUG=true to enable detailed logging
- Creates web_tools_debug_UUID.json in ./logs directory
- Captures all tool calls, results, and compression metrics

Usage:
    from web_tools import web_search_tool, web_extract_tool
    
    # Search the web
    results = web_search_tool("Python machine learning libraries", limit=3)
    
    # Extract content from URLs  
    content = web_extract_tool(["https://example.com"], format="markdown")
    N)ListDictAnyOptionalTYPE_CHECKING)	Firecrawl)r   _firecrawl_backend_help_suffix_get_firecrawl_client_get_firecrawl_gateway_url_is_tool_gateway_readycheck_firecrawl_api_key)_normalize_tavily_documents _normalize_tavily_search_results_tavily_request)_get_async_parallel_client_get_parallel_client)_get_exa_client_firecrawl_client_firecrawl_client_config_parallel_client_async_parallel_client_exa_client)DebugSession)build_vendor_gateway_urlpeek_nous_access_tokenread_nous_access_tokenresolve_managed_tool_gateway)managed_nous_tools_enabled%nous_tool_gateway_unavailable_messageprefers_gateway)async_is_safe_urlnormalize_url_for_requestsensitive_query_param_namenamereturnc                     	 ddl m}  ||           }n# t          $ r d}Y nw xY w|t          j        | d          }|pd                                S )u  Resolve ``name`` via Hermes config-aware env, falling back to process env.

    Mirrors the SearXNG provider's ``_searxng_url()`` so that values set
    through Hermes' config/.env layer (``hermes config set``, ``hermes tools``)
    are honored here too — not just raw process-env exports. Without this,
    a config-only ``SEARXNG_URL`` (or any provider key) leaves the backend
    auto-detect cascade and ``check_web_api_key()`` blind to it. See #34290.
    r   )get_env_valueN )hermes_cli.configr'   	Exceptionosgetenvstrip)r$   r'   vals      5/home/rurouni/.hermes/hermes-agent/tools/web_tools.py
_env_valuer0   l   sz    333333mD!!   
{ib!!I2s    ##c                 :    t          t          |                     S N)boolr0   )r$   s    r/   _has_envr4      s    
4  !!!    c                  ~    	 ddl m}   |                                 di           S # t          t          f$ r i cY S w xY w)z5Load the ``web:`` section from ~/.hermes/config.yaml.r   load_configweb)r)   r8   getImportErrorr*   r7   s    r/   _load_web_configr<      s^    111111{}}  +++#   			s   #& <<>   exaxaiddgstavilysearxngparallel	firecrawl
brave-freebackendc                     | sdS 	 ddl m}  ||           S # t          $ r'}t                              d| |           Y d}~dS d}~ww xY w)al  Return a plugin-registered web provider by name, or ``None``.

    Consults ``agent.web_search_registry`` so backends contributed by the
    plugin system (which are absent from :data:`_LEGACY_WEB_BACKENDS`) are
    discoverable during availability/selection resolution. Returns ``None``
    on any lookup failure so callers can fall through to legacy checks.
    Nr   )get_providerz.web provider registry lookup failed for %r: %s)agent.web_search_registryrG   r*   loggerdebug)rE   rG   excs      r/   _registered_web_providerrL      s|      t::::::|G$$$   EwPSTTTttttts    
AAAc                     t          |           }|dS 	 t          |                                          S # t          $ r'}t                              d| |           Y d}~dS d}~ww xY w)u+  Availability of a *registered* web provider, or ``None`` if unregistered.

    Returns ``True``/``False`` when *backend* names a registered provider
    (calling its ``is_available()``), or ``None`` when it isn't registered —
    letting the caller fall through to the legacy built-in probes.
    N)web provider %r.is_available() raised: %sF)rL   r3   is_availabler*   rI   rJ   )rE   providerrK   s      r/   "_registered_web_provider_availablerQ      s~     (00HtH))++,,,   @'3OOOuuuuus    6 
A' A""A'c                      	 ddl m}   |             S # t          $ r'}t                              d|           g cY d}~S d}~ww xY w)zCReturn all plugin-registered web providers (empty list on failure).r   )list_providersz%web provider registry list failed: %sN)rH   rS   r*   rI   rJ   )rS   rK   s     r/   _list_registered_web_providersrT      sj    <<<<<<~   <cBBB						s    
A>AAc            
         t                                          d          pd                                                                } | t          v st          |           | S dt          d          fdt          d          fdt          d	          fd
t          d          pt          d          fd
t                      fdt          d          fdt          d          fdt                      ff}|D ]\  }}|r|c S t                      D ]g}|j
        t          v r	 |                                r	|j
        c S 0# t          $ r+}t                              d|j
        |           Y d}~`d}~ww xY wd
S )zDetermine which web backend to use (shared fallback).

    Reads ``web.backend`` from config.yaml (set by ``hermes tools``).
    Falls back to whichever API key is present for users who configured
    keys manually without running setup.
    rE   r(   Nr@   TAVILY_API_KEYr=   EXA_API_KEYrB   PARALLEL_API_KEYrC   FIRECRAWL_API_KEYFIRECRAWL_API_URLrA   SEARXNG_URLrD   BRAVE_SEARCH_API_KEYr?   rN   )r<   r:   lowerr-   _LEGACY_WEB_BACKENDSrL   r4   r   _ddgs_package_importablerT   r$   rO   r*   rI   rJ   )
configuredbackend_candidatesrE   	availablerP   rK   s         r/   _get_backendrc      s    #$$((339r@@BBHHJJJ)))-Ej-Q-Q-] 
8,--.	''(	X0112	h233Tx@S7T7TU	,../	H]++,	x 6778	)++,	 1   	NNN	 344 Z Z=000	Z$$&& %}$$$% 	Z 	Z 	ZLLDhmUXYYYYYYYY	Z ;s   D==
E2!E--E2c                       t          d          S )uj  Determine which backend to use for web_search specifically.

    Selection priority:
    1. ``web.search_backend`` (per-capability override)
    2. ``web.backend`` (shared fallback — existing behavior)
    3. Auto-detect from env vars

    This enables using different providers for search vs extract
    (e.g. SearXNG for search + Firecrawl for extract).
    search_get_capability_backend r5   r/   _get_search_backendri      s     #8,,,r5   c                       t          d          S )u   Determine which backend to use for web_extract specifically.

    Selection priority:
    1. ``web.extract_backend`` (per-capability override)
    2. ``web.backend`` (shared fallback — existing behavior)
    3. Auto-detect from env vars
    extractrf   rh   r5   r/   _get_extract_backendrl     s     #9---r5   
capabilityc                     t                      }|                    |  d          pd                                                                }|rt	          |          r|S t                      S )zShared helper for per-capability backend selection.

    Reads ``web.{capability}_backend`` from config; if set and available,
    uses it. Otherwise falls through to the shared ``_get_backend()``.
    _backendr(   )r<   r:   r]   r-   _is_backend_availablerc   )rm   cfgspecifics      r/   rg   rg     sm     

C:///006B==??EEGGH )(33 >>r5   c                     | pd                                                                 } | t          vrt          |           }||S | dk    rt	          d          S | dk    rt	          d          S | dk    rt                      S | dk    rt	          d	          S | d
k    rt	          d          S | dk    rt	          d          S | dk    rt                      S | dk    r"	 ddlm}  |            S # t          $ r Y dS w xY wdS )u4  Return True when the selected backend is currently usable.

    For plugin-registered backends (any name outside
    :data:`_LEGACY_WEB_BACKENDS`), availability is delegated to the
    provider's ``is_available()`` via the web_search_registry. This is the
    single chokepoint through which ``_get_backend``,
    ``_get_capability_backend``, and ``check_web_api_key`` all resolve
    availability — fixing custom-provider discovery for every caller at once
    (issues #28651, #31873, #32698). Built-in backends keep their cheap
    hardcoded probes below.
    r(   Nr=   rW   rB   rX   rC   r@   rV   rA   r[   rD   r\   r?   r>   r   )has_xai_credentialsF)
r]   r-   r^   rQ   r4   r   r_   tools.xai_httprt   r*   )rE   
registeredrt   s      r/   rp   rp   $  sP    }"##%%++--G***7@@
!%&&&**++++&((((())))&&&,.///&')))%
	::::::&&((( 	 	 	55	5s   C- -
C;:C;c                  2    	 ddl } dS # t          $ r Y dS w xY w)aJ  Return True when the ``ddgs`` Python package can be imported.

    ddgs is the only backend whose availability is driven by a package
    presence rather than an env var / config entry.  Wrapped in a helper
    so auto-detect and ``_is_backend_available`` share the same check
    (and tests can monkeypatch a single symbol).
    r   NTF)r?   r;   )r?   s    r/   r_   r_   P  s7    t   uus    
c                  
    g dS )u  Return tool metadata env vars for the currently enabled web backends.

    The gateway env vars are always reported — they're metadata strings
    used by the tool registry to light up the tool when the variable is
    set.  Gating them on ``managed_nous_tools_enabled()`` only saved
    string noise in the metadata list, but cost a synchronous HTTP
    refresh against the Nous portal on every CLI startup (invoked at
    tool-registration time).  The behavioral contract is: if the env var
    is set, the tool sees it; if not, it doesn't.  Not-logged-in users
    simply don't have the vars set, so the extra entries are harmless.
    )	rW   rX   rV   rY   rZ   FIRECRAWL_GATEWAY_URLTOOL_GATEWAY_DOMAINTOOL_GATEWAY_SCHEMETOOL_GATEWAY_USER_TOKENrh   rh   r5   r/   _web_requires_envr}   h  s    
 
 
 
r5   i:  i 	web_toolsWEB_TOOLS_DEBUG)env_varc                      	 t                                          d          } | -t          |           }t          dt	          |d                    S n# t
          t          f$ r Y nw xY wt          S )zFResolve the per-page char budget from config, clamped to a sane range.extract_char_limitN    )r<   r:   intmaxmin	TypeError
ValueErrorDEFAULT_EXTRACT_CHAR_LIMIT)r`   values     r/   _get_extract_char_limitr     s    %''++,@AA
!
OOE tS00111	 "
 z"   %%s   AA A'&A'textc                     dddt           fd}t          j        d          }|                    ||           }t          j        dd|          }t          j        dd|          }|S )	a  Replace inline base64 image blobs with labeled markdown links.

    base64 image payloads are token bombs (a single inline PNG can be tens of
    thousands of characters), so we never send the raw bytes to the model. But
    we preserve the fact that an image was there, and its alt text, as an
    inspectable placeholder. Real (http/https) markdown image links are left
    untouched so the agent can ``web_extract`` / ``vision_analyze`` them.

    Transformations:
      ``![alt](data:image/png;base64,AAAA...)``  -> ``[IMAGE: alt](base64 image omitted)``
      ``(data:image/png;base64,AAAA...)``        -> ``[IMAGE]``
      bare ``data:image/...;base64,AAAA...``     -> ``[IMAGE]``
    mzre.Match[str]r%   c                 h    |                      d          pd                                }|rd| dndS )Naltr(   z[IMAGE: ][IMAGE])groupr-   )r   r   s     r/   _md_replz0convert_base64_images_to_links.<locals>._md_repl  s>    wwu~~#**,,$'6 #    Y6r5   zD!\[(?P<alt>[^\]]*)\]\(\s*data:image/[^;]+;base64,[A-Za-z0-9+/=\s]+\)z0\(\s*data:image/[^;]+;base64,[A-Za-z0-9+/=\s]+\)r   z'data:image/[^;]+;base64,[A-Za-z0-9+/=]+)strrecompilesub)r   r   md_b64outs       r/   convert_base64_images_to_linksr     s    7O 7 7 7 7 7 ZO F **Xt
$
$C &DiQT
U
UC
&;Y
L
LCJr5   urlcontentc                    	 ddl }ddlm} ddlm}  |dd          }|                    dd            ||           j        pd	                    d
d          }t          j	        dd|          dd         
                    d          pd	}|                    |                     d                                                    dd         }|| d| dz  }	t          |          t          k    r-|dt                   dt          ddt          |          ddz   }|	                    |d           t#          |	          S # t$          $ r'}
t&                              d| |
           Y d}
~
dS d}
~
ww xY w)a  Write the full extracted page to cache/web and return its absolute path.

    The file is mounted read-only into remote backends (Docker/Modal/SSH) via
    credential_files._CACHE_DIRS, so the agent's terminal/read_file tools can
    page through the complete text on any backend. Returns None on failure
    (storage is best-effort; truncated content is still returned to the model).
    r   N)urlparse)get_hermes_dirz	cache/web	web_cacheT)parentsexist_okpage:_z[^A-Za-z0-9._-]-<   zutf-8
   z.mdz 

[... stored copy truncated at ,z
 chars of z2; re-extract a more specific URL for the rest ...])encodingz0Failed to store full web_extract text for %s: %s)hashliburllib.parser   hermes_constantsr   mkdirhostnamereplacer   r   r-   sha256encode	hexdigestlenMAX_STORED_TEXT_CHARS
write_textr   r*   rI   rJ   )r   r   r   r   r   	cache_dirhostslugdigestpathrK   s              r/   _store_full_textr     s   ))))))333333"N;<<	t444&0&99#sCCv(#t44SbS9??DDN

7 3 344>>@@"Ed00V0000 w<<///.../Y7LW Y Y'llXY Y YY 
 	'2224yy   GcRRRttttts   EE 
E7E22E7
char_limitc           	      V   t          |           |k    r| dfS t          |dz            }||z
  }| d|         }| | d         }|                    d          }||dz  k    r
|d|         }|                    d          }d|cxk    r	|dz  k     rn n||dz   d         }t          |           }t	          ||           }	t          |          t          |          z   }
dd	d
t          |          ddt          |          dd|ddg}|	rM|                    d          dz   }|                    d|	            |                    d|	 d| d           n|                    d           |                    d           |dz   |z   }|dd                    |          z   z  }|dfS )u  Return (model_text, was_truncated) for one page's clean content.

    Pages at or under ``char_limit`` are returned whole. Larger pages get a
    head+tail window (~75% head / ~25% tail) cut on a markdown line boundary
    where possible, plus an explicit footer telling the model exactly how much
    it is seeing, where the full text is stored, and which read_file call pages
    in the omitted middle. Deterministic — no model involvement.
    Fg      ?N
g      ?r      r(   u=   ──────── [TRUNCATED] ────────zShowing r   z chars (head) + z chars (tail) of z total clean characters.   zFull text saved to: z,To read the omitted middle: read_file path="z	" offset=zS limit=200  (the file is the complete page; raise/lower offset to page through it).zwFull text could not be stored; re-run web_extract on a more specific URL or use browser_navigate for the complete page.uW   ─────────────────────────────u+   

[... middle omitted — see footer ...]

T)r   r   rfindfindr   countappendjoin)r   r   r   head_budgettail_budgetheadtailnltotalstored_pathshownfooter_linesmiddle_start_line
model_texts                 r/   _truncate_with_footerr     s=    7||z!!~j4'((K{*K<K< DK<==!D	D		B	K#CRCy	4BB""""s""""""BFGG}LLE"300KIID		!E 	/	03t99K 	0 	0D		K 	0 	0/	0 	0 	0L  

 !JJt,,q0@;@@AAA7; 7 7'7 7 7	
 	
 	
 	
 	J	
 	
 	
 
###IIDPJ$<0000Jtr5   c                      	 ddl m}   |              dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)u  Idempotently trigger plugin discovery so the web registry is populated.

    Every bundled web provider (brave-free, ddgs, searxng, exa, parallel,
    tavily, firecrawl) registers itself via ``plugins/web/<vendor>/__init__.py``
    during plugin discovery. Tool dispatch can be reached from contexts that
    haven't already triggered discovery — subprocess agent runs, delegate
    children, standalone scripts, certain test paths — and without it the
    registry is empty and ``get_provider('firecrawl')`` returns ``None`` even
    when the user has ``web.extract_backend: firecrawl`` configured and
    ``FIRECRAWL_API_KEY`` set. The symptom is a misleading "No web extract
    provider configured" error (issue #27580).

    Mirrors :func:`tools.browser_tool._ensure_browser_plugins_loaded` exactly:
    the underlying discovery call is idempotent and cheap on subsequent
    invocations.
    r   )_ensure_plugins_discoveredz+Web plugin discovery failed (non-fatal): %sN)hermes_cli.pluginsr   r*   rI   warning)r   rK   s     r/   _ensure_web_plugins_loadedr   ;  s{    "	KAAAAAA""$$$$$ K K K
 	DcJJJJJJJJJKs    
A?A   querylimitc                 \   	 t          |          }n# t          t          f$ r d}Y nw xY wt          t	          |d          d          }| |dddddd}	 ddlm}  |            rt          d	d
          S t                       ddl	m
}m} t                      }|r ||          nd}||                                s
 |            }|d
dd}n8t                              d|j        | |           |                    | |          }t%          |                    di                               dg                     |d<   t)          j        |dd
          }	t%          |	          |d<   t,                              d|           t,                                           |	S # t2          $ r}
dt5          |
           }t                              d|           ||d<   t,                              d|           t,                                           t          |          cY d}
~
S d}
~
ww xY w)aE  
    Search the web for information using available search API backend.

    This function provides a generic interface for web search that can work
    with multiple backends (Parallel or Firecrawl).

    Note: This function returns search result metadata only (URLs, titles, descriptions).
    Use web_extract_tool to get full content from specific URLs.
    
    Args:
        query (str): The search query to look up
        limit (int): Maximum number of results to return (default: 5)
    
    Returns:
        str: JSON string containing search results with the following structure:
             {
                 "success": bool,
                 "data": {
                     "web": [
                         {
                             "title": str,
                             "url": str,
                             "description": str,
                             "position": int
                         },
                         ...
                     ]
                 }
             }
    
    Raises:
        Exception: If search fails or API key is not set
    r   r   d   r   r   Nr   )
parameterserrorresults_countoriginal_response_sizefinal_response_size)is_interruptedInterruptedF)success)get_active_search_providerrG   zDNo web search provider configured. Run `hermes tools` to set one up.r   r   z#Web search via %s: '%s' (limit: %d)datar9   r   r   indentensure_asciir   web_search_toolzError searching web: %sr   )r   r   r   r   r   tools.interruptr   
tool_errorr   rH   r   rG   ri   supports_searchrI   infor$   re   r   r:   jsondumps_debuglog_callsaver*   r   rJ   )r   r   debug_call_datar   r   _wsp_get_providerrE   rP   response_dataresult_jsone	error_msgs               r/   r   r   X  s   DE

z"   E1s##E 
 
 "# 	 	O5%222222> 	<mU;;;; 	#$$$	
 	
 	
 	
 	
 	
 	
 	

 &''18B$$W---d8#;#;#=#= 2133H 8 MM KK5ue   %OOE599M+.}/@/@/L/L/P/PQVXZ/[/[+\+\(jquMMM14[1A1A-.)?;;; % % %4CFF44	T9%%%#, )?;;;)$$$$$$$$%s2    (( F" 6D+F" "
H+,A4H& H+&H+urlsformatc           	      v  K   ddl m} ddlm} g }| D ]}t	          |          }|                    |          sQ|                     ||                    s3|                    |          s|                     ||                    rt          j        ddd          c S t          |          }|rt          j        dd| dd          c S |	                    |           |||d	d
ddddg g d}		 t                              dt          |                     g }
g }|D ]G}t          |           d
{V s|	                    |dddd           2|
	                    |           H|
sg }n1t                      }t                       ddlm}m} |r ||          nd
}||                                s\|5|                                s!t          j        d|j         ddd          S  |            }|t          j        dddd          S t                              d|j        t          |
                     dd
l}|                    |j                  r|                    |
|           d
{V }n"t3          j        |j        |
|           d
{V }|r||z   }d|i}t          |                    dg                     }t                              d|           ||	d<   t          t          j        |                    |	d<   ||nt9                      }	 t;          dt=          t?          |          d                    }n# t@          tB          f$ r
 tD          }Y nw xY w|	d         	                    d           |                    dg           D ]3}|                    d          r|                    dd          }|                    d d          p|                    d!d          }|s^tG          |          }tI          |||          \  }}||d!<   |r|	d"xx         d#z  cc<   |	d$         	                    |t          |          t          |          d%           t                              d&|t          |          t          |                     
t                              d'|t          |                     5d( |                    dg           D             }d|i}|                    d          g k    rtK          d)          }nt          j        |d*d+          }tG          |          }t          |          |	d,<   |	d         	                    d-           tL          '                    d.|	           tL          (                                 |S # tR          $ r}d/tU          |           } t          +                    d0|            | |	d<   tL          '                    d.|	           tL          (                                 tK          |           cY d
}~S d
}~ww xY w)1a   
    Extract content from specific web pages using available extraction API backend.

    Returns clean page content (markdown/text) with NO LLM summarization. The
    extract backends (Firecrawl, Tavily, Exa, Parallel) already return clean,
    boilerplate-stripped content, so we return it directly and fast. Pages over
    ``char_limit`` are head+tail truncated with an explicit footer; the full
    text is stored under cache/web and the footer tells the model how to
    read_file the omitted middle. Inline base64 images are replaced with
    ``[IMAGE: alt]`` placeholders (real image URLs are preserved as links).

    Args:
        urls (List[str]): List of URLs to extract content from
        format (str): Desired output format ("markdown" or "html", optional)
        char_limit (Optional[int]): Per-page char budget sent to the model
            (default: web.extract_char_limit or 15000). Larger pages truncate.

    Security: URLs are checked for embedded secrets before fetching.

    Returns:
        str: JSON string with a ``results`` list; each entry has
             ``url``, ``title``, ``content``, ``error``. ``content`` is the
             (possibly truncated) clean page text.

    Raises:
        Exception: If extraction fails or API key is not set
    r   )
_PREFIX_RE)unquoteFz_Blocked: URL contains what appears to be an API key or token. Secrets must not be sent in URLs.r   z9Blocked: URL contains a credential-like query parameter (z). Web extract backends are third-party readers; remove the sensitive query parameter or use a local browser session when this access is explicitly required.)r   r   r   N)r   r   pages_extractedpages_truncatedr   r   truncation_metricsprocessing_appliedz!Extracting content from %d URL(s)r(   z:Blocked: URL targets a private or internal network addressr   titler   r   )get_active_extract_providerrG   zy is a search-only backend and cannot extract URL content. Set web.extract_backend to firecrawl, tavily, exa, or parallel.)r   zcNo web extract provider configured. Set web.extract_backend to firecrawl, tavily, exa, or parallel.zWeb extract via %s: %d URL(s))r   resultszExtracted content from %d pagesr  r   r   r   r  truncate_and_storer   r   raw_contentr   r  r   r  )r   original_size	sent_sizez%s (truncated %d -> %d chars)z%s (%d chars, whole)c                     g | ]h}|                     d d          |                     dd          |                     dd          |                     d          dd|v r
d|d         ini iS )r   r(   r	  r   r   r  blocked_by_policy)r:   ).0rs     r/   
<listcomp>z$web_extract_tool.<locals>.<listcomp>  s     	
 	
 	
  uuUB''w++55B//w	 
 GZ]^F^F^)1-@+ABBdf	
 	
 	
r5   z%Content was inaccessible or not foundr   r   r   base64_image_conversionweb_extract_toolzError extracting content: r   ),agent.redactr  r   r  r"   re   r   r   r#   r   rI   r   r   r!   rl   r   rH   r
  rG   supports_extractdisplay_namer$   inspectiscoroutinefunctionrk   asyncio	to_threadr:   r   r   r   r   r   r   r   r   r   r   r   r   r   r*   r   rJ   )!r   r   r   r  r  normalized_urls_urlnormalized_urlsensitive_query_keyr   	safe_urlsssrf_blockedr   r  rE   r
  r   rP   r  responser  effective_char_limitresultr  cleanr   	truncatedtrimmed_resultstrimmed_responser   cleaned_resultr   r   s!                                    r/   r  r    s     D ('''''$$$$$$!#O / /2488d##
	  //
	   00
	   !8!899	
	 : =      
 9HH 		: O+O O O       	~.... $$
 

 "#    Oi%7_9M9MNNN 	-/" 	& 	&C*3//////// &##Y% %    
   %%%%  C	GG*,,G '(((       
 6=F((111$Hx'@'@'B'B '0I0I0K0K':',#+#8 !< !< !<  &+    7688#:',!<  &+
 
 
 
 KK/I   NNN**8+;<<  ( 0 06 0 J JJJJJJJ !( 1$i! ! !      
  	-"W,Gw'hll9b99::5GGG-<)*47
88L8L4M4M01-7-CzzI`IbIb	>#&tS5I1J1JG-T-T#U#U  :& 	> 	> 	>#=   	> 	,-445IJJJll9b11 	E 	EFzz'"" **UB''C **]B77T6::iQS;T;TK 2;??E$9%FZ$[$[!J	 *F9 	E 1222a7222 45<<%(ZZ!$Z> >   
 ;S#e**cR\oo^^^^2CUDDDD	
 	
 \\)R00	
 	
 	
 &7	**b00$%LMMKK*%5aeTTTK
 8DD14^1D1D-.,-445NOOO 	*O<<< % % %9Q99	T9%%%#, *O<<<)$$$$$$$$%sR   7C;V/ 3$V/ DV/ )+M V/ M0-V/ /M00H>V/ /
X89A4X3-X83X8c                     t                                          dd                                                                          } | rt	          |           rdS t          d t          D                       rdS 	 ddlm}m	}  |            dup |            duS # t          $ r&}t                              d|           Y d}~d	S d}~ww xY w)
u  Check whether the configured web backend is available.

    Used as the ``check_fn`` gate for the ``web_search`` and ``web_extract``
    tool registry entries — so a plugin-registered provider that reports
    ``is_available()`` must light the tools up even when no built-in backend
    has credentials (issues #28651, #31873). Resolution funnels through
    :func:`_is_backend_available`, which delegates non-legacy names to the
    registry.
    rE   r(   Tc              3   4   K   | ]}t          |          V  d S r2   )rp   )r  rE   s     r/   	<genexpr>z$check_web_api_key.<locals>.<genexpr>  s+      
N
Ng ))
N
N
N
N
N
Nr5   r   )r   r
  Nz3web provider registry availability check failed: %sF)r<   r:   r]   r-   rp   anyr^   rH   r   r
  r*   rI   rJ   )r`   r   r
  rK   s       r/   check_web_api_keyr0    s    "##''	266<<>>DDFFJ +J77 t 
N
N9M
N
N
NNN t
	
 	
 	
 	
 	
 	
 	
 	
 '&((4 9**,,D8	
    JCPPPuuuuus   ;B 
C%CC__main__u    🌐 Standalone Web Tools Modulez(========================================rY   r(   rZ   u   ✅ Web backend: r=   z!   Using Exa API (https://exa.ai)rB   z+   Using Parallel API (https://parallel.ai)r@   z(   Using Tavily API (https://tavily.com)rA   z    Using SearXNG (search only): r[   rD   z-   Using Brave Search free tier (search only)r?   z2   Using DuckDuckGo via ddgs package (search only)z    Using self-hosted Firecrawl: /z#   Using direct Firecrawl cloud APIz!   Using Firecrawl tool-gateway: z0   Firecrawl backend selected but not configuredu$   ❌ No web search backend configuredzWSet EXA_API_KEY, PARALLEL_API_KEY, TAVILY_API_KEY, FIRECRAWL_API_KEY, FIRECRAWL_API_URLr   u!   🛠️  Web tools ready for use!z   Extract char limit: zE chars (pages over this are truncated; full text stored in cache/web)u&   🐛 Debug mode ENABLED - Session ID: z    Debug logs will be saved to: z/web_tools_debug_z.jsonu=   🐛 Debug mode disabled (set WEB_TOOLS_DEBUG=true to enable)z
Basic usage:z9  from web_tools import web_search_tool, web_extract_toolz  import asyncioz  # Search (synchronous)z/  results = web_search_tool('Python tutorials')u9     # Extract (asynchronous, no LLM — truncate-and-store)z  async def main():z?      content = await web_extract_tool(['https://example.com'])z#      # bigger budget for one call:zU      content = await web_extract_tool(['https://docs.python.org'], char_limit=40000)z  asyncio.run(main())z
Debug mode:z  export WEB_TOOLS_DEBUG=truez3  # Logs saved to: ./logs/web_tools_debug_UUID.json)registryr   
web_searcha  Search the web for information. Returns up to 5 results by default with titles, URLs, and descriptions. The query is passed through to the configured backend, so operators such as site:domain, filetype:pdf, intitle:word, -term, and "exact phrase" may work when the backend supports them.objectstringzThe search query to look up on the web. You may include backend-supported operators such as site:example.com, filetype:pdf, intitle:word, -term, or "exact phrase".)typedescriptionintegerz3Maximum number of results to return. Defaults to 5.r   )r7  r8  minimummaximumdefaultr   )r7  
propertiesrequired)r$   r8  r   web_extractu+  Extract content from web page URLs. Returns clean page content in markdown/text (no LLM summarization — fast). Also works with PDF URLs (arxiv papers, documents) — pass the PDF link directly. Pages within the char budget (default 15000) return whole; larger pages return a head+tail window with a footer telling you the full text's saved file path and the read_file call to page through the omitted middle. Inline images appear as [IMAGE: alt] placeholders; real image URLs are kept as links. If a URL fails or times out, use the browser tool instead.arrayr7  z:List of URLs to extract content from (max 5 URLs per call))r7  itemsr8  maxItemszOptional per-page character budget sent back (default 15000). Pages larger than this are head+tail truncated with the full text stored to disk. Raise it when you need more of a long page inline.r   )r7  r8  r:  )r   r   r9   c                 t    t          |                     dd          |                     dd                    S )Nr   r(   r   r   )r   )r   r:   argskws     r/   <lambda>rG  c  s1    txx/D/DDHHU\^_L`L`aaa r5   u   🔍i )r$   toolsetschemahandlercheck_fnrequires_envemojimax_result_size_charsc                     t          t          |                     d          t                    r|                     dg           d d         ng d|                     d                    S )Nr   r   markdownr   )r   )r  
isinstancer:   listrD  s     r/   rG  rG  m  sb    /$.txx/?/?$F$FNRaR  B88L))      r5   Tu   📄)	r$   rH  rI  rJ  rK  rL  is_asyncrM  rN  )r%   N)r   )NN)l__doc__r   loggingr+   r   r  typingr   r   r   r   r   httpxrC   r   plugins.web.firecrawl.providerr	   r
   r   r   r   plugins.web.tavily.providerr   r   r   plugins.web.parallel.providerr   r   plugins.web.exa.providerr   r   __annotations__r   r   r   r   tools.debug_helpersr   tools.managed_tool_gatewayr   r   _peek_nous_access_tokenr   _read_nous_access_tokenr   tools.tool_backend_helpersr   r   r    tools.url_safetyr!   r"   r#   sys	getLogger__name__rI   r   r0   r3   r4   dictr<   	frozensetr^   rL   rQ   rT   rc   ri   rl   rg   rp   r_   rR  r}   r   r   r   r   r   r   r   tupler   r   r   r  r0  printweb_availabletool_gateway_availabler,   r-   firecrawl_key_availablefirecrawl_url_availablerE   rstripexitactive
session_idlog_dirtools.registryr3  r   WEB_SEARCH_SCHEMAWEB_EXTRACT_SCHEMAregisterrh   r5   r/   <module>rw     s  # # #J   				 				  ; ; ; ; ; ; ; ; ; ; ; ; ; ;   $######                                5 4 4 4 4 4
 $( 8C= ' ' '*. (3- . . ."& (3- & & &(,  , , ,!Xc] ! ! ! , , , , , ,                    
 f e e e e e e e e e 



		8	$	$
S S    ("3 "4 " " " "$    * !yVVV  
c    &    "  /c / / / /d-S - - - -.c . . . .
 
 
 
 
 
)3 )4 ) ) ) )X$    049    V #  " 	k+<	=	=	=& & & & &     <!# ! ! ! ! ! !H==	= = 39	= = = =TK K K K:h% h%3 h%s h%3 h% h% h% h%Z  $w% w%
s)w%w% w% 		w% w% w% w%v!4 ! ! ! !H z 
E
,---	E(OOO &%''M3355"d929-@"#E#E#K#K#M#MNN"d929-@"#E#E#K#K#M#MNN 
,..+'++,,,eE56666
""E?@@@@  E<====	!!EPZZ5N5NPPQQQQ$$EABBBBEFGGGG$ 	FEiYRY?R5S5S5Y5Y5[5[5b5bcf5g5giijjjj$ 	FE78888# 	FET6P6P6R6RTTUUUUEDEEEE45552--//2 2	
 	
 	

  	E
-...	E K$;$;$=$= K K K L L L } OJv7HJJKKKjjjRXRcjjjkkkkMNNN	E
	E
EFFF	E
	E"III	E
$%%%	E
;<<<	E"III	E
EFFF	E
   	E
KLLL	E
/000	E
abbb	E
!"""	E/	E
)***	E
?@@@ 0 / / / / / / /  w !  G 
 "T 
 
 I   .  A	   (+[	  "  d 
 
 H   ,  	aa""$$
!	 	 	 	  	 
 ""$$
!     r5   