
    sj%?                    D   U d Z ddlmZ ddlZddlZddlmZmZmZ ddl	m
Z
 ddlmZ  ej        e          Zi Zded<   i Zd	ed
<    ej                    Zddd-dZddd.dZddd/dZddd/dZddd0dZd1dZd Zd!Zd2d#Zd3d&Zd4d'Zd5dd(d6d)Z d7d*Z!d7d+Z"d8d,Z#dS )9u.  
Web Search Provider Registry
============================

Central map of registered web providers. Populated by plugins at import-time
via :meth:`PluginContext.register_web_search_provider`; consumed by the
``web_search`` and ``web_extract`` tool wrappers in :mod:`tools.web_tools` to
dispatch each call to the active backend.

Active selection
----------------
The active provider is chosen by configuration with this precedence:

1. ``web.search_backend`` / ``web.extract_backend``
   (per-capability override).
2. ``web.backend`` (shared fallback).
3. If exactly one capability-eligible provider is registered AND available,
   use it.
4. Legacy preference order — ``firecrawl`` → ``parallel`` → ``tavily`` →
   ``exa`` → ``searxng`` → ``brave-free`` → ``ddgs`` — filtered by
   availability. Matches the historic ``tools.web_tools._get_backend()``
   candidate order so installs that never set a config key keep landing
   on the same provider they did before the plugin migration.
5. Otherwise ``None`` — the tool surfaces a helpful error pointing at
   ``hermes tools``.

The capability filter (``supports_search`` / ``supports_extract``) is
applied at every step so a search-only provider (``brave-free``)
configured as ``web.extract_backend`` correctly falls through to an
extract-capable backend.
    )annotationsN)DictListOptional)WebSearchProvider)hermes_home_keyzDict[str, WebSearchProvider]
_providersz'Dict[str, Dict[str, WebSearchProvider]]_scoped_providers)scopeproviderr   r   Optional[str]returnNonec                  t          | t                    s$t          dt          |           j                   | j        }t          |t                    r|                                st          d          |                                }t          5  |t          nt                              |i           }|                    |          }| ||<   ddd           n# 1 swxY w Y   |0t                              d|t          |          j                   dS t                              d|t          |           j                   dS )u   Register a web search/extract provider.

    Re-registration (same ``name``) overwrites the previous entry and logs
    a debug message — makes hot-reload scenarios (tests, dev loops) behave
    predictably.
    z>register_provider() expects a WebSearchProvider instance, got z-Web provider .name must be a non-empty stringNz(Web provider '%s' re-registered (was %r)z!Registered web provider '%s' (%s))
isinstancer   	TypeErrortype__name__namestrstrip
ValueError_lockr	   r
   
setdefaultgetloggerdebug)r   r   raw_namer   targetexistings         =/home/agent/.hermes/hermes-agent/agent/web_search_registry.pyregister_providerr"   2   s    h 122 
->>*- -
 
 	
 }Hh$$ JHNN,<,< JHIII>>D	    $}2C2N2NuVX2Y2Y::d##t                              6$x..)	
 	
 	
 	
 	

 	/$x..)	
 	
 	
 	
 	
s   ?CC#&C#List[WebSearchProvider]c                F   t           5  t          t                    }|                    t                              | pt                      i                      t          |                                          }ddd           n# 1 swxY w Y   t          |d           S )z0Return all registered providers, sorted by name.Nc                    | j         S N)r   )ps    r!   <lambda>z list_providers.<locals>.<lambda>X   s    qv     )key)
r   dictr	   updater
   r   r   listvaluessorted)r   mergeditemss      r!   list_providersr2   R   s    	 & &j!!'++E,F_5F5FKKLLLV]]__%%& & & & & & & & & & & & & & & %--....s   A2BB
