
    `gjX                        d Z ddlmZ ddlZddlZddlZddlmZmZm	Z	m
Z
 ddlmZ ddlmZmZmZ  ej"                  e      ZdZdZd	Z ej.                  d
ej0                        ZddZddZ G d de      Zy)u  xAI Web Search — plugin form.

Routes ``web_search`` tool calls through xAI's agentic Web Search tool
(server-side ``web_search`` on the Responses API). Grok runs the actual
searching and page-browsing server-side; we ask it to return the top
results as structured JSON so we can hand back the same
``{title, url, description, position}`` rows every other Hermes web
provider produces.

Reference: https://docs.x.ai/developers/tools/web-search

Config keys this provider responds to::

    web:
      search_backend: "xai"           # explicit per-capability
      backend: "xai"                  # shared fallback

Optional knobs (under ``web.xai`` in ``config.yaml``)::

    web:
      xai:
        model: "grok-build-0.1"       # reasoning model required by web_search
        allowed_domains: ["x.ai"]     # max 5 — mutually exclusive with excluded_domains
        excluded_domains: ["bad.com"] # max 5 — mutually exclusive with allowed_domains
        timeout: 90                   # seconds (default 90)

Auth: reuses :func:`tools.xai_http.resolve_xai_http_credentials`, which
prefers Hermes-managed xAI Grok OAuth (via ``hermes auth``) and falls back
to ``XAI_API_KEY`` (resolved through ``~/.hermes/.env``, then
``os.environ``).
    )annotationsN)AnyDictListOptional)WebSearchProvider)has_xai_credentialshermes_xai_user_agentresolve_xai_http_credentialszgrok-build-0.1Z      z\{[\s\S]*\}c                 .   	 ddl m}   |        }t        |t              r|j	                  d      nd}t        |t              r|j	                  d      nd}t        |t              r|S i S # t
        $ r"}t        j                  d|       i cY d}~S d}~ww xY w)z7Read ``web.xai`` from config.yaml (returns {} on miss).r   )load_configwebNxaiz!Could not load web.xai config: %s)hermes_cli.configr   
isinstancedictget	Exceptionloggerdebug)r   cfgweb_sectionxai_sectionexcs        K/root/.hermes/venv/lib/python3.12/site-packages/plugins/web/xai/provider.py_load_xai_web_configr   @   s{    	1m(23(=cggen40:;0Mkooe,SW(d;{CC 8#>	s$   A$A) 'A) )	B2B	BBc                    t        | t              sg S g }| D ]V  }t        |t              r/|j                         r|j	                  |j                                t        |      t        k\  sU |S  |S )z<Coerce a config value to a clean list of <=5 domain strings.)r   liststrstripappendlen_MAX_DOMAIN_FILTERS)valuecleaneditems      r   _coerce_domain_listr)   N   se    eT"	G dC TZZ\NN4::<(w<..N
 N    c                      e Zd ZdZedd       Zedd       ZddZddZddZ	dddZ
edd       Ze	 	 	 	 	 	 dd	       Ze	 	 	 	 dd
       Ze	 	 	 	 	 	 dd       Ze	 	 	 	 	 	 	 	 dd       ZddZy)XAIWebSearchProvideruv  Search-only provider backed by xAI's agentic Web Search tool.

    Sends a structured prompt to Grok with ``tools=[{"type": "web_search"}]``
    enabled and asks it to return the top *limit* results as JSON. Falls
    back to the Responses API ``citations`` list if Grok ignores the JSON
    schema instruction (rare for grok-4.3 but cheap insurance).

    No extract capability — pair with Firecrawl / Tavily / Exa for
    ``web_extract`` if you need page content.

    Trust model
    -----------
    Unlike index-backed providers (Brave / Tavily / Exa) which return
    verbatim search-engine results, this backend is an LLM in a trench
    coat: Grok decides which URLs to surface, generates the titles and
    descriptions itself, and is influenced by the *content of the query*.
    A maliciously crafted query (e.g. injected via untrusted upstream
    input the agent picked up) can in principle steer Grok into emitting
    attacker-chosen URLs. Callers that pipe untrusted text directly into
    ``web_search`` should treat returned URLs the same way they would
    treat any model-generated link — validate before fetching.
    c                     y)Nr    selfs    r   namezXAIWebSearchProvider.namex   s    r*   c                     y)NxAI Web Search (Grok)r.   r/   s    r   display_namez!XAIWebSearchProvider.display_name|   s    &r*   c                    t               S )u  Cheap availability probe — env var OR auth-store has OAuth tokens.

        Delegates to :func:`tools.xai_http.has_xai_credentials`, which is
        deliberately *not* the same as :func:`resolve_xai_http_credentials`:
        it never triggers OAuth token refresh or acquires the auth-store
        lock. The ABC contract requires this method to be safe to call on
        every ``hermes tools`` repaint and at tool-registration time.
        Token freshness / refresh is handled inside :meth:`search`.
        )r	   r/   s    r   is_availablez!XAIWebSearchProvider.is_available   s     #$$r*   c                     y)NTr.   r/   s    r   supports_searchz$XAIWebSearchProvider.supports_search   s    r*   c                     y)NFr.   r/   s    r   supports_extractz%XAIWebSearchProvider.supports_extract   s    r*   c                	   	 ddl m}  |       rdddS 	 t               }t	        |j                  d      xs d      j                         }t	        |j                  d      xs d	      j                         j                  d
      }|sdddS 	 t        |      }t        dt        |d            }t               }t        |j                  d      t              r|j                  d      nt        }|j                         xs t        }	 t!        |j                  dt"                    }	t%        |j                  d            }
t%        |j                  d            }|
r|rdddS ddi}|
rd|
i|d<   n	|rd|i|d<   | j'                  ||      }|d|dg|gdgd}d| dt)               d}	 ddl}t.        j1                  d ||||       |j                  d!      d"k(  }d}t3        d#      D ],  }	 |j5                  | d$|||	%      }|j7                           n |dd2dS 	 |jE                         }t        |tF              r|j                  d5      nd}t        |tF              rF|j                  d6      xs |j                  d7      xs d8}t.        j?                  d9|       dd:| dS | jI                  ||;      }|sd(d<g id=S d(d<|id=S # t        $ r Y w xY w# t        t        f$ r d}Y Xw xY w# t        t        f$ r
 t"        }	Y w xY w# t,        $ r dddcY S w xY w# |j8                  $ r;}|j:                  |j:                  j<                  nd}|d&k(  r|dk(  r|rt.        j1                  d'       	 t        d(|)      }t	        |j                  d      xs d      j                         }|r||k7  r|}d| |d*<   Y d}~n,# t        $ r }t.        j?                  d+|       Y d}~nd}~ww xY wd}	 |j:                  |j:                  j@                  dd, nd}n# t        $ r d}Y nw xY wt.        j?                  d-||       dd.| d/| j                         dcY d}~c S d}~w|jB                  $ r*}t.        j?                  d0|       dd1| dcY d}~c S d}~ww xY w# t        $ r%}t.        j?                  d3|       dd4dcY d}~S d}~ww xY w)>zExecute a Grok-backed web search.

        Returns ``{"success": True, "data": {"web": [{title, url, description, position}, ...]}}``
        on success, ``{"success": False, "error": str}`` on failure.
        r   )is_interruptedFInterrupted)successerrorapi_key base_urlzhttps://api.x.ai/v1/z_No xAI credentials found. Run `hermes auth` to sign in with xAI Grok OAuth, or set XAI_API_KEY.r      d   modeltimeoutallowed_domainsexcluded_domainszZweb.xai.allowed_domains and web.xai.excluded_domains cannot both be set (xAI restriction).type
web_searchfiltersuser)rolecontentno_inline_citations)rF   inputtoolsincludezBearer zapplication/json)AuthorizationzContent-Typez
User-AgentNz4httpx is not installed (required for xAI web search)z0xAI web search via %s: '%s' (limit=%d, model=%s)providerz	xai-oauth   z
/responses)headersjsonrG   i  zQxAI web search got 401 on first attempt; forcing OAuth refresh and retrying once.T)force_refreshapi_key_hintrT   z1xAI web search OAuth refresh after 401 failed: %si,  zxAI web search HTTP %d: %szxAI web search returned HTTP z: z xAI web search request error: %szCould not reach xAI: z#xAI web search produced no responsezxAI web search bad JSON: %sz/Could not parse xAI Responses API reply as JSONr?   messagecodezunknown errorz*xAI web search returned error envelope: %szxAI returned an error: limitr   )r>   data)%tools.interruptr<   r   r   r!   r   r"   rstripint	TypeError
ValueErrormaxminr   r   DEFAULT_MODELfloatDEFAULT_TIMEOUTr)   _build_promptr
   httpxImportErrorr   inforangepostraise_for_statusHTTPStatusErrorresponsestatus_codewarningtextRequestErrorrX   r   _extract_results)r0   queryr^   r<   credsr@   rB   r   rF   rG   allowedexcludedweb_search_toolpromptpayloadrW   rk   is_oauth_pathrespattemptr   status	refreshedrefreshed_keyrefresh_excbodyr_   	api_errorerr_msgweb_resultss                                 r   searchzXAIWebSearchProvider.search   sG   	6#(=AA  
 -.eii	*0b1779uyy,E0EFLLNUUVYZ : 	JE As5#'"$$.swww/?$E =.	&CGGI?@G &cgg.?&@A&sww/A'BCx !<  ,2<*@*;W)EOI&*<h)GOI&##E51 %&9:%& ..#
  'wi0./1
	 	>eUE	
  :.+=Qx .	RG-Rzzj
+# #	 "  %%'.	R` <$/TUU	99;D *4D$)?DHHW%T	i&i( #==(#" 
 NNGQ$1H	/RSS++D+>  $eR[99%)=>>g  		* :& 	E	 :& 	&%G	&R  	 O 	D (( !58\\5M11STS=W\mKK5$@*.)0%	 ),IMM),D,J(K(Q(Q(S(]g-E&3G9@	7JGO4$ % O' 
 69ll6N3<<,,Tc2TVD  D;VTJ$<VHBtfMTTV  %% RA3G#(5J3%3PQQR  	NN8#> J 	s   K K K5 ,L .(L%!R* 	KKK21K25LLL"!L"%R'5AQ+;A
OQ+	O5O0+Q+0O55Q+;'P#"Q+#P1.Q+0P112Q+#R'+R'=R"R'"R'*	S3SSSc                    d| d|  S )a7  Compose the prompt that asks Grok to act as a search engine.

        We deliberately ask for a JSON object (not bare array) so we can
        match it cheaply with ``_JSON_BLOCK_RE``; we explicitly forbid
        prose, markdown fences, and inline-citation links to keep the
        payload parseable.
        u<  Use the web_search tool to find current information for the query below, then respond with ONLY a single JSON object — no prose, no markdown fences, no inline citation links — matching this exact schema:

{"results": [{"title": "string", "url": "string", "description": "1-2 sentence summary"}]}

Return at most zy results, ordered by relevance, with absolute https:// URLs. If no usable results exist, return {"results": []}.

Query: r.   )rx   r^   s     r   rj   z"XAIWebSearchProvider._build_promptV  s$    
 $W % W
	
r*   c          	        | j                  |      \  }}|D ]  }| j                  ||      }|s|c S  |r)dj                  |      }| j                  |||      }|r|S |j	                  d      xs g }	t        |	t              rRt        |	d|       D 
cg c]8  \  }
}t        |t              r#|j                         rdt        |      d|
dz   d: c}}
S g S c c}}
w )u<  Pull a ``[{title, url, description, position}, ...]`` list out of a
        Responses-API reply.

        Strategy:

        1. Walk ``output[*].content[*].text`` for ``output_text`` blocks and
           try to parse the first JSON object that has a ``results`` list.
        2. If the JSON path fails, fall back to the message annotations
           (``url_citation`` entries) — every annotation carries a URL and
           a ``title`` (citation number); we pair those URLs with surrounding
           text from the message body as a best-effort description.
        r]   
	citationsNrA   rD   titleurldescriptionposition)
