
    .cjyP                       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dPdZdQdZ dRdZ!dSdZ"dSdZ#dTdZ$dUdZ%dVd Z&dWd#Z'dXd'Z(dYd)Z)dZd+Z*d[d.Z+	 	 	 	 	 	 d\d]d6Z,d7d8d9d:d;d<d=d>d:id?d@d=d>d:idAd@d:dBd<d:dCd<dDdEd0dFdDdGd0dFdHd1gdIdJZ-dK Z. ej/        d7d7e-e.e%dLgdMdNO           dS )^u  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
   returnDict[str, Any]c                 t    	 ddl m}   |                                 di           pi S # t          $ r i cY S w xY w)Nr   load_configx_search)hermes_cli.configr   get	Exceptionr   s    5/home/ice/.hermes/hermes-agent/tools/x_search_tool.py_load_x_search_configr!   F   s^    111111{}}  R006B6   			s   %( 77strc                     t                      } t          |                     d          pd                                          pt          S )Nmodel )r!   r"   r   stripDEFAULT_X_SEARCH_MODEL)cfgs    r    _get_x_search_modelr)   O   s>    

!
!C  &B''--//I3IJ    Optional[str]c                 n   t                      } |                     d          }|!t          |                                          sd S t          |                                                                          }|t
          vr0d                    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_effortr7   T   s    

!
!C*++II 4 4 6 6t^^!!##))++F///))677# # ## # #
 
 	
 Mr*   intc                     t                      } |                     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maxr8   r   r(   r4   s     r    _get_x_search_timeout_secondsr?   d   sd    

!
!C)+KLLI02s9~~&&& 0 0 0////0   A AAc                     t                      } |                     dt                    }	 t          dt	          |                    S # t
          $ r
 t          cY S w xY w)Nretriesr   )r!   r   DEFAULT_X_SEARCH_RETRIESr=   r8   r   r>   s     r    _get_x_search_retriesrD   m   sc    

!
!C	#;<<I(1c)nn%%% ( ( (''''(r@   Tuple[str, str, str]c                    t                      } t          |                     d          pd                                          }|st	          d          t          |                     d          pt
                                                                        d          }t          |                     d          p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)credsrG   rH   sources       r    _resolve_xai_bearerrQ   z   s     )**E%))I&&,"--3355G 
O
 
 	
 599Z((@,@AAGGIIPPQTUUH:&&/%00FHf$$r*   boolc                     	 t                      } t          t          |                     d          pd                                                    S # t
          $ r Y dS 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.
    rG   r%   F)r   rR   r"   r   r&   r   )rO   s    r    check_x_search_requirementsrT      sg    ,..C		),,23399;;<<<   uus   AA 
A! A!handlesOptional[List[str]]
field_name	List[str]c                   g }| pg D ]O}t          |pd                                                              d          }|r|                    |           Pt	          |          t
          k    rt          | dt
           d          |S )Nr%   @z supports at most z handles)r"   r&   lstripappendlenMAX_HANDLESr3   )rU   rW   cleanedhandle
normalizeds        r    _normalize_handlesrb      s    G-R ' '2&&,,..55c::
 	'NN:&&&
7||k!!JOO+OOOPPPNr*   valuer   c                    |                                  }	 t          j        |d                                          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   r3   )rc   rW   rawexcs       r    _parse_iso_daterh      s}     ++--C j1166888   <<C<<<
 
	s   &= 
A"AA"	from_dateto_dateNonec                   d}d}|                                  rt          | d          }|                                 rt          |d          }|rB|r@||k    r:t          d|                                 d|                                 d          |pt	          j        t          j                                                  }||k    r<t          d|                                 d|                                 d          dS dS )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").
    Nri   rj   z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&   rh   r3   	isoformatr   nowr   utcr   )ri   rj   parsed_from	parsed_to	today_utcs        r    _validate_date_rangers      s?    #'K $I >%i==}} 8#GY77	 
y 
[9%<%<1+//11 1 1!++--1 1 1
 
 	
 L..3355	"",k3355 , ,&&((, , ,   ""r*   payloadc                4   t          |                     d          pd                                          }|r|S g }|                     dg           pg D ]}|                    d          dk    r|                    dg           pg D ]h}|                    d          }|dv rMt          |                    d          pd                                          }|r|                    |           id	                    |                                          S )
