NLPGenius commited on
Commit
48cec82
Β·
1 Parent(s): e06a21d

Robust fixes for Firebase filtering and content processing

Browse files

- Enhanced Firebase language filtering with data structure analysis
- Added fallback mechanisms for insufficient English articles
- Improved document conversion with multiple field name mappings
- Better content validation and chunking in vector store processing
- Added comprehensive error handling and debugging output
- Implemented content-based language detection as fallback
- Enhanced article processing with quality validation
- Added detailed test suite for verification

cve_factchecker/firebase_loader.py CHANGED
@@ -53,119 +53,202 @@ class FirebaseNewsLoader:
53
 
54
  remaining = None if (limit is None or (isinstance(limit, int) and limit <= 0)) else int(limit)
55
  articles: List[NewsArticle] = []
56
- request_count = 0
57
- max_requests = 20 # Limit total requests to avoid rate limiting
58
 
59
- # Build structured query with language filter
60
- query_data = {
 
 
 
61
  "structuredQuery": {
62
  "from": [{"collectionId": collection_name}],
63
- "where": {
64
- "fieldFilter": {
65
- "field": {"fieldPath": "language"},
66
- "op": "EQUAL",
67
- "value": {"stringValue": language}
68
- }
69
- },
70
- "orderBy": [
71
- {
72
- "field": {"fieldPath": "__name__"},
73
- "direction": "DESCENDING"
74
- }
75
- ]
76
  }
77
  }
78
 
79
- # Add limit if specified
80
- if remaining and remaining > 0:
81
- query_data["structuredQuery"]["limit"] = min(remaining, 1000) # Firestore max limit per query
82
 
83
- print(f"πŸ” Fetching {language} articles from Firebase...")
84
 
85
- while True:
86
- if remaining is not None and remaining <= 0:
87
- break
88
- if request_count >= max_requests:
89
- print(f"⏳ Reached max requests limit ({max_requests}), stopping to avoid rate limits")
90
- break
91
 
92
- # Add delay between requests to avoid rate limiting
93
- if request_count > 0:
94
- time.sleep(0.5) # 500ms delay between requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- headers = {'Content-Type': 'application/json'}
97
- params = {"key": self.api_key}
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  resp = requests.post(query_url, json=query_data, headers=headers, params=params, timeout=30)
100
- request_count += 1
101
-
102
- if resp.status_code == 429: # Rate limit
103
- retry_after = int(resp.headers.get('Retry-After', 60))
104
- print(f"❌ Firebase API rate limited: waiting {retry_after}s")
105
- time.sleep(retry_after)
106
- continue
107
- elif resp.status_code != 200:
108
- print(f"❌ Firebase structured query failed: {resp.status_code}")
109
- if resp.status_code >= 500: # Server error, might be temporary
110
- time.sleep(5)
111
- continue
112
- break
113
-
114
- data = resp.json()
115
-
116
- # Handle the structured query response format
117
- # Firebase structured query returns an array of results
118
- if isinstance(data, list):
119
- query_results = data
120
- else:
121
- query_results = data.get("result", data.get("documents", []))
122
 
123
- if not query_results:
124
- break
125
-
126
- batch_articles = []
127
- for result in query_results:
128
- # Handle different response formats
129
- doc = None
130
- if isinstance(result, dict):
131
- if "document" in result:
132
- doc = result.get("document")
133
- else:
134
- # Direct document format
135
- doc = result
136
 
137
- if doc:
138
- art = self._convert_doc(doc)
139
- if art:
140
- batch_articles.append(art)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- articles.extend(batch_articles)
143
-
144
- if remaining is not None:
145
- remaining -= len(batch_articles)
146
-
147
- # Check if we have more results
148
- if len(batch_articles) < query_data["structuredQuery"].get("limit", 1000):
149
- break # No more results
150
 
151
- # Update query for next batch (if we need pagination)
152
- if remaining and remaining > 0 and batch_articles:
153
- # For next batch, start after the last document
154
- last_doc_name = batch_articles[-1].article_id
155
- query_data["structuredQuery"]["startAfter"] = {
156
- "values": [{"referenceValue": f"projects/{self.project_id}/databases/(default)/documents/{collection_name}/{last_doc_name}"}]
157
- }
158
- else:
159
- break
160
 
161
  print(f"βœ… Fetched {len(articles)} {language} articles from Firebase")
