
    ,j-                    n   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%m&Z&m'Z' ddl(m)Z) ddl*m+Z+m,Z-m.Z/m0Z0 ddl1m2Z2m3Z3m4Z4 ddl5m6Z6m7Z7 ddl8Z8 ej9        e:          Z;de<de<fdZ=de<de>fdZ?de@fdZA eBh d          ZC eBh d          ZDdde<de<fdZEde<fdZFde<fdZGde<de<fdZHd e<de>fd!ZIde>fd"ZJdeKe<         fd#ZLd$ZMd%e	de>fd&ZNdd'e
e<         deOe
e	         e
e<         ee<e	f         f         fd(ZPde
e<         fd)ZQ e)d*d+,          ZRd-d-deMfd.e<d/e<d0e<d'e
e<         d1eSde
e<         fd2ZT	 	 	 dd.e<d5e<d'e
e<         d6eSd7e>d8e<de
e<         fd9ZUd.e<d5e<d'e
e<         d:eSd;eSde
e<         fd<ZVd=e<de<fd>ZWdd?ZXdd@ZYddBe<dCeSde<fdDZZddEdeMfdFee<         dGe<dHe>d'e
e<         d1eSde<fdIZ[de>fdJZ\de>fdKZ]d e<de>fdLZ^de>fdMZ_de>fdNZ`e:dOk    r	  eadP            eadQ            e_            Zb e            Zc e> ejd        dRd-          e                                          Zf e> ejd        dSd-          e                                          Zg e`            Zh eQ            Ziebr eE            Zj eadTej            ejdUk    r eadV           nejdWk    r eadX           nejdYk    r eadZ           nejd[k    r ead\ e=d]                      nejd^k    r ead_           nejd`k    r eada           negrB eadb ejd        dS          e                                k                    dc                      nTefr eadd           nFecr eade e                        n- eadf           n! eadg            eadh e                        ehs" eadi            eadj            eadk           n eadlei            ebs e8jl        dm            eadn           ehr eadoei             eadpeM dq           eRjm        r0 eadreRjn                     eadseRjo         dteRjn         du           n eadv            eadw            eadx            eady            ead-            eadz            ead{            ead-            ead|            ead}            ead~            ead           ehr ead            ead            ead            ead-            ead            ead            ead            ead            ead            ead            ead-            ead            ead            ead            ead            ead            ead            ead            ead            ead            ead            ead            ead           ddlpmqZqmrZr dddddddddmddAdddBgddZsddddFdddiddAdidFgddZt eqju        ddesd e\ eL            dd            eqju        ddetd e\ eL            dEdd	  	         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)async_call_llmextract_content_or_reasoningget_async_text_auxiliary_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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      4/home/ubuntu/.hermes/hermes-agent/tools/web_tools.py
_env_valuer2   q   sz    333333mD!!   
{ib!!I2s    ##c                 :    t          t          |                     S N)boolr2   )r&   s    r1   _has_envr6      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+   r:   getImportErrorr,   r9   s    r1   _load_web_configr>      s^    111111{}}  +++#   			s   #& <<>   exaxaiddgstavilysearxngparallel	firecrawl
brave-free>   r@   rA   rC   rF   search
capabilityc                    t                                          d          pd                                                                }|t          v r|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dt                      ff	}|D ]\  }}|s| dk    r
|t          v r|c S dS )at  Determine 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.

    ``capability`` ("search" | "extract") only affects auto-detect: for
    ``extract`` we skip search-only backends (``_SEARCH_ONLY_BACKENDS``) so a
    search-only credential never shadows the keyless Parallel free-MCP extract
    fallback. An explicit ``web.backend`` value is honored as-is (explicit wins,
    surfacing that backend's own search-only error rather than rerouting).
    backendr*   rB   TAVILY_API_KEYr?   EXA_API_KEYrD   PARALLEL_API_KEYrE   FIRECRAWL_API_KEYFIRECRAWL_API_URLrC   SEARXNG_URLrF   BRAVE_SEARCH_API_KEY)rD   TrA   extract)	r>   r<   lowerr/   _KNOWN_WEB_BACKENDSr6   r   _ddgs_package_importable_SEARCH_ONLY_BACKENDS)rH   
configuredbackend_candidatesrJ   	availables        r1   _get_backendrZ      s7    #$$((339r@@BBHHJJJ((( 
8,--.	''(	X0112	h233Tx@S7T7TU	,../	H]++,	x 6778 		)++, 1   	 ""w2G'G'G :r7   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).
    rG   _get_capability_backend r7   r1   _get_search_backendr_      s     #8,,,r7   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
    rR   r\   r^   r7   r1   _get_extract_backendra      s     #9---r7   c                     t                      }|                    |  d          pd                                                                }|r|S t	          |           S )uP  Shared helper for per-capability backend selection.

    Reads ``web.{capability}_backend`` from config. Any explicit value is
    honored **regardless of availability** — including unrecognized typos like
    ``parrallel`` — so the dispatcher surfaces that backend's own setup/config
    error rather than silently rerouting to the keyless Parallel default (which
    would send user queries to a different provider and hide the
    misconfiguration). This matches ``web_search_registry``'s "explicit config
    wins" rule. Only an *unset* value falls through to ``_get_backend()``.
    _backendr*   )r>   r<   rS   r/   rZ   )rH   cfgspecifics      r1   r]   r]      sa     

C:///006B==??EEGGH 
###r7   rJ   c                 x   | 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 )z:Return True when the selected backend is currently usable.r?   rL   rD   rM   rE   rB   rK   rC   rP   rF   rQ   rA   r@   r   )has_xai_credentialsF)r6   r   rU   tools.xai_httprg   r,   )rJ   rg   s     r1   _is_backend_availableri      s
   %&&&* *++++&((((())))&&&,.///&')))%
	::::::&&((( 	 	 	55	5s   B) )
B76B7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)rA   r=   )rA   s    r1   rU   rU     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.
    )	rL   rM   rK   rN   rO   FIRECRAWL_GATEWAY_URLTOOL_GATEWAY_DOMAINTOOL_GATEWAY_SCHEMETOOL_GATEWAY_USER_TOKENr^   r^   r7   r1   _web_requires_envrp   5  s    
 
 
 
r7     clientc                     ddl m} t          t          | dd          pd          } ||          j        pd                                }|dk    p|                    d          S )z?Return True when the resolved auxiliary backend is Nous Portal.r   )urlparsebase_urlr*   znousresearch.comz.nousresearch.com)urllib.parsert   strgetattrhostnamerS   endswith)rr   rt   ru   hosts       r1   _is_nous_auxiliary_clientr|   ]  ss    %%%%%%76:r228b99HHX'-24466D%%K7J)K)KKr7   modelc                     t          d          \  }}t          j        dd                                          }| p|p|}i }|1t	          |          r"ddlm} ddlm}  |            pd |            i}|||fS )	zHResolve the current web-extract auxiliary client, model, and extra body.web_extractAUXILIARY_WEB_EXTRACT_MODELr*   Nr   )get_auxiliary_extra_body)nous_portal_tagstags)	r   r-   r.   r/   r|   agent.auxiliary_clientr   agent.portal_tagsr   )r}   rr   default_modelconfigured_modeleffective_model
extra_bodyr   r   s           r1   _resolve_web_extract_auxiliaryr   f  s    ;MJJFMy!>CCIIKK@/@=O!#J7??CCCCCC666666--//OF<L<L<N<N3O
?J..r7   c                  *    t                      \  } }} |S )zBReturn the current default model for web extraction summarization.r   )_r}   s     r1   _get_default_summarizer_modelr   u  s    022KAuaLr7   	web_toolsWEB_TOOLS_DEBUG)env_varr*   contenturltitle
min_lengthc                   K   d}d}d}d}	 t          |           }	|	|k    r'|	dz  }
t                              d|
           d|
dd	S |	|k     rt                              d
|	|           dS g }|r|                    d|            |r|                    d|            |rd                    |          dz   nd}|	|k    r4t                              d|	           t          | ||||           d{V S t                              d|	           t          | ||           d{V }|r\t          |          |k    r|d|         dz   }t          |          }|	dk    r||	z  nd}t                              d|	||dz             |S # t          $ rt}t                              dt          |          dd                    | d|         }t          |           |k    r|d|ddt          |           ddz  }|cY d}~S d}~ww xY w)a  
    Process web content using LLM to create intelligent summaries with key excerpts.
    
    This function uses Gemini 3 Flash Preview (or specified model) via OpenRouter API 
    to intelligently extract key information and create markdown summaries,
    significantly reducing token usage while preserving all important information.
    
    For very large content (>500k chars), uses chunked processing with synthesis.
    For extremely large content (>2M chars), refuses to process entirely.
    
    Args:
        content (str): The raw content to process
        url (str): The source URL (for context, optional)
        title (str): The page title (for context, optional)
        model (str): The model to use for processing (default: google/gemini-3-flash-preview)
        min_length (int): Minimum content length to trigger processing (default: 5000)
        
    Returns:
        Optional[str]: Processed markdown content, or None if content too short or processing fails
    i i  順 rq   i@B z<Content too large (%.1fMB > 2MB limit). Refusing to process.z[Content too large to process: z.1fz#MB. Try a more focused source URL.]z:Content too short (%d < %d chars), skipping LLM processingNzTitle: zSource: 


r*   z5Content large (%d chars). Using chunked processing...z+Processing content with LLM (%d characters)4

[... summary truncated for context management ...]r         ?z*Content processed: %d -> %d chars (%.1f%%)d   zweb_extract LLM summarization failed (%s). Tip: increase auxiliary.web_extract.timeout in config.yaml or switch to a faster auxiliary model.x   u'   

[Content truncated — showing first , of z chars. LLM summarization timed out. To fix: increase auxiliary.web_extract.timeout in config.yaml, or use a faster auxiliary model. Use browser_navigate for the full page.])lenloggerwarningdebugappendjoininfo_process_large_content_chunked_call_summarizer_llmr,   rw   )r   r   r   r}   r   MAX_CONTENT_SIZECHUNK_THRESHOLD
CHUNK_SIZEMAX_OUTPUT_SIZEcontent_lensize_mbcontext_infocontext_strprocessed_contentprocessed_lengthcompression_ratioe	truncateds                     r1   process_content_with_llmr   }  s     8 !OJO@'ll )))!I-GNNY[bccceWeeeee ##LLUWbdnooo4  	3 1% 1 1222 	2 03 0 0111:FNdii--66B ((KKOQ\]]]7eZ        
 	A;OOO"6wU"S"SSSSSSS 	~$%%77$56F6F$G  KC  %C!  ##455BMPQ// 0; > >WZKKDkScevy|e|}}}     5 FF4C4L		
 	
 	
 ,_,-	w<</))]OS ] ]w<<H] ] ]I %s2   ;F
 "F
 ,BF
 8BF
 

HA)H=HH N  Fr   
max_tokensis_chunk
chunk_infoc           
        K   |rd}d| | d|  d}nd}d| d|  d}d	}d	}	d
}
t          |          D ]}	 t          |          \  }}}||st                              d            d
S d|d|dd|dgd|d}|r||d<   t	          di | d
{V }t          |          } | r| c S t                              d|dz   |           ||dz
  k     r.t          j        |	           d
{V  t          |	d	z  d          }	| c S # t          $ r t                              d           Y  d
S t          $ r}|}
||dz
  k     r~t                              d|dz   |t          |          d
d                    t                              d|	           t          j        |	           d
{V  t          |	d	z  d          }	n|
Y d
}~d
}~ww xY wd
S )a  
    Make a single LLM call to summarize content.
    
    Args:
        content: The content to summarize
        context_str: Context information (title, URL)
        model: Model to use
        max_tokens: Maximum output tokens
        is_chunk: Whether this is a chunk of a larger document
        chunk_info: Information about chunk position (e.g., "Chunk 2/5")
        
    Returns:
        Summarized content or None on failure
    a  You are an expert content analyst processing a SECTION of a larger document. Your job is to extract and summarize the key information from THIS SECTION ONLY.

Important guidelines for chunk processing:
1. Do NOT write introductions or conclusions - this is a partial document
2. Focus on extracting ALL key facts, figures, data points, and insights from this section
3. Preserve important quotes, code snippets, and specific details verbatim
4. Use bullet points and structured formatting for easy synthesis later
5. Note any references to other sections (e.g., "as mentioned earlier", "see below") without trying to resolve them

Your output will be combined with summaries of other sections, so focus on thorough extraction rather than narrative flow.zAExtract key information from this SECTION of a larger document:

z

SECTION CONTENT:
z

Extract all important information from this section in a structured format. Focus on facts, data, insights, and key details. Do not add introductions or conclusions.a~  You are an expert content analyst. Your job is to process web content and create a comprehensive yet concise summary that preserves all important information while dramatically reducing bulk.

Create a well-structured markdown summary that includes:
1. Key excerpts (quotes, code snippets, important facts) in their original format
2. Comprehensive summary of all other important information
3. Proper markdown formatting with headers, bullets, and emphasis

Your goal is to preserve ALL important information while reducing length. Never lose key facts, figures, insights, or actionable information. Make it scannable and well-organized.zNPlease process this web content and create a comprehensive markdown summary:

zCONTENT TO PROCESS:
z

Create a markdown summary that captures all key information in a well-organized, scannable format. Include important quotes and code snippets in their original formatting. Focus on actionable information, specific details, and unique insights.   Nz7No auxiliary model available for web content processingr   systemroler   user皙?taskr}   messagestemperaturer   r   z4LLM returned empty content (attempt %d/%d), retrying   <   z'LLM API call failed (attempt %d/%d): %sr   zRetrying in %ds...r^   )ranger   r   r   r   r   asynciosleepminRuntimeErrorr,   rw   )r   r   r}   r   r   r   system_promptuser_promptmax_retriesretry_delay
last_errorattempt
aux_clientr   r   call_kwargsresponse	api_errors                     r1   r   r     s     ,  &w	~iii i
 	i i iwww w 	w w w KKJ%% -! -!,	!6TUZ6[6[3J!!XYYYtt%(%-@@#<<  #( K  7,6L)+::k::::::::H28<<G NNQSZ]^S^`klllq((mK000000000!+/266NNN 	 	 	NNTUUU444 	! 	! 	!"Jq((H'TU+Wbdghqdrdrswtwswdxyyy3[AAAmK000000000!+/266   	! 4s1   1D+;D)AD?D$G
+	G
4BGG

chunk_sizemax_output_sizec           	        K   g t          dt          |           |          D ]$}| |||z            }                    |           %t                              dt                    |           dt
          dt          dt          t
          t          t                   f         ffdfdt                    D             }t          j        |dd	i d
{V }g }	|D ]H}
t          |
t                    rt                              d|
           3|	                    |
           Ig }t          |	d           D ]%\  }}|r|                    d|dz    d|            &|st                              d           dS t                              dt          |          t                               t          |          dk    r*|d         }t          |          |k    r|d
