From 60f78dab9c5d79d8cd571f61e2a3181e37015db6 Mon Sep 17 00:00:00 2001 From: Steve White Date: Thu, 27 Feb 2025 17:39:34 -0600 Subject: [PATCH] Implement Phase 1 of Report Generation Module: Document Scraping and SQLite Storage --- report/__init__.py | 19 ++ report/database/__init__.py | 14 ++ report/database/db_manager.py | 393 ++++++++++++++++++++++++++++++++ report/database/documents.db | Bin 0 -> 258048 bytes report/document_scraper.py | 400 +++++++++++++++++++++++++++++++++ report/report_generator.py | 130 +++++++++++ requirements.txt | 8 + tests/__init__.py | 3 + tests/test_document_scraper.py | 82 +++++++ 9 files changed, 1049 insertions(+) create mode 100644 report/__init__.py create mode 100644 report/database/__init__.py create mode 100644 report/database/db_manager.py create mode 100644 report/database/documents.db create mode 100644 report/document_scraper.py create mode 100644 report/report_generator.py create mode 100644 tests/__init__.py create mode 100644 tests/test_document_scraper.py diff --git a/report/__init__.py b/report/__init__.py new file mode 100644 index 0000000..43b1c4e --- /dev/null +++ b/report/__init__.py @@ -0,0 +1,19 @@ +""" +Report generation module for the intelligent research system. + +This module provides functionality to generate reports from search results +by scraping documents, storing them in a database, and synthesizing them +into a comprehensive report. +""" + +from report.report_generator import get_report_generator, initialize_report_generator +from report.document_scraper import get_document_scraper +from report.database.db_manager import get_db_manager, initialize_database + +__all__ = [ + 'get_report_generator', + 'initialize_report_generator', + 'get_document_scraper', + 'get_db_manager', + 'initialize_database' +] diff --git a/report/database/__init__.py b/report/database/__init__.py new file mode 100644 index 0000000..5bc29f5 --- /dev/null +++ b/report/database/__init__.py @@ -0,0 +1,14 @@ +""" +Database module for the report generation module. + +This module provides functionality to create, manage, and query the SQLite database +for storing scraped documents and their metadata. +""" + +from report.database.db_manager import get_db_manager, initialize_database, DBManager + +__all__ = [ + 'get_db_manager', + 'initialize_database', + 'DBManager' +] diff --git a/report/database/db_manager.py b/report/database/db_manager.py new file mode 100644 index 0000000..45d2951 --- /dev/null +++ b/report/database/db_manager.py @@ -0,0 +1,393 @@ +""" +SQLite database manager for the report generation module. + +This module provides functionality to create, manage, and query the SQLite database +for storing scraped documents and their metadata. +""" + +import os +import json +import aiosqlite +import asyncio +import logging +from datetime import datetime +from typing import Dict, List, Any, Optional, Tuple, Union + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class DBManager: + """ + Database manager for the report generation module. + + This class provides methods to create, manage, and query the SQLite database + for storing scraped documents and their metadata. + """ + + def __init__(self, db_path: str = "report/database/documents.db"): + """ + Initialize the database manager. + + Args: + db_path: Path to the SQLite database file + """ + self.db_path = db_path + self._ensure_dir_exists() + + def _ensure_dir_exists(self): + """Ensure the directory for the database file exists.""" + db_dir = os.path.dirname(self.db_path) + if not os.path.exists(db_dir): + os.makedirs(db_dir) + logger.info(f"Created directory: {db_dir}") + + async def initialize_db(self): + """ + Initialize the database by creating necessary tables if they don't exist. + + This method creates the documents and metadata tables. + """ + async with aiosqlite.connect(self.db_path) as db: + # Create documents table + await db.execute(''' + CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + url TEXT UNIQUE NOT NULL, + title TEXT, + content TEXT NOT NULL, + scrape_date TIMESTAMP NOT NULL, + content_type TEXT, + token_count INTEGER, + hash TEXT UNIQUE + ) + ''') + + # Create metadata table + await db.execute(''' + CREATE TABLE IF NOT EXISTS metadata ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT, + FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, + UNIQUE (document_id, key) + ) + ''') + + # Create index on url for faster lookups + await db.execute('CREATE INDEX IF NOT EXISTS idx_documents_url ON documents (url)') + + # Create index on document_id for faster metadata lookups + await db.execute('CREATE INDEX IF NOT EXISTS idx_metadata_document_id ON metadata (document_id)') + + await db.commit() + logger.info("Database initialized successfully") + + async def document_exists(self, url: str) -> bool: + """ + Check if a document with the given URL already exists in the database. + + Args: + url: URL of the document to check + + Returns: + True if the document exists, False otherwise + """ + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute('SELECT id FROM documents WHERE url = ?', (url,)) + result = await cursor.fetchone() + return result is not None + + async def get_document_by_url(self, url: str) -> Optional[Dict[str, Any]]: + """ + Get a document by its URL. + + Args: + url: URL of the document to retrieve + + Returns: + Document as a dictionary, or None if not found + """ + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(''' + SELECT id, url, title, content, scrape_date, content_type, token_count, hash + FROM documents + WHERE url = ? + ''', (url,)) + + document = await cursor.fetchone() + if not document: + return None + + # Convert to dictionary + doc_dict = dict(document) + + # Get metadata + cursor = await db.execute(''' + SELECT key, value + FROM metadata + WHERE document_id = ? + ''', (doc_dict['id'],)) + + metadata = await cursor.fetchall() + doc_dict['metadata'] = {row['key']: row['value'] for row in metadata} + + return doc_dict + + async def add_document(self, url: str, title: str, content: str, + content_type: str, token_count: int, + metadata: Dict[str, str], doc_hash: str) -> int: + """ + Add a document to the database. + + Args: + url: URL of the document + title: Title of the document + content: Content of the document + content_type: Type of content (e.g., 'markdown', 'html', 'text') + token_count: Number of tokens in the document + metadata: Dictionary of metadata key-value pairs + doc_hash: Hash of the document content for deduplication + + Returns: + ID of the added document + + Raises: + aiosqlite.Error: If there's an error adding the document + """ + async with aiosqlite.connect(self.db_path) as db: + try: + # Begin transaction + await db.execute('BEGIN TRANSACTION') + + # Insert document + cursor = await db.execute(''' + INSERT INTO documents (url, title, content, scrape_date, content_type, token_count, hash) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (url, title, content, datetime.now().isoformat(), content_type, token_count, doc_hash)) + + document_id = cursor.lastrowid + + # Insert metadata + for key, value in metadata.items(): + await db.execute(''' + INSERT INTO metadata (document_id, key, value) + VALUES (?, ?, ?) + ''', (document_id, key, value)) + + # Commit transaction + await db.commit() + logger.info(f"Added document: {url} (ID: {document_id})") + return document_id + + except aiosqlite.Error as e: + # Rollback transaction on error + await db.execute('ROLLBACK') + logger.error(f"Error adding document: {str(e)}") + raise + + async def update_document(self, document_id: int, content: str = None, + title: str = None, token_count: int = None, + metadata: Dict[str, str] = None) -> bool: + """ + Update an existing document in the database. + + Args: + document_id: ID of the document to update + content: New content (optional) + title: New title (optional) + token_count: New token count (optional) + metadata: New or updated metadata (optional) + + Returns: + True if the document was updated, False otherwise + + Raises: + aiosqlite.Error: If there's an error updating the document + """ + async with aiosqlite.connect(self.db_path) as db: + try: + # Begin transaction + await db.execute('BEGIN TRANSACTION') + + # Update document fields if provided + update_parts = [] + params = [] + + if content is not None: + update_parts.append("content = ?") + params.append(content) + + if title is not None: + update_parts.append("title = ?") + params.append(title) + + if token_count is not None: + update_parts.append("token_count = ?") + params.append(token_count) + + if update_parts: + update_query = f"UPDATE documents SET {', '.join(update_parts)} WHERE id = ?" + params.append(document_id) + await db.execute(update_query, params) + + # Update metadata if provided + if metadata: + for key, value in metadata.items(): + # Check if metadata key exists + cursor = await db.execute(''' + SELECT id FROM metadata + WHERE document_id = ? AND key = ? + ''', (document_id, key)) + + result = await cursor.fetchone() + + if result: + # Update existing metadata + await db.execute(''' + UPDATE metadata SET value = ? + WHERE document_id = ? AND key = ? + ''', (value, document_id, key)) + else: + # Insert new metadata + await db.execute(''' + INSERT INTO metadata (document_id, key, value) + VALUES (?, ?, ?) + ''', (document_id, key, value)) + + # Commit transaction + await db.commit() + logger.info(f"Updated document ID: {document_id}") + return True + + except aiosqlite.Error as e: + # Rollback transaction on error + await db.execute('ROLLBACK') + logger.error(f"Error updating document: {str(e)}") + raise + + async def delete_document(self, document_id: int) -> bool: + """ + Delete a document from the database. + + Args: + document_id: ID of the document to delete + + Returns: + True if the document was deleted, False otherwise + """ + async with aiosqlite.connect(self.db_path) as db: + try: + # Begin transaction + await db.execute('BEGIN TRANSACTION') + + # Delete document (metadata will be deleted via ON DELETE CASCADE) + await db.execute('DELETE FROM documents WHERE id = ?', (document_id,)) + + # Commit transaction + await db.commit() + logger.info(f"Deleted document ID: {document_id}") + return True + + except aiosqlite.Error as e: + # Rollback transaction on error + await db.execute('ROLLBACK') + logger.error(f"Error deleting document: {str(e)}") + return False + + async def search_documents(self, query: str, limit: int = 10, offset: int = 0) -> List[Dict[str, Any]]: + """ + Search for documents matching the query. + + Args: + query: Search query (will be matched against title and content) + limit: Maximum number of results to return + offset: Number of results to skip + + Returns: + List of matching documents as dictionaries + """ + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + + # Search documents + cursor = await db.execute(''' + SELECT id, url, title, content, scrape_date, content_type, token_count + FROM documents + WHERE title LIKE ? OR content LIKE ? + ORDER BY scrape_date DESC + LIMIT ? OFFSET ? + ''', (f'%{query}%', f'%{query}%', limit, offset)) + + documents = await cursor.fetchall() + results = [] + + # Get metadata for each document + for doc in documents: + doc_dict = dict(doc) + + cursor = await db.execute(''' + SELECT key, value + FROM metadata + WHERE document_id = ? + ''', (doc_dict['id'],)) + + metadata = await cursor.fetchall() + doc_dict['metadata'] = {row['key']: row['value'] for row in metadata} + + results.append(doc_dict) + + return results + + async def get_documents_by_urls(self, urls: List[str]) -> List[Dict[str, Any]]: + """ + Get multiple documents by their URLs. + + Args: + urls: List of URLs to retrieve + + Returns: + List of documents as dictionaries + """ + results = [] + for url in urls: + doc = await self.get_document_by_url(url) + if doc: + results.append(doc) + return results + + async def count_documents(self) -> int: + """ + Get the total number of documents in the database. + + Returns: + Number of documents + """ + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute('SELECT COUNT(*) as count FROM documents') + result = await cursor.fetchone() + return result[0] if result else 0 + + +# Create a singleton instance for global use +db_manager = DBManager() + +async def initialize_database(): + """Initialize the database.""" + await db_manager.initialize_db() + +def get_db_manager() -> DBManager: + """ + Get the global database manager instance. + + Returns: + DBManager instance + """ + return db_manager + +# Run database initialization if this module is executed directly +if __name__ == "__main__": + asyncio.run(initialize_database()) diff --git a/report/database/documents.db b/report/database/documents.db new file mode 100644 index 0000000000000000000000000000000000000000..54edcfab9b2e4fc82f932e6af4d7b5222c46b714 GIT binary patch literal 258048 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%0D}}~N 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 LXo<_MnvD`v5S@Wgf% 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 literal 0 HcmV?d00001 diff --git a/report/document_scraper.py b/report/document_scraper.py new file mode 100644 index 0000000..d2000f2 --- /dev/null +++ b/report/document_scraper.py @@ -0,0 +1,400 @@ +""" +Document scraper module for the report generation module. + +This module provides functionality to scrape web pages and extract clean content +using Jina Reader API or fallback methods. +""" + +import os +import re +import json +import hashlib +import logging +import asyncio +import aiohttp +import validators +import tiktoken +from typing import Dict, List, Any, Optional, Tuple, Union +from datetime import datetime +from urllib.parse import urlparse, urljoin +from bs4 import BeautifulSoup +import html2text + +from config.config import get_config +from report.database.db_manager import get_db_manager, DBManager + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class DocumentScraper: + """ + Document scraper for the report generation module. + + This class provides methods to scrape web pages and extract clean content + using Jina Reader API or fallback methods. + """ + + def __init__(self): + """Initialize the document scraper.""" + self.config = get_config() + self.api_key = self._get_api_key() + self.endpoint = "https://api.jina.ai/v1/reader" + self.db_manager = get_db_manager() + self.tokenizer = tiktoken.get_encoding("cl100k_base") # Using OpenAI's tokenizer + + def _get_api_key(self) -> str: + """ + Get the Jina AI API key. + + Returns: + The API key as a string + + Raises: + ValueError: If the API key is not found + """ + try: + return self.config.get_api_key('jina') + except ValueError as e: + logger.warning(f"Jina AI API key not found. Fallback methods will be used. {str(e)}") + return "" + + def _count_tokens(self, text: str) -> int: + """ + Count the number of tokens in a text. + + Args: + text: The text to count tokens for + + Returns: + Number of tokens in the text + """ + return len(self.tokenizer.encode(text)) + + def _compute_hash(self, content: str) -> str: + """ + Compute a hash of the document content for deduplication. + + Args: + content: The document content + + Returns: + Hash of the content + """ + return hashlib.sha256(content.encode('utf-8')).hexdigest() + + def _normalize_url(self, url: str) -> str: + """ + Normalize a URL by removing fragments and unnecessary query parameters. + + Args: + url: The URL to normalize + + Returns: + Normalized URL + """ + parsed = urlparse(url) + # Remove fragment + normalized = parsed._replace(fragment="") + + # TODO: Add more normalization rules if needed + + return normalized.geturl() + + def _validate_url(self, url: str) -> bool: + """ + Validate a URL. + + Args: + url: The URL to validate + + Returns: + True if the URL is valid, False otherwise + """ + return validators.url(url) is True + + async def _extract_metadata_from_html(self, html: str, url: str) -> Dict[str, str]: + """ + Extract metadata from HTML content. + + Args: + html: The HTML content + url: The URL of the page + + Returns: + Dictionary of metadata + """ + metadata = { + "source_url": url, + "scrape_date": datetime.now().isoformat() + } + + try: + soup = BeautifulSoup(html, 'html.parser') + + # Extract title + if soup.title: + metadata["title"] = soup.title.string + + # Extract meta tags + for meta in soup.find_all('meta'): + # Author + if meta.get('name') and meta.get('name').lower() == 'author' and meta.get('content'): + metadata["author"] = meta.get('content') + + # Description + if meta.get('name') and meta.get('name').lower() == 'description' and meta.get('content'): + metadata["description"] = meta.get('content') + + # Keywords + if meta.get('name') and meta.get('name').lower() == 'keywords' and meta.get('content'): + metadata["keywords"] = meta.get('content') + + # Publication date + if meta.get('property') and meta.get('property').lower() in ['article:published_time', 'og:published_time'] and meta.get('content'): + metadata["publication_date"] = meta.get('content') + + # Open Graph data + if meta.get('property') and meta.get('property').lower().startswith('og:') and meta.get('content'): + og_key = meta.get('property').lower().replace('og:', 'og_') + metadata[og_key] = meta.get('content') + + # Extract structured data (JSON-LD) + for script in soup.find_all('script', type='application/ld+json'): + try: + ld_data = json.loads(script.string) + if isinstance(ld_data, dict): + # Extract date published + if ld_data.get('@type') in ['Article', 'NewsArticle', 'BlogPosting'] and ld_data.get('datePublished'): + metadata["publication_date"] = ld_data.get('datePublished') + + # Extract author + if ld_data.get('author'): + author = ld_data.get('author') + if isinstance(author, dict) and author.get('name'): + metadata["author"] = author.get('name') + elif isinstance(author, str): + metadata["author"] = author + except (json.JSONDecodeError, AttributeError): + pass + + except Exception as e: + logger.warning(f"Error extracting metadata: {str(e)}") + + return metadata + + async def _html_to_markdown(self, html: str) -> str: + """ + Convert HTML to Markdown. + + Args: + html: The HTML content + + Returns: + Markdown content + """ + converter = html2text.HTML2Text() + converter.ignore_links = False + converter.ignore_images = False + converter.ignore_tables = False + converter.body_width = 0 # No wrapping + + return converter.handle(html) + + async def _scrape_with_jina_reader(self, url: str) -> Tuple[Optional[str], Optional[Dict[str, str]]]: + """ + Scrape a web page using Jina Reader API. + + Args: + url: The URL to scrape + + Returns: + Tuple of (content, metadata) + """ + if not self.api_key: + logger.warning("Jina API key not available. Using fallback method.") + return None, None + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}", + "Accept": "application/json" + } + + data = { + "url": url, + "format": "markdown" # Request markdown format + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(self.endpoint, headers=headers, json=data) as response: + if response.status != 200: + logger.warning(f"Jina Reader API error: {response.status} - {await response.text()}") + return None, None + + result = await response.json() + + if "content" not in result: + logger.warning(f"Jina Reader API returned no content: {result}") + return None, None + + content = result.get("content", "") + metadata = result.get("metadata", {}) + + # Add source URL to metadata + metadata["source_url"] = url + + return content, metadata + + except Exception as e: + logger.error(f"Error calling Jina Reader API: {str(e)}") + return None, None + + async def _scrape_with_fallback(self, url: str) -> Tuple[Optional[str], Optional[Dict[str, str]]]: + """ + Scrape a web page using fallback method (aiohttp + BeautifulSoup). + + Args: + url: The URL to scrape + + Returns: + Tuple of (content, metadata) + """ + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, headers={"User-Agent": "Mozilla/5.0"}) as response: + if response.status != 200: + logger.warning(f"Failed to fetch URL: {url} - Status: {response.status}") + return None, None + + html = await response.text() + + # Extract metadata + metadata = await self._extract_metadata_from_html(html, url) + + # Convert to markdown + content = await self._html_to_markdown(html) + + return content, metadata + + except Exception as e: + logger.error(f"Error in fallback scraping: {str(e)}") + return None, None + + async def scrape_url(self, url: str, force_refresh: bool = False) -> Optional[Dict[str, Any]]: + """ + Scrape a web page and store the content in the database. + + Args: + url: The URL to scrape + force_refresh: If True, scrape the URL even if it's already in the database + + Returns: + Document dictionary if successful, None otherwise + """ + # Validate URL + if not self._validate_url(url): + logger.warning(f"Invalid URL: {url}") + return None + + # Normalize URL + normalized_url = self._normalize_url(url) + + # Check if document already exists in database + if not force_refresh and await self.db_manager.document_exists(normalized_url): + logger.info(f"Document already exists in database: {normalized_url}") + return await self.db_manager.get_document_by_url(normalized_url) + + # Try Jina Reader first + content, metadata = await self._scrape_with_jina_reader(normalized_url) + + # Fallback to custom scraping if Jina Reader fails + if content is None: + logger.info(f"Falling back to custom scraping for URL: {normalized_url}") + content, metadata = await self._scrape_with_fallback(normalized_url) + + if content is None or not content.strip(): + logger.warning(f"Failed to extract content from URL: {normalized_url}") + return None + + # Count tokens + token_count = self._count_tokens(content) + + # Compute hash for deduplication + doc_hash = self._compute_hash(content) + + # Get title from metadata or use URL as fallback + title = metadata.get("title", urlparse(normalized_url).netloc) + + # Store in database + try: + document_id = await self.db_manager.add_document( + url=normalized_url, + title=title, + content=content, + content_type="markdown", + token_count=token_count, + metadata=metadata, + doc_hash=doc_hash + ) + + # Return the document + return await self.db_manager.get_document_by_url(normalized_url) + + except Exception as e: + logger.error(f"Error storing document in database: {str(e)}") + return None + + async def scrape_urls(self, urls: List[str], force_refresh: bool = False) -> List[Dict[str, Any]]: + """ + Scrape multiple URLs in parallel. + + Args: + urls: List of URLs to scrape + force_refresh: If True, scrape URLs even if they're already in the database + + Returns: + List of document dictionaries + """ + tasks = [self.scrape_url(url, force_refresh) for url in urls] + results = await asyncio.gather(*tasks) + + # Filter out None results + return [doc for doc in results if doc is not None] + + +# Create a singleton instance for global use +document_scraper = DocumentScraper() + +def get_document_scraper() -> DocumentScraper: + """ + Get the global document scraper instance. + + Returns: + DocumentScraper instance + """ + return document_scraper + +# Example usage +async def test_scraper(): + """Test the document scraper with a sample URL.""" + from report.database.db_manager import initialize_database + + # Initialize database + await initialize_database() + + # Scrape a URL + scraper = get_document_scraper() + document = await scraper.scrape_url("https://en.wikipedia.org/wiki/Web_scraping") + + if document: + print(f"Successfully scraped document: {document['title']}") + print(f"Token count: {document['token_count']}") + print(f"Content preview: {document['content'][:500]}...") + else: + print("Failed to scrape document") + +# Run test if this module is executed directly +if __name__ == "__main__": + asyncio.run(test_scraper()) diff --git a/report/report_generator.py b/report/report_generator.py new file mode 100644 index 0000000..af27631 --- /dev/null +++ b/report/report_generator.py @@ -0,0 +1,130 @@ +""" +Report generator module for the intelligent research system. + +This module provides functionality to generate reports from search results +by scraping documents, storing them in a database, and synthesizing them +into a comprehensive report. +""" + +import os +import asyncio +import logging +from typing import Dict, List, Any, Optional, Tuple, Union + +from report.database.db_manager import get_db_manager, initialize_database +from report.document_scraper import get_document_scraper + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class ReportGenerator: + """ + Report generator for the intelligent research system. + + This class provides methods to generate reports from search results + by scraping documents, storing them in a database, and synthesizing them + into a comprehensive report. + """ + + def __init__(self): + """Initialize the report generator.""" + self.db_manager = get_db_manager() + self.document_scraper = get_document_scraper() + + async def initialize(self): + """Initialize the report generator by setting up the database.""" + await initialize_database() + logger.info("Report generator initialized") + + async def process_search_results(self, search_results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + Process search results by scraping the URLs and storing them in the database. + + Args: + search_results: List of search results, each containing at least a 'url' field + + Returns: + List of processed documents + """ + # Extract URLs from search results + urls = [result.get('url') for result in search_results if result.get('url')] + + # Scrape URLs and store in database + documents = await self.document_scraper.scrape_urls(urls) + + # Log results + logger.info(f"Processed {len(documents)} documents out of {len(urls)} URLs") + + return documents + + async def get_document_by_url(self, url: str) -> Optional[Dict[str, Any]]: + """ + Get a document by its URL. + + Args: + url: URL of the document + + Returns: + Document as a dictionary, or None if not found + """ + return await self.db_manager.get_document_by_url(url) + + async def search_documents(self, query: str, limit: int = 10) -> List[Dict[str, Any]]: + """ + Search for documents in the database. + + Args: + query: Search query + limit: Maximum number of results to return + + Returns: + List of matching documents + """ + return await self.db_manager.search_documents(query, limit) + + +# Create a singleton instance for global use +report_generator = ReportGenerator() + +async def initialize_report_generator(): + """Initialize the report generator.""" + await report_generator.initialize() + +def get_report_generator() -> ReportGenerator: + """ + Get the global report generator instance. + + Returns: + ReportGenerator instance + """ + return report_generator + +# Example usage +async def test_report_generator(): + """Test the report generator with sample search results.""" + # Initialize report generator + await initialize_report_generator() + + # Sample search results + search_results = [ + {"url": "https://en.wikipedia.org/wiki/Web_scraping", "title": "Web scraping - Wikipedia"}, + {"url": "https://en.wikipedia.org/wiki/Natural_language_processing", "title": "Natural language processing - Wikipedia"} + ] + + # Process search results + generator = get_report_generator() + documents = await generator.process_search_results(search_results) + + # Print results + print(f"Processed {len(documents)} documents") + for doc in documents: + print(f"Title: {doc['title']}") + print(f"URL: {doc['url']}") + print(f"Token count: {doc['token_count']}") + print(f"Content preview: {doc['content'][:200]}...") + print("-" * 80) + +# Run test if this module is executed directly +if __name__ == "__main__": + asyncio.run(test_report_generator()) diff --git a/requirements.txt b/requirements.txt index 6cd3c9c..167716a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,11 @@ litellm>=1.0.0 gradio>=4.0.0 pyyaml>=6.0 python-dotenv>=1.0.0 +beautifulsoup4>=4.12.0 +aiosqlite>=0.19.0 +asyncio>=3.4.3 +aiohttp>=3.9.0 +validators>=0.22.0 +markdown>=3.5.0 +html2text>=2020.1.16 +feedparser>=6.0.10 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..92d4a33 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Test modules for the intelligent research system. +""" diff --git a/tests/test_document_scraper.py b/tests/test_document_scraper.py new file mode 100644 index 0000000..4f23602 --- /dev/null +++ b/tests/test_document_scraper.py @@ -0,0 +1,82 @@ +""" +Test script for the document scraper module. + +This script tests the functionality of the document scraper module +by scraping a few sample URLs and storing them in the database. +""" + +import os +import sys +import asyncio +import logging +from typing import List, Dict, Any + +# Add parent directory to path to allow importing modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from report.database.db_manager import initialize_database, get_db_manager +from report.document_scraper import get_document_scraper + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# Sample URLs for testing +TEST_URLS = [ + "https://en.wikipedia.org/wiki/Web_scraping", + "https://en.wikipedia.org/wiki/Natural_language_processing", + "https://en.wikipedia.org/wiki/SQLite" +] + +async def test_document_scraper(): + """Test the document scraper with sample URLs.""" + # Initialize database + await initialize_database() + logger.info("Database initialized") + + # Get document scraper + scraper = get_document_scraper() + + # Scrape URLs + logger.info(f"Scraping {len(TEST_URLS)} URLs...") + documents = await scraper.scrape_urls(TEST_URLS) + + # Print results + logger.info(f"Successfully scraped {len(documents)} documents") + for doc in documents: + logger.info(f"Title: {doc['title']}") + logger.info(f"URL: {doc['url']}") + logger.info(f"Token count: {doc['token_count']}") + logger.info(f"Content preview: {doc['content'][:200]}...") + logger.info("-" * 80) + + # Test database search + db_manager = get_db_manager() + search_results = await db_manager.search_documents("scraping") + logger.info(f"Found {len(search_results)} documents matching 'scraping'") + + # Test document retrieval by URL + doc = await db_manager.get_document_by_url(TEST_URLS[0]) + if doc: + logger.info(f"Retrieved document by URL: {doc['title']}") + else: + logger.error(f"Failed to retrieve document by URL: {TEST_URLS[0]}") + + # Count documents in database + count = await db_manager.count_documents() + logger.info(f"Total documents in database: {count}") + + return True + +if __name__ == "__main__": + try: + success = asyncio.run(test_document_scraper()) + if success: + logger.info("All tests passed!") + sys.exit(0) + else: + logger.error("Tests failed!") + sys.exit(1) + except Exception as e: + logger.exception(f"Error running tests: {str(e)}") + sys.exit(1)