162
- return articles
163
 
164
  except Exception as e:
165
  print(f"❌ Error in filtered fetch: {e}")
166
- # Fallback to simple fetch without filter
 
 
167
  return self._fetch_articles_simple(collection_name, limit)
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  def _fetch_articles_simple(self, collection_name: str, limit: int) -> List[NewsArticle]:
170
  """Original simple fetch method without filtering."""
171
  try:
@@ -226,26 +309,91 @@ class FirebaseNewsLoader:
226
  return []
227
 
228
  def _convert_doc(self, doc: Dict[str, Any]) -> Optional[NewsArticle]:
 
229
  try:
230
  doc_name = doc.get("name", "")
231
  doc_id = doc_name.split("/")[-1] if doc_name else "unknown"
232
  fields = doc.get("fields", {})
 
 
233
  data: Dict[str, Any] = {}
234
  for fname, fval in fields.items():
235
  if fval and isinstance(fval, dict):
236
- ftype = list(fval.keys())[0]
237
- data[fname] = fval[ftype]
238
- return NewsArticle(
239
- title=data.get("Title", data.get("title", "Untitled")),
240
- content=data.get("Article_text", data.get("content", "")),
241
- url=data.get("URL", data.get("url", f"firebase://doc/{doc_id}")),
242
- source=data.get("source", "Firebase"),
243
- published_date=data.get("Date", data.get("createdAt", datetime.now().isoformat())),
244
- scraped_date=data.get("scrapedAt", data.get("createdAt", datetime.now().isoformat())),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  article_id=doc_id,
246
  )
 
 
 
247
  except Exception as e:
248
- print(f"⚠️ Conversion error: {e}")
249
  return None
250
 
251
  def load_news_articles(self, collection_name: str = "Articles", limit: int = 100) -> List[NewsArticle]:
 
53
 
54
  remaining = None if (limit is None or (isinstance(limit, int) and limit <= 0)) else int(limit)
55
  articles: List[NewsArticle] = []
 
 
56
 
