| from flask import Flask, render_template, request
|
| import joblib
|
| import requests
|
| import re
|
| import numpy as np
|
| import torch
|
|
|
| from sentence_transformers import SentenceTransformer, util
|
| from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| from sklearn.metrics.pairwise import cosine_similarity
|
|
|
|
|
| app = Flask(__name__)
|
|
|
| model = joblib.load("hal_model.pkl")
|
|
|
| SENTENCE_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
| NLI_MODEL = "cross-encoder/nli-MiniLM2-L6-H768"
|
| MAX_LENGTH = 256
|
|
|
| TOPIC_SIMILARITY_THRESHOLD = 0.25
|
| NLI_CONTRADICTION_THRESHOLD = 0.50
|
|
|
| embedder = SentenceTransformer(SENTENCE_MODEL)
|
|
|
| nli_tokenizer = AutoTokenizer.from_pretrained(NLI_MODEL)
|
| nli_model = AutoModelForSequenceClassification.from_pretrained(NLI_MODEL).to("cpu")
|
| nli_model.eval()
|
|
|
| def clean_search_query(user_prompt):
|
| query = user_prompt.strip()
|
|
|
| patterns = [
|
| r"summarize the main facts about (.+)",
|
| r"summarize (.+)",
|
| r"tell me about (.+)",
|
| r"explain the relationship between (.+)",
|
| r"explain (.+)",
|
| r"describe (.+)",
|
| r"what is (.+)",
|
| r"who is (.+)",
|
| r"give me information about (.+)",
|
| r"write about (.+)"
|
| ]
|
|
|
| lowered = query.lower()
|
|
|
| for pattern in patterns:
|
| match = re.search(pattern, lowered)
|
| if match:
|
| query = match.group(1)
|
| break
|
|
|
| query = re.sub(r"[?.!,]+$", "", query)
|
| query = re.sub(r"^(the|a|an)\s+", "", query, flags=re.IGNORECASE)
|
| query = re.sub(r"\s+", " ", query).strip()
|
|
|
| return query if query else user_prompt
|
|
|
|
|
| def get_wikipedia_reference(user_prompt):
|
| search_url = "https://en.wikipedia.org/w/api.php"
|
|
|
| headers = {
|
| "User-Agent": "HaluDetect/1.0 student-project (https://huggingface.co/spaces/jr-0/AI/tree/main)"
|
| }
|
|
|
| search_query = clean_search_query(user_prompt)
|
|
|
| search_params = {
|
| "action": "query",
|
| "generator": "search",
|
| "gsrsearch": search_query,
|
| "gsrlimit": 10,
|
| "prop": "extracts|pageprops",
|
| "exintro": True,
|
| "explaintext": True,
|
| "format": "json"
|
| }
|
|
|
| response = requests.get(
|
| search_url,
|
| params=search_params,
|
| headers=headers,
|
| timeout=10
|
| )
|
|
|
| if response.status_code != 200:
|
| return None, None, search_query
|
|
|
| try:
|
| data = response.json()
|
| except ValueError:
|
| return None, None, search_query
|
|
|
| pages = data.get("query", {}).get("pages", {})
|
|
|
| candidates = []
|
|
|
| for page in pages.values():
|
| title = page.get("title", "")
|
| text = page.get("extract", "")
|
| pageprops = page.get("pageprops", {})
|
|
|
| if not title or not text:
|
| continue
|
|
|
| if "disambiguation" in pageprops:
|
| continue
|
|
|
| if "(disambiguation)" in title.lower():
|
| continue
|
|
|
| candidates.append({
|
| "title": title,
|
| "text": text
|
| })
|
|
|
| if not candidates:
|
| return None, None, search_query
|
|
|
| normalized_query = search_query.lower().strip()
|
|
|
| for candidate in candidates:
|
| if candidate["title"].lower() == normalized_query:
|
| return candidate["title"], candidate["text"], search_query
|
|
|
| query_embedding = embedder.encode(search_query, convert_to_tensor=True)
|
|
|
| best_candidate = None
|
| best_score = -1
|
|
|
| for candidate in candidates:
|
| candidate_text = candidate["title"] + ". " + candidate["text"]
|
| candidate_embedding = embedder.encode(candidate_text, convert_to_tensor=True)
|
|
|
| score = util.cos_sim(query_embedding, candidate_embedding).item()
|
|
|
| if score > best_score:
|
| best_score = score
|
| best_candidate = candidate
|
|
|
| return best_candidate["title"], best_candidate["text"], search_query
|
|
|
|
|
| def get_topic_similarity(reference_text, llm_response):
|
| reference_embedding = embedder.encode(reference_text, convert_to_tensor=True)
|
| response_embedding = embedder.encode(llm_response, convert_to_tensor=True)
|
|
|
| similarity = util.cos_sim(reference_embedding, response_embedding).item()
|
|
|
| return similarity
|
|
|
|
|
| def get_nli_scores(reference_text, response_for_model):
|
| inputs = nli_tokenizer(
|
| [reference_text],
|
| [response_for_model],
|
| padding=True,
|
| truncation=True,
|
| max_length=MAX_LENGTH,
|
| return_tensors="pt"
|
| )
|
|
|
| with torch.no_grad():
|
| outputs = nli_model(**inputs)
|
| probs = torch.softmax(outputs.logits, dim=1)
|
|
|
| return probs.cpu().numpy()
|
|
|
|
|
| def get_nli_label_scores(reference_text, llm_response):
|
| inputs = nli_tokenizer(
|
| [reference_text],
|
| [llm_response],
|
| padding=True,
|
| truncation=True,
|
| max_length=MAX_LENGTH,
|
| return_tensors="pt"
|
| )
|
|
|
| with torch.no_grad():
|
| outputs = nli_model(**inputs)
|
| probs = torch.softmax(outputs.logits, dim=1)[0]
|
|
|
| label_scores = {}
|
|
|
| for index, label in nli_model.config.id2label.items():
|
| label_scores[label.lower()] = probs[index].item()
|
|
|
| return label_scores
|
|
|
|
|
| def build_features(reference_text, response_for_model):
|
| doc_vec = embedder.encode(
|
| [reference_text],
|
| convert_to_numpy=True,
|
| show_progress_bar=False
|
| )
|
|
|
| response_vec = embedder.encode(
|
| [response_for_model],
|
| convert_to_numpy=True,
|
| show_progress_bar=False
|
| )
|
|
|
| cosine = cosine_similarity(doc_vec, response_vec).reshape(1, 1)
|
|
|
| nli_scores = get_nli_scores(reference_text, response_for_model)
|
|
|
| features = np.concatenate([
|
| doc_vec,
|
| response_vec,
|
| np.abs(doc_vec - response_vec),
|
| cosine,
|
| nli_scores
|
| ], axis=1)
|
|
|
| return features
|
|
|
| def split_into_sentences(text):
|
| sentences = re.split(r"(?<=[.!?])\s+", text.strip())
|
| return [sentence.strip() for sentence in sentences if sentence.strip()]
|
|
|
| def get_sentence_nli_summary(reference_text, llm_response):
|
| sentences = split_into_sentences(llm_response)
|
|
|
| if not sentences:
|
| return {
|
| "max_contradiction": 0,
|
| "min_entailment": 0,
|
| "weakest_sentence": "",
|
| "suspicious_sentences": []
|
| }
|
|
|
| max_contradiction = 0
|
| min_entailment = 1
|
| weakest_sentence = ""
|
| suspicious_sentences = []
|
|
|
| for sentence in sentences:
|
| scores = get_nli_label_scores(reference_text, sentence)
|
|
|
| contradiction = scores.get("contradiction", 0)
|
| entailment = scores.get("entailment", 0)
|
| neutral = scores.get("neutral", 0)
|
|
|
| if contradiction > max_contradiction:
|
| max_contradiction = contradiction
|
| weakest_sentence = sentence
|
|
|
| if entailment < min_entailment:
|
| min_entailment = entailment
|
|
|
| if neutral > 0.65 and entailment < 0.35:
|
| weakest_sentence = sentence
|
|
|
| if neutral > 0.80 and entailment < 0.05 and contradiction < 0.40:
|
| suspicious_sentences.append({
|
| "sentence": sentence,
|
| "neutral": round(neutral, 3),
|
| "entailment": round(entailment, 3)
|
| })
|
|
|
| return {
|
| "max_contradiction": max_contradiction,
|
| "min_entailment": min_entailment,
|
| "weakest_sentence": weakest_sentence,
|
| "suspicious_sentences": suspicious_sentences
|
| }
|
|
|
|
|
| @app.route("/", methods=["GET", "POST"])
|
| def index():
|
| prediction = None
|
| confidence = None
|
| wiki_title = None
|
| reference_text = None
|
| search_query = None
|
| topic_similarity = None
|
| error = None
|
| suspicious_sentences = []
|
|
|
| if request.method == "POST":
|
| user_prompt = request.form.get("user_prompt", "").strip()
|
| llm_response = request.form.get("llm_response", "").strip()
|
|
|
| if not user_prompt or not llm_response:
|
| error = "Please enter both a user prompt and an LLM response."
|
| else:
|
| try:
|
| wiki_title, reference_text, search_query = get_wikipedia_reference(user_prompt)
|
|
|
| if not reference_text:
|
| error = "Could not find a useful Wikipedia reference for this prompt."
|
| else:
|
| response_for_model = (
|
| "Question: " + user_prompt +
|
| " Answer: " + llm_response
|
| )
|
|
|
| topic_similarity = get_topic_similarity(reference_text, llm_response)
|
|
|
| nli_label_scores = get_nli_label_scores(reference_text, llm_response)
|
| contradiction_score = nli_label_scores.get("contradiction", 0)
|
| entailment_score = nli_label_scores.get("entailment", 0)
|
| neutral_score = nli_label_scores.get("neutral", 0)
|
|
|
| sentence_nli = get_sentence_nli_summary(reference_text, llm_response)
|
|
|
| sentence_contradiction = sentence_nli["max_contradiction"]
|
| sentence_min_entailment = sentence_nli["min_entailment"]
|
| weakest_sentence = sentence_nli["weakest_sentence"]
|
| suspicious_sentences = sentence_nli["suspicious_sentences"]
|
|
|
| X = build_features(reference_text, response_for_model)
|
| probabilities = model.predict_proba(X)[0]
|
|
|
| not_hallucinated_prob = probabilities[0]
|
| hallucinated_prob = probabilities[1]
|
|
|
| confidence = round(max(probabilities) * 100, 2)
|
|
|
| if topic_similarity < TOPIC_SIMILARITY_THRESHOLD:
|
| prediction = "Hallucinated"
|
| confidence = round((1 - topic_similarity) * 100, 2)
|
|
|
| elif contradiction_score >= NLI_CONTRADICTION_THRESHOLD:
|
| prediction = "Hallucinated"
|
| confidence = round(contradiction_score * 100, 2)
|
|
|
| elif contradiction_score <= 0.20 and topic_similarity >=0.70:
|
| prediction = "Not Hallucinated"
|
| confidence = round(((1 - contradiction_score) + topic_similarity) / 2 * 100, 2)
|
|
|
| elif hallucinated_prob >= 0.85:
|
| prediction = "Hallucinated"
|
| confidence = round(hallucinated_prob * 100, 2)
|
|
|
| elif not_hallucinated_prob >= 0.65:
|
| prediction = "Not Hallucinated"
|
| confidence = round(not_hallucinated_prob * 100, 2)
|
|
|
| else:
|
| prediction = "Uncertain"
|
| confidence = round(max(probabilities) * 100, 2)
|
|
|
| except Exception as e:
|
| error = f"Something went wrong: {str(e)}"
|
|
|
| return render_template(
|
| "index.html",
|
| prediction=prediction,
|
| confidence=confidence,
|
| wiki_title=wiki_title,
|
| reference_text=reference_text,
|
| search_query=search_query,
|
| topic_similarity=topic_similarity,
|
| suspicious_sentences=suspicious_sentences,
|
| error=error
|
| )
|
|
|
|
|
| if __name__ == "__main__":
|
| app.run(host="0.0.0.0", port=7860, debug=False) |