o
    ưi                     @   s   d Z ddlZddlZddlmZmZmZmZmZm	Z	m
Z
 ddlZddlmZ ddlmZ ddlmZ ddlmZ ddlmZmZmZmZ dd	lmZ dd
lmZ ddlmZ G dd deZ dS )z
WebSearch Interception Handler

CustomLogger that intercepts WebSearch tool calls for models that don't
natively support web search (e.g., Bedrock/Claude) and executes them
server-side using litellm router's search tools.
    N)AnyDictListOptionalTupleUnioncast)verbose_logger)messages)LITELLM_WEB_SEARCH_TOOL_NAME)CustomLogger)get_litellm_web_search_tool"get_litellm_web_search_tool_openaiis_web_search_tool"is_web_search_tool_chat_completion)WebSearchTransformation)WebSearchInterceptionConfig)LlmProvidersc                       st  e Zd ZdZ		d8deeeeef   dee f fddZ	de
eef dee d	ee fd
dZeded	d fddZdedee
 de
d	ee
 fddZdededee
 deee
  dedede
d	eee
f fddZdededee
 deee
  dedede
d	eee
f fddZde
dedee
 dedede
dedede
d	efddZde
dedee
 ded e
dedede
d	efd!d"Zed e
de
d	efd#d$Zede
d	e
fd%d&Zdedee
 d'ee
 d(ee
 de
dedede
d	efd)d*Zd+ed	efd,d-Z	.d9dedee
 d'ee
 d e
dedede
d/ed	efd0d1Zd	efd2d3Zed4e
eef d5e
eef d	d fd6d7Z   Z!S ):WebSearchInterceptionLoggerab  
    CustomLogger that intercepts WebSearch tool calls for models that don't
    natively support web search.

    Implements agentic loop:
    1. Detects WebSearch tool_use in model response
    2. Executes litellm.asearch() for each query using router's search tools
    3. Makes follow-up request with search results
    4. Returns final response
    Nenabled_providerssearch_tool_namec                    s@   t    |du rtjjg| _ndd |D | _|| _d| _dS )a  
        Args:
            enabled_providers: List of LLM providers to enable interception for.
                              Use LlmProviders enum values (e.g., [LlmProviders.BEDROCK])
                              If None or empty list, enables for ALL providers.
                              Default: None (all providers enabled)
            search_tool_name: Name of search tool configured in router's search_tools.
                             If None, will attempt to use first available search tool.
        Nc                 S   s    g | ]}t |tr|jn|qS  )
isinstancer   value).0pr   r   j/home/app/Keep/.python/lib/python3.10/site-packages/litellm/integrations/websearch_interception/handler.py
<listcomp>@   s    z8WebSearchInterceptionLogger.__init__.<locals>.<listcomp>F)super__init__r   ZBEDROCKr   r   r   Z_request_has_websearch)selfr   r   	__class__r   r   r   -   s   

z$WebSearchInterceptionLogger.__init__kwargs	call_typereturnc           
   
      s  | ddp| di  dd}|s/ztj| ddd\}}}}W n ty.   d}Y nw || jvr6dS | d}|s?dS tdd	 |D }|sLdS td
 g }|D ]*}t|rzt	 }	|
|	 td| dd d| dd dt  qU|
| qU||d< |S )aE  
        Pre-call hook to convert native Anthropic web_search tools to regular tools.

        This prevents Bedrock from trying to execute web search server-side (which fails).
        Instead, we convert it to a regular tool so the model returns tool_use blocks
        that we can intercept and execute ourselves.
        custom_llm_provider litellm_paramsmodel)r)   Ntoolsc                 s       | ]}t |V  qd S Nr   r   tr   r   r   	<genexpr>b       zMWebSearchInterceptionLogger.async_pre_call_deployment_hook.<locals>.<genexpr>zMWebSearchInterception: Converting native web_search tools to LiteLLM standard!WebSearchInterception: Converted nameunknown (type=typenone) to )getlitellmZget_llm_provider	Exceptionr   anyr	   debugr   r   appendr   )
r    r#   r$   r&   _r*   has_websearchconverted_toolstoolZconverted_toolr   r   r   async_pre_call_deployment_hookG   sD     



