chanderbawa1983/factual-feed
0
1"""2Unified Open-Source LLM Processor3Supports multiple free alternatives: Ollama, Hugging Face, and Groq4"""5 6import os7import requests8import json9import logging10from typing import List, Dict, Optional, Union11from ollama_processor import OllamaProcessor12from huggingface_processor import HuggingFaceProcessor13 14logging.basicConfig(level=logging.INFO)15logger = logging.getLogger(__name__)16 17class GroqProcessor:18 """Groq API processor - fast inference with free tier"""19 20 def __init__(self, api_key: str, model: str = "llama-3.1-8b-instant"):21 self.api_key = api_key22 self.model = model23 self.base_url = "https://api.groq.com/openai/v1"24 25 def _make_request(self, messages: List[Dict], max_tokens: int = 500) -> str:26 """Make request to Groq API"""27 try:28 headers = {29 "Authorization": f"Bearer {self.api_key}",30 "Content-Type": "application/json"31 }32 33 payload = {34 "model": self.model,35 "messages": messages,36 "max_tokens": max_tokens,37 "temperature": 0.338 }39 40 response = requests.post(41 f"{self.base_url}/chat/completions",42 headers=headers,43 json=payload,44 timeout=3045 )46 47 if response.status_code == 200:48 result = response.json()49 return result["choices"][0]["message"]["content"].strip()50 else:51 logger.error(f"Groq API error: {response.status_code} - {response.text}")52 return ""53 54 except Exception as e:55 logger.error(f"Error with Groq API: {e}")56 return ""57 58 # ---- Public helpers to match other processors ----59 def generate_summary(self, article: Dict) -> str:60 """Summarize an article in 3-4 sentences via Groq."""61 title = article.get('title', '')62 content = (article.get('content') or '')[:1500]63 messages = [64 {"role": "system", "content": "You are a helpful assistant that writes concise, factual news summaries in 3-4 sentences."},65 {"role": "user", "content": f"Title: {title}\n\nArticle:\n{content}\n\nWrite a 3-4 sentence summary:"}66 ]67 summary = self._make_request(messages, max_tokens=220)68 if summary and len(summary) > 20:69 return summary.strip()70 # Fallback extractive71 sentences = (article.get('content') or '').split('. ')72 return '. '.join(sentences[:3]) + '.' if sentences else ''73 74 def generate_draft_trivia(self, article: Dict, entities: List[Dict]) -> List[Dict]:75 """Lightweight trivia generation based on top entities (no API needed)."""76 questions = []77 top_entities = entities[:3]78 for entity in top_entities:79 entity_text = entity.get('text', '')80 entity_type = entity.get('label', '')81 context = entity.get('context_sentence', '')82 83 if entity_type == 'PERSON':84 q_text = f"Who is mentioned in relation to: {context[:60]}?"85 distractors = ["John Smith", "Jane Doe", "Alex Johnson"]86 elif entity_type == 'ORG':87 q_text = "Which organization is mentioned in the article?"88 distractors = ["Microsoft", "Google", "Amazon"]89 elif entity_type == 'GPE':90 q_text = "Which location is mentioned in the article?"91 distractors = ["New York", "London", "Tokyo"]92 elif entity_type in ['DATE', 'CARDINAL', 'MONEY', 'PERCENT']:93 q_text = f"What {entity_type.lower()} is mentioned in the article?"94 distractors = ["2022", "2023", "2024"] if entity_type == 'DATE' else ["Option A", "Option B", "Option C"]95 else:96 q_text = f"What {entity_type.lower() or 'detail'} is mentioned in the article?"97 distractors = ["Option A", "Option B", "Option C"]98 99 questions.append({100 'question': q_text,101 'correct_answer': entity_text,102 'source_sentence': context or f"The article mentions {entity_text}.",103 'distractors': distractors,104 'difficulty': 'medium',105 'entity_type': entity_type,106 'article_url': article.get('url', ''),107 'article_title': article.get('title', ''),108 'source_article': article.get('content', '')109 })110 111 logger.info(f"Generated {len(questions)} draft trivia questions via Groq processor helper")112 return questions113 114 def verify_fact(self, claim: str, source_text: str) -> Dict:115 """Simple keyword-based verification to avoid extra API calls and stay robust."""116 claim_lower = (claim or '').lower()117 source_lower = (source_text or '').lower()118 claim_words = {w for w in claim_lower.split() if len(w) > 3}119 source_words = set(source_lower.split())120 overlap = len(claim_words & source_words)121 total = len(claim_words) or 1122 ratio = overlap / total123 if ratio >= 0.7:124 status = 'SUPPORTED'125 explanation = f'High word overlap ({ratio:.0%})'126 elif ratio >= 0.4:127 status = 'PARTIALLY_SUPPORTED'128 explanation = f'Moderate word overlap ({ratio:.0%})'129 else:130 status = 'NOT_SUPPORTED'131 explanation = f'Low word overlap ({ratio:.0%})'132 return {133 'status': status,134 'explanation': explanation,135 'relevant_quote': source_text[:200] if source_text else ''136 }137 138class OpenSourceProcessor:139 """Unified processor that tries multiple open-source options"""140 141 def __init__(self, preferred_backend: str = "auto"):142 """143 Initialize with preferred backend144 145 Args:146 preferred_backend: "ollama", "huggingface", "groq", or "auto"147 """148 self.preferred_backend = preferred_backend149 self.active_processor = None150 self.backend_type = None151 152 self._initialize_processor()153 154 def _initialize_processor(self):155 """Initialize the best available processor"""156 157 if self.preferred_backend == "ollama" or self.preferred_backend == "auto":158 # Try Ollama first159 try:160 processor = OllamaProcessor()161 if processor._test_connection():162 self.active_processor = processor163 self.backend_type = "ollama"164 logger.info("✅ Using Ollama backend")165 return166 except Exception as e:167 logger.info(f"Ollama not available: {e}")168 169 if self.preferred_backend == "groq" or self.preferred_backend == "auto":170 # Try Groq API171 groq_key = os.getenv("GROQ_API_KEY")172 if groq_key:173 try:174 processor = GroqProcessor(groq_key)175 # Test with a simple request176 test_response = processor._make_request([177 {"role": "user", "content": "Hello"}178 ], max_tokens=10)179 if test_response:180 self.active_processor = processor181 self.backend_type = "groq"182 logger.info("✅ Using Groq backend")183 return184 except Exception as e:185 logger.info(f"Groq not available: {e}")186 187 if self.preferred_backend == "huggingface" or self.preferred_backend == "auto":188 # Fallback to Hugging Face189 try:190 processor = HuggingFaceProcessor()191 if processor.model is not None:192 self.active_processor = processor193 self.backend_type = "huggingface"194 logger.info("✅ Using Hugging Face backend")195 return196 except Exception as e:197 logger.info(f"Hugging Face not available: {e}")198 199 logger.error("❌ No open-source LLM backend available")200 self.active_processor = None201 self.backend_type = None202 203 def generate_summary(self, article: Dict) -> str:204 """Generate article summary using available backend"""205 if not self.active_processor:206 # Fallback to extractive summary207 sentences = article['content'].split('. ')208 return '. '.join(sentences[:3]) + '.'209 210 return self.active_processor.generate_summary(article)211 212 def generate_draft_trivia(self, article: Dict, entities: List[Dict]) -> List[Dict]:213 """Generate draft trivia questions"""214 if not self.active_processor:215 return []216 217 return self.active_processor.generate_draft_trivia(article, entities)218 219 def verify_fact(self, claim: str, source_text: str) -> Dict:220 """Verify fact using available backend"""221 if not self.active_processor:222 # Simple keyword-based fallback223 claim_lower = claim.lower()224 source_lower = source_text.lower()225 226 if any(word in source_lower for word in claim_lower.split() if len(word) > 3):227 return {228 'status': 'SUPPORTED',229 'explanation': 'Keywords found in source text',230 'relevant_quote': source_text[:200]231 }232 else:233 return {234 'status': 'NOT_SUPPORTED',235 'explanation': 'Keywords not found in source text',236 'relevant_quote': ''237 }238 239 return self.active_processor.verify_fact(claim, source_text)240 241 # Compatibility helper to match AIProcessor API242 def improve_distractors(self, question: Dict) -> Dict:243 """Improve or ensure distractors exist.244 If the active backend exposes improve_distractors, delegate to it.245 Otherwise, generate simple type-based distractors to keep pipeline robust.246 """247 try:248 if hasattr(self.active_processor, 'improve_distractors'):249 return self.active_processor.improve_distractors(question)250 except Exception:251 # Fall through to basic handling252 pass253 254 q = dict(question)255 answer = q.get('correct_answer', '')256 entity_type = (q.get('entity_type') or '').upper()257 distractors = q.get('distractors') or []258 259 # Ensure at least 3 distractors260 needed = max(0, 3 - len(distractors))261 pool = []262 if entity_type == 'PERSON':263 pool = ["John Smith", "Jane Doe", "Alex Johnson", "Chris Lee"]264 elif entity_type == 'ORG':265 pool = ["Microsoft", "Google", "Amazon", "Meta"]266 elif entity_type == 'GPE':267 pool = ["New York", "London", "Tokyo", "Paris"]268 elif entity_type == 'DATE':269 pool = ["2021", "2022", "2023", "2024"]270 elif entity_type == 'MONEY':271 pool = ["$1 million", "$5 million", "$10 million", "$500,000"]272 else:273 pool = ["Option A", "Option B", "Option C", "Option D"]274 275 # Remove the correct answer if present276 pool = [p for p in pool if p != answer]277 distractors = distractors + pool[:needed]278 q['distractors'] = distractors[:3]279 return q280 281 def get_backend_info(self) -> Dict:282 """Get information about active backend"""283 return {284 'backend_type': self.backend_type,285 'model_name': getattr(self.active_processor, 'model', 'Unknown'),286 'available': self.active_processor is not None287 }288 289def get_setup_instructions() -> Dict[str, str]:290 """Get setup instructions for each backend"""291 return {292 "ollama": """293# Ollama Setup (Recommended)2941. Install: brew install ollama2952. Start: ollama serve2963. Pull model: ollama pull llama3.12974. Available models: llama3.1, mistral, codellama, phi3298 """,299 300 "groq": """301# Groq Setup (Fast & Free)3021. Sign up: https://console.groq.com/3032. Get API key from dashboard3043. Add to .env: GROQ_API_KEY=your_key_here3054. Free tier: 14,400 requests/day306 """,307 308 "huggingface": """309# Hugging Face Setup (Local)3101. Already installed with transformers3112. Models download automatically3123. Requires 4-8GB RAM for good models3134. GPU recommended for larger models314 """315 }316 