|         dz   }|S t                              dt          |                     d                    |          }d| d d| d}	 t%                    \  }}}||sQt                              d           d                    |          }t          |          |k    r|d
|         dz   }|S d|ddd d!|d gd"d#d$}|r||d%<   t'          d.i | d
{V }t)          |          }|s;t                              d&           t'          d.i | d
{V }t)          |          }|sQt                              d'           d                    |          }t          |          |k    r|d
|         dz   }|S t          |          |k    r|d
|         d(z   }t          |           }t          |          }|dk    r||z  nd)}t                              d*|||d+z             |S # t*          $ rq}t                              d,t          |          d
d+                    d                    |          }t          |          |k    r|d
|         d-z   }|cY d
}~S d
}~ww xY w)/a  
    Process large content by chunking, summarizing each chunk in parallel,
    then synthesizing the summaries.
    
    Args:
        content: The large content to process
        context_str: Context information
        model: Model to use
        chunk_size: Size of each chunk in characters
        max_output_size: Maximum final output size
        
    Returns:
        Synthesized summary or None on failure
    r   z&Split into %d chunks of ~%d chars each	chunk_idxchunk_contentr'   c           	        K   	 d| dz    dt                     d}t          |dd|           d{V }|rHt                              d	| dz   t                    t          |          t          |                     | |fS # t          $ rP}t                              d
