CoolFace
Apppublic

Khanishka/AI-document-analyzer-mini

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
ai_engine.py246 linesDownload Raw Back to modules
1 2def _detect_doc_type(text: str) -> str:3    text_lower = text.lower()4    opinion_signals = [5        "i believe", "i think", "i feel", "happiness",6        "fear", "love", "hate", "amazing", "terrible",7        "story", "anecdote", "lesson", "mindset",8        "you should", "the best", "the worst"9    ]10    formal_signals = [11        "whereas", "hereby", "pursuant", "therefore",12        "section", "clause", "regulation", "the party",13        "agreement", "terms and conditions"14    ]15    opinion_count = sum(1 for s in opinion_signals if s in text_lower)16    formal_count = sum(1 for s in formal_signals if s in text_lower)17    if formal_count >= 3 and formal_count > opinion_count:18        return "formal"19    return "opinion"20 21import re22 23def _is_noise_amount(value):24    val = value.strip()25    if re.match(r'^97[89][0-9\-]{10,}$', val): return True26    if re.match(r'^[+]?[0-9 ()\-.]{7,}$', val) and len(val) > 8: return True27    return False28 29import re as _re30 31def _clean_text_for_summary(text: str) -> str:32    """Remove noise before sending to AI — emails, phones, URLs, symbols"""33    # Remove emails34    text = _re.sub(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '', text)35    # Remove URLs36    text = _re.sub(r'http[s]?://\S+', '', text)37    # Remove phone numbers38    text = _re.sub(r'[\+]?[0-9][\s\-\.]?[(]?[0-9]{3}[)]?[\s\-\.]?[0-9]{3}[\s\-\.]?[0-9]{4,}', '', text)39    # Remove LinkedIn/GitHub URLs40    text = _re.sub(r'(linkedin|github)\.com/\S+', '', text)41    # Remove lines that are just symbols or single words (headers)42    lines = text.split('\n')43    clean_lines = []44    for line in lines:45        stripped = line.strip()46        # Skip very short lines (likely headers/labels)47        if len(stripped) < 15 and stripped.isupper():48            continue49        # Skip lines with mostly special characters50        alpha_ratio = sum(c.isalpha() for c in stripped) / max(len(stripped), 1)51        if alpha_ratio < 0.4 and len(stripped) > 0:52            continue53        clean_lines.append(line)54    return '\n'.join(clean_lines).strip()55 56"""57ai_engine.py58------------59Core AI features powered by Groq LLM + RAG Pipeline:60  - Summarization        (Groq llama-3.3-70b-versatile)61  - Question Answering   (RAG pipeline — TF-IDF retrieval + Groq LLM)62  - Sentiment Analysis   (Groq llama-3.3-70b-versatile)63  - Language Detection   (langdetect)64 65API Key is loaded from environment variable GROQ_API_KEY.66Never hardcode API keys in source files.67"""68 69import os70import re71import time72 73from langdetect import detect, DetectorFactory74from langdetect.lang_detect_exception import LangDetectException75from sklearn.feature_extraction.text import TfidfVectorizer76from sklearn.metrics.pairwise import cosine_similarity77import numpy as np78from groq import Groq79 80DetectorFactory.seed = 4281 82_client = None83 84def _get_client():85    global _client86    if _client is None:87        api_key = os.environ.get("GROQ_API_KEY", "")88        if not api_key:89            raise ValueError(90                "GROQ_API_KEY not set. "91                "Run: import os; os.environ['GROQ_API_KEY'] = 'your-key'"92            )93        _client = Groq(api_key=api_key)94    return _client95 96 97def _ask(prompt: str, max_tokens: int = 400) -> str:98    try:99        r = _get_client().chat.completions.create(100            model="llama-3.3-70b-versatile",101            max_tokens=max_tokens,102            messages=[{"role": "user", "content": prompt}]103        )104        return r.choices[0].message.content.strip()105    except Exception as e:106        print(f"[ai_engine] Groq error: {e}")107        return ""108 109 110def _truncate(text: str, max_chars: int = 4000) -> str:111    return str(text)[:max_chars] if len(str(text)) > max_chars else str(text)112 113 114def _skip_boilerplate(text: str) -> str:115    """Skip copyright/ISBN pages and get to actual content."""116    lines = text.split("\n")117    clean = []118    skip_keywords = ["isbn", "copyright", "all rights reserved", 119                     "published by", "cataloguing", "tel:", "email:",120                     "website:", "harriman", "first published"]121    for line in lines:122        if any(k in line.lower() for k in skip_keywords) and len(line) < 100:123            continue124        clean.append(line)125    result = "\n".join(clean)126    # Skip first 200 chars if they look like metadata127    if result[:200].lower().count("\n") > 5:128        result = result[200:]129    return result.strip()130 131def summarize(text: str) -> dict:132    start = time.time()133    cleaned = _skip_boilerplate(text)134    t = _truncate(cleaned, 4000)135    if len(t.split()) < 30:136        return {"summary": t, "confidence": 1.0, "processing_time_ms": 0}137    out = _ask(138        "Read the document below and write a clear, accurate 3-5 sentence summary.\n"139        "Focus on the main ideas and themes, not metadata or copyright info.\n"140        "Write ONLY the summary sentences. No labels, no bullet points, no preamble.\n\n"141        f"Document:\n{t}", max_tokens=250)142    if not out:143        sentences = re.split(r"(?<=[.!?])\s+", t.strip())144        out = " ".join(sentences[:3])145    confidence = round(min(1.0, len(out.split()) / max(1, len(t.split()) * 0.3)), 2)146    return {"summary": out, "confidence": confidence,147            "processing_time_ms": round((time.time() - start) * 1000, 2)}148 149 150def _split_chunks(text: str, chunk_size: int = 200, overlap: int = 50) -> list:151    words = str(text).split()152    chunks, i = [], 0153    while i < len(words):154        end = min(i + chunk_size, len(words))155        chunks.append(" ".join(words[i:end]))156        if end == len(words): break157        i += chunk_size - overlap158    return chunks159 160 161def _top_chunks(question: str, chunks: list, top_k: int = 5) -> list:162    if not chunks: return chunks[:top_k]163    try:164        corpus = chunks + [question]165        mat = TfidfVectorizer(stop_words="english", ngram_range=(1, 2)).fit_transform(corpus)166        sims = cosine_similarity(mat[-1], mat[:-1]).flatten()167        return [chunks[j] for j in np.argsort(sims)[::-1][:top_k]]168    except Exception:169        return chunks[:top_k]170 171 172def answer_question(context: str, question: str, history: list = None) -> dict:173    start  = time.time()174    chunks = _split_chunks(context)175    tops   = _top_chunks(question, chunks)176    ctx    = " ".join(tops)177    178    # Build conversation history string179    history_str = ""180    if history:181        for turn in history[-4:]:  # last 4 turns max182            history_str += f"User: {turn['question']}\nAssistant: {turn['answer']}\n\n"183    184    prompt = (185        "Answer the question using ONLY the document excerpt below.\n"186        "Give a detailed, specific answer in 3-5 sentences.\n"187        "If the answer is not present say: 'This information is not found in the document.'\n\n"188        f"Document excerpt:\n{ctx[:2500]}\n\n"189    )190    if history_str:191        prompt += f"Previous conversation:\n{history_str}\n"192    prompt += f"Question: {question}\n\nAnswer:"193    194    out = _ask(prompt, max_tokens=350)195    if not out:196        out = "Could not find a specific answer. Try rephrasing your question."197    conf = 0.88 if out and "not found in the document" not in out.lower() else 0.2198    return {"answer": out, "confidence": conf,199            "processing_time_ms": round((time.time() - start) * 1000, 2)}200 201 202def analyze_sentiment(text: str) -> dict:203    start = time.time()204    out = _ask(205        "Analyze the overall sentiment and tone of this text.\n"206        "Rules:\n"207        "- Books about personal growth, wealth, happiness, motivation = POSITIVE\n"208        "- News about disasters, crime, failures = NEGATIVE\n"209        "- Legal documents, technical manuals = NEUTRAL\n"210        "Reply ONLY one word: POSITIVE, NEGATIVE, or NEUTRAL.\n\n"211        f"Text: {_truncate(text, 1500)}", max_tokens=5)212    label = out.strip().upper()213    if label not in ["POSITIVE", "NEGATIVE", "NEUTRAL"]:214        label = "NEUTRAL"215    conf_map = {"POSITIVE": 0.92, "NEGATIVE": 0.91, "NEUTRAL": 0.85}216    return {"label": label, "confidence": conf_map[label],217            "processing_time_ms": round((time.time() - start) * 1000, 2)}218 219 220LANGUAGE_NAMES = {221    "en": "English", "fr": "French", "de": "German", "es": "Spanish",222    "it": "Italian", "pt": "Portuguese", "nl": "Dutch", "ru": "Russian",223    "zh-cn": "Chinese (Simplified)", "zh-tw": "Chinese (Traditional)",224    "ja": "Japanese", "ko": "Korean", "ar": "Arabic", "hi": "Hindi",225    "ta": "Tamil", "te": "Telugu", "ml": "Malayalam", "bn": "Bengali",226    "ur": "Urdu", "tr": "Turkish", "pl": "Polish", "sv": "Swedish",227    "da": "Danish", "fi": "Finnish", "no": "Norwegian", "cs": "Czech",228    "hu": "Hungarian", "ro": "Romanian", "vi": "Vietnamese", "th": "Thai",229    "id": "Indonesian", "ms": "Malay", "el": "Greek", "he": "Hebrew",230    "fa": "Persian", "uk": "Ukrainian", "ca": "Catalan", "sk": "Slovak",231}232 233 234def detect_language(text: str) -> dict:235    start  = time.time()236    sample = str(text)[:1000]237    try:238        lang_code     = detect(sample)239        language_name = LANGUAGE_NAMES.get(lang_code, lang_code.upper())240        confidence    = 0.95241    except LangDetectException:242        lang_code, language_name, confidence = "unknown", "Unknown", 0.0243    return {"language_code": lang_code, "language_name": language_name,244            "confidence": confidence,245            "processing_time_ms": round((time.time() - start) * 1000, 2)}246