B
r   r   Optional[WebSearchProvider]c               F   t          | t                    sdS t          5  |                                 }t                              |pt                      i                               |          pt                              |          cddd           S # 1 swxY w Y   dS )z5Return the provider registered under *name*, or None.N)r   r   r   r   r
   r   r   r	   )r   r   r*   s      r!   get_providerr5   [   s    dC   t	 e ejjll $$U%?o.?.?DDHHMMdQ[Q_Q_`cQdQde e e e e e e e e e e e e e e e e es   A*BBBc                   t           5  |t          nt                              |i           }|                    |                                           cd d d            S # 1 swxY w Y   d S r&   )r   r	   r
   r   r   )r   r   r   s      r!   snapshot_registrationr7   d   s     
 ( ($}2C2G2Gr2R2Rzz$**,,''( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (s   AA  A$'A$currentpreviousboolc               x   |                                  }t          5  |t          nt                              |i           }|                    |          |ur	 ddd           dS ||                    |d           n|||<   ||st                              |d           ddd           n# 1 swxY w Y   dS )zERestore a plugin registration only when *current* is still installed.NFT)r   r   r	   r
   r   r   pop)r   r8   r9   r   r*   r   s         r!   restore_registrationr=   l   s    **,,C	 	/ 	/$}2C2N2NuVX2Y2Y::c??'))	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ JJsD!!!!"F3KV!!%...	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 4s   =B/&=B//B36B3pathc                    	 ddl m}  |            }|}| D ]/}t          |t                    s dS |                    |          }0t          |t
                    r(|                                r|                                S nF# t          $ r9}t          	                    dd
                    |           |           Y d}~nd}~ww xY wdS )zGResolve a dotted config key from ``config.yaml``. Returns None on miss.r   )load_config_readonlyNzCould not read config %s: %s.)hermes_cli.configr@   r   r+   r   r   r   	Exceptionr   r   join)r>   r@   cfgcursegmentexcs         r!   _read_config_keyrI      s    J::::::""$$ 	# 	#Gc4(( tt'''""CCc3 	CIIKK 	99;; J J J3SXXd^^SIIIIIIIIJ4s   ,B AB 
C/CC)	firecrawlparalleltavilyexasearxngz
brave-freeddgs)rM   rK   rL   rJ   keenabletuplec                    	 ddl mm}  | t                    z  t	          fdt          t                              D                       S # t          $ r%}t                              d|           Y d}~nd}~ww xY wt          S )a  Return the keyless walk order for resolution.

    Delegates the entry-vendor choice to the ring cursor in
    :mod:`plugins.web.keyless_mcp` (round-robin per request, seeded by the
    per-process random session id) so resolution and dispatch agree on
    which vendor a fresh install starts at. The remaining vendors follow
    in ring order as fallbacks for registration gaps.
    r   )_KEYLESS_RING_ring_cursorc              3  N   K   | ]}|z   t                    z           V   d S r&   )len).0irS   starts     r!   	<genexpr>z&_keyless_preference.<locals>.<genexpr>   sM       
 
 519M(:(::;
 
 
 
 
 
r)   z"keyless ring order unavailable: %sN)
plugins.web.keyless_mcprS   rT   rV   rQ   rangerC   r   r   _KEYLESS_PREFERENCE)rT   rH   rS   rY   s     @@r!   _keyless_preferencer^      s    	@GGGGGGGGs=111 
 
 
 
 
3}--..
 
 
 
 
 	
  @ @ @93????????@s   AA 
BA>>B
configured
capabilityc                 	 t           5  t          t                    }|                    t                              t                      i                      ddd           n# 1 swxY w Y   dfddd	| r^|                    |           }| |          r|S |t                              d|            nt                              d	|            	fd
|	                                D             }t          |          dk    r|d         S t          D ]3}|                    |          }| |          r 	|          r|c S 4t                      rt                      D ]q}|                    |          }| |          s%	 |                                r|c S ?# t          $ r&}t                              d||           Y d}~jd}~ww xY wdS )u  Resolve the active provider for a capability ("search" | "extract").

    Resolution rules (in order):

    1. **Explicit config wins, ignoring availability.** If
       ``web.{capability}_backend`` or ``web.backend`` names a registered
       provider that supports *capability*, return it even if its
       :meth:`is_available` returns False — the dispatcher will surface a
       precise "X_API_KEY is not set" error to the user instead of silently
       routing somewhere else. Matches legacy
       :func:`tools.web_tools._get_backend` behavior for configured names.

    2. **Single-provider shortcut.** When only one registered provider
       supports *capability* AND ``is_available()`` reports True, return it.

    3. **Legacy preference walk, filtered by availability.** Walk the
       :data:`_LEGACY_PREFERENCE` order (firecrawl → parallel → tavily →
       exa → searxng → brave-free → ddgs) looking for a provider whose
       ``supports_<capability>()`` is True AND whose ``is_available()`` is
       True. Matches the historic ``tools.web_tools._get_backend()``
       candidate order so users with credentials but no explicit config
       key keep landing on the same provider as pre-migration. This is
       the path that fires when no config key is set — pick the
       highest-priority backend the user actually has credentials for.

    Returns None when no provider is configured AND no available provider
    matches the legacy preference; the dispatcher then returns a "set up a
    provider" error to the user.
    Nr'   r   r   r:   c                    dk    r!t          |                                           S dk    r!t          |                                           S dS )NsearchextractF)r:   supports_searchsupports_extract)r'   r`   s    r!   _capablez_resolve.<locals>._capable   sR    !!))++,,,""**,,---ur)   c                    	 t          |                                           S # t          $ r,}t                              d| j        |           Y d}~dS d}~ww xY w)zDWrap ``is_available()`` so a buggy provider doesn't kill resolution.z$provider %s.is_available() raised %sNF)r:   is_availablerC   r   r   r   )r'   rH   s     r!   _is_available_safez$_resolve.<locals>._is_available_safe   sd    	(())) 	 	 	LL?MMM55555	s    # 
