
    jOL                       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dMdZdNdZdOdZ dOdZ!dPdZ"dQdZ#dRdZ$dSd Z%dTd$Z&dUd&Z'dVd(Z(dWd+Z)	 	 	 	 	 	 dXdYd3Z*d4d5d6d7d8d9d:d;d7id<d=d:d;d7id>d=d7d?d9d7d@d9dAdBd-dCdAdDd-dCdEd.gdFdGZ+dH Z, ej-        d4d4e+e,e#dIgdJdKL           dS )Zu  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.20-reasoning      
   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    8/home/ubuntu/.hermes/hermes-agent/tools/x_search_tool.py_load_x_search_configr   E   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%   N   s>    

!
!C  &B''--//I3IJ    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maxr'   r   r$   	raw_values     r   _get_x_search_timeout_secondsr/   S   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,   r'   r   r-   s     r   _get_x_search_retriesr4   \   sc    

!
!C	#;<<I(1c)nn%%% ( ( (''''(r0   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)credsr7   r8   sources       r   _resolve_xai_bearerrA   i   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.
    r7   r!   F)r   rB   r   r   r"   r   )r?   s    r   check_x_search_requirementsrD      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_HANDLES
ValueError)rE   rG   cleanedhandle
normalizeds        r   _normalize_handlesrS      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 )N)r"   r   strptimer   rO   )rT   rG   rawexcs       r   _parse_iso_daterZ      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").
    Nr[   r\   zfrom_date (z ) must be on or before to_date (rV   zC) is in the future; X Search only indexes past posts (today UTC is )	r"   rZ   rO   	isoformatr   nowr   utcr   )r[   r\   parsed_from	parsed_to	today_utcs        r   _validate_date_rangere      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>   textrh   rm   z

)r   r   r"   rL   join)rf   rh   partsitemrl   ctyperm   s          r   _extract_response_textrr      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 )Nri   rj   rk   rl   r   url_citationurlr!   titlestart_index	end_index)rv   rw   rx   ry   )r   rL   )rf   	citationsrp   rl   
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&   rY   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: rm     )getattrr   jsonr   
isinstancedictr   r"   )rY   r   rf   r   r   rk   rm   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                x   | 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ddi}|r||d<   |r||d<   |                                 r|                                 |d<   |                                 r|                                 |d<   |rd	|d
<   |rd	|d<   t                      d|                                  dg|gd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        t/          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        t/          dd|dz   z                       Y d }~Zd }~ww xY w|t          d          |                                }t7          |          }t9          |                    d          pg           }t=          |          }g }|r|                    d           |r|                    d           |                                 r|                    d           |                                 r|                    d           tA          |          o| o| }|rdd !                    |           nd }t5          j"        d	d!|	d|d"         |                                  |||||d#d$          S # t          j        $ rb}t$          #                    d%|d	&           t5          j"        dd!dt)          |          tI          |          j%        d'd$          cY d }~S d }~wt          j        $ re}t$          #                    d(|d	&           t5          j"        dd!dd)t                       d*tI          |          j%        d'd$          cY d }~S d }~wtL          $ rb}t$          #                    d%|d	&           t5          j"        dd!dt	          |          tI          |          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 togetherrj   r   r[   r\   Tr   r   user)rolerl   F)r    inputtoolsstore   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 responserz   z'no citations returned despite filters: z, r;   r    )successr:   credential_sourcetoolr    r   answerrz   inline_citationsdegradeddegraded_reason)ensure_asciizx_search failed: %s)exc_info)r   r:   r   r   
error_typezx_search timed out: %szxAI x_search timed out after z seconds)'r"   r   rA   r<   r   rS   re   rO   r%   r/   r4   rangerequestspostr   raise_for_status	HTTPErrorr   loggerwarningr   timesleepminReadTimeoutConnectionErrorr   rr   listr   r|   rL   rB   rn   dumpsr   rj   __name__r   )r   r   r   r[   r\   r   r   r7   r8   r@   rY   allowedexcludedtool_defrf   r)   max_retriesr   attempter   datar   rz   r   active_filtersr   r   s                               r   x_search_toolr     sN     < <:;;;$$7$9$9!66 $ $ $#c((########$a
$%68KLL%&8:NOO 	bx 	b`aaa	( G4444 	( 	( 	(c#hh''''''''	( %+J#7 	4,3H() 	6-5H)*?? 	6$-OO$5$5H[!==?? 	2")--//HY% 	:59H12% 	:59H12 )** #${{}}  Z

 

 8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*.2Q, "B3 2Q, 3
C#=CC#Q, C##CQ, :AG?<Q, ?LBJQ, L8AL
Q, LEQ, ,V9;ASV9V9*AU
V9
V9AV4.V94V9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.)rj   descriptionarrayrj   z;Optional list of X handles to include exclusively (max 10).)rj   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.)rj   r   defaultz?Whether xAI should analyze videos attached to matching X posts.r   r   r   r[   r\   r   r   )rj   
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   r[   r\   r   Fr   r   )r   r   rB   )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   r5   )r   rB   )rE   rF   rG   r   r   rH   )rT   r   rG   r   r   r   )r[   r   r\   r   r   r]   )rf   r   r   r   )rf   r   r   rs   )rY   r}   r   r   )NNr!   r!   FF)r   r   r   rF   r   rF   r[   r   r\   r   r   rB   r   rB   r   r   ).__doc__
__future__r   r   loggingr   r   r   r   typingr   r   r	   r
   r   r   tools.registryr   r   tools.xai_httpr   r   	getLoggerr   r   r=   r#   r+   r3   rN   r   r%   r/   r4   rA   rD   rS   rZ   re   rr   r|   r   r   X_SEARCH_SCHEMAr   register r&   r   <module>r      s5  ' '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',',r
 r
 r
 r
 r
l 	;  !6 
   (+\" "   (+P# # !J 
 !H 
 "` + + "` + +9!
 !
D II% %. .b	 	 	  	(
!	 	 	 	 	 	r&   