
    `gjO                       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 ddl	m
Z
mZmZmZmZ ddlZddlmZmZ ddlmZmZ  ej,                  e      ZdZd	Zd
ZdZdZdZd7dZd8dZ d9dZ!d:dZ"d:dZ#d;dZ$d<dZ%d=dZ&d>dZ'd?dZ(d@dZ)dAdZ*dBdZ+	 	 	 	 	 	 dC	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dDdZ,ddd d!d"d#d$d%d!id&d'd$d%d!id(d'd!d)d#d!d*d#d+d,dd-d+d.dd-d/dgd0d1Z-d2 Z. ej^                  dde-e.e%d3gd4d56       y)Eu  X Search tool backed by xAI's built-in ``x_search`` Responses API tool.

Authentication
--------------
The tool registers when **either** xAI credential path is available:

* ``XAI_API_KEY`` is set in ``~/.hermes/.env`` or the process environment
  (paid xAI API key), OR
* The user is signed in via xAI Grok OAuth — SuperGrok subscription —
  i.e. ``hermes auth add xai-oauth`` has been run and the stored refresh
  token still works.

Credential preference at call time matches
:func:`tools.xai_http.resolve_xai_http_credentials`: SuperGrok OAuth first,
direct OAuth resolver second, ``XAI_API_KEY`` last. That helper also
auto-refreshes the OAuth access token when it's within the refresh skew
window, so a ``True`` from :func:`check_x_search_requirements` means the
bearer is fetchable AND non-empty.

Defensive output
----------------
The tool surfaces two additional signals beyond xAI's raw response so callers
can tell a real citation-backed answer from an unsourced one:

* ``from_date`` / ``to_date`` are validated client-side before the HTTP call.
  Malformed (non ``YYYY-MM-DD``), inverted (``from_date > to_date``), and
  pure-future ranges (``from_date`` later than today UTC) fail fast with a
  clear error instead of burning an API call. ``to_date`` in the future is
  still allowed so callers can legitimately request "from yesterday to
  tomorrow".
* Successful responses carry ``degraded`` and ``degraded_reason`` fields.
  ``degraded`` is ``True`` when any narrowing filter (handles or dates) was
  active AND xAI returned no citations in either the top-level ``citations``
  array or the inline ``url_citation`` annotations. In that case the
  ``answer`` came from the model's own knowledge rather than the X index,
  and the caller should treat the result as unsourced.

Salvaged from PR #10786 (originally by @Jaaneek); credential resolution
reworked to honor both auth modes per Teknium's design.
    )annotationsN)datedatetimetimezone)AnyDictListOptionalTuple)registry
tool_error)hermes_xai_user_agentresolve_xai_http_credentialszhttps://api.x.ai/v1zgrok-4.5      )lowmediumhighxhigh
   c                 h    	 ddl m}   |        j                  di       xs i S # t        $ r i cY S w xY w)Nr   load_configx_search)hermes_cli.configr   get	Exceptionr   s    F/root/.hermes/venv/lib/python3.12/site-packages/tools/x_search_tool.py_load_x_search_configr   F   s8    1}  R06B6 	s    # 11c                 ~    t               } t        | j                  d      xs d      j                         xs t        S )Nmodel )r   strr   stripDEFAULT_X_SEARCH_MODEL)cfgs    r   _get_x_search_modelr'   O   s2    

!C &B'--/I3IJ    c                     t               } | j                  d      }|t        |      j                         sy t        |      j                         j	                         }|t
        vr'dj                  t
              }t        d| d|d      |S )Nreasoning_effort, z*x_search.reasoning_effort must be one of: z (got ))r   r   r#   r$   lowerX_SEARCH_REASONING_EFFORTSjoin
ValueError)r&   	raw_valueeffortalloweds       r   _get_x_search_reasoning_effortr4   T   s    

!C*+II 4 4 6^!!#))+F//))678	 B=#
 	
 Mr(   c                     t               } | j                  dt              }	 t        dt	        |            S # t
        $ r	 t        cY S w xY w)Ntimeout_seconds   )r   r    DEFAULT_X_SEARCH_TIMEOUT_SECONDSmaxintr   r&   r1   s     r   _get_x_search_timeout_secondsr<   d   sG    

!C)+KLI02s9~&& 0//0   7 A	A	c                     t               } | j                  dt              }	 t        dt	        |            S # t
        $ r	 t        cY S w xY w)Nretriesr   )r   r   DEFAULT_X_SEARCH_RETRIESr9   r:   r   r;   s     r   _get_x_search_retriesrA   m   sF    

!C	#;<I(1c)n%% (''(r=   c                 L   t               } t        | j                  d      xs d      j                         }|st	        d      t        | j                  d      xs t
              j                         j                  d      }t        | j                  d      xs d      }|||fS )u  Return ``(api_key, base_url, source)``.

    ``source`` is one of ``"xai-oauth"`` or ``"xai"`` so callers (and tests)
    can tell which credential path won. Raises ``RuntimeError`` if no usable
    credential is available — the registered :func:`check_x_search_requirements`
    gate makes that case unreachable in normal operation, but the runtime
    check exists so a credential that expires between registration and
    invocation produces a clean tool error instead of a 401.
    api_keyr"   z~No xAI credentials available. Run `hermes auth add xai-oauth` to sign in with your SuperGrok subscription, or set XAI_API_KEY.base_url/providerxai)r   r#   r   r$   RuntimeErrorDEFAULT_XAI_BASE_URLrstrip)credsrC   rD   sources       r   _resolve_xai_bearerrM   z   s     )*E%))I&,"-335GO
 	
 599Z(@,@AGGIPPQTUH:&/%0FHf$$r(   c                     	 t               } t        t        | j                  d      xs d      j	                               S # t
        $ r Y yw xY w)a)  Return True when xAI credentials are available AND valid.

    ``resolve_xai_http_credentials`` calls
    :func:`hermes_cli.auth.resolve_xai_oauth_runtime_credentials` which
    auto-refreshes the OAuth access token if it's expiring; a successful
    return therefore implies a usable bearer.
    rC   r"   F)r   boolr#   r   r$   r   )rK   s    r   check_x_search_requirementsrP      sH    ,.C		),2399;<< s   >A 	AAc                    g }| xs g D ]B  }t        |xs d      j                         j                  d      }|s2|j                  |       D t	        |      t
        kD  rt        | dt
         d      |S )Nr"   @z supports at most z handles)r#   r$   lstripappendlenMAX_HANDLESr0   )handles
field_namecleanedhandle
normalizeds        r   _normalize_handlesr\      sw    G-R '2&,,.55c:
NN:&' 7|k!J<'9+hOPPNr(   c                    | j                         }	 t        j                  |d      j                         S # t        $ r}t	        | d|d      |d}~ww xY w)a  Parse a strict YYYY-MM-DD string into a ``date``.

    xAI accepts any string in the ``from_date``/``to_date`` slots and silently
    returns an answer with no citations when the value is malformed or refers
    to a window where no posts can exist. That behavior burns a billable API
    call and produces a confident-sounding fluff answer that's hard for callers
    to distinguish from a real result. Validating client-side fails fast and
    gives the agent a clear error to act on.
    z%Y-%m-%dz must be YYYY-MM-DD (got r,   N)r$   r   strptimer   r0   )valuerX   rawexcs       r   _parse_iso_daterb      sa     ++-C  j16688 l3C7!<
	s   #6 	AAAc                   d}d}| j                         rt        | d      }|j                         rt        |d      }|r5|r3||kD  r.t        d|j                          d|j                          d      |et	        j
                  t        j                        j                         }||kD  r.t        d|j                          d|j                          d      yy)u  Validate ``from_date`` / ``to_date`` before they reach xAI.

    Rules:
      * Either field, if non-empty, must parse as ``YYYY-MM-DD``.
      * When both are set, ``from_date <= to_date``.
      * ``from_date`` must not be later than today UTC — no posts can exist
        in a window that hasn't started yet, so the call would be guaranteed
        to return zero citations. ``to_date`` in the future is allowed
        (callers may legitimately set "from yesterday to tomorrow").
    N	from_dateto_datezfrom_date (z ) must be on or before to_date (r,   zC) is in the future; X Search only indexes past posts (today UTC is )	r$   rb   r0   	isoformatr   nowr   utcr   )rd   re   parsed_from	parsed_to	today_utcs        r   _validate_date_rangerl      s     #'K $I%i=}}#GY7	y[9%<+//12 3!++-.a1
 	
 LL.335	"k3356 7B&&(),  # r(   c                   t        | j                  d      xs d      j                         }|r|S g }| j                  dg       xs g D ]  }|j                  d      dk7  r|j                  dg       xs g D ]X  }|j                  d      }|dv st        |j                  d      xs d      j                         }|sH|j                  |       Z  d	j	                  |      j                         S )
Noutput_textr"   outputtypemessagecontent>   textrn   rs   z

)r#   r   r$   rT   r/   )payloadrn   partsitemrr   ctypers   s          r   _extract_response_textrx      s    gkk-06B7==?KEHb)/R '88Fy(xx	2.4" 	'GKK'E//7;;v.4"5;;=LL&	'' ;;u##%%r(   c                   g }| j                  dg       xs g D ]  }|j                  d      dk7  r|j                  dg       xs g D ]  }|j                  dg       xs g D ]k  }|j                  d      dk7  r|j                  |j                  dd      |j                  d	d      |j                  d
      |j                  d      d       m   |S )Nro   rp   rq   rr   r   url_citationurlr"   titlestart_index	end_index)r{   r|   r}   r~   )r   rT   )rt   	citationsrv   rr   
annotations        r   _extract_inline_citationsr      s    &(IHb)/R 88Fy(xx	2.4" 	G%kk-<B 

>>&)^;  )~~eR8!+!<'1~~m'D%/^^K%@	
	 r(   c                   t        | dd       }|t        |       S 	 |j                         }t	        |t
              rt        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }|xs t        |      }|r||vr| d| }|xs t        |       S t        t        |dd      xs d      j                         }|r|d d S t        |       S # t        $ r d }Y w xY w)Nresponsecoder"   errorz: rs     )getattrr#   jsonr   
isinstancedictr   r$   )ra   r   rt   r   r   rq   rs   s          r   _http_error_messager     s   sJ-H3x--/ '4 7;;v&,"-335GKK(.B/557'3w<D'b	*G"#c("wx,2399;DDSzs8O  s   C9 9DDFqueryc                
   | r| j                         st        d      S 	 t               \  }}}		 t        |d      }t        |d      }|r|rt        d      S 	 t        ||       	 t               }ddi}|r||d<   |r||d<   |j                         r|j                         |d<   |j                         r|j                         |d<   |rd	|d
<   |rd	|d<   t               d| j                         dg|gdd}|rd|i|d<   t               }t               }d }t        |dz         D ]?  }	 t        j                  | dd| dt               d||      }|j!                           n |t        d      |j7                         }t9        |      }t;        |j=                  d       xs g       }t?        |      }g }|r|jA                  d       |r|jA                  d       |j                         r|jA                  d       |j                         r|jA                  d       tC        |      xr | xr | }|rd!d"jE                  |       nd }t7        jF                  d	d#|	d|d$   | j                         |||||d%d&      S # t        $ r}
t        t	        |
            cY d }
~
S d }
~
ww xY w# t        $ r}
t        t	        |
            cY d }
~
S d }
~
ww xY w# t        $ r}
t        t	        |
            cY d }
~
S d }
~
ww xY w# t        j"                  $ r|}t%        t%        |dd       dd       }|
|dk  s||k\  r t&        j)                  d|dz   |dz   t+        |             t-        j.                  t1        dd|dz   z               Y d }~fd }~wt        j2                  t        j4                  f$ rT}||k\  r t&        j)                  d|dz   |dz   |       t-        j.                  t1        dd|dz   z               Y d }~d }~ww xY w# t        j"                  $ r[}t&        jI                  d'|d	(       t7        jF                  dd#dt+        |      tK        |      jL                  d)d&      cY d }~S d }~wt        j2                  $ r^}t&        jI                  d*|d	(       t7        jF                  dd#dd+t                d,tK        |      jL                  d)d&      cY d }~S d }~wtN        $ r[}t&        jI                  d'|d	(       t7        jF                  dd#dt	        |      tK        |      jL                  d)d&      cY d }~S d }~ww xY w)-Nzquery is required for x_searchallowed_x_handlesexcluded_x_handlesz@allowed_x_handles and excluded_x_handles cannot be used togetherrp   r   rd   re   Tenable_image_understandingenable_video_understandinguser)rolerr   F)r!   inputtoolsstorer2   	reasoning   z
/responseszBearer zapplication/json)AuthorizationzContent-Typez
User-Agent)headersr   timeoutr   status_coder   z.x_search upstream failure on attempt %s/%s: %sg      @g      ?z/x_search transient failure on attempt %s/%s: %sz*x_search request did not return a responser   z'no citations returned despite filters: r+   rG   r!   )successrF   credential_sourcetoolr!   r   answerr   inline_citationsdegradeddegraded_reason)ensure_asciizx_search failed: %s)exc_info)r   rF   r   r   
error_typezx_search timed out: %szxAI x_search timed out after z seconds)(r$   r   rM   rH   r#   r\   rl   r0   r4   r'   r<   rA   rangerequestspostr   raise_for_status	HTTPErrorr   loggerwarningr   timesleepminReadTimeoutConnectionErrorr   rx   listr   r   rT   rO   r/   dumpsr   rp   __name__r   )r   r   r   rd   re   r   r   rC   rD   rL   ra   r3   excludedr*   tool_defrt   r6   max_retriesr   attempter   datar   r   r   active_filtersr   r   s                                r   x_search_toolr   #  s[    :;;$$7$9!6h
$%68KL%&8:NOx`aa	( G4	(=? %+J#7,3H()-5H)*??$-OO$5H[!==?")--/HY%59H12%59H12 )* #${{} Z

 $,.>#?GK 79+-04[1_- "	:G!:#==j
++27))<(:&;&=
 !+	 ))+"	:H KLL}}'-+.4"5	4T: %'!!"56!!"67??!!+.==?!!),'R	MRBR>R  6dii6O5PQ 	 zz!%+" ) &$4$#2 
 	
