import requests import logging from typing import Iterator, List, Literal, Union from langchain_core.document_loaders import BaseLoader from langchain_core.documents import Document from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) class TavilyLoader(BaseLoader): """Extract web page content from URLs using Tavily Extract API. This is a LangChain document loader that uses Tavily's Extract API to retrieve content from web pages and return it as Document objects. Args: urls: URL or list of URLs to extract content from. api_key: The Tavily API key. extract_depth: Depth of extraction, either "basic" or "advanced". continue_on_failure: Whether to continue if extraction of a URL fails. """ def __init__( self, urls: Union[str, List[str]], api_key: str, extract_depth: Literal["basic", "advanced"] = "basic", continue_on_failure: bool = True, ) -> None: """Initialize Tavily Extract client. Args: urls: URL or list of URLs to extract content from. api_key: The Tavily API key. include_images: Whether to include images in the extraction. extract_depth: Depth of extraction, either "basic" or "advanced". advanced extraction retrieves more data, including tables and embedded content, with higher success but may increase latency. basic costs 1 credit per 5 successful URL extractions, advanced costs 2 credits per 5 successful URL extractions. continue_on_failure: Whether to continue if extraction of a URL fails. """ if not urls: raise ValueError("At least one URL must be provided.") self.api_key = api_key self.urls = urls if isinstance(urls, list) else [urls] self.extract_depth = extract_depth self.continue_on_failure = continue_on_failure self.api_url = "https://api.tavily.com/extract" def lazy_load(self) -> Iterator[Document]: """Extract and yield documents from the URLs using Tavily Extract API.""" batch_size = 20 for i in range(0, len(self.urls), batch_size): batch_urls = self.urls[i : i + batch_size] try: headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}", } # Use string for single URL, array for multiple URLs urls_param = batch_urls[0] if len(batch_urls) == 1 else batch_urls payload = {"urls": urls_param, "extract_depth": self.extract_depth} # Make the API call response = requests.post(self.api_url, headers=headers, json=payload) response.raise_for_status() response_data = response.json() # Process successful results for result in response_data.get("results", []): url = result.get("url", "") content = result.get("raw_content", "") if not content: log.warning(f"No content extracted from {url}") continue # Add URLs as metadata metadata = {"source": url} yield Document( page_content=content, metadata=metadata, ) for failed in response_data.get("failed_results", []): url = failed.get("url", "") error = failed.get("error", "Unknown error") log.error(f"Failed to extract content from {url}: {error}") except Exception as e: if self.continue_on_failure: log.error(f"Error extracting content from batch {batch_urls}: {e}") else: raise e