| dz   t                    t          |          dd                    | dfcY d}~S d}~ww xY w)zSummarize a single chunk.z[Processing chunk r   r   ]i'  T)r   r   r   Nz&Chunk %d/%d summarized: %d -> %d charszChunk %d/%d failed: %s2   )r   r   r   r   r,   r   rw   )r   r   r   summaryr   chunksr   r}   s        r1   summarize_chunkz7_process_large_content_chunked.<locals>.summarize_chunku  sL     	#Oi!mOOVOOOJ0 %        G  DDiRSmUXY_U`U`befsbtbtvy  {B  wC  wC  D  D  Dg%% 	# 	# 	#NN3Y]CKKQTUVQWQWX[Y[X[Q\]]]d?""""""	#s   BB 
C!ACC!C!c                 .    g | ]\  }} ||          S r^   r^   ).0ichunkr   s      r1   
<listcomp>z2_process_large_content_chunked.<locals>.<listcomp>  s)    III81e__Q&&IIIr7   return_exceptionsTNz#Chunk summarization task failed: %sc                     | d         S )Nr   r^   )xs    r1   <lambda>z0_process_large_content_chunked.<locals>.<lambda>  s
    qt r7   )keyz## Section r   r   zAll chunk summarizations failedzB[Failed to process large content: all chunk summarizations failed]zGot %d/%d chunk summariesz

