File size: 4,693 Bytes
29c4d1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, urljoin
import logging
from src.document_processing.chunking import chunk_text
from typing import List, Optional, Set, Dict, Callable
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.documents.base import Document

logger = logging.getLogger(__name__)

class WebScraper:
    
    def __init__(self, headers: Optional[dict] = None):
        self.headers = headers or {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
        }
        self.visited_urls = set()
    
    def scrape_url(self, url: str) -> str:
        try:
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
        
            soup = BeautifulSoup(response.content, "html.parser")
            
            for script in soup(["script", "style", "nav", "footer", "header"]):
                script.extract()
                
            text = soup.get_text(separator='\n')
            
            lines = (line.strip() for line in text.splitlines())
            chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
            text = '\n'.join(chunk for chunk in chunks if chunk)
            
            return text, soup
            
        except Exception as e:
            logger.error(f"Error scraping {url}: {str(e)}")
            return f"Error scraping {url}: {str(e)}", None
    
    def extract_links(self, url: str, soup: BeautifulSoup) -> List[str]:
        """Extract all hyperlinks from a webpage"""
        links = []
        base_url = urlparse(url)
        domain = base_url.netloc
        
        for link in soup.find_all('a', href=True):
            href = link['href']
            # Create absolute URL
            if href.startswith('/'):
                full_url = f"{base_url.scheme}://{domain}{href}"
            elif href.startswith(('http://', 'https://')):
                full_url = href
            else:
                full_url = urljoin(url, href)
                
            # Only include links from same domain
            if urlparse(full_url).netloc == domain:
                links.append(full_url)
                
        return links
    
    def get_domain(self, url: str) -> str:
        return urlparse(url).netloc
    
    def crawl_website(self, start_url: str, max_depth: int = 2, max_pages: int = 20, 
                     callback: Optional[Callable[[str], None]] = None) -> Dict[str, str]:
        self.visited_urls = set()
        results = {}
        
        def _crawl(url: str, depth: int):
            if depth > max_depth or len(results) >= max_pages or url in self.visited_urls:
                return
            
            self.visited_urls.add(url)
            if callback:
                callback(url)
            logger.info(f"Scraping {url} (depth {depth})")

            content, soup = self.scrape_url(url)
            if soup is not None:
                results[url] = content
                links = self.extract_links(url, soup)
                for link in links:
                    _crawl(link, depth + 1)
        
        _crawl(start_url, 1)
        return results
            
def process_web_content(urls: List[str], vector_store_path: str, 
                       max_depth: int = 1, max_pages: int = 20,
                       ui_callback: Optional[Callable[[str], None]] = None) -> FAISS:
    scraper = WebScraper()
    documents = []
    
    for start_url in urls:
        if ui_callback:
            ui_callback(f"Starting to crawl from: {start_url}")
        page_contents = scraper.crawl_website(
            start_url, 
            max_depth=max_depth, 
            max_pages=max_pages,
            callback=ui_callback
        )

        for url, content in page_contents.items():
            domain = scraper.get_domain(url)
            chunks = chunk_text(content)
            for i, chunk in enumerate(chunks):
                metadata = {
                    "source": url,
                    "domain": domain,
                    "chunk": i
                }
                documents.append(Document(page_content=chunk, metadata=metadata))
    
    if ui_callback:
        ui_callback(f"Creating vector embeddings for {len(documents)} chunks...")
    
    embeddings = HuggingFaceEmbeddings(
        model_name="sentence-transformers/all-MiniLM-L6-v2",
        model_kwargs={'device': 'cpu'}
    )
    
    vector_store = FAISS.from_documents(documents, embeddings)
    vector_store.save_local(vector_store_path)
    return vector_store