CoolFace
Apppublic

chanderbawa1983/factual-feed

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
open_source_processor.py316 linesDownload Raw Back to root
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