q  $#c(##$  	(c#h''	(
  	(c#h''	(` %% 
:%gaT&BMSWX&+*;w+?UDaK!O'*	 

3sC7Q;$7899(((*B*BC 	:k)EaK!O	 

3sC7Q;$7899	:z  
*A=zz !",Q/"1g.. 	
 		
  
-q4@zz !"89V9X8YYab"1g.. 	
 		
  
*A=zz !"Q"1g.. 	
 		

s   J  &P J* #
K -B8P &;K>!DP  	J'	J"J'"J'*	K3KKP KP 	K;K60K;1P 6K;;P >PA1NP &P.A	O=7P =PP UAQ.(U.UASUU)AT?9U?Ur   zSearch X (Twitter) posts, profiles, and threads using xAI's built-in X Search tool. Use this for current discussion, reactions, or claims on X rather than general web pages. Available when xAI credentials are configured (SuperGrok OAuth or XAI_API_KEY).objectstringzWhat to look up on X.)rp   descriptionarrayrp   z;Optional list of X handles to include exclusively (max 10).)rp   itemsr   z/Optional list of X handles to exclude (max 10).z)Optional start date in YYYY-MM-DD format.z'Optional end date in YYYY-MM-DD format.booleanz?Whether xAI should analyze images attached to matching X posts.)rp   r   defaultz?Whether xAI should analyze videos attached to matching X posts.r   r   r   rd   re   r   r   )rp   
propertiesrequired)namer   
parametersc                &   t        | j                  dd      | j                  d      | j                  d      | j                  dd      | j                  dd      t        | j                  dd            t        | j                  d	d            
      S )Nr   r"   r   r   rd   re   r   Fr   r   )r   r   rO   )argskws     r   _handle_x_searchr     s{    hhw#((#6788$89((;+B'#'1Mu(U#V#'1Mu(U#V r(   XAI_API_KEYu   🐦i )r   toolsetschemahandlercheck_fnrequires_envemojimax_result_size_chars)returnDict[str, Any])r   r#   )r   zOptional[str])r   r:   )r   zTuple[str, str, str])r   rO   )rW   Optional[List[str]]rX   r#   r   z	List[str])r_   r#   rX   r#   r   r   )rd   r#   re   r#   r   None)rt   r   r   r#   )rt   r   r   zList[Dict[str, Any]])ra   zrequests.HTTPErrorr   r#   )NNr"   r"   FF)r   r#   r   r   r   r   rd   r#   re   r#   r   rO   r   rO   r   r#   )0__doc__
__future__r   r   loggingr   r   r   r   typingr   r   r	   r
   r   r   tools.registryr   r   tools.xai_httpr   r   	getLoggerr   r   rI   r%   r8   r@   r.   rV   r   r'   r4   r<   rA   rM   rP   r\   rb   rl   rx   r   r   r   X_SEARCH_SCHEMAr   register r(   r   <module>r      s  'R #    - - 3 3  / N			8	$, # #&   ? K
 0(%,&&@&$(< .2.2',',y
y
*y
 ,y
 	y

 y
 !%y
 !%y
 	y
z 	;  !6
   (+\"   (+P# !J
 !H
 "` + "` +9!
D II%.b	   	(
!	r(   