ravimohan19 commited on
Commit
0056bc7
Β·
verified Β·
1 Parent(s): 517982e

Upload web_crawler.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. web_crawler.py +182 -61
web_crawler.py CHANGED
@@ -1,10 +1,19 @@
1
  """
2
  Tavily-powered web crawler for retrieving polymer datasheets.
 
 
 
 
 
 
 
 
3
  """
4
 
5
  from __future__ import annotations
6
 
7
  import logging
 
8
  from typing import Any
9
 
10
  from tavily import TavilyClient
@@ -13,96 +22,204 @@ import config
13
 
14
  logger = logging.getLogger(__name__)
15
 
 
 
 
 
 
 
 
16
 
17
- def build_search_queries(
18
- manufacturer: str,
19
- polymer_family: str,
20
- grade: str = "",
21
- ) -> list[str]:
22
- """
23
- Generate multiple targeted search queries to maximize the chance
24
- of finding the correct technical datasheet.
25
- """
26
- base_terms = []
27
- if manufacturer:
28
- base_terms.append(manufacturer)
29
- if polymer_family:
30
- base_terms.append(polymer_family)
31
- if grade:
32
- base_terms.append(grade)
33
 
34
- base = " ".join(base_terms)
35
 
36
- queries = [
37
- f"{base} technical data sheet properties",
38
- f"{base} TDS material properties datasheet",
39
- f"{base} mechanical thermal physical properties",
40
- ]
 
 
 
 
 
 
41
 
42
- # Add manufacturer-specific datasheet portal query
43
- if manufacturer:
 
 
 
 
44
  queries.append(
45
- f"site:{_guess_domain(manufacturer)} {polymer_family} {grade} datasheet"
46
  )
47
 
48
- # Add aggregator queries
49
- queries.append(f"{base} datasheet matweb OR omnexus OR UL Prospector")
50
-
 
51
  return queries
52
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  def _guess_domain(manufacturer: str) -> str:
55
- """Attempt to guess manufacturer domain for site-scoped search."""
56
- name = manufacturer.lower().replace(" ", "")
57
  for domain in config.TRUSTED_DOMAINS:
58
- if name in domain:
59
  return domain
60
  return f"{name}.com"
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def search_datasheets(
64
  manufacturer: str,
65
  polymer_family: str,
66
  grade: str = "",
67
  ) -> tuple[list[dict[str, Any]], str]:
68
  """
69
- Execute Tavily searches and return (results_list, aggregated_raw_content).
 
70
  """
71
  client = TavilyClient(api_key=config.TAVILY_API_KEY)
72
 
73
- queries = build_search_queries(manufacturer, polymer_family, grade)
74
  all_results: list[dict[str, Any]] = []
75
  seen_urls: set[str] = set()
76
  raw_texts: list[str] = []
77
 