[... truncated ...]zSynthesizing %d summaries...z

---

a'  You have been given summaries of different sections of a large document. 
Synthesize these into ONE cohesive, comprehensive summary that:
1. Removes redundancy between sections
2. Preserves all key facts, figures, and actionable information
3. Is well-organized with clear structure
4. Is under z characters

zSECTION SUMMARIES:
z,

Create a single, unified markdown summary.z9No auxiliary model for synthesis, concatenating summariesr   r   r   zdYou synthesize multiple summaries into one cohesive, comprehensive summary. Be thorough but concise.r   r   r   r   r   r   z3Synthesis LLM returned empty content, retrying onceu>   Synthesis failed after retry — concatenating chunk summariesr   r   z+Synthesis complete: %d -> %d chars (%.2f%%)r   zSynthesis failed: %sz.

[... truncated due to synthesis failure ...]r^   )r   r   r   r   r   intrw   tupler   	enumerater   gather
isinstanceBaseExceptionr   sortedr   r   r   r   r   r,   )r   r   r}   r   r   r   r   tasksresultssuccessful_resultsresult_item	summariesr   r   resultcombined_summariessynthesis_promptr   r   r   fallbackr   r   final_summaryoriginal_len	final_lencompressionr   r   r   s    ``                         @@r1   r   r   W  sr     , F1c'llJ//  !j.()e
KK8#f++zRRR# #S #U3PXY\P]K]E^ # # # # # # # #( JIIIy7H7HIIIE NEBTBBBBBBBBG  / /k=11 	NN@+NNN!!+....I$%7^^LLL G G	7 	GE9q=EEGEEFFF T6777SS
KK+S^^S[[III 9~~1v;;((,_,-0IIF KK.I???&++I66
.
 
. 
. 
. 
. 
. 
. 
.72PQV2W2W/
OZ_NNVWWW{{9--H8}}..#$4_$458QQO "$!  /U  V  V,<== 	
 	
  	3(2K%'66+666666664X>>  	CNNPQQQ+::k::::::::H8BBM  	NN[\\\{{9--H8}}..#$4_$458QQO }//)*:?*:;>vvM7||&&	2>2B2Bi,..A<QZ\gjm\mnnn   -s1vvdsd|<<<;;y))x==?** 0 014ffHs.   A'O  +CO  3A,O   
Q*A&QQQtextc                 f    d}d}t          j        |d|           }t          j        |d|          }|S )a  
    Remove base64 encoded images from text to reduce token count and clutter.
    
    This function finds and removes base64 encoded images in various formats:
    - (data:image/png;base64,...)
    - (data:image/jpeg;base64,...)
    - (data:image/svg+xml;base64,...)
    - data:image/[type];base64,... (without parentheses)
    
    Args:
        text: The text content to clean
        
    Returns:
        Cleaned text with base64 images replaced with placeholders
    z+\(data:image/[^;]+;base64,[A-Za-z0-9+/=]+\)z'data:image/[^;]+;base64,[A-Za-z0-9+/=]+z[BASE64_IMAGE_REMOVED])resub)r   base64_with_parens_patternbase64_patterncleaned_texts       r1   clean_base64_imagesr    sG    $ "P @N 646NPTUUL 6.*BLQQLr7   c                  8   	 ddl m}   |              n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w	 ddlm}  |d          t                       dS dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)uG  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.

    Triggering discovery is necessary but not *sufficient*: the sweep can
    finish without registering the bundled web providers (its exception
    swallowed below as a warning, a packaged layout where discovery ran before
    the bundled tree was importable, or a stale empty-discovery cache). When
    that happens the registry is empty and *both* web_search and web_extract
    dead-end on "No web {search,extract} provider configured" — even though the
    keyless Parallel default is supposed to work with zero setup. So after
    discovery we verify the keyless default landed and, if not, register the
    bundled providers directly (see
    :func:`_register_bundled_web_providers_directly`).
    r   )_ensure_plugins_discoveredz+Web plugin discovery failed (non-fatal): %sNget_providerrD   z.Bundled web provider fallback check failed: %s)	hermes_cli.pluginsr	  r,   r   r   agent.web_search_registryr  (_register_bundled_web_providers_directlyr   )r	  excr  s      r1   _ensure_web_plugins_loadedr    s   8	KAAAAAA""$$$$ K K K
 	DcJJJJJJJJKL::::::<