Noutput_textr%   outputtypemessagecontent>   textrv   r{   z

)r"   r   r&   r\   r2   )rt   rv   partsitemrz   ctyper{   s          r    _extract_response_textr      s$   gkk-006B77==??K EHb))/R ' '88Fy((xx	2..4" 	' 	'GKK''E///7;;v..4"55;;== 'LL&&&	' ;;u##%%%r*   List[Dict[str, Any]]c                   g }|                      dg           pg D ]}|                     d          dk    r|                     dg           pg D ]}|                     dg           pg D ]}|                     d          dk    r|                    |                     dd          |                     d	d          |                     d
          |                     d          d           |S )Nrw   rx   ry   rz   r   url_citationurlr%   titlestart_index	end_index)r   r   r   r   )r   r\   )rt   	citationsr}   rz   
annotations        r    _extract_inline_citationsr      s   &(IHb))/R  88Fy((xx	2..4" 	 	G%kk-<<B 
 

>>&))^;;  )~~eR88!+!<!<'1~~m'D'D%/^^K%@%@	    
	 r*   rg   requests.HTTPErrorc                   t          | dd           }|t          |           S 	 |                                }n# t          $ r d }Y nw xY wt	          |t
                    rt          |                    d          pd                                          }t          |                    d          pd                                          }|pt          |          }|r||vr| d| }|pt          |           S t          t          |dd          pd                                          }|r
|d d         S t          |           S )Nresponsecoder%   errorz: r{     )getattrr"   jsonr   
isinstancedictr   r&   )rg   r   rt   r   r   ry   r{   s          r    _http_error_messager     sU   sJ--H3xx--//    '4   #7;;v&&,"--3355GKK((.B//5577'3w<< 	+D''****G"#c(("wx,,23399;;D DSDzs88Os   9 AAr%   Fqueryallowed_x_handlesexcluded_x_handlesenable_image_understandingenable_video_understandingc                   | r|                                  st          d          S 	 t                      \  }}}	n3# t          $ r&}
t          t	          |
                    cY d }
~
S d }
~
ww xY w	 t          |d          }t          |d          }|r|rt          d          S 	 t          ||           n3# t          $ r&}
t          t	          |
                    cY d }
~
S d }
~
ww xY w	 t                      }n3# t          $ r&}
t          t	          |
                    cY d }
~
S d }
~
ww xY wddi}|r||d<   |r||d<   |                                 r|                                 |d<   |                                 r|                                 |d<   |rd	|d
<   |rd	|d<   t                      d|                                  dg|gdd}|rd|i|d<   t                      }t                      }d }t          |dz             D ]`}	 t          j        | dd| dt                      d||          }|                                  n# t          j        $ r}t%          t%          |dd           dd           }||dk     s||k    r t&                              d|dz   |dz   t+          |                     t-          j        t1          dd|dz   z                       Y d }~d }~wt          j        t          j        f$ r]}||k    r t&                              d|dz   |dz   |           t-          j        t1          dd|dz   z                       Y d }~Zd }~ww xY w|t          d          |                                }t9          |          }t;          |                    d           pg           }t?          |          }g }|r|                     d           |r|                     d           |                                 r|                     d           |                                 r|                     d           tC          |          o| o| }|rd!d""                    |           nd }t7          j#        d	d#|	d|d$         |                                  |||||d%d&          S # t          j        $ rb}t&          $                    d'|d	(           t7          j#        dd#dt+          |          tK          |          j&        d)d&          cY d }~S d }~wt          j        $ re}t&          $                    d*|d	(           t7          j#        dd#dd+t                       d,tK          |          j&        d)d&          cY d }~S d }~wtN          $ rb}t&          $                    d'|d	(           t7          j#        dd#dt	          |          tK          |          j&        d)d&          cY d }~S d }~ww xY w)-Nzquery is required for x_searchr   r   z@allowed_x_handles and excluded_x_handles cannot be used togetherrx   r   ri   rj   Tr   r   user)rolerz   F)r$   inputtoolsstorer5   	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.   rK   r$   )successrJ   credential_sourcetoolr$   r   answerr   inline_citationsdegradeddegraded_reason)ensure_asciizx_search failed: %s)exc_info)r   rJ   r   r   
error_typezx_search timed out: %szxAI x_search timed out after z seconds)(r&   r   rQ   rL   r"   rb   rs   r3   r7   r)   r?   rD   rangerequestspostr   raise_for_status	HTTPErrorr   loggerwarningr   timesleepminReadTimeoutConnectionErrorr   r   listr   r   r\   rR   r2   dumpsr   rx   __name__r   )r   r   r   ri   rj   r   r   rG   rH   rP   rg   r6   excludedr-   tool_defrt   r:   max_retriesr   attempter   datar   r   r   active_filtersr   r   s                                r    x_search_toolr   #  s     < <:;;;$$7$9$9!66 $ $ $#c((########$h
$%68KLL%&8:NOO 	bx 	b`aaa	( G4444 	( 	( 	(c#hh''''''''	(	(=?? 	( 	( 	(c#hh''''''''	( %+J#7 	4,3H() 	6-5H)*?? 	6$-OO$5$5H[!==?? 	2")--//HY% 	:59H12% 	:59H12 )** #${{}}  Z

 

  	@$,.>#?GK 799+--04[1_-- "	: "	:G!:#=+++)<7)<)<(:&;&=&= 
 !+	 	 	 ))+++% 
: 
: 
:%gaT&B&BMSWXX&+*;*;w+?U?UDaK!O'**	   
3sC7Q;$78899999999((*BC 	: 	: 	:k))EaK!O	   
3sC7Q;$78899999999	: KLLL}}'--+..4"55	4T:: %' 	7!!"5666 	8!!"6777?? 	/!!+...==?? 	-!!),,,''R	MRBR>R Qdii6O6OQQQ 	 z!%+" ) &$4$#2  
 
 
 	
   
 
 
*A===z !",Q//"1gg.  	
 	
 	
 		
 		
 		
 		
 		
 		
  
 
 