78
- for query in queries:
79
- try:
80
- logger.info("Searching: %s", query)
81
- response = client.search(
82
- query=query,
83
- search_depth=config.TAVILY_SEARCH_DEPTH,
84
- max_results=config.TAVILY_MAX_RESULTS,
85
- include_raw_content=config.TAVILY_INCLUDE_RAW_CONTENT,
86
- include_domains=config.TRUSTED_DOMAINS,
87
- )
88
-
89
- for result in response.get("results", []):
90
- url = result.get("url", "")
91
- if url in seen_urls:
92
- continue
93
- seen_urls.add(url)
94
-
95
- all_results.append(result)
96
-
97
- # Collect raw content for LLM parsing
98
- raw = result.get("raw_content") or result.get("content", "")
99
- if raw:
100
- raw_texts.append(
101
- f"--- Source: {url} ---\n{raw[:8000]}\n"
102
- )
103
-
104
- except Exception as exc:
105
- logger.warning("Search failed for query '%s': %s", query, exc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  aggregated = "\n".join(raw_texts)
108
 
@@ -119,6 +236,10 @@ def search_datasheets(
119
  return all_results, aggregated
120
 
121
 
 
 
 
 
122
  def extract_from_url(url: str) -> tuple[list[dict[str, Any]], str]:
123
  """
124
  Use Tavily extract to get content from a specific URL.
 
1
  """
2
  Tavily-powered web crawler for retrieving polymer datasheets.
3
+
4
+ Strategy:
5
+ Phase 1 β€” Open web search (no domain filter) for broad discovery.
6
+ Phase 2 β€” Targeted aggregator search (MatWeb, Omnexus, UL Prospector).
7
+ Phase 3 β€” Manufacturer-specific search on their own site.
8
+
9
+ Results are de-duplicated, PDF-only URLs are deprioritised (Tavily can't
10
+ read them), and content is scored by relevance before being sent to the LLM.
11
  """
12
 
13
  from __future__ import annotations
14
 
15
  import logging
16
+ import re
17
  from typing import Any
18
 
19
  from tavily import TavilyClient
 
22
 
23
  logger = logging.getLogger(__name__)
24
 
25
+ # ── Keywords that signal real datasheet content ──────────────────────────────
26
+ _QUALITY_KEYWORDS = [
27
+ "tensile", "flexural", "density", "melt flow", "elongation",
28
+ "modulus", "impact", "hardness", "HDT", "heat deflection",
29
+ "glass transition", "melting point", "dielectric", "flammability",
30
+ "ISO", "ASTM", "g/cm", "MPa", "kJ/m", "J/m", "Β°C", "shore",
31
+ ]
32
 
33
+ # Domains that are database aggregators (best sources for structured data)
34
+ _AGGREGATOR_DOMAINS = [
35
+ "matweb.com",
36
+ "omnexus.specialchem.com",
37
+ "prospector.ides.com",
38
+ "campusplastics.com",
39
+ "plastics.ulprospector.com",
40
+ "polymerdatabase.com",
41
+ "matmatch.com",
42
+ "materialstoday.com",
43
+ ]
 
 
 
 
 
44
 
 
45
 
46
+ # ══════════════════════════════════════════════════════════════════════════════
47
+ # Query builders
48
+ # ══════════════════════════════════════════════════════════════════════════════
49
+
50
+ def _build_open_queries(
51
+ manufacturer: str, polymer_family: str, grade: str,
52
+ ) -> list[str]:
53
+ """Phase 1: broad web queries with NO domain restriction."""
54
+ parts = [p for p in (manufacturer, polymer_family, grade) if p]
55
+ base = " ".join(parts)
56
+ queries = []
57
 
58
+ if grade:
59
+ # If a specific grade is given, lead with it
60
+ queries.append(f"{grade} technical data sheet material properties")
61
+ queries.append(f"{grade} {polymer_family} datasheet density tensile")
62
+ else:
63
+ queries.append(f"{base} technical data sheet material properties")
64
  queries.append(
65
+ f"{base} datasheet density tensile modulus thermal"
66
  )
67
 
68
+ # A query phrased as a question often surfaces different results
69
+ queries.append(
70
+ f"What are the mechanical and thermal properties of {base}?"
71
+ )
72
  return queries
73
 
74
 
75
+ def _build_aggregator_queries(
76
+ manufacturer: str, polymer_family: str, grade: str,
77
+ ) -> list[str]:
78
+ """Phase 2: search restricted to well-known aggregator databases."""
79
+ parts = [p for p in (manufacturer, polymer_family, grade) if p]
80
+ base = " ".join(parts)
81
+ return [
82
+ f"{base} properties datasheet",
83
+ ]
84
+
85
+
86
+ def _build_manufacturer_queries(
87
+ manufacturer: str, polymer_family: str, grade: str,
88
+ ) -> list[str]:
89
+ """Phase 3: search the manufacturer's own website."""
90
+ if not manufacturer:
91
+ return []
92
+ domain = _guess_domain(manufacturer)
93
+ parts = [p for p in (polymer_family, grade) if p]
94
+ material = " ".join(parts) if parts else "polymer"
95
+ return [
96
+ f"site:{domain} {material} datasheet properties",
97
+ ]
98
+
99
+
100
  def _guess_domain(manufacturer: str) -> str:
101
+ """Best-effort manufacturer β†’ domain mapping."""
102
+ name = manufacturer.lower().replace(" ", "").replace("-", "")
103
  for domain in config.TRUSTED_DOMAINS:
104
+ if name in domain.replace(".", ""):
105
  return domain
106
  return f"{name}.com"
107
 
108
 
109
+ # ══════════════════════════════════════════════════════════════════════════════
110
+ # Content quality helpers
111
+ # ══════════════════════════════════════════════════════════════════════════════
112
+
113
+ def _is_pdf_url(url: str) -> bool:
114
+ """Return True if the URL likely points directly to a PDF file."""
115
+ return bool(re.search(r"\.pdf(\?|#|$)", url, re.IGNORECASE))
116
+
117
+
118
+ def _content_quality_score(text: str) -> int:
119
+ """
120
+ Score how many datasheet-relevant keywords appear in the text.
121
+ Higher = more likely to contain useful property data.
122
+ """
123
+ lower = text.lower()
124
+ return sum(1 for kw in _QUALITY_KEYWORDS if kw.lower() in lower)
125
+
126
+
127
+ def _pick_best_source_url(results: list[dict[str, Any]]) -> str:
128
+ """Return the URL of the highest-quality non-PDF result."""
129
+ best_url, best_score = "", -1
130
+ for r in results:
131
+ url = r.get("url", "")
132
+ text = r.get("raw_content") or r.get("content", "")
133
+ if _is_pdf_url(url):
134
+ continue # Tavily rarely extracts useful text from PDFs
135
+ score = _content_quality_score(text)
136
+ if score > best_score:
137
+ best_score = score
138
+ best_url = url
139
+ return best_url or (results[0].get("url", "") if results else "")
140
+
141
+
142
+ # ══════════════════════════════════════════════════════════════════════════════
143
+ # Main search function
144
+ # ══════════════════════════════════════════════════════════════════════════════
145
+
146
  def search_datasheets(
147
  manufacturer: str,
148
  polymer_family: str,
149
  grade: str = "",
150
  ) -> tuple[list[dict[str, Any]], str]:
151
  """
152
+ Execute a multi-phase Tavily search and return
153
+ (results_list, aggregated_raw_content).
154
  """
155
  client = TavilyClient(api_key=config.TAVILY_API_KEY)
156
 
 
157
  all_results: list[dict[str, Any]] = []
158
  seen_urls: set[str] = set()
159
  raw_texts: list[str] = []
160
 
161
+ def _run_queries(
162
+ queries: list[str],
163
+ include_domains: list[str] | None = None,
164
+ max_results: int = 5,
165
+ ) -> None:
166
+ """Run a batch of queries and collect unique results."""
167
+ for query in queries:
168
+ try:
169
+ logger.info("Searching: %s (domains=%s)", query, include_domains or "any")
170
+ kwargs: dict[str, Any] = dict(
171
+ query=query,
172
+ search_depth=config.TAVILY_SEARCH_DEPTH,
173
+ max_results=max_results,
174
+ include_raw_content=config.TAVILY_INCLUDE_RAW_CONTENT,
175
+ )
176
+ if include_domains:
177
+ kwargs["include_domains"] = include_domains
178
+
179
+ response = client.search(**kwargs)
180
+
181
+ for result in response.get("results", []):
182
+ url = result.get("url", "")
183
+ if url in seen_urls:
184
+ continue
185
+ seen_urls.add(url)
186
+
187
+ # Skip direct PDF links β€” Tavily returns no useful text
188
+ if _is_pdf_url(url):
189
+ content = result.get("raw_content") or result.get("content", "")
190
+ if len(content.strip()) < 200:
191
+ logger.info("Skipping PDF URL with no text: %s", url)
192
+ continue
193
+
194
+ all_results.append(result)
195
+
196
+ raw = result.get("raw_content") or result.get("content", "")
197
+ if raw and raw.strip():
198
+ raw_texts.append(
199
+ f"--- Source: {url} ---\n{raw[:8000]}\n"
200
+ )
201
+
202
+ except Exception as exc:
203
+ logger.warning("Search failed for query '%s': %s", query, exc)
204
+
205
+ # Phase 1 β€” Open web (no domain filter) for broad discovery
206
+ open_queries = _build_open_queries(manufacturer, polymer_family, grade)
207
+ _run_queries(open_queries, include_domains=None, max_results=5)
208
+
209
+ # Phase 2 β€” Aggregator databases (MatWeb, Omnexus, etc.)
210
+ agg_queries = _build_aggregator_queries(manufacturer, polymer_family, grade)
211
+ _run_queries(agg_queries, include_domains=_AGGREGATOR_DOMAINS, max_results=5)
212
+
213
+ # Phase 3 β€” Manufacturer's own website
214
+ mfr_queries = _build_manufacturer_queries(manufacturer, polymer_family, grade)
215
+ if mfr_queries:
216
+ _run_queries(mfr_queries, include_domains=None, max_results=3)
217
+
218
+ # Sort raw_texts so highest-quality content comes first for the LLM
219
+ raw_texts.sort(
220
+ key=lambda t: _content_quality_score(t),
221
+ reverse=True,
222
+ )
223
 
224
  aggregated = "\n".join(raw_texts)
225
 
 
236
  return all_results, aggregated
237
 
238
 
239
+ # ══════════════════════════════════════════════════════════════════════════════
240
+ # Single-URL extraction
241
+ # ══════════════════════════════════════════════════════════════════════════════
242
+
243
  def extract_from_url(url: str) -> tuple[list[dict[str, Any]], str]:
244
  """
245
  Use Tavily extract to get content from a specific URL.