##+466666 ,+ L L LEsKKKKKKKKKLs*    
A=AA) )
B3BBc                  ^   	 ddl m} m} n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w |            dz  }|                                sdS  |             }ddlm ddl	m
  G fdd	          } |            }ddl}t          |                                          D ]}|                                s|d
z                                  s|dz                                  sFd|j         |v s d|j                            dd           |v rs	 |                    d|j                   }	t%          |	dd          }
t'          |
          r |
|           # t          $ r+}t                              d|j        |           Y d}~d}~ww xY wdS )uI  Register the repo's bundled web providers without the plugin manager.

    The normal path is the general plugin sweep
    (:func:`hermes_cli.plugins._ensure_plugins_discovered`), which auto-loads
    every ``plugins/web/<name>`` backend (they are ``kind: backend``). This
    fallback exists for the runtimes where that sweep does not leave the web
    registry populated — so the keyless Parallel default (and any bundled
    backend the user explicitly configured) keeps working instead of
    surfacing a misleading "No web provider configured" error.

    Imports each bundled ``plugins/web/<name>`` package and calls its
    ``register()`` directly against :mod:`agent.web_search_registry`. Idempotent
    (re-register overwrites) and honors an explicit ``plugins.disabled`` entry
    so a backend the user turned off stays off.
    r   )_get_disabled_pluginsget_bundled_plugins_dirz-Bundled web provider fallback unavailable: %sNr;   )WebSearchProvider)register_providerc                   "    e Zd ZdZd fdZdS )H_register_bundled_web_providers_directly.<locals>._DirectRegistrationCtxz;Minimal plugin ctx exposing only web-provider registration.r'   Nc                 B    t          |          r |           d S d S r4   )r   )selfproviderr  r  s     r1   register_web_search_providerze_register_bundled_web_providers_directly.<locals>._DirectRegistrationCtx.register_web_search_providerw  s7    ($566 ,!!(+++++, ,r7   r'   N)__name__
__module____qualname____doc__r  )r  r  s   r1   _DirectRegistrationCtxr  t  s>        II	, 	, 	, 	, 	, 	, 	, 	, 	,r7   r!  zplugin.yamlz
plugin.ymlzweb/zweb-r   -zplugins.web.registerz;Direct registration of bundled web provider '%s' failed: %s)r  r  r  r,   r   r   is_diragent.web_search_providerr  r  r  	importlibr   iterdirexistsr&   replaceimport_modulerx   callable)r  r  r  web_dirdisabledr!  ctxr&  childmoduleregister_fnr  r  s              @@r1   r  r  R  s    	
 	
 	
 	
 	
 	
 	
 	
 	
    DcJJJ &%''%/G>> $$&&H;;;;;;;;;;;;, , , , , , , , , , , !
 
"
"C))**  ||~~ 	%--// 	9M8U8U8W8W 	  5:8++4ej((c2244@@		,,-HEJ-H-HIIF!&*d;;K$$ !C    	 	 	LLM
C       	# s)    
=8=,AE55
F*?!F%%F*   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
    r2  r   r   r3  r4  Nr   )