-q4@@@z !"b9V9X9Xbbb"1gg.  	
 	
 	
 		
 		
 		
 		
 		
 		
  
 
 
*A===z !"Q"1gg.  	
 	
 	
 		
 		
 		
 		
 		
 		

s  : 
A*A%A*%A*.2R8 "B3 2R8 3
C#=CC#R8 C##R8 'C6 5R8 6
D& D!D&R8 !D&&CR8 AIR8 M!BK&!R8 &M!AMR8 M!!ER8 8XAT$X$X6AVXX#AX :X Xr   u  Search X (Twitter) posts, profiles, and threads using xAI's built-in X Search tool. Read-only discovery only: use this for current discussion, reactions, or claims on public X rather than general web pages. Do not use it to post, reply, like, DM, upload media, delete, or inspect the user's authenticated X account — those require a separate authenticated X API surface outside this tool. Available when xAI credentials are configured (SuperGrok OAuth or XAI_API_KEY).objectstringzWhat to look up on X.)rx   descriptionarrayrx   z;Optional list of X handles to include exclusively (max 10).)rx   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.)rx   r   defaultz?Whether xAI should analyze videos attached to matching X posts.r   r   r   ri   rj   r   r   )rx   
propertiesrequired)namer   
parametersc                v   t          |                     dd          |                     d          |                     d          |                     dd          |                     dd          t          |                     dd                    t          |                     d	d                    
          S )Nr   r%   r   r   ri   rj   r   Fr   r   )r   r   rR   )argskws     r    _handle_x_searchr     s    hhw##((#67788$899((;++B''#'1Mu(U(U#V#V#'1Mu(U(U#V#V   r*   XAI_API_KEYu   🐦i )r   toolsetschemahandlercheck_fnrequires_envemojimax_result_size_chars)r   r   )r   r"   )r   r+   )r   r8   )r   rE   )r   rR   )rU   rV   rW   r"   r   rX   )rc   r"   rW   r"   r   r   )ri   r"   rj   r"   r   rk   )rt   r   r   r"   )rt   r   r   r   )rg   r   r   r"   )NNr%   r%   FF)r   r"   r   rV   r   rV   ri   r"   rj   r"   r   rR   r   rR   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   rM   r'   r<   rC   r1   r^   r!   r)   r7   r?   rD   rQ   rT   rb   rh   rs   r   r   r   r   X_SEARCH_SCHEMAr   register r*   r    <module>r      sP  ' 'R # " " " " "    - - - - - - - - - - 3 3 3 3 3 3 3 3 3 3 3 3 3 3  / / / / / / / / N N N N N N N N		8	$	$, # #&   ?    K K K K
    0 0 0 0( ( ( (% % % %,   &      &   @& & & &$   (   < .2.2',',y
 y
 y
 y
 y
z 	P  !6 
   (+\" "   (+P# # !J 
 !H 
 "` + + "` + +9!
 !
D II% %1 1h	 	 	  	(
!	 	 	 	 	 	r*   