Spaces:
Sleeping
Sleeping
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 +248 -100
- cve_factchecker/retriever.py +66 -3
- test_improvements.py +185 -0
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 |
-
#
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
| 61 |
"structuredQuery": {
|
| 62 |
"from": [{"collectionId": collection_name}],
|
| 63 |
-
"
|
| 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 |
-
|
| 80 |
-
|
| 81 |
-
query_data["structuredQuery"]["limit"] = min(remaining, 1000) # Firestore max limit per query
|
| 82 |
|
| 83 |
-
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
if request_count >= max_requests:
|
| 89 |
-
print(f"β³ Reached max requests limit ({max_requests}), stopping to avoid rate limits")
|
| 90 |
-
break
|
| 91 |
|
| 92 |
-
#
|
| 93 |
-
if
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 124 |
-
|
| 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
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
if len(batch_articles) < query_data["structuredQuery"].get("limit", 1000):
|
| 149 |
-
break # No more results
|
| 150 |
|
| 151 |
-
#
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
article_id=doc_id,
|
| 246 |
)
|
|
|
|
|
|
|
|
|
|
| 247 |
except Exception as e:
|
| 248 |
-
print(f"β οΈ
|
| 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 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|