A!AAz<web backend '%s' configured but not registered; falling backzCweb backend '%s' configured but does not support '%s'; falling backc                @    g | ]} |           |          |S  rl   )rW   r'   rg   rj   s     r!   
<listcomp>z_resolve.<locals>.<listcomp>  sJ       8A;;--a00	  r)      r   z,provider %s.is_keyless_available() raised %s)r'   r   r   r:   )r   r+   r	   r,   r
   r   r   r   r   r.   rV   _LEGACY_PREFERENCE_keyless_tier_enabledr^   is_keyless_availablerC   )
r_   r`   snapshotr   eligiblelegacyr   rH   rg   rj   s
    `      @@r!   _resolveru      s   < 
 F F
##)--o.?.?DDEEEF F F F F F F F F F F F F F F          <<
++HHX$6$6OLLN   
 LLUJ      ??$$  H 8}}{$  <<'' "" !""8,, ! OOO  ')) 
	 
	D||D))Hxx'9'90022 $#OOO$   BD#       
 4s*   AA&&A*-A*F--
G7GGc                     	 ddl m}   |                                 d          pi }t          |                    dd                    S # t          $ r&}t
                              d|           Y d}~dS d}~ww xY w)zBRead ``web.keyless_fallback`` from config.yaml (default: enabled).r   )load_configwebkeyless_fallbackTz'keyless_fallback config read failed: %sN)rB   rw   r   r:   rC   r   r   )rw   web_cfgrH   s      r!   rp   rp   <  s    111111+--##E**0bGKK 2D99:::   >DDDttttts   AA
 