z:WebSearchInterceptionLogger.async_pre_call_deployment_hookconfigc              	   C   sp   | dd}| dd}d}|dur2g }|D ]}zt|}|| W q ty1   || Y qw | ||dS )a  
        Initialize WebSearchInterceptionLogger from proxy config.yaml parameters.

        Args:
            config: Configuration dictionary from litellm_settings.websearch_interception_params

        Returns:
            Configured WebSearchInterceptionLogger instance

        Example:
            From proxy_config.yaml:
                litellm_settings:
                  websearch_interception_params:
                    enabled_providers: ["bedrock"]
                    search_tool_name: "my-perplexity-search"

            Usage:
                config = litellm_settings.get("websearch_interception_params", {})
                logger = WebSearchInterceptionLogger.from_config_yaml(config)
        r   Nr   )r   r   )r9   r   r>   
ValueError)clsrD   Zenabled_providers_strr   r   providerZprovider_enumr   r   r   from_config_yaml~   s    z,WebSearchInterceptionLogger.from_config_yamlr)   r
   c           
   
      sF  | di  dd}td| d| jpd  | jdur1|| jvr1td| d	| j  dS | d
}|s:dS tdd |D }|sGdS td|  g }|D ]*}t|rxt }	||	 td| dd d| dd dt  qS|| qS||d
< tddd |D   | drtd d|d< d|d< |S )a  
        Pre-request hook to convert native web search tools to LiteLLM standard.

        This hook is called before the API request is made, allowing us to:
        1. Detect native web search tools (web_search_20250305, etc.)
        2. Convert them to LiteLLM standard format (litellm_web_search)
        3. Convert stream=True to stream=False for interception

        This prevents providers like Bedrock from trying to execute web search
        natively (which fails), and ensures our agentic loop can intercept tool_use.

        Returns:
            Modified kwargs dict with converted tools, or None if no modifications needed
        r(   r&   r'   zEWebSearchInterception: Pre-request hook called - custom_llm_provider=z - enabled_providers=ALLNz+WebSearchInterception: Skipping - provider z not in r*   c                 s   r+   r,   r-   r.   r   r   r   r0      r1   zEWebSearchInterceptionLogger.async_pre_request_hook.<locals>.<genexpr>z?WebSearchInterception: Pre-request hook triggered for provider=r2   r3   r4   r5   r6   r7   r8   z/WebSearchInterception: Tools after conversion: c                 S   s   g | ]}| d qS )r3   )r9   r.   r   r   r   r      s    zFWebSearchInterceptionLogger.async_pre_request_hook.<locals>.<listcomp>streamz=WebSearchInterception: Converting stream=True to stream=FalseFTZ(_websearch_interception_converted_stream)	r9   r	   r=   r   r<   r   r   r>   r   )
r    r)   r
   r#   r&   r*   r@   rA   rB   Zstandard_toolr   r   r   async_pre_request_hook   sb   



z2WebSearchInterceptionLogger.async_pre_request_hookresponser*   rJ   r&   c                    s  t d| d|  t dt|  | jdur1|| jvr1t d| d| j d di fS td	d
 |p8g D }|sGt d di fS tj||dd\}	}
|	s\t d di fS t dt|
 d g }t|t	ru|
dg }nt|dg p|g }|D ]H}t|t	r|
d}nt|dd}|dv rt|t	r|| qd|i}|dkrt|dd|d< t|dd|d< nt|dd|d< || q|rt dt| d |
d|d|d}d|fS )z
        Check if WebSearch tool interception is needed for Anthropic Messages API.
        
        This is the legacy method for Anthropic-style responses.
        For chat completions, use async_should_run_chat_completion_agentic_loop instead.
        z-WebSearchInterception: Hook called! provider=	, stream=&WebSearchInterception: Response type: N)WebSearchInterception: Skipping provider  (not in enabled list: )Fc                 s   r+   r,   r-   r.   r   r   r   r0     r1   zLWebSearchInterceptionLogger.async_should_run_agentic_loop.<locals>.<genexpr>z4WebSearchInterception: No web search tool in requestZ	anthropicrL   rJ   response_formatzAWebSearchInterception: No WebSearch tool_use detected in response WebSearchInterception: Detected / WebSearch tool call(s), executing agentic loopcontentr6   )thinkingZredacted_thinkingrW   r'   	signaturedataz!WebSearchInterception: Extracted z  thinking block(s) from response	websearch)