57
+ # First, let's check what the data actually looks like
58
+ print(f"πŸ” Analyzing Firebase data structure for language filtering...")
59
+
60
+ # Get a small sample first to understand the data structure
61
+ sample_query = {
62
  "structuredQuery": {
63
  "from": [{"collectionId": collection_name}],
64
+ "limit": 3
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
  }
67
 
68
+ headers = {'Content-Type': 'application/json'}
69
+ params = {"key": self.api_key}
 
70
 
71
+ sample_resp = requests.post(query_url, json=sample_query, headers=headers, params=params, timeout=30)
72
 
73
+ if sample_resp.status_code == 200:
74
+ sample_data = sample_resp.json()
75
+ print(f"πŸ“‹ Sample response contains {len(sample_data)} items")
 
 
 
76
 
77
+ # Analyze the structure of the first document
78
+ if isinstance(sample_data, list) and len(sample_data) > 0:
79
+ first_item = sample_data[0]
80
+ if "document" in first_item:
81
+ doc = first_item["document"]
82
+ if "fields" in doc:
83
+ fields = doc["fields"]
84
+ available_fields = list(fields.keys())
85
+ print(f"πŸ“Š Available fields: {available_fields}")
86
+
87
+ # Check language field specifically
88
+ if "language" in fields:
89
+ lang_field = fields["language"]
90
+ print(f"πŸ”€ Language field structure: {lang_field}")
91
+ if "stringValue" in lang_field:
92
+ print(f"πŸ”€ Language value: '{lang_field['stringValue']}'")
93
+ else:
94
+ print("⚠️ No 'language' field found! Looking for alternatives...")
95
+ # Check for alternative language field names
96
+ lang_candidates = [f for f in available_fields if 'lang' in f.lower()]
97
+ if lang_candidates:
98
+ print(f"πŸ” Possible language fields: {lang_candidates}")
99
+ # Use the first candidate
100
+ alt_field = lang_candidates[0]
101
+ print(f"πŸ”„ Using '{alt_field}' as language field")
102
+ language_field = alt_field
103
+ else:
104
+ print("❌ No language field found. Falling back to content analysis.")
105
+ return self._fetch_with_content_filter(collection_name, limit, language)
106
+
107
+ # Sample a few more documents to see language distribution
108
+ lang_values = set()
109
+ for item in sample_data:
110
+ if "document" in item and "fields" in item["document"]:
111
+ doc_fields = item["document"]["fields"]
112
+ if "language" in doc_fields and "stringValue" in doc_fields["language"]:
113
+ lang_values.add(doc_fields["language"]["stringValue"])
114
+
115
+ print(f"🌐 Language values found in sample: {list(lang_values)}")
116
+
117
+ # Now try to query with language filter
118
+ language_variants = [language, language.lower(), language.upper(), language.capitalize()]
119
+
120
+ for lang_variant in language_variants:
121
+ print(f"πŸ” Trying language filter: '{lang_variant}'")
122
 
123
+ query_data = {
124
+ "structuredQuery": {
125
+ "from": [{"collectionId": collection_name}],
126
+ "where": {
127
+ "fieldFilter": {
128
+ "field": {"fieldPath": "language"},
129
+ "op": "EQUAL",
130
+ "value": {"stringValue": lang_variant}
131
+ }
132
+ },
133
+ "limit": min(remaining or 1000, 1000)
134
+ }
135
+ }
136
 
137
  resp = requests.post(query_url, json=query_data, headers=headers, params=params, timeout=30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ if resp.status_code == 200:
140
+ data = resp.json()
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ if isinstance(data, list):
143
+ filtered_count = len(data)
144
+ print(f"πŸ“ˆ Found {filtered_count} articles with language='{lang_variant}'")
145
+
146
+ if filtered_count > 0:
147
+ # Process the results
148
+ for result in data:
149
+ if "document" in result:
150
+ doc = result["document"]
151
+ art = self._convert_doc(doc)
152
+ if art:
153
+ articles.append(art)
154
+
155
+ # If we got good results, continue with this variant
156
+ if len(articles) >= 10: # Good number of articles
157
+ print(f"βœ… Using language variant '{lang_variant}' - found {len(articles)} articles")
158
+
159
+ # If we need more articles, do additional queries
160
+ if remaining and len(articles) < remaining:
161
+ additional_needed = remaining - len(articles)
162
+ # Implement pagination here if needed
163
+ pass
164
+
165
+ break
166
 
167
+ time.sleep(0.2) # Small delay between attempts
168
+
169
+ # If we still don't have enough articles, fall back to content filtering
170
+ if len(articles) < 100:
171
+ print(f"⚠️ Only found {len(articles)} articles with language filter. Trying content-based filtering...")
172
+ fallback_articles = self._fetch_with_content_filter(collection_name, remaining or 1000, language)
 
 
173
 
174
+ # Merge results, avoiding duplicates
175
+ existing_ids = {art.article_id for art in articles}
176
+ for art in fallback_articles:
177
+ if art.article_id not in existing_ids:
178
+ articles.append(art)
179
+ if remaining and len(articles) >= remaining:
180
+ break
 
 
181
 
182
  print(f"βœ… Fetched {len(articles)} {language} articles from Firebase")
183
+ return articles[:remaining] if remaining else articles
184
 
185
  except Exception as e:
186
  print(f"❌ Error in filtered fetch: {e}")
187
+ import traceback
188
+ traceback.print_exc()
189
+ # Fallback to simple fetch
190
  return self._fetch_articles_simple(collection_name, limit)
191
 
192
+ def _fetch_with_content_filter(self, collection_name: str, limit: int, language: str) -> List[NewsArticle]:
193
+ """Fetch articles and filter by content analysis (fallback method)."""
194
+ print(f"πŸ”„ Fetching articles and filtering by content for {language}...")
195
+
196
+ # Fetch more articles to filter from
197
+ raw_articles = self._fetch_articles_simple(collection_name, min(2000, limit * 3))
198
+ filtered_articles = []
199
+
200
+ for article in raw_articles:
201
+ if self._is_likely_language(article.content, language):
202
+ filtered_articles.append(article)
203
+ if len(filtered_articles) >= limit:
204
+ break
205
+
206
+ print(f"πŸ“Š Content filtering: {len(filtered_articles)} {language} articles from {len(raw_articles)} total")
207
+ return filtered_articles
208
+
209
+ def _is_likely_language(self, text: str, target_language: str) -> bool:
210
+ """Simple heuristic to check if text is likely in the target language."""
211
+ if not text or len(text) < 50:
212
+ return False
213
+
214
+ if target_language.lower() in ["english", "en"]:
215
+ return self._is_likely_english(text)
216
+
217
+ # For other languages, we'll need different heuristics
218
+ # For now, default to True
219
+ return True
220
+
221
+ def _is_likely_english(self, text: str) -> bool:
222
+ """Simple heuristic to check if text is likely English."""
223
+ if not text or len(text) < 50:
224
+ return False
225
+
226
+ # Common English words and patterns
227
+ english_indicators = {
228
+ 'the', 'be', 'to', 'of', 'and', 'a', 'in', 'that', 'have', 'i', 'it', 'for', 'not', 'on', 'with',
229
+ 'he', 'as', 'you', 'do', 'at', 'this', 'but', 'his', 'by', 'from', 'they', 'we', 'say', 'her',
230
+ 'she', 'or', 'an', 'will', 'my', 'one', 'all', 'would', 'there', 'their', 'what', 'so', 'up',
231
+ 'out', 'if', 'about', 'who', 'get', 'which', 'go', 'me', 'when', 'make', 'can', 'like', 'time',
232
+ 'security', 'vulnerability', 'attack', 'system', 'software', 'data', 'network', 'computer',
233
+ 'application', 'server', 'database', 'information', 'technology', 'cyber', 'malware', 'breach'
234
+ }
235
+
236
+ # Convert to lowercase and split into words
237
+ words = text.lower().replace(',', ' ').replace('.', ' ').split()[:100] # Check first 100 words
238
+ if len(words) < 10:
239
+ return False
240
+
241
+ # Count English indicators
242
+ english_count = 0
243
+ for word in words:
244
+ # Remove punctuation for matching
245
+ clean_word = ''.join(c for c in word if c.isalnum())
246
+ if clean_word in english_indicators:
247
+ english_count += 1
248
+
249
+ ratio = english_count / len(words)
250
+ return ratio > 0.15 # At least 15% English indicators
251
+
252
  def _fetch_articles_simple(self, collection_name: str, limit: int) -> List[NewsArticle]:
253
  """Original simple fetch method without filtering."""
254
  try:
 
309
  return []
310
 
311
  def _convert_doc(self, doc: Dict[str, Any]) -> Optional[NewsArticle]:
312
+ """Convert Firebase document to NewsArticle with improved field mapping."""
313
  try:
314
  doc_name = doc.get("name", "")
315
  doc_id = doc_name.split("/")[-1] if doc_name else "unknown"
316
  fields = doc.get("fields", {})
317
+
318
+ # Extract field values with better handling
319
  data: Dict[str, Any] = {}
320
  for fname, fval in fields.items():
321
  if fval and isinstance(fval, dict):
322
+ # Handle different Firestore value types
323
+ if "stringValue" in fval:
324
+ data[fname] = fval["stringValue"]
325
+ elif "integerValue" in fval:
326
+ data[fname] = fval["integerValue"]
327
+ elif "doubleValue" in fval:
328
+ data[fname] = fval["doubleValue"]
329
+ elif "timestampValue" in fval:
330
+ data[fname] = fval["timestampValue"]
331
+ elif "booleanValue" in fval:
332
+ data[fname] = fval["booleanValue"]
333
+ else:
334
+ # Get the first available value type
335
+ ftype = list(fval.keys())[0]
336
+ data[fname] = fval[ftype]
337
+
338
+ # Try multiple field name variations for content
339
+ content_candidates = [
340
+ "Article_text", "article_text", "content", "Content",
341
+ "text", "Text", "body", "Body", "description", "Description",
342
+ "summary", "Summary", "article_content", "articleContent"
343
+ ]
344
+
345
+ content = ""
346
+ content_field = None
347
+ for candidate in content_candidates:
348
+ if candidate in data and data[candidate]:
349
+ content = str(data[candidate]).strip()
350
+ content_field = candidate
351
+ break
352
+
353
+ # Try multiple field name variations for title
354
+ title_candidates = [
355
+ "Title", "title", "headline", "Headline", "subject", "Subject", "name", "Name"
356
+ ]
357
+
358
+ title = "Untitled"
359
+ for candidate in title_candidates:
360
+ if candidate in data and data[candidate]:
361
+ title = str(data[candidate]).strip()
362
+ break
363
+
364
+ # Try multiple field name variations for URL
365
+ url_candidates = [
366
+ "URL", "url", "link", "Link", "href", "source_url", "sourceUrl"
367
+ ]
368
+
369
+ url = f"firebase://doc/{doc_id}"
370
+ for candidate in url_candidates:
371
+ if candidate in data and data[candidate]:
372
+ url = str(data[candidate]).strip()
373
+ break
374
+
375
+ # Debug output for empty content
376
+ if not content or len(content) < 50:
377
+ available_fields = list(data.keys())
378
+ print(f"⚠️ Article {doc_id[:8]}... has minimal content:")
379
+ print(f" Content field '{content_field}': {len(content)} chars")
380
+ print(f" Available fields: {available_fields}")
381
+ print(f" Sample data: {str(data)[:200]}...")
382
+
383
+ article = NewsArticle(
384
+ title=title,
385
+ content=content,
386
+ url=url,
387
+ source=data.get("source", data.get("Source", "Firebase")),
388
+ published_date=data.get("Date", data.get("date", data.get("published_date", data.get("createdAt", datetime.now().isoformat())))),
389
+ scraped_date=data.get("scrapedAt", data.get("scraped_date", data.get("createdAt", datetime.now().isoformat()))),
390
  article_id=doc_id,
391
  )
392
+
393
+ return article
394
+
395
  except Exception as e:
396
+ print(f"⚠️ Document conversion error for {doc_id}: {e}")
397
  return None
398
 
399
  def load_news_articles(self, collection_name: str = "Articles", limit: int = 100) -> List[NewsArticle]:
cve_factchecker/retriever.py CHANGED
@@ -95,10 +95,70 @@ class VectorNewsRetriever:
95
 
96
  splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
97
  docs: List[Document] = []
 
 
 
 
 
98
  for art in articles:
99
- chunks = splitter.split_text(art.content or "")
100
- for chunk in chunks:
101
- docs.append(Document(page_content=f"Title: {art.title}\n\n{chunk}", metadata={"url": art.url, "source": art.source, "published_date": art.published_date, "scraped_date": art.scraped_date, "id": art.article_id}))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  # Process documents in batches to avoid Chroma batch size limits
104
  batch_size = 4000 # Conservative batch size for Chroma
@@ -116,8 +176,11 @@ class VectorNewsRetriever:
116
  self.vector_store.add_documents(batch_docs)
117
  else:
118
  self.vector_store.add_texts([d.page_content for d in batch_docs], metadatas=[d.metadata for d in batch_docs])
 
119
  except Exception as e:
120
  print(f"❌ Failed to store batch {batch_num}: {e}")
 
 
121
  continue
122
  continue
123
 
 
95
 
96
  splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
97
  docs: List[Document] = []
98
+
99
+ # Process articles and filter out those with insufficient content
100
+ valid_articles = 0
101
+ skipped_articles = 0
102
+
103
  for art in articles:
104
+ content = art.content or ""
105
+ title = art.title or "Untitled"
106
+
107
+ # Skip articles with very little content
108
+ if len(content.strip()) < 50:
109
+ print(f"⚠️ Skipping article '{title[:50]}...' - insufficient content ({len(content)} chars)")
110
+ skipped_articles += 1
111
+ continue
112
+
113
+ # Create chunks from the content
114
+ try:
115
+ chunks = splitter.split_text(content)
116
+
117
+ if not chunks:
118
+ print(f"⚠️ No chunks created for article '{title[:50]}...'")
119
+ skipped_articles += 1
120
+ continue
121
+
122
+ # Create documents for each chunk
123
+ for i, chunk in enumerate(chunks):
124
+ if len(chunk.strip()) < 30: # Skip very small chunks
125
+ continue
126
+
127
+ # Create comprehensive page content
128
+ page_content = f"Title: {title}\n\n{chunk}"
129
+
130
+ # Add source information if it's not already in the content
131
+ if art.source and art.source not in chunk:
132
+ page_content += f"\n\nSource: {art.source}"
133
+
134
+ metadata = {
135
+ "url": art.url,
136
+ "source": art.source,
137
+ "published_date": art.published_date,
138
+ "scraped_date": art.scraped_date,
139
+ "id": art.article_id,
140
+ "chunk_id": f"{art.article_id}_{i}",
141
+ "title": title
142
+ }
143
+
144
+ docs.append(Document(page_content=page_content, metadata=metadata))
145
+
146
+ valid_articles += 1
147
+
148
+ except Exception as e:
149
+ print(f"❌ Error processing article '{title[:50]}...': {e}")
150
+ skipped_articles += 1
151
+ continue
152
+
153
+ print(f"πŸ“Š Article processing summary:")
154
+ print(f" Total articles: {len(articles)}")
155
+ print(f" Valid articles: {valid_articles}")
156
+ print(f" Skipped articles: {skipped_articles}")
157
+ print(f" Generated chunks: {len(docs)}")
158
+
159
+ if not docs:
160
+ print("❌ No valid document chunks to store!")
161
+ return
162
 
163
  # Process documents in batches to avoid Chroma batch size limits
164
  batch_size = 4000 # Conservative batch size for Chroma
 
176
  self.vector_store.add_documents(batch_docs)
177
  else:
178
  self.vector_store.add_texts([d.page_content for d in batch_docs], metadatas=[d.metadata for d in batch_docs])
179
+ print(f"βœ… Successfully stored batch {batch_num}")
180
  except Exception as e:
181
  print(f"❌ Failed to store batch {batch_num}: {e}")
182
+ # Continue with next batch instead of failing completely
183
+ continue
184
  continue
185
  continue
186
 
test_improvements.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify the Firebase loading and content processing improvements.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+
9
+ # Add the parent directory to Python path
10
+ current_dir = os.path.dirname(os.path.abspath(__file__))
11
+ sys.path.insert(0, current_dir)
12
+
13
+ def test_firebase_loading():
14
+ """Test Firebase loading with language filtering."""
15
+ print("πŸ§ͺ Testing Firebase Loading and Content Processing")
16
+ print("=" * 60)
17
+
18
+ try:
19
+ from cve_factchecker.firebase_loader import FirebaseNewsLoader
20
+
21
+ # Test Firebase loader
22
+ loader = FirebaseNewsLoader()
23
+ print(f"βœ… Firebase loader initialized - Project: {loader.project_id}")
24
+
25
+ # Test with very small limit first
26
+ print("\nπŸ” Testing with small sample (5 articles)...")
27
+ articles = loader.fetch_articles(limit=5)
28
+
29
+ if articles:
30
+ print(f"βœ… Fetched {len(articles)} sample articles")
31
+
32
+ # Analyze the first article
33
+ first_article = articles[0]
34
+ print(f"\nπŸ“‹ Sample Article Analysis:")
35
+ print(f" Title: {first_article.title[:100]}...")
36
+ print(f" Content length: {len(first_article.content)} characters")
37
+ print(f" URL: {first_article.url}")
38
+ print(f" Source: {first_article.source}")
39
+ print(f" Article ID: {first_article.article_id}")
40
+
41
+ # Show content preview
42
+ if first_article.content:
43
+ content_preview = first_article.content[:300].replace('\n', ' ')
44
+ print(f" Content preview: {content_preview}...")
45
+ else:
46
+ print(" ⚠️ No content found!")
47
+
48
+ # Test language-specific fetching
49
+ print("\n🌐 Testing English language filtering...")
50
+ english_articles = loader.fetch_articles_by_language("English", limit=10)
51
+
52
+ if english_articles:
53
+ print(f"βœ… Fetched {len(english_articles)} English articles")
54
+
55
+ # Check content quality
56
+ valid_content = 0
57
+ for article in english_articles[:3]: # Check first 3
58
+ if article.content and len(article.content) > 100:
59
+ valid_content += 1
60
+ print(f" πŸ“„ '{article.title[:50]}...' - {len(article.content)} chars")
61
+ else:
62
+ print(f" ⚠️ '{article.title[:50]}...' - insufficient content")
63
+
64
+ print(f" Content quality: {valid_content}/{min(3, len(english_articles))} articles have substantial content")
65
+ else:
66
+ print("❌ No English articles found")
67
+
68
+ return english_articles
69
+
70
+ except Exception as e:
71
+ print(f"❌ Firebase test failed: {e}")
72
+ import traceback
73
+ traceback.print_exc()
74
+ return []
75
+
76
+ def test_vector_processing(articles):
77
+ """Test vector store processing."""
78
+ print("\nπŸ” Testing Vector Store Processing")
79
+ print("=" * 60)
80
+
81
+ try:
82
+ from cve_factchecker.retriever import VectorRetriever
83
+
84
+ # Test vector retriever
85
+ retriever = VectorRetriever(persist_directory="/tmp/test_vector_db")
86
+ print("βœ… Vector retriever initialized")
87
+
88
+ # Test article storage
89
+ print(f"\nπŸ“¦ Testing storage of {len(articles)} articles...")
90
+ retriever.store_articles_in_vector_db(articles, clear_first=True)
91
+
92
+ # Test retrieval
93
+ print("\nπŸ” Testing document retrieval...")
94
+ test_query = "security vulnerability"
95
+ results = retriever.search(test_query, k=3)
96
+
97
+ if results:
98
+ print(f"βœ… Found {len(results)} relevant documents for '{test_query}'")
99
+ for i, doc in enumerate(results):
100
+ content_preview = doc.page_content[:100].replace('\n', ' ')
101
+ print(f" {i+1}. {content_preview}...")
102
+ else:
103
+ print("⚠️ No documents found for test query")
104
+
105
+ return True
106
+
107
+ except Exception as e:
108
+ print(f"❌ Vector processing test failed: {e}")
109
+ import traceback
110
+ traceback.print_exc()
111
+ return False
112
+
113
+ def test_full_system():
114
+ """Test the complete system integration."""
115
+ print("\nπŸš€ Testing Full System Integration")
116
+ print("=" * 60)
117
+
118
+ try:
119
+ from cve_factchecker.orchestrator import FactCheckSystem
120
+
121
+ # Test system initialization
122
+ system = FactCheckSystem(vector_dir="/tmp/test_system_vector")
123
+ print("βœ… Fact check system initialized")
124
+
125
+ # Test Firebase ingestion
126
+ print("\nπŸ”„ Testing Firebase ingestion...")
127
+ result = system.ingest_firebase(limit=10)
128
+
129
+ if result.get("success"):
130
+ print(f"βœ… Ingestion successful: {result.get('synced')} articles")
131
+ else:
132
+ print(f"⚠️ Ingestion issues: {result.get('error', 'Unknown error')}")
133
+
134
+ # Test fact checking
135
+ print("\n🧠 Testing fact checking...")
136
+ test_claim = "A new security vulnerability was discovered in popular software"
137
+ fact_result = system.fact_check(test_claim)
138
+
139
+ print(f" Claim: {test_claim}")
140
+ print(f" Verdict: {fact_result.get('verdict', 'Unknown')}")
141
+ print(f" Confidence: {fact_result.get('confidence', 0)}")
142
+ reasoning = fact_result.get('reasoning', 'No reasoning provided')
143
+ print(f" Reasoning: {reasoning[:200]}...")
144
+
145
+ return True
146
+
147
+ except Exception as e:
148
+ print(f"❌ Full system test failed: {e}")
149
+ import traceback
150
+ traceback.print_exc()
151
+ return False
152
+
153
+ def main():
154
+ """Run all tests."""
155
+ print("πŸ§ͺ CVE Fact Checker - Comprehensive Test Suite")
156
+ print("=" * 80)
157
+
158
+ # Test 1: Firebase loading
159
+ articles = test_firebase_loading()
160
+
161
+ # Test 2: Vector processing (if we have articles)
162
+ if articles:
163
+ vector_success = test_vector_processing(articles)
164
+ else:
165
+ print("\n⚠️ Skipping vector processing test - no articles loaded")
166
+ vector_success = False
167
+
168
+ # Test 3: Full system integration
169
+ system_success = test_full_system()
170
+
171
+ # Summary
172
+ print("\nπŸ“Š Test Results Summary")
173
+ print("=" * 80)
174
+ print(f"Firebase Loading: {'βœ… PASS' if articles else '❌ FAIL'}")
175
+ print(f"Vector Processing: {'βœ… PASS' if vector_success else '❌ FAIL'}")
176
+ print(f"System Integration: {'βœ… PASS' if system_success else '❌ FAIL'}")
177
+
178
+ overall_success = bool(articles) and vector_success and system_success
179
+ print(f"\nOverall Result: {'βœ… ALL TESTS PASSED' if overall_success else '❌ SOME TESTS FAILED'}")
180
+
181
+ return overall_success
182
+
183
+ if __name__ == "__main__":
184
+ success = main()
185
+ sys.exit(0 if success else 1)