parameterserrorresults_countoriginal_response_sizefinal_response_size)is_interruptedInterruptedF)success)get_active_search_providerr  zDNo web search provider configured. Run `hermes tools` to set one up.r>  r8  z#Web search via %s: '%s' (limit: %d)datar;   r9  r   indentensure_asciir;  web_search_toolzError searching web: %sr8  )r   	TypeError
ValueErrorr   maxtools.interruptr<  
tool_errorr  r  r?  r  r_   supports_searchr   r   r&   rG   r   r<   jsondumps_debuglog_callsaver,   rw   r   )r3  r4  debug_call_datar<  r?  _wsp_get_providerrJ   r  response_dataresult_jsonr   	error_msgs               r1   rE  rE    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+Turlsformatuse_llm_processingc           	      v  &'K   ddl m} ddlm} g }| D ]}t	          |          }	|                    |          sQ|                     ||                    s3|                    |	          s|                     ||	                    rt          j        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|                    |           Hd}|sg }nGt                      }|dk    ot          d           }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<   |pt9                      &t;                      }|rq|rnt          
                    d           |
d                             d           &fd'|                    dg           }'fd|D             }t3          j        |dd i d{V }|D ]}t?          |t@                    rt          !                    d!|           3|\  }}}|                    d"d#          }|d$k    rG|
d%                             |           |
d&xx         d'z  cc<   t          
                    d(|           |d)k    r7|
d%                             |           t          
                    d*|           t          !                    d+|           n|r7|s5t          !                    d,           |
d                             d-           |                    dg           D ]W}|                    d"d#          }t          |                    d.d                    }t          
                    d/||           Xd0 |                    dg           D             } d| i}!|r
d|!d1<   d2|!d3<   |!                    d          g k    rtE          d4          }"tG          |"          }#n&t          j        |!d5d6          }"tG          |"          }#t          |#          |
d7<   |
d                             d8           tH          %                    d9|
           tH          &                                 |#S # tN          $ r}$d:tQ          |$           }%t          )                    d;|%           |%|