tool_calls	tool_typerG   rS   thinking_blocksT)r	   r=   r6   r   r<   r   transform_requestlenr   dictr9   getattrr>   )r    rL   r)   r
   r*   rJ   r&   r#   has_websearch_toolshould_interceptr[   r]   rV   blockZ
block_typeZthinking_block_dict
tools_dictr   r   r   async_should_run_agentic_loop   s|   





z9WebSearchInterceptionLogger.async_should_run_agentic_loopc                    s   t d| d|  t dt|  | jdur1|| jvr1t d| d| j d di fS td	d
 |p8g D }|sGt d di fS tj||dd\}	}
|	s\t d di fS t dt|
 d |
d|dd}d|fS )z
        Check if WebSearch tool interception is needed for Chat Completions API.
        
        Similar to async_should_run_agentic_loop but for OpenAI-style chat completions.
        z=WebSearchInterception: Chat completion hook called! provider=rM   rN   NrO   rP   rQ   Fc                 s   r+   r,   )r   r.   r   r   r   r0   }  r1   z\WebSearchInterceptionLogger.async_should_run_chat_completion_agentic_loop.<locals>.<genexpr>z<WebSearchInterception: No litellm_web_search tool in requestopenairR   zCWebSearchInterception: No WebSearch tool_calls detected in responserT   rU   rZ   )r[   r\   rG   rS   T)r	   r=   r6   r   r<   r   r^   r_   )r    rL   r)   r
   r*   rJ   r&   r#   rb   rc   r[   re   r   r   r   -async_should_run_chat_completion_agentic_loopb  sB   
zIWebSearchInterceptionLogger.async_should_run_chat_completion_agentic_loop"anthropic_messages_provider_config*anthropic_messages_optional_request_paramslogging_objc
              
      sL   |d }
| dg }tdt|
 d | j|||
|||||	dI dH S )z
        Execute agentic loop with WebSearch execution for Anthropic Messages API.
        
        This is the legacy method for Anthropic-style responses.
        r[   r]   z2WebSearchInterception: Executing agentic loop for  search(es))r)   r
   r[   r]   rj   rk   rJ   r#   N)r9   r	   r=   r_   _execute_agentic_loop)r    r*   r)   r
   rL   ri   rj   rk   rJ   r#   r[   r]   r   r   r   async_run_agentic_loop  s    z2WebSearchInterceptionLogger.async_run_agentic_loopoptional_paramsc	              
      sL   |d }	| dd}
