""" Calls Linkup's /search endpoint to search the web. Linkup API Reference: https://docs.linkup.so/pages/documentation/api-reference/endpoint/post-search """ from typing import Dict, List, Literal, Optional, TypedDict, Union import httpx from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.base_llm.search.transformation import ( BaseSearchConfig, SearchResponse, SearchResult, ) from litellm.secret_managers.main import get_secret_str class _LinkupSearchRequestRequired(TypedDict): """Required fields for Linkup Search API request.""" q: str # Required - The natural language question for which you want to retrieve context depth: Literal["deep", "standard"] # Required - Defines the precision of the search outputType: Literal[ "searchResults", "sourcedAnswer", "structured" ] # Required - The type of output class LinkupSearchRequest(_LinkupSearchRequestRequired, total=False): """ Linkup Search API request format. Based on: https://docs.linkup.so/pages/documentation/api-reference/endpoint/post-search """ structuredOutputSchema: str # Required only when outputType is "structured" includeSources: bool # Optional - Include sources in response (default false) includeImages: bool # Optional - Include images in results (default false) fromDate: str # Optional - Start date for results (YYYY-MM-DD) toDate: str # Optional - End date for results (YYYY-MM-DD) includeDomains: List[str] # Optional - Domains to search on (max 100) excludeDomains: List[str] # Optional - Domains to exclude includeInlineCitations: bool # Optional - Include inline citations (default false) maxResults: int # Optional - Maximum number of results to return class LinkupSearchConfig(BaseSearchConfig): LINKUP_API_BASE = "https://api.linkup.so/v1" @staticmethod def ui_friendly_name() -> str: return "Linkup" def validate_environment( self, headers: Dict, api_key: Optional[str] = None, api_base: Optional[str] = None, **kwargs, ) -> Dict: """ Validate environment and return headers. """ api_key = api_key or get_secret_str("LINKUP_API_KEY") if not api_key: raise ValueError( "LINKUP_API_KEY is not set. Set `LINKUP_API_KEY` environment variable." ) headers["Authorization"] = f"Bearer {api_key}" headers["Content-Type"] = "application/json" return headers def get_complete_url( self, api_base: Optional[str], optional_params: dict, data: Optional[Union[Dict, List[Dict]]] = None, **kwargs, ) -> str: """ Get complete URL for Search endpoint. """ api_base = api_base or get_secret_str("LINKUP_API_BASE") or self.LINKUP_API_BASE # Append "/search" to the api base if it's not already there if not api_base.endswith("/search"): api_base = f"{api_base}/search" return api_base def transform_search_request( self, query: Union[str, List[str]], optional_params: dict, **kwargs, ) -> Dict: """ Transform Search request to Linkup API format. Transforms Perplexity unified spec parameters: - query -> q - max_results -> maxResults - search_domain_filter -> includeDomains - country -> (not directly supported) - max_tokens_per_page -> (not applicable) All other Linkup-specific parameters are passed through as-is. Args: query: Search query (string or list of strings). Linkup only supports single string queries. optional_params: Optional parameters for the request Returns: Dict with typed request data following LinkupSearchRequest spec """ if isinstance(query, list): # Linkup only supports single string queries, join with spaces query = " ".join(query) request_data: LinkupSearchRequest = { "q": query, "depth": optional_params.get("depth", "standard"), "outputType": optional_params.get("outputType", "searchResults"), } # Transform Perplexity unified spec parameters to Linkup format if "max_results" in optional_params: request_data["maxResults"] = optional_params["max_results"] if "search_domain_filter" in optional_params: request_data["includeDomains"] = optional_params["search_domain_filter"] # Convert to dict before dynamic key assignments result_data = dict(request_data) # pass through all other parameters as-is for param, value in optional_params.items(): if ( param not in self.get_supported_perplexity_optional_params() and param not in result_data ): result_data[param] = value return result_data def transform_search_response( self, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, **kwargs, ) -> SearchResponse: """ Transform Linkup API response to LiteLLM unified SearchResponse format. Linkup -> LiteLLM mappings: - results[].name -> SearchResult.title - results[].url -> SearchResult.url - results[].content -> SearchResult.snippet - No date field in results (set to None) - No last_updated field in Linkup response (set to None) Args: raw_response: Raw httpx response from Linkup API logging_obj: Logging object for tracking Returns: SearchResponse with standardized format """ response_json = raw_response.json() # Transform results to SearchResult objects results = [] # Process results array raw_results = response_json.get("results", []) for result in raw_results: # Handle both text and image result types result_type = result.get("type", "text") if result_type == "text": search_result = SearchResult( title=result.get("name", ""), url=result.get("url", ""), snippet=result.get("content", ""), date=None, last_updated=None, ) results.append(search_result) elif result_type == "image": # For image results, use the URL as both title and snippet if name not provided search_result = SearchResult( title=result.get("name", result.get("url", "")), url=result.get("url", ""), snippet=result.get("content", ""), date=None, last_updated=None, ) results.append(search_result) return SearchResponse( results=results, object="search", )