_collect_output_text_try_parse_json_resultsjoin_results_from_annotationsr   r   r    	enumerater!   r"   )clsresponse_datar^   text_blocksr   blockparsedjoined_textannotation_resultsr   ius               r   rw   z%XAIWebSearchProvider._extract_resultsk  s   & $'#;#;M#J [ ! 	E00e0DF	 ))K0K!$!>!>[ "? " ")) "%%k28b	i& &i&78	 Aqa%!'')  q6#% !A		 	 		s   =Cc                p   g }g }| j                  d      }t        |t              s||fS |D ]  }t        |t              r|j                  d      dk7  r)|j                  d      }t        |t              sK|D ]  }t        |t              r|j                  d      dk7  r(|j                  d      }t        |t              r!|j                         r|j                  |       |j                  d      }t        |t              s|D ]%  }	t        |	t              s |j                  |	       '   ||fS )zEReturn (text_blocks, annotations) extracted from ``response.output``.outputrJ   r[   rO   output_textru   r   )r   r   r    r   r!   r"   r#   )
r   r   r   r   r(   rO   chunkru   chunk_annotationsanns
             r   r   z)XAIWebSearchProvider._collect_output_text  s$   
 "$,.""8,&$'++ 	4DdD)TXXf-=-Jhhy)Ggt,  
