| import xml.etree.ElementTree as ET |
| from typing import Dict, Any, Optional, List |
| import re |
|
|
| from .tool import Tool, Toolkit |
| from .storage_handler import FileStorageHandler |
| from .request_base import RequestBase |
|
|
|
|
| class ArxivBase(RequestBase): |
| """ |
| Extended RequestBase class for arXiv API interactions. |
| Provides specialized methods for working with arXiv's Atom XML API. |
| """ |
| |
| def __init__(self, **kwargs): |
| super().__init__(**kwargs) |
| self.base_url = "http://export.arxiv.org/api/query" |
| self.atom_namespace = "http://www.w3.org/2005/Atom" |
| self.arxiv_namespace = "http://arxiv.org/schemas/atom" |
| self.opensearch_namespace = "http://a9.com/-/spec/opensearch/1.1/" |
| |
| def search_arxiv(self, search_query: str = None, id_list: List[str] = None, |
| start: int = 0, max_results: int = 10) -> Dict[str, Any]: |
| """ |
| Search arXiv using the API and return structured results. |
| |
| Args: |
| search_query: Search query string (e.g., "all:electron", "cat:cs.AI") |
| id_list: List of arXiv IDs to retrieve |
| start: Starting index for results |
| max_results: Maximum number of results to return |
| |
| Returns: |
| Dictionary containing parsed search results |
| """ |
| |
| params = { |
| 'start': start, |
| 'max_results': max_results |
| } |
| |
| if search_query: |
| params['search_query'] = search_query |
| |
| if id_list: |
| params['id_list'] = ','.join(id_list) |
| |
| try: |
| |
| response = self.request( |
| url=self.base_url, |
| method='GET', |
| params=params |
| ) |
| |
| |
| return self._parse_atom_response(response.text) |
| |
| except Exception as e: |
| return { |
| 'success': False, |
| 'error': str(e), |
| 'query': search_query or str(id_list) |
| } |
| |
| def _parse_atom_response(self, xml_content: str) -> Dict[str, Any]: |
| """ |
| Parse the Atom XML response from arXiv API. |
| |
| Args: |
| xml_content: Raw XML content from the API response |
| |
| Returns: |
| Dictionary with parsed paper information |
| """ |
| try: |
| |
| root = ET.fromstring(xml_content) |
| |
| |
| namespaces = { |
| 'atom': self.atom_namespace, |
| 'arxiv': self.arxiv_namespace, |
| 'opensearch': self.opensearch_namespace |
| } |
| |
| |
| total_results = root.find('.//opensearch:totalResults', namespaces) |
| start_index = root.find('.//opensearch:startIndex', namespaces) |
| items_per_page = root.find('.//opensearch:itemsPerPage', namespaces) |
| |
| result = { |
| 'success': True, |
| 'total_results': int(total_results.text) if total_results is not None else 0, |
| 'start_index': int(start_index.text) if start_index is not None else 0, |
| 'items_per_page': int(items_per_page.text) if items_per_page is not None else 0, |
| 'papers': [] |
| } |
| |
| |
| entries = root.findall('.//atom:entry', namespaces) |
| |
| for entry in entries: |
| paper = self._parse_paper_entry(entry, namespaces) |
| result['papers'].append(paper) |
| |
| return result |
| |
| except ET.ParseError as e: |
| return { |
| 'success': False, |
| 'error': f'XML parsing error: {str(e)}', |
| 'raw_content': xml_content[:500] + '...' if len(xml_content) > 500 else xml_content |
| } |
| except Exception as e: |
| return { |
| 'success': False, |
| 'error': str(e) |
| } |
| |
| def _parse_paper_entry(self, entry, namespaces) -> Dict[str, Any]: |
| """ |
| Parse a single paper entry from the XML. |
| |
| Args: |
| entry: XML element for a paper entry |
| namespaces: Namespace mappings |
| |
| Returns: |
| Dictionary with paper information |
| """ |
| paper = {} |
| |
| |
| paper['id'] = self._get_text(entry, 'atom:id', namespaces) |
| paper['title'] = self._get_text(entry, 'atom:title', namespaces, clean=True) |
| paper['summary'] = self._get_text(entry, 'atom:summary', namespaces, clean=True) |
| paper['published'] = self._get_text(entry, 'atom:published', namespaces) |
| paper['updated'] = self._get_text(entry, 'atom:updated', namespaces) |
| |
| |
| if paper['id']: |
| paper['arxiv_id'] = paper['id'].split('/')[-1] |
| |
| |
| authors = entry.findall('.//atom:author', namespaces) |
| paper['authors'] = [] |
| for author in authors: |
| name = self._get_text(author, 'atom:name', namespaces) |
| if name: |
| paper['authors'].append(name) |
| |
| |
| categories = entry.findall('.//atom:category', namespaces) |
| paper['categories'] = [] |
| for category in categories: |
| term = category.get('term') |
| if term: |
| paper['categories'].append(term) |
| |
| |
| primary_cat = entry.find('.//arxiv:primary_category', namespaces) |
| if primary_cat is not None: |
| paper['primary_category'] = primary_cat.get('term') |
| |
| |
| links = entry.findall('.//atom:link', namespaces) |
| paper['links'] = {} |
| for link in links: |
| rel = link.get('rel') |
| href = link.get('href') |
| title = link.get('title') |
| |
| if rel == 'alternate': |
| paper['links']['html'] = href |
| elif title == 'pdf': |
| paper['links']['pdf'] = href |
| |
| |
| paper['comment'] = self._get_text(entry, 'arxiv:comment', namespaces) |
| paper['journal_ref'] = self._get_text(entry, 'arxiv:journal_ref', namespaces) |
| paper['doi'] = self._get_text(entry, 'arxiv:doi', namespaces) |
| |
| |
| |
| if paper.get('links', {}).get('html'): |
| paper['url'] = paper['links']['html'] |
| elif paper.get('arxiv_id'): |
| paper['url'] = f"https://arxiv.org/abs/{paper['arxiv_id']}" |
| else: |
| paper['url'] = '' |
| |
| paper['published_date'] = paper.pop('published', '') |
| paper['updated_date'] = paper.pop('updated', '') |
| |
| |
| paper.pop('id', None) |
| |
| return paper |
| |
| def _get_text(self, element, xpath, namespaces, clean=False) -> str: |
| """ |
| Helper method to extract text from XML elements. |
| |
| Args: |
| element: XML element to search in |
| xpath: XPath expression |
| namespaces: Namespace mappings |
| clean: Whether to clean whitespace |
| |
| Returns: |
| Text content or empty string |
| """ |
| found = element.find(xpath, namespaces) |
| if found is not None: |
| text = found.text or '' |
| if clean: |
| |
| text = re.sub(r'\s+', ' ', text.strip()) |
| return text |
| return '' |
| |
| def download_pdf(self, pdf_url: str, save_path: str, storage_handler: FileStorageHandler = None) -> Dict[str, Any]: |
| """ |
| Download a PDF from arXiv. |
| |
| Args: |
| pdf_url: URL of the PDF to download |
| save_path: Local path to save the PDF |
| storage_handler: Storage handler for file operations |
| |
| Returns: |
| Dictionary with download status |
| """ |
| try: |
| response = self.request(url=pdf_url, method='GET') |
| |
| |
| pdf_content = response.content |
| |
| |
| result = storage_handler.save(save_path, pdf_content) |
| if result["success"]: |
| return { |
| 'success': True, |
| 'file_path': save_path, |
| 'size': len(pdf_content), |
| 'url': pdf_url, |
| 'storage_handler': type(storage_handler).__name__ |
| } |
| else: |
| return { |
| 'success': False, |
| 'error': f"Failed to save PDF: {result.get('error', 'Unknown error')}", |
| 'url': pdf_url, |
| 'save_path': save_path |
| } |
| |
| except Exception as e: |
| return { |
| 'success': False, |
| 'error': str(e), |
| 'url': pdf_url |
| } |
|
|
|
|
| class ArxivSearchTool(Tool): |
| """Tool for searching papers on arXiv.""" |
| |
| name: str = "arxiv_search" |
| description: str = "Search for academic papers on arXiv using queries or paper IDs" |
| inputs: Dict[str, Dict[str, str]] = { |
| "search_query": { |
| "type": "string", |
| "description": "Search query (e.g., 'all:machine learning', 'cat:cs.AI', 'au:smith')" |
| }, |
| "id_list": { |
| "type": "array", |
| "description": "List of arXiv IDs to retrieve (e.g., ['1706.03762', '1810.04805'])" |
| }, |
| "max_results": { |
| "type": "integer", |
| "description": "Maximum number of results to return (default: 10)" |
| }, |
| "start": { |
| "type": "integer", |
| "description": "Starting index for pagination (default: 0)" |
| } |
| } |
| required: Optional[List[str]] = [] |
| |
| def __init__(self, arxiv_base: ArxivBase = None): |
| super().__init__() |
| self.arxiv_base = arxiv_base |
| |
| def __call__(self, search_query: str = None, id_list: list = None, |
| max_results: int = 10, start: int = 0) -> Dict[str, Any]: |
| """ |
| Search arXiv for papers. |
| |
| Args: |
| search_query: Search query string |
| id_list: List of arXiv IDs |
| max_results: Maximum results to return |
| start: Starting index for pagination |
| |
| Returns: |
| Dictionary with search results |
| """ |
| if not search_query and not id_list: |
| return { |
| 'success': False, |
| 'error': 'Either search_query or id_list must be provided' |
| } |
| |
| return self.arxiv_base.search_arxiv( |
| search_query=search_query, |
| id_list=id_list, |
| start=start, |
| max_results=max_results |
| ) |
|
|
|
|
| class ArxivDownloadTool(Tool): |
| """Tool for downloading papers from arXiv.""" |
| |
| name: str = "arxiv_download" |
| description: str = "Download PDF papers from arXiv" |
| inputs: Dict[str, Dict[str, str]] = { |
| "pdf_url": { |
| "type": "string", |
| "description": "URL of the PDF to download" |
| }, |
| "save_path": { |
| "type": "string", |
| "description": "Local path to save the PDF file" |
| } |
| } |
| required: Optional[List[str]] = ["pdf_url", "save_path"] |
| |
| def __init__(self, arxiv_base: ArxivBase = None, storage_handler: FileStorageHandler = None): |
| super().__init__() |
| self.arxiv_base = arxiv_base |
| self.storage_handler = storage_handler |
|
|
| def __call__(self, pdf_url: str, save_path: str) -> Dict[str, Any]: |
| """ |
| Download a PDF from arXiv. |
| |
| Args: |
| pdf_url: URL of the PDF |
| save_path: Where to save the file |
| |
| Returns: |
| Dictionary with download status |
| """ |
| return self.arxiv_base.download_pdf(pdf_url, save_path, self.storage_handler) |
|
|
|
|
| class ArxivToolkit(Toolkit): |
| def __init__(self, name: str = "ArxivToolkit", storage_handler: FileStorageHandler = None): |
| |
| if storage_handler is None: |
| from .storage_handler import LocalStorageHandler |
| storage_handler = LocalStorageHandler() |
| |
| |
| arxiv_base = ArxivBase() |
| |
| |
| tools = [ |
| ArxivSearchTool(arxiv_base=arxiv_base), |
| ArxivDownloadTool(arxiv_base=arxiv_base, storage_handler=storage_handler) |
| ] |
| |
| |
| super().__init__(name=name, tools=tools) |
| |
| |
| self.arxiv_base = arxiv_base |
| self.storage_handler = storage_handler |