tdt|	 d | j|||	|||||
dI dH S )z
        Execute agentic loop with WebSearch execution for Chat Completions API.
        
        Similar to async_run_agentic_loop but for OpenAI-style chat completions.
        r[   rS   rg   zBWebSearchInterception: Executing chat completion agentic loop for rl   )r)   r
   r[   ro   rk   rJ   r#   rS   N)r9   r	   r=   r_   %_execute_chat_completion_agentic_loop)r    r*   r)   r
   rL   ro   rk   rJ   r#   r[   rS   r   r   r   &async_run_chat_completion_agentic_loop  s    zBWebSearchInterceptionLogger.async_run_chat_completion_agentic_loopc                 C   s   |  d| dd}|  d}|rDt|trD| d}|durDt|ttfrDt|rD|dkrD||krDt|d }t	d||| |}|S )zExtract max_tokens and validate against thinking.budget_tokens.

        Anthropic API requires ``max_tokens > thinking.budget_tokens``.
        If the constraint is violated, auto-adjust to ``budget_tokens + 1024``.
        
max_tokensi   rW   budget_tokensNr   zvWebSearchInterception: max_tokens=%s <= thinking.budget_tokens=%s, adjusting to %s to satisfy Anthropic API constraint)
r9   r   r`   intfloatmathisfiniteceilr	   r=   )ro   r#   rr   Zthinking_paramrs   adjustedr   r   r   _resolve_max_tokens  s*   



z/WebSearchInterceptionLogger._resolve_max_tokensc                    s   dh  fdd|   D S )u  Build kwargs for the follow-up call, excluding internal keys.

        ``litellm_logging_obj`` MUST be excluded so the follow-up call creates
        its own ``Logging`` instance via ``function_setup``.  Reusing the
        initial call's logging object triggers the dedup flag
        (``has_logged_async_success``) which silently prevents the initial
        call's spend from being recorded — the root cause of the
        SpendLog / AWS billing mismatch.
        litellm_logging_objc                    (   i | ]\}}| d s| vr||qS _websearch_interception
startswithr   kvZ_internal_keysr   r   
<dictcomp>      zHWebSearchInterceptionLogger._prepare_followup_kwargs.<locals>.<dictcomp>)items)r#   r   r   r   _prepare_followup_kwargs  s   
z4WebSearchInterceptionLogger._prepare_followup_kwargsr[   r]   c	                    s  g }	|D ]/}
|
d  d}|r"td| d |	| | qtd|
d  d |	|   qtdt|	 d	 tj|	d
diI dH }g }t	|D ]C\}}t
|trrtd| dt|  |dt|  qQt
|tr|tt| qQtdt| d|  |t| qQtj|||d\}}||tt|g }td tdt|  td|  t|ddp| dd}|}z\| ||}td| d dd | D }| |}|dur|j di }| d|}td|  tjd#|||d||I dH }td t|  td!|  |W S  tyG } ztd"||t|t|t|  d}~ww )$z3Execute litellm.search() and make follow-up requestinputquery1WebSearchInterception: Queuing search for query=''!WebSearchInterception: Tool call id has no query!WebSearchInterception: Executing  search(es) in parallelreturn_exceptionsTNWebSearchInterception: Search  failed with error: Search failed: .WebSearchInterception: Unexpected result type 
 at index )r[   search_resultsr]   zCWebSearchInterception: Making follow-up request with search results1WebSearchInterception: Follow-up messages count: z3WebSearchInterception: Last message (tool_result): Zlitellm_call_idr4   z(WebSearchInterception: Using max_tokens=z for follow-up requestc                 S   s   i | ]\}}|d kr||qS )rr   r   r   r   r   r   r   v  
    zEWebSearchInterceptionLogger._execute_agentic_loop.<locals>.<dictcomp>Zagentic_loop_paramsr)   )WebSearchInterception: Using model name: )rr   r
   r)   CWebSearchInterception: Follow-up request completed, response type: z'WebSearchInterception: Final response: zaWebSearchInterception: Follow-up request failed [call_id=%s model=%s messages=%d searches=%d]: %sr   )r9   r	   r=   r>   _execute_search_create_empty_search_resultr_   asynciogather	enumerater   r;   errorstrr   r6   r   transform_responser   ra   rz   r   r   Zmodel_call_detailsanthropic_messagesZacreate	exception)r    r)   r
   r[   r]   rj   rk   rJ   r#   search_tasks	tool_callr   r   final_search_resultsiresultassistant_messageZuser_messagefollow_up_messagesZ_call_idfull_model_namerr   Z"optional_params_without_max_tokenskwargs_for_followupZagentic_paramsfinal_responseer   r   r   rm     s   







z1WebSearchInterceptionLogger._execute_agentic_loopr   c           
   
      s  zzddl m} W n ty   td d}Y nw d}|durvt|drv jrZ fdd|jD }|rP|d }|di d	}td
 j d| d n
td j d |sv|jrv|jd }|di d	}td| d |sd}td| d td| d| d t	j
||dI dH }t|}td| dt| d |W S  ty }	 ztd| dt|	   d}	~	ww )z7Execute a single web search using router's search toolsr   )
llm_routerz~WebSearchInterception: Could not import llm_router from proxy_server, falling back to direct litellm.asearch() with perplexityNsearch_toolsc                    s    g | ]}| d  jkr|qS )r   )r9   r   )r   rB   r    r   r   r     s
    z?WebSearchInterceptionLogger._execute_search.<locals>.<listcomp>r(   search_providerz*WebSearchInterception: Found search tool 'z' with provider 'r   z$WebSearchInterception: Search tool 'zD' not found in router, falling back to first available or perplexityzHWebSearchInterception: Using first available search tool with provider 'Z
perplexityzUWebSearchInterception: No search tools configured in router, using default provider 'z-WebSearchInterception: Executing search for 'z' using provider ')r   r   z-WebSearchInterception: Search completed for 'z', got z charsz*WebSearchInterception: Search failed for 'z': )Zlitellm.proxy.proxy_serverr   ImportErrorr	   r=   hasattrr   r   r9   r:   Zasearchr   Zformat_search_responser_   r;   r   r   )
r    r   r   r   Zmatching_toolsZsearch_toolZ
first_toolr   Zsearch_result_textr   r   r   r   r     st   





z+WebSearchInterceptionLogger._execute_searchrg   rS   c	              
      s  g }	|D ][}
d}d|
v rt |
d tr|
d d}nd|
v r9|
d }t |tr9|di }t |tr9|d}|rMtd| d |	| | qtd|
d	 d
 |	|   qtdt|	 d t	j
|	ddiI dH }g }t|D ]C\}}t |trtd| dt|  |dt|  q}t |tr|tt| q}tdt| d|  |t| q}tj|||d\}}|dkr||g ttt | }n	||tt|g }td tdt|  z`h d  fdd| D }|}d|v r!|d }||s!d|vr!| d| }td|  |d}dd | D }tjd#|||d ||I dH }td!t|  |W S  tyl } ztd"t|   d}~ww )$zCExecute litellm.search() and make follow-up chat completion requestNr   r   function	argumentsr   r   r   r   r   r   r   r   Tr   r   r   r   r   )r[   r   rS   rg   zSWebSearchInterception: Making follow-up chat completion request with search resultsr   >   r~   model_alias_mapr{   r&   acompletionstream_responsecustom_prompt_dictc                    r|   r}   r   r   Zinternal_paramsr   r   r   E  r   zUWebSearchInterceptionLogger._execute_chat_completion_agentic_loop.<locals>.<dictcomp>r&   /r   r*   c                 S   s   i | ]\}}|d vr||qS )>   Z
extra_bodyr   r   r*   r   r   r   r   r   r   r   ]  r   )r)   r
   r*   r   z1WebSearchInterception: Follow-up request failed: r   )r   r`   r9   r	   r=   r>   r   r   r_   r   r   r   r;   r   r   r   r6   r   r   r   r   r   r   r:   r   r   )r    r)   r
   r[   ro   rk   rJ   r#   rS   r   r   r   funcargsr   r   r   r   r   Ztool_messages_or_userr   r   r   r&   Ztools_paramZoptional_params_cleanr   r   r   r   r   rp     s   







	


zAWebSearchInterceptionLogger._execute_chat_completion_agentic_loopc                    s   dS )z<Create an empty search result for tool calls without querieszNo search query providedr   r   r   r   r   r   t  s   z7WebSearchInterceptionLogger._create_empty_search_resultlitellm_settingscallback_specific_paramsc                 C   s0   i }d| v r| d }nd|v r|d }t |S )a  
        Static method to initialize WebSearchInterceptionLogger from proxy config.

        Used in callback_utils.py to simplify initialization logic.

        Args:
            litellm_settings: Dictionary containing litellm_settings from proxy_config.yaml
            callback_specific_params: Dictionary containing callback-specific parameters

        Returns:
            Configured WebSearchInterceptionLogger instance

        Example:
            From callback_utils.py:
                websearch_obj = WebSearchInterceptionLogger.initialize_from_proxy_config(
                    litellm_settings=litellm_settings,
                    callback_specific_params=callback_specific_params
                )
        Zwebsearch_interception_paramsZwebsearch_interception)r   rH   )r   r   Zwebsearch_paramsr   r   r   initialize_from_proxy_configx  s   

z8WebSearchInterceptionLogger.initialize_from_proxy_config)NN)rg   )"__name__
__module____qualname____doc__r   r   r   r   r   r   r   r   r`   rC   classmethodr   rH   rK   boolr   rf   rh   rn   rq   staticmethodrt   rz   r   rm   r   rp   r   r   __classcell__r   r   r!   r   r   !   sL   

7-
M

	
i

	
<	

$	

# 	

 P	

 

r   )!r   r   rv   typingr   r   r   r   r   r   r   r:   Zlitellm._loggingr	   Zlitellm.anthropic_interfacer
   r   Zlitellm.constantsr   Z"litellm.integrations.custom_loggerr   Z1litellm.integrations.websearch_interception.toolsr   r   r   r   Z:litellm.integrations.websearch_interception.transformationr   Z1litellm.types.integrations.websearch_interceptionr   Zlitellm.types.utilsr   r   r   r   r   r   <module>   s    $