d<<   tH          %                    d9|
           tH          &                                 tE          |%          cY d}$~$S d}$~$ww xY w)=a#  
    Extract content from specific web pages using available extraction API backend.

    This function provides a generic interface for web content extraction that
    can work with multiple backends. Currently uses Firecrawl.

    Args:
        urls (List[str]): List of URLs to extract content from
        format (str): Desired output format ("markdown" or "html", optional)
        use_llm_processing (bool): Whether to process content with LLM for summarization (default: True)
        model (Optional[str]): The model to use for LLM processing (defaults to current auxiliary backend model)
        min_length (int): Minimum content length to trigger LLM processing (default: 5000)

    Security: URLs are checked for embedded secrets before fetching.
    
    Returns:
        str: JSON string containing extracted content. If LLM processing is enabled and successful,
             the 'content' field will contain the processed markdown summary instead of raw content.
    
    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@  )rW  rX  rY  r}   r   N)r7  r8  pages_extractedpages_processed_with_llmr:  r;  compression_metricsprocessing_appliedz!Extracting content from %d URL(s)r*   z:Blocked: URL targets a private or internal network addressr   r   r   r8  rD   rM   )get_active_extract_providerr  zy is a search-only backend and cannot extract URL content. Set web.extract_backend to firecrawl, tavily, exa, or parallel.)rD  zcNo web extract provider configured. Set web.extract_backend to firecrawl, tavily, exa, or parallel.zWeb extract via %s: %d URL(s))rX  r   zExtracted content from %d pagesr]  r:  z3Processing extracted content with LLM (parallel)...r`  llm_processingc                   K   |                      dd          }|                      dd          }|                      dd          p|                      dd          }|s| ddfS t          |          }t          |||	
           d{V }|r3t          |          }|d	k    r||z  nd
}|| d<   || d<   ||||	d}| |dfS |||d
ddd}| |dfS )zHProcess a single result with LLM and return updated result with metrics.r   Unknown URLr   r*   raw_contentr   N
no_contentr   r   )r   original_sizeprocessed_sizer   
model_used	processedcontent_too_short)r   rh  ri  r   rj  reason	too_short)r<   r   r   )r   r   r   rf  rh  rk  ri  r   metricsr   r   s            r1   process_single_resultz/web_extract_tool.<locals>.process_single_result  sR     jj66

7B//$jj;;Xvzz)UW?X?X" 6!455 #K 0 0 #;e_j# #      	  8%(^^NJWZ[J[J[(F(Fad% )2F9%,7F=)  #)6*8->&5 G "7K77  #)6*7-0&*"5 G "7K77r7   c                 &    g | ]} |          S r^   r^   )r   r   rp  s     r1   r   z$web_extract_tool.<locals>.<listcomp>  s%    NNNv**622NNNr7   r   Tz%Web result processing task failed: %sr   re  rk  r_  r^  r   z%s (processed)rn  z&%s (no processing - content too short)z%s (no content to process)zPLLM processing requested but no auxiliary model available, returning raw contentllm_processing_unavailablerf  z%s (%d characters)c                     g | ]h}|                     d d          |                     dd          |                     dd          |                     d          dd|v r
d|d         ini iS )r   r*   r   r   r8  ra  blocked_by_policy)r<   )r   rs     r1   r   z$web_extract_tool.<locals>.<listcomp>  s     	
 	
 	
  uuUB''w++55B//w	 
 GZ]^F^F^)1-@+ABBdf	
 	
 	
r7   r  zMExtraction powered by the free Parallel Web Search MCP (https://parallel.ai).attributionz%Content was inaccessible or not foundr   rB  r;  base64_image_removalweb_extract_toolzError extracting content: rF  r8  )*agent.redactr[  rv   r\  r%   rG   rM  rN  r   r   r   r   r$   ra   r6   r  r  rb  r  supports_extractdisplay_namer&   inspectiscoroutinefunctionrR   r   	to_threadr<   r   check_auxiliary_modelr   r   r   r   rK  r  rO  rP  rQ  r,   rw   r   )(rW  rX  rY  r}   r   r[  r\  normalized_urls_urlnormalized_urlrR  	safe_urlsssrf_blockedr   _free_parallel_extractr   rJ   rb  rS  r  r|  r   r]  auxiliary_availableresults_listr   processed_resultsr   r   ro  statuscontent_lengthtrimmed_resultstrimmed_responserU  cleaned_resultr   rV  r   rp  s(       `                                 @@r1   rx  rx    s     > ('''''$$$$$$!#O / /2488d##
	  //
	   00
	   !8!899	
	 : =      
 	~.... $"4$
 
 $%"# !  O"m%7_9M9MNNN 	-/" 	& 	&C*3//////// &##Y% %    
   %%%%
 "'  F	GG*,,G:%Jh7I.J.J*J # '(((       
 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B#@#B#B355  O	G"5 O	GKKMNNN01889IJJJ)8 )8 )8 )8 )8 )8X $<<	266LNNNNNNNE '.ne&Tt&T&T T T T T T T  1 F Fk=99 NN#JKXXX*5'jj66[((#$9:AA'JJJ#$>???1D???KK 0#6666{**#$9:AA'JJJKK H#NNNNNN#?EEEEF  " [*= [qrrr 45<<=YZZZ",,y"55 G Gjj66!$VZZr%B%B!C!C0#~FFFF	
 	
 \\)R00	
 	
 	
 &7! 	 ,6Z() ]+
 	**b00$%LMMK0==NN *%5aeTTTK0==N14^1D1D-.,-445KLLL 	*O<<< % % %9Q99	T9%%%#, *O<<<)$$$$$$$$%s-   DX/ "$X/ P'X/ /
Z89A4Z3-Z83Z8c                      dS )a  Whether the web tools should be registered. Always True.

    Registration is decoupled from credential readiness: with no credentials,
    search/extract fall back to Parallel's free hosted Search MCP, and an
    explicitly configured-but-unavailable backend must stay registered so
    dispatch surfaces that backend's own setup error rather than the tool
    silently vanishing. For "is web actually configured?" use
    :func:`check_web_api_key`.
    Tr^   r^   r7   r1   web_tools_registeredr  3  s	     4r7   c                  h    t                       	 ddlm}   | d          duS # t          $ r Y dS w xY w)zTrue when the bundled ``web-parallel`` provider is registered/enabled.

    Plugin discovery skips disabled plugins, so a disabled (``plugins.disabled``)
    or otherwise-unregistered parallel provider yields ``None`` here.
    r   r
  rD   NF)r  r  r  r,   r
  s    r1   _parallel_provider_registeredr  @  s^        ::::::|J''t33   uus   # 
11c                 f    | dk    rt          d          st                      S t          |           S )zTrue when *backend* can service calls. Keyless Parallel counts (free MCP).

    Unknown/typo'd backend names are not usable (so an explicit typo is reported
    as a config problem rather than masked by the keyless fallback).
    rD   rM   )r6   r  ri   )rJ   s    r1   _backend_usabler  O  s9     *X.@%A%A
 -... )))r7   c                  n    t          t                                ot          t                                S )uG  Usability probe: True when the selected web backends can service calls.

    Probes the backends that :func:`_get_search_backend` /
    :func:`_get_extract_backend` actually select (not just shared
    ``web.backend``), so an explicit per-capability backend with missing
    credentials — or a typo'd name — reports unusable instead of being masked by
    the keyless Parallel fallback. Keyless Parallel itself genuinely services
    calls, so a zero-setup install reports usable. Distinct from
    :func:`web_tools_registered` (always True — whether the tool is offered).
    )r  r_   ra   r^   r7   r1   check_web_api_keyr  ^  s-     .0011]oFZF\F\6]6]]r7   c                  .    t                      \  } }}| duS )zICheck if an auxiliary text model is available for LLM content processing.Nr   )rr   r   s     r1   r  r  l  s    133LFAqr7   __main__u    🌐 Standalone Web Tools Modulez(========================================rN   rO   u   ✅ Web backend: r?   z!   Using Exa API (https://exa.ai)rD   z+   Using Parallel API (https://parallel.ai)rB   z(   Using Tavily API (https://tavily.com)rC   z    Using SearXNG (search only): rP   rF   z-   Using Brave Search free tier (search only)rA   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_URLu;   ❌ No auxiliary model available for LLM content processingzVSet OPENROUTER_API_KEY, configure Nous Portal, or set OPENAI_BASE_URL + OPENAI_API_KEYuK   ⚠️  Without an auxiliary model, LLM content processing will be disabledu   ✅ Auxiliary model available: r   u!   🛠️  Web tools ready for use!u+   🧠 LLM content processing available with z&   Default min length for processing: z charsu&   🐛 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')z  # Extract (asynchronous)z  async def main():z?      content = await web_extract_tool(['https://example.com'])z  asyncio.run(main())z
LLM-enhanced usage:zC  # Content automatically processed for pages >5000 chars (default)zA  content = await web_extract_tool(['https://python.org/about/'])z#  # Customize processing parametersz#  content = await web_extract_tool(z"      ['https://docs.python.org'],z,      model='google/gemini-3-flash-preview',z      min_length=3000z  )z  # Disable LLM processingzY  raw_content = await web_extract_tool(['https://example.com'], use_llm_processing=False)z
Debug mode:z  # Enable debug loggingz  export WEB_TOOLS_DEBUG=truez  # Debug logs capture:z$  # - All tool calls with parametersz  # - Original API responsesz  # - LLM compression metricsz  # - Final processed resultsz3  # Logs saved to: ./logs/web_tools_debug_UUID.jsonuL   
📝 Run 'python test_web_tools_llm.py' to test LLM processing capabilities)registryrK  
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   )r  r  minimummaximumdefaultr6  )r  
propertiesrequired)r&   r  r7  r   u  Extract content from web page URLs. Returns page content in markdown format. Also works with PDF URLs (arxiv papers, documents, etc.) — pass the PDF link directly and it converts to markdown text. Pages under 5000 chars return full markdown; larger pages are LLM-summarized and capped at ~5000 chars per page. Pages over 2M chars are refused. If a URL fails or times out, use the browser tool to access it instead.arrayr  z:List of URLs to extract content from (max 5 URLs per call))r  itemsr  maxItemsr;   c                 t    t          |                     dd          |                     dd                    S )Nr3  r*   r4  r2  )r4  )rE  r<   argskws     r1   r   r     s1    txx/D/DDHHU\^_L`L`aaa r7   u   🔍r   )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          S )NrW  r2  markdown)rx  r   r<   listr  s     r1   r   r     sP    /$.txx/?/?$F$FNRaR  BPZ \  \ r7   u   📄)	r&   r  r  r  r  r  is_asyncr  r  )rG   r4   )r   Fr*   r  )r2  )vr   rM  loggingr-   r  r   typingr   r   r   r   r   httpxrE   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   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%   sys	getLoggerr  r   rw   r2   r5   r6   dictr>   	frozensetrT   rV   rZ   r_   ra   r]   ri   rU   r  rp   $DEFAULT_MIN_LENGTH_FOR_SUMMARIZATIONr|   r   r   r   rO  r   r   r   r   r  r  r  rE  rx  r  r  r  r  r  printweb_availabletool_gateway_availabler.   r/   firecrawl_key_availablefirecrawl_url_availablenous_availabledefault_summarizer_modelrJ   rstripexitactive
session_idlog_dirtools.registryr  rK  WEB_SEARCH_SCHEMAWEB_EXTRACT_SCHEMAr#  r^   r7   r1   <module>r     s  # # #J   				 				  ; ; ; ; ; ; ; ; ; ; ; ; ; ;   $######                                5 4 4 4 4 4
 $( 8C= ' ' '*. (3- . . ."& (3- & & &(,  , , ,!Xc] ! ! !         
 - , , , , ,                    
 J I I I I I I I 



		8	$	$
S S    ("3 "4 " " " "$      iVVV   "	"J"J"JKK 2 2S 2 2 2 2 2j-S - - - -.c . . . .$ $ $ $ $ $$3 4    >$    049    L (, $Lc Ld L L L L/ /(3- /5RUX`adXegkloqtltguIuCv / / / /x}    
 
k+<	=	=	=
 :a aa	a a C=	a
 a c]a a a aP s sss C=s 	s
 s s c]s s s slYYY C=Y 	Y
 Y c]Y Y Y Yxc c    T2L 2L 2L 2LjA A A AHh% h%3 h%s h%3 h% h% h% h%Z #:o% o%
s)o%o% o% C=	o%
 o% 	o% o% o% o%d	
d 
 
 
 
t    *S *T * * * *^4 ^ ^ ^ ^t     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**,,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	
 	
 	

  LKLLLfggg[\\\\J0HJJKKK 	E
-... eV<TVVWWWc7[cccddd } 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
   	E
KLLL	E
!""" k%&&&STTTQRRRb			344434442333<===%&&&eb			*+++ijjj	E/	E
$%%%	E
)***	E
#$$$	E
0111	E
()))	E
)***	E
)***	E
?@@@	E
YZZZ 0 / / / / / / /  w !  G 
 "T 
 
 I   .  v (+[	 
 H   "  	aa!""$$
!	 	 	 	  	\ \!""$$
!     r7   