A:A55A:r`   c                   dd}| s'|dv r#t          d| d          pt          dd          } | sd	S  ||           }	 d
dlm}  |            }|j                                        D ]s\  }}t          |t                    r|                    d          s0|j        r8|j	        dk    rD|
                    dd          d         } ||          |k    r|c S tn2# t          $ r%}	t                              d|	           Y d	}	~	nd	}	~	ww xY wd	S )u  Return the plugin key of a *disabled* bundled web plugin that would
    have provided the configured backend, or None.

    When a user sets ``web.extract_backend: firecrawl`` (or the search
    equivalent) but also lists ``web-firecrawl`` in ``plugins.disabled``,
    the provider never registers and the dispatcher would otherwise emit a
    misleading "No web extract provider configured. Set web.extract_backend
    to ..." error — even though the backend IS configured correctly. The
    real fix is to re-enable the plugin. This helper detects that case so
    the dispatcher can point the user at the actual cause (issue #40190
    follow-up: pi314's disabled-plugin symptom).

    Pass ``capability`` ("search" | "extract") to resolve the configured
    name straight from ``config.yaml`` (``web.<capability>_backend`` →
    ``web.backend``). This is more reliable than the resolved backend the
    dispatcher fell back to, since a disabled provider fails the
    ``_is_backend_available`` gate and the dispatcher silently drops to
    the shared default. An explicit ``configured`` name still wins when
    given.

    Matching is by convention: bundled web plugins live under the
    ``web/<vendor>`` key with the provider ``name`` differing only in
    hyphen/underscore (``brave-free`` provider ⇄ ``web/brave_free`` key,
    ``firecrawl`` ⇄ ``web/firecrawl``). We normalize both sides before
    comparing so every bundled provider is covered without hardcoding a
    per-vendor table.
    sr   r   c                v    |                                                                                      dd          S )N-_)r   lowerreplace)r}   s    r!   _normz'_disabled_web_plugin_for.<locals>._normd  s*    wwyy  ((c222r)   )rc   rd   rx   _backendbackendNr   )get_plugin_managerzweb/zdisabled via config/rn   z%disabled-web-plugin lookup failed: %s)r}   r   r   r   )rI   hermes_cli.pluginsr   _pluginsr1   r   r   
startswithenablederrorsplitrC   r   r   )
r_   r`   r   wantr   pmr*   loadedvendorrH   s
             r!   _disabled_web_plugin_forr   H  s   83 3 3 3  
*(===Uz$;$;$;<< 2y11 	  t5DC999999!!;,,.. 		 		KCc3'' s~~f/E/E ~ |444YYsA&&q)FuV}}$$


 %		  C C C<cBBBBBBBBC4s   BC C 
D&DDc                 d    t          dd          pt          dd          } t          | d          S )zResolve the currently-active web search provider.

    Reads ``web.search_backend`` (preferred) or ``web.backend`` (shared
    fallback) from config.yaml; falls back per the module docstring.
    rx   search_backendr   rc   r{   rI   ru   explicits    r!   get_active_search_providerr     s8      '788^<LUT]<^<^HH2222r)   c                 d    t          dd          pt          dd          } t          | d          S )zResolve the currently-active web extract provider.

    Reads ``web.extract_backend`` (preferred) or ``web.backend`` (shared
    fallback) from config.yaml; falls back per the module docstring.
    rx   extract_backendr   rd   r{   r   r   s    r!   get_active_extract_providerr     s8      '899_=MeU^=_=_HH3333r)   c                     t           5  t                                           t                                           ddd           dS # 1 swxY w Y   dS )z"Clear the registry. **Test-only.**N)r   r	   clearr
   rl   r)   r!   _reset_for_testsr     s    	 " "!!!" " " " " " " " " " " " " " " " " "s   3AAA)r   r   r   r   r   r   )r   r   r   r#   )r   r   r   r   r   r3   )
r   r   r8   r   r9   r3   r   r   r   r:   )r>   r   r   r   )r   rQ   )r_   r   r`   r   r   r3   )r   r:   r&   )r_   r   r`   r   r   r   )r   r3   )r   r   )$__doc__
__future__r   logging	threadingtypingr   r   r   agent.web_search_providerr   hermes_constantsr   	getLoggerr   r   r	   __annotations__r
   Lockr   r"   r2   r5   r7   r=   rI   ro   r]   r^   ru   rp   r   r   r   r   rl   r)   r!   <module>r      sP    @ # " " " " "      ' ' ' ' ' ' ' ' ' ' 7 7 7 7 7 7 , , , , , ,		8	$	$ ,.
 - - - -=?  ? ? ? ?	 NR 
 
 
 
 
 
@ .2 / / / / / / 7; e e e e e e *.( ( ( ( ( (       6   0 $    ,i i i iX	 	 	 	8^b 8 8 8 8 8 8v3 3 3 34 4 4 4" " " " " "r)   