4!%.%))F2C}2Tyy(dC(TZZ\&&t,$)IIm$<!/60 4%c40.K..s34
4	4" K''r*   c                  | g}t         j                  |       }|r4|j                  d      | k7  r |j                  |j                  d             |D ]  }	 t	        j
                  |      }t        |t              s+|j                  d      }t        |t              sMg }|d| D ]  }t        |t              st        |j                  dd            j                         }	|	s@|j                  t        |j                  dd            j                         |	t        |j                  dd            j                         t        |      dz   d	        |s
|c S  y# t        j                  t        f$ r Y ,w xY w)
aR  Parse a JSON object with a ``results`` array out of ``text``.

        Returns the normalized result list on success, ``None`` when the
        block has no valid JSON object or no ``results`` key. Tolerates
        leading/trailing prose because reasoning models sometimes prefix a
        short narration even when told not to.
        r   resultsNr   rA   r   r   rD   r   )_JSON_BLOCK_REr   groupr#   rX   loadsJSONDecodeErrorrd   r   r   r   r    r!   r"   r$   )
ru   r^   
candidatesmatch	candidater   r   
normalizedrowr   s
             r   r   z,XAIWebSearchProvider._try_parse_json_results  se    V
%%d+U[[^t+ekk!n-# 	"II. fd+jj+Ggt,/1Jv !#t,#''%,-335!!!$SWWWb%9!:!@!@!B"'*377="+E'F'L'L'N %(
Oa$7
" !!;	"< 7 ((*5 s   E!!E>=E>c          	        t               }g }| D ]0  }|j                  d      dk7  rt        |j                  dd            j                         }|r||v rI|j	                  |       d}|j                  d      }|j                  d      }	t        |t              rpt        |	t              r`d|cxk  r|	cxk  rt        |      k  rFn nCt        d|dz
        }
||
| j                         }t        |      dkD  r|d	d
 j                         }|j                  d||t        |      dz   d       t        |      |k\  s0 |S  |S )a	  Best-effort fallback when JSON parsing fails.

        Uses each ``url_citation`` annotation's ``url`` (the citation
        title is just the integer label, so we don't surface it) and
        slices ~200 characters of surrounding text as the description.
        rJ   url_citationr   rA   start_index	end_indexr      i8NrD   r   )
setr   r!   r"   addr   rb   r$   re   r#   )r   r   r^   seenr   r   r   r   startendwindow_starts              r   r   z.XAIWebSearchProvider._results_from_annotations  s>    (* 	Cwwv.0cggeR()//1C#+HHSMKGGM*E''+&C%%*S#*>1CgPSCgWZ[fWgCg"1eck2),u=CCE{#c)"-de"4":":"<KNN#. #Gq 0	 7|u$7	6 r*   c                    dddg ddS )Nr3   paiduU   Agentic web search via Grok's web_search tool — uses xAI Grok OAuth or XAI_API_KEY.xai_grok)r1   badgetagenv_vars
post_setupr.   r/   s    r   get_setup_schemaz%XAIWebSearchProvider.get_setup_schema#  s!    
 ,- $	
 		
r*   N)returnr!   )r   bool)r   )rx   r!   r^   rb   r   Dict[str, Any])rx   r!   r^   rb   r   r!   )r   r   r^   rb   r   List[Dict[str, Any]])r   r   r   z&tuple[List[str], List[Dict[str, Any]]])ru   r!   r^   rb   r   zOptional[List[Dict[str, Any]]])r   r   r   r!   r^   rb   r   r   r   r   )__name__
__module____qualname____doc__propertyr1   r4   r6   r8   r:   r   staticmethodrj   classmethodrw   r   r   r   r   r.   r*   r   r,   r,   `   s   .   ' '
%
~?D 
 
( 6%6 	6
 
6 6p (%(	/( (: 00 0 
(	0 0d )))) 	)
 
) )Z
r*   r,   r   )r&   r   r   z	List[str])r   
__future__r   rX   loggingretypingr   r   r   r   agent.web_search_providerr   tools.xai_httpr	   r
   r   	getLoggerr   r   rg   ri   r%   compile	MULTILINEr   r   r)   r,   r.   r*   r   <module>r      s   @ #   	 , , 7  
		8	$  
 NBLL9
$P
, P
r*   