Theresa commited on
Commit ·
bcabbeb
1
Parent(s): dbac707
fixed language check and hallu check, added language check to input rails, fixed email sanitize, removed dummy relevance score, refactorings, fixed requirements for installments, refactored model initialization, adapted CLI output
Browse files- .gitignore +1 -0
- app.py +39 -26
- helper.py +56 -3
- model/model.py +1 -3
- rag/retriever.py +4 -3
- rails/input.py +19 -4
- rails/output.py +96 -71
- requirements.txt +2 -1
.gitignore
CHANGED
|
@@ -5,6 +5,7 @@ env/
|
|
| 5 |
.venv
|
| 6 |
|
| 7 |
notes.txt
|
|
|
|
| 8 |
|
| 9 |
# Python
|
| 10 |
__pycache__/
|
|
|
|
| 5 |
.venv
|
| 6 |
|
| 7 |
notes.txt
|
| 8 |
+
test.ipynb
|
| 9 |
|
| 10 |
# Python
|
| 11 |
__pycache__/
|
app.py
CHANGED
|
@@ -10,17 +10,30 @@ from rails.output import OutputGuardrails
|
|
| 10 |
import secrets_local
|
| 11 |
from helper import ROLE_ASSISTANT, AUTO_ANSWERS
|
| 12 |
from rag import retriever
|
|
|
|
|
|
|
| 13 |
|
| 14 |
# ============================================
|
| 15 |
# BACKEND INTEGRATION
|
| 16 |
# ============================================
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
start_time = time.time()
|
| 20 |
|
| 21 |
# 1. Input Guardrails
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# 2. Context Retrieval
|
| 26 |
retrieved_docs = retriever.search(user_query)
|
|
@@ -35,19 +48,20 @@ def query_rag_pipeline(user_query: str, model: RAGModel, output_guardRails: Outp
|
|
| 35 |
|
| 36 |
# 5. Final Answer
|
| 37 |
if all(gr_result.passed for gr_result in gr_result.values()):
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
| 43 |
|
| 44 |
else:
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
|
| 52 |
|
| 53 |
|
|
@@ -90,11 +104,11 @@ def main():
|
|
| 90 |
st.write(message["content"])
|
| 91 |
|
| 92 |
# Zeige Quellen wenn vorhanden
|
| 93 |
-
if
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
|
| 99 |
# Chat-Eingabe
|
| 100 |
if prompt := st.chat_input("Stelle eine Frage..."):
|
|
@@ -111,21 +125,20 @@ def main():
|
|
| 111 |
response = query_rag_pipeline(prompt, model, output_guardrails)
|
| 112 |
|
| 113 |
# Antwort anzeigen
|
| 114 |
-
st.write(response
|
| 115 |
|
| 116 |
# Antwort in Historie speichern
|
| 117 |
st.session_state.messages.append({
|
| 118 |
"role": ROLE_ASSISTANT,
|
| 119 |
-
"content": response
|
| 120 |
-
"sources": response.
|
| 121 |
})
|
| 122 |
|
| 123 |
# Quellen anzeigen wenn vorhanden
|
| 124 |
-
if response.
|
| 125 |
with st.expander("📚 Verwendete Quellen"):
|
| 126 |
-
for source in response
|
| 127 |
-
|
| 128 |
-
st.write(f"• {source['title']} (Relevanz: {relevance_percent:.0f}%)")
|
| 129 |
|
| 130 |
|
| 131 |
|
|
|
|
| 10 |
import secrets_local
|
| 11 |
from helper import ROLE_ASSISTANT, AUTO_ANSWERS
|
| 12 |
from rag import retriever
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
from typing import List
|
| 15 |
|
| 16 |
# ============================================
|
| 17 |
# BACKEND INTEGRATION
|
| 18 |
# ============================================
|
| 19 |
|
| 20 |
+
@dataclass
|
| 21 |
+
class Answer():
|
| 22 |
+
answer: str
|
| 23 |
+
sources: List[str]
|
| 24 |
+
processing_time: float
|
| 25 |
+
|
| 26 |
+
def query_rag_pipeline(user_query: str, model: RAGModel, output_guardRails: OutputGuardrails) -> Answer:
|
| 27 |
start_time = time.time()
|
| 28 |
|
| 29 |
# 1. Input Guardrails
|
| 30 |
+
checked_answer = input_guard.is_valid(user_query)
|
| 31 |
+
if not checked_answer.accepted:
|
| 32 |
+
return Answer(
|
| 33 |
+
answer=checked_answer.reason if checked_answer.reason else "Invalid input. Please try again.",
|
| 34 |
+
sources=[],
|
| 35 |
+
processing_time=start_time - time.time()
|
| 36 |
+
)
|
| 37 |
|
| 38 |
# 2. Context Retrieval
|
| 39 |
retrieved_docs = retriever.search(user_query)
|
|
|
|
| 48 |
|
| 49 |
# 5. Final Answer
|
| 50 |
if all(gr_result.passed for gr_result in gr_result.values()):
|
| 51 |
+
sources = [{"title": str(output_guardRails.sanitize(doc))} for doc in retrieved_docs]
|
| 52 |
+
return Answer(
|
| 53 |
+
answer = output_guardRails.sanitize(response),
|
| 54 |
+
sources = sources,
|
| 55 |
+
processing_time = end_time - start_time
|
| 56 |
+
)
|
| 57 |
|
| 58 |
else:
|
| 59 |
+
return Answer(
|
| 60 |
+
answer = output_guardRails.format_guardrail_issues(gr_result),
|
| 61 |
+
sources = sources,
|
| 62 |
+
processing_time = end_time - start_time
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
|
| 66 |
|
| 67 |
|
|
|
|
| 104 |
st.write(message["content"])
|
| 105 |
|
| 106 |
# Zeige Quellen wenn vorhanden
|
| 107 |
+
if isinstance(message, Answer):
|
| 108 |
+
if message.sources:
|
| 109 |
+
with st.expander("📚 Verwendete Quellen"):
|
| 110 |
+
for source in message.sources:
|
| 111 |
+
st.write(f"• {source['title']}")
|
| 112 |
|
| 113 |
# Chat-Eingabe
|
| 114 |
if prompt := st.chat_input("Stelle eine Frage..."):
|
|
|
|
| 125 |
response = query_rag_pipeline(prompt, model, output_guardrails)
|
| 126 |
|
| 127 |
# Antwort anzeigen
|
| 128 |
+
st.write(response.answer if response.answer else AUTO_ANSWERS.UNEXPECTED_ERROR.value)
|
| 129 |
|
| 130 |
# Antwort in Historie speichern
|
| 131 |
st.session_state.messages.append({
|
| 132 |
"role": ROLE_ASSISTANT,
|
| 133 |
+
"content": response.answer,
|
| 134 |
+
"sources": response.sources
|
| 135 |
})
|
| 136 |
|
| 137 |
# Quellen anzeigen wenn vorhanden
|
| 138 |
+
if response.sources:
|
| 139 |
with st.expander("📚 Verwendete Quellen"):
|
| 140 |
+
for source in response.sources:
|
| 141 |
+
st.write(f"• {source['title']}")
|
|
|
|
| 142 |
|
| 143 |
|
| 144 |
|
helper.py
CHANGED
|
@@ -1,12 +1,63 @@
|
|
| 1 |
from enum import Enum
|
| 2 |
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
MODEL_NAME = 'google/gemma-2-2b-it'
|
| 5 |
-
SIMILARITY_MODEL_NAME = 'all-MiniLM-L6-v2'
|
| 6 |
-
HALLUCINATION_MODEL_NAME="vectara/hallucination_evaluation_model"
|
| 7 |
ROLE_ASSISTANT ="assistant"
|
| 8 |
EMAIL_PATTERN = re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
class AUTO_ANSWERS(Enum):
|
| 11 |
COULD_NOT_GENERATE = "Could not generate an answer."
|
| 12 |
REQUEST_TIMED_OUT = "The request timed out."
|
|
@@ -17,4 +68,6 @@ class AUTO_ANSWERS(Enum):
|
|
| 17 |
HALLUCINATION_CHECK_FAILED = "Hallucination check failed."
|
| 18 |
NOT_SUPPORTED_BY_CONTEXT = "Many facts not supported by context."
|
| 19 |
INCONSISTENT_WITH_CONTEXT = "Inconsistent with context."
|
| 20 |
-
CORRECTNESS_CHECK_FAILED = "Correctness check failed."
|
|
|
|
|
|
|
|
|
| 1 |
from enum import Enum
|
| 2 |
import re
|
| 3 |
+
from sentence_transformers import SentenceTransformer
|
| 4 |
+
from transformers import pipeline, AutoTokenizer
|
| 5 |
+
from functools import lru_cache
|
| 6 |
+
|
| 7 |
+
|
| 8 |
|
| 9 |
MODEL_NAME = 'google/gemma-2-2b-it'
|
|
|
|
|
|
|
| 10 |
ROLE_ASSISTANT ="assistant"
|
| 11 |
EMAIL_PATTERN = re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
|
| 12 |
|
| 13 |
+
@lru_cache(maxsize=1)
|
| 14 |
+
def get_similarity_model():
|
| 15 |
+
return SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2', device="cpu")
|
| 16 |
+
|
| 17 |
+
@lru_cache(maxsize=1)
|
| 18 |
+
def get_toxicity_model():
|
| 19 |
+
return pipeline(
|
| 20 |
+
"text-classification",
|
| 21 |
+
model="unitary/toxic-bert",
|
| 22 |
+
top_k=None
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
@lru_cache(maxsize=1)
|
| 26 |
+
def get_hallucination_model():
|
| 27 |
+
return pipeline(
|
| 28 |
+
"text-classification",
|
| 29 |
+
model='vectara/hallucination_evaluation_model',
|
| 30 |
+
tokenizer=AutoTokenizer.from_pretrained('google/flan-t5-base'),
|
| 31 |
+
trust_remote_code=True
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
def check_toxicity(text: str):
|
| 35 |
+
""" Toxicity check with toxicity check pipeline
|
| 36 |
+
Args:
|
| 37 |
+
response (str): response generated by model
|
| 38 |
+
Returns:
|
| 39 |
+
|
| 40 |
+
"""
|
| 41 |
+
try:
|
| 42 |
+
# output sequence can be little longer due to tokens != sequence length, this makes sure that text is cropped to model input max size
|
| 43 |
+
if len(text) > 512:
|
| 44 |
+
text = text[:500]
|
| 45 |
+
|
| 46 |
+
result = get_toxicity_model()(text)[0]
|
| 47 |
+
# print(result)
|
| 48 |
+
if result:
|
| 49 |
+
toxicity_score = next((s['score'] for s in result if s['score'] > 0.85), sum(s['score'] for s in result) / len(result))
|
| 50 |
+
# print(toxicity_score)
|
| 51 |
+
toxicity_passed = True if toxicity_score < 0.6 else False
|
| 52 |
+
text = AUTO_ANSWERS.LANGUAGE_INAPPROPRIATE.value if not toxicity_passed else ""
|
| 53 |
+
return toxicity_passed, toxicity_score, text
|
| 54 |
+
else:
|
| 55 |
+
raise ValueError("Could not yield result.")
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"Error while checking language: {e}")
|
| 58 |
+
return False, 1.0, AUTO_ANSWERS.UNEXPECTED_ERROR.value
|
| 59 |
+
|
| 60 |
+
|
| 61 |
class AUTO_ANSWERS(Enum):
|
| 62 |
COULD_NOT_GENERATE = "Could not generate an answer."
|
| 63 |
REQUEST_TIMED_OUT = "The request timed out."
|
|
|
|
| 68 |
HALLUCINATION_CHECK_FAILED = "Hallucination check failed."
|
| 69 |
NOT_SUPPORTED_BY_CONTEXT = "Many facts not supported by context."
|
| 70 |
INCONSISTENT_WITH_CONTEXT = "Inconsistent with context."
|
| 71 |
+
CORRECTNESS_CHECK_FAILED = "Correctness check failed."
|
| 72 |
+
LANGUAGE_INAPPROPRIATE = "Inappropriate language detected!"
|
| 73 |
+
REPHRASE_SENTENCE = "Try to rephrase your request."
|
model/model.py
CHANGED
|
@@ -60,9 +60,7 @@ class RAGModel:
|
|
| 60 |
temperature=0.7, # Adjust creativity (higher = more random)
|
| 61 |
top_p=0.9, # Nucleus sampling parameter
|
| 62 |
)
|
| 63 |
-
|
| 64 |
-
print(f"RESPONSE {response}")
|
| 65 |
-
|
| 66 |
if response:
|
| 67 |
return response.choices[0].message.content
|
| 68 |
else:
|
|
|
|
| 60 |
temperature=0.7, # Adjust creativity (higher = more random)
|
| 61 |
top_p=0.9, # Nucleus sampling parameter
|
| 62 |
)
|
| 63 |
+
|
|
|
|
|
|
|
| 64 |
if response:
|
| 65 |
return response.choices[0].message.content
|
| 66 |
else:
|
rag/retriever.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import chromadb
|
| 2 |
from sentence_transformers import SentenceTransformer
|
| 3 |
import sqlite3
|
|
|
|
| 4 |
|
| 5 |
def search(query: str, top_k: int = 5):
|
| 6 |
"""
|
|
@@ -14,10 +15,10 @@ def search(query: str, top_k: int = 5):
|
|
| 14 |
conn.close()
|
| 15 |
return [{"name": student[0]} for student in students]
|
| 16 |
|
| 17 |
-
#
|
| 18 |
-
model =
|
| 19 |
|
| 20 |
-
# Create the query embedding
|
| 21 |
query_embedding = model.encode([query])
|
| 22 |
|
| 23 |
# Initialize ChromaDB client and get the collection
|
|
|
|
| 1 |
import chromadb
|
| 2 |
from sentence_transformers import SentenceTransformer
|
| 3 |
import sqlite3
|
| 4 |
+
from helper import get_similarity_model
|
| 5 |
|
| 6 |
def search(query: str, top_k: int = 5):
|
| 7 |
"""
|
|
|
|
| 15 |
conn.close()
|
| 16 |
return [{"name": student[0]} for student in students]
|
| 17 |
|
| 18 |
+
# get SentenceTransformer model
|
| 19 |
+
model = get_similarity_model()
|
| 20 |
|
| 21 |
+
# Create the query embedding - model from helper instantiated only once
|
| 22 |
query_embedding = model.encode([query])
|
| 23 |
|
| 24 |
# Initialize ChromaDB client and get the collection
|
rails/input.py
CHANGED
|
@@ -1,12 +1,20 @@
|
|
| 1 |
import re
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
Validates the user's query.
|
| 6 |
"""
|
| 7 |
# Check for query length
|
| 8 |
if len(query) > 500:
|
| 9 |
-
return False
|
| 10 |
|
| 11 |
# Check for SQL injection patterns
|
| 12 |
sql_injection_patterns = [
|
|
@@ -15,9 +23,16 @@ def is_valid(query: str) -> bool:
|
|
| 15 |
]
|
| 16 |
for pattern in sql_injection_patterns:
|
| 17 |
if re.search(pattern, query, re.IGNORECASE):
|
| 18 |
-
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
|
|
|
| 21 |
|
| 22 |
if __name__ == '__main__':
|
| 23 |
# Example usage
|
|
|
|
| 1 |
import re
|
| 2 |
+
from helper import check_toxicity, AUTO_ANSWERS
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
|
| 6 |
+
@dataclass
|
| 7 |
+
class CheckedInput():
|
| 8 |
+
accepted: bool
|
| 9 |
+
reason: Optional[str]
|
| 10 |
+
|
| 11 |
+
def is_valid(query: str) -> CheckedInput:
|
| 12 |
"""
|
| 13 |
Validates the user's query.
|
| 14 |
"""
|
| 15 |
# Check for query length
|
| 16 |
if len(query) > 500:
|
| 17 |
+
return CheckedInput(False, get_output("Input too long."))
|
| 18 |
|
| 19 |
# Check for SQL injection patterns
|
| 20 |
sql_injection_patterns = [
|
|
|
|
| 23 |
]
|
| 24 |
for pattern in sql_injection_patterns:
|
| 25 |
if re.search(pattern, query, re.IGNORECASE):
|
| 26 |
+
return CheckedInput(False, get_output("Invalid input."))
|
| 27 |
+
|
| 28 |
+
t_passed, _ , text = check_toxicity(query)
|
| 29 |
+
if not t_passed:
|
| 30 |
+
return CheckedInput(False, get_output(text))
|
| 31 |
+
|
| 32 |
+
return CheckedInput(True, None)
|
| 33 |
|
| 34 |
+
def get_output(reason: str) -> str:
|
| 35 |
+
return "\n".join([reason, AUTO_ANSWERS.REPHRASE_SENTENCE.value])
|
| 36 |
|
| 37 |
if __name__ == '__main__':
|
| 38 |
# Example usage
|
rails/output.py
CHANGED
|
@@ -1,21 +1,16 @@
|
|
| 1 |
from sentence_transformers import SentenceTransformer
|
| 2 |
from typing import Dict
|
| 3 |
-
from helper import
|
| 4 |
-
from transformers import pipeline
|
| 5 |
from sklearn.metrics.pairwise import cosine_similarity
|
| 6 |
-
#from guardrails import Guard
|
| 7 |
-
#from guardrails.hub import DetectPII, ToxicLanguage
|
| 8 |
-
#from guardrails.validators import ValidLength
|
| 9 |
-
from helper import AUTO_ANSWERS
|
| 10 |
from typing import Dict, List
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from enum import Enum
|
| 13 |
-
import torch
|
| 14 |
import re
|
| 15 |
|
| 16 |
@dataclass
|
| 17 |
class GuardrailResult:
|
| 18 |
-
"""
|
| 19 |
passed: bool
|
| 20 |
confidence: float
|
| 21 |
issues: List[str]
|
|
@@ -25,10 +20,10 @@ class GuardrailType(Enum):
|
|
| 25 |
RELEVANCE = "relevance"
|
| 26 |
HALLUCINATION = "hallucination"
|
| 27 |
CORRECTNESS = "correctness"
|
| 28 |
-
|
| 29 |
|
| 30 |
class OutputGuardrails:
|
| 31 |
-
"""
|
| 32 |
|
| 33 |
def __init__(self):
|
| 34 |
|
|
@@ -43,27 +38,22 @@ class OutputGuardrails:
|
|
| 43 |
re.compile(r'\b\d+%\b'), # %
|
| 44 |
re.compile(r'\b\d+\s*(Euro|Dollar|meter|kilometer|km|kg|tons)\b', re.IGNORECASE),
|
| 45 |
]
|
| 46 |
-
|
| 47 |
-
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 48 |
-
self.sentence_model = SentenceTransformer(SIMILARITY_MODEL_NAME, device=device)
|
| 49 |
-
#self.sentence_model = self.sentence_model.to(device)
|
| 50 |
|
| 51 |
-
# Hallucination Detection Model
|
| 52 |
try:
|
| 53 |
-
self.
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
except Exception as e:
|
| 59 |
print(f"Could not retrieve hallucination check pipeline: {e}.")
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
)'''
|
| 67 |
|
| 68 |
|
| 69 |
def check_query_relevance(self,
|
|
@@ -98,7 +88,7 @@ class OutputGuardrails:
|
|
| 98 |
if relevance_score < 0.3:
|
| 99 |
issues.append(AUTO_ANSWERS.ANSWER_NOT_RELEVANT_TO_QUERY.value)
|
| 100 |
if context_relevance < 0.4:
|
| 101 |
-
issues.append(AUTO_ANSWERS.ANSWER_NOT_RELEVANT_TO_CONTEXT)
|
| 102 |
|
| 103 |
passed = combined_relevance >= 0.5 and len(issues) == 0
|
| 104 |
|
|
@@ -167,18 +157,25 @@ class OutputGuardrails:
|
|
| 167 |
|
| 168 |
# Check if model facts are consistent with context
|
| 169 |
if self.hallucination_model:
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
else:
|
| 177 |
# Do semantic similarity check if hallucination model pipeline is unavail
|
| 178 |
context_embedding = self.sentence_model.encode([context_text])
|
| 179 |
response_embedding = self.sentence_model.encode([response])
|
| 180 |
similarity = cosine_similarity(context_embedding, response_embedding)[0][0]
|
| 181 |
-
hallucination_score = 1.0 - similarity
|
| 182 |
|
| 183 |
# check for known hallucination patterns
|
| 184 |
pattern_score = 0
|
|
@@ -192,7 +189,7 @@ class OutputGuardrails:
|
|
| 192 |
|
| 193 |
# Gesamtscore
|
| 194 |
total_hallucination_score = min(
|
| 195 |
-
hallucination_score + pattern_score + contradiction_score, 1.0
|
| 196 |
)
|
| 197 |
|
| 198 |
if total_hallucination_score > 0.7:
|
|
@@ -323,12 +320,9 @@ class OutputGuardrails:
|
|
| 323 |
else:
|
| 324 |
support_ratio = 1.0 # no checkable facts
|
| 325 |
|
| 326 |
-
|
| 327 |
consistency_score = self._check_consistency(response, context_text)
|
| 328 |
-
|
| 329 |
completeness_score = self._check_completeness(response, context_text)
|
| 330 |
-
|
| 331 |
-
# Overall, might want to adapt weights
|
| 332 |
correctness_score = (
|
| 333 |
support_ratio * 0.5 +
|
| 334 |
consistency_score * 0.3 +
|
|
@@ -358,25 +352,27 @@ class OutputGuardrails:
|
|
| 358 |
print(f"{AUTO_ANSWERS.CORRECTNESS_CHECK_FAILED.value} {e}")
|
| 359 |
return GuardrailResult(False, 0.0, [AUTO_ANSWERS.CORRECTNESS_CHECK_FAILED.value], {})
|
| 360 |
|
| 361 |
-
|
| 362 |
-
def
|
| 363 |
-
""" Toxicity
|
| 364 |
Args:
|
| 365 |
response (str): response generated by model
|
|
|
|
|
|
|
| 366 |
"""
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
toxicity_passed = guard_result.validation_passed
|
| 370 |
-
toxicity_issues = [str(error) for error in guard_result.reask_messages] if hasattr(guard_result, 'reask_messages') else []
|
| 371 |
-
return toxicity_passed, toxicity_issues
|
| 372 |
-
except Exception as e:
|
| 373 |
-
print(f"Guardrails toxicity validation failed: {e}")
|
| 374 |
-
toxicity_passed = False
|
| 375 |
-
toxicity_issues = []
|
| 376 |
-
'''
|
| 377 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
def sanitize(self, response: str) -> str:
|
| 379 |
-
|
|
|
|
| 380 |
|
| 381 |
def check(self,
|
| 382 |
query: str,
|
|
@@ -395,28 +391,57 @@ class OutputGuardrails:
|
|
| 395 |
self.check_query_relevance(query, response, context),
|
| 396 |
self.check_hallucination(response, context),
|
| 397 |
self.check_correctness(response, context),
|
|
|
|
| 398 |
]
|
| 399 |
|
| 400 |
-
relevance_result, hallucination_result, correctness_result = tasks
|
| 401 |
|
| 402 |
results[GuardrailType.RELEVANCE] = relevance_result
|
| 403 |
results[GuardrailType.HALLUCINATION] = hallucination_result
|
| 404 |
results[GuardrailType.CORRECTNESS] = correctness_result
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
result = self.toxicity_check(response)
|
| 408 |
-
if result:
|
| 409 |
-
toxicity_passed, toxicity_issues = result
|
| 410 |
-
results[GuardrailType.TOXICITY] = GuardrailResult(
|
| 411 |
-
passed=toxicity_passed,
|
| 412 |
-
confidence=1.0 if toxicity_passed else 0.0,
|
| 413 |
-
issues=toxicity_issues,
|
| 414 |
-
metrics={"toxicity_passed": toxicity_passed}
|
| 415 |
-
)
|
| 416 |
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from sentence_transformers import SentenceTransformer
|
| 2 |
from typing import Dict
|
| 3 |
+
from helper import EMAIL_PATTERN, get_similarity_model, AUTO_ANSWERS, get_toxicity_model, get_hallucination_model, check_toxicity
|
| 4 |
+
from transformers import pipeline, AutoTokenizer
|
| 5 |
from sklearn.metrics.pairwise import cosine_similarity
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from typing import Dict, List
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from enum import Enum
|
|
|
|
| 9 |
import re
|
| 10 |
|
| 11 |
@dataclass
|
| 12 |
class GuardrailResult:
|
| 13 |
+
"""Guardrail Result Dataclass"""
|
| 14 |
passed: bool
|
| 15 |
confidence: float
|
| 16 |
issues: List[str]
|
|
|
|
| 20 |
RELEVANCE = "relevance"
|
| 21 |
HALLUCINATION = "hallucination"
|
| 22 |
CORRECTNESS = "correctness"
|
| 23 |
+
TOXICITY = "toxicity"
|
| 24 |
|
| 25 |
class OutputGuardrails:
|
| 26 |
+
"""Guardrails for LLM output validation"""
|
| 27 |
|
| 28 |
def __init__(self):
|
| 29 |
|
|
|
|
| 38 |
re.compile(r'\b\d+%\b'), # %
|
| 39 |
re.compile(r'\b\d+\s*(Euro|Dollar|meter|kilometer|km|kg|tons)\b', re.IGNORECASE),
|
| 40 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
|
|
|
| 42 |
try:
|
| 43 |
+
self.sentence_model = get_similarity_model()
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"Could not retrieve sentence model for similarity checks: {e}.")
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
self.hallucination_model = get_hallucination_model()
|
| 49 |
except Exception as e:
|
| 50 |
print(f"Could not retrieve hallucination check pipeline: {e}.")
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
self.toxicity_classifier = get_toxicity_model()
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"Could not toxicity check pipeline: {e}.")
|
| 56 |
+
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
def check_query_relevance(self,
|
|
|
|
| 88 |
if relevance_score < 0.3:
|
| 89 |
issues.append(AUTO_ANSWERS.ANSWER_NOT_RELEVANT_TO_QUERY.value)
|
| 90 |
if context_relevance < 0.4:
|
| 91 |
+
issues.append(AUTO_ANSWERS.ANSWER_NOT_RELEVANT_TO_CONTEXT.value)
|
| 92 |
|
| 93 |
passed = combined_relevance >= 0.5 and len(issues) == 0
|
| 94 |
|
|
|
|
| 157 |
|
| 158 |
# Check if model facts are consistent with context
|
| 159 |
if self.hallucination_model:
|
| 160 |
+
|
| 161 |
+
prompt = f"<pad> Determine if the hypothesis is true given the premise?\n\nPremise: {context}\n\nHypothesis: {response}"
|
| 162 |
+
|
| 163 |
+
classifier = pipeline(
|
| 164 |
+
"text-classification",
|
| 165 |
+
model='vectara/hallucination_evaluation_model',
|
| 166 |
+
tokenizer=AutoTokenizer.from_pretrained('google/flan-t5-base'),
|
| 167 |
+
trust_remote_code=True
|
| 168 |
+
)
|
| 169 |
+
full_scores = classifier(prompt, top_k=None) # List[List[Dict[str, float]]]
|
| 170 |
+
hallucinated_entry = next((item for item in full_scores if item['label'] == 'hallucinated'), None)
|
| 171 |
+
hallucination_score = hallucinated_entry['score']
|
| 172 |
+
|
| 173 |
else:
|
| 174 |
# Do semantic similarity check if hallucination model pipeline is unavail
|
| 175 |
context_embedding = self.sentence_model.encode([context_text])
|
| 176 |
response_embedding = self.sentence_model.encode([response])
|
| 177 |
similarity = cosine_similarity(context_embedding, response_embedding)[0][0]
|
| 178 |
+
hallucination_score = 1.0 - similarity
|
| 179 |
|
| 180 |
# check for known hallucination patterns
|
| 181 |
pattern_score = 0
|
|
|
|
| 189 |
|
| 190 |
# Gesamtscore
|
| 191 |
total_hallucination_score = min(
|
| 192 |
+
hallucination_score*0.4 + pattern_score*0.3 + contradiction_score*0.3, 1.0
|
| 193 |
)
|
| 194 |
|
| 195 |
if total_hallucination_score > 0.7:
|
|
|
|
| 320 |
else:
|
| 321 |
support_ratio = 1.0 # no checkable facts
|
| 322 |
|
| 323 |
+
|
| 324 |
consistency_score = self._check_consistency(response, context_text)
|
|
|
|
| 325 |
completeness_score = self._check_completeness(response, context_text)
|
|
|
|
|
|
|
| 326 |
correctness_score = (
|
| 327 |
support_ratio * 0.5 +
|
| 328 |
consistency_score * 0.3 +
|
|
|
|
| 352 |
print(f"{AUTO_ANSWERS.CORRECTNESS_CHECK_FAILED.value} {e}")
|
| 353 |
return GuardrailResult(False, 0.0, [AUTO_ANSWERS.CORRECTNESS_CHECK_FAILED.value], {})
|
| 354 |
|
| 355 |
+
|
| 356 |
+
def check_toxicity(self, response: str):
|
| 357 |
+
""" Toxicity check with toxicity check pipeline
|
| 358 |
Args:
|
| 359 |
response (str): response generated by model
|
| 360 |
+
Returns:
|
| 361 |
+
|
| 362 |
"""
|
| 363 |
+
print("CHECKING OUTPUT LANGUAGE: ")
|
| 364 |
+
toxicity_passed, toxicity_score, text = check_toxicity(response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
+
return GuardrailResult(
|
| 367 |
+
passed=toxicity_passed,
|
| 368 |
+
confidence=1 - toxicity_score,
|
| 369 |
+
issues=[text] if text else [],
|
| 370 |
+
metrics={"toxicity_passed": toxicity_passed}
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
def sanitize(self, response: str) -> str:
|
| 374 |
+
EMAIL_PATTERN = re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
|
| 375 |
+
return EMAIL_PATTERN.sub('[REDACTED_EMAIL]', response)
|
| 376 |
|
| 377 |
def check(self,
|
| 378 |
query: str,
|
|
|
|
| 391 |
self.check_query_relevance(query, response, context),
|
| 392 |
self.check_hallucination(response, context),
|
| 393 |
self.check_correctness(response, context),
|
| 394 |
+
self.check_toxicity(response)
|
| 395 |
]
|
| 396 |
|
| 397 |
+
relevance_result, hallucination_result, correctness_result, toxicity_result = tasks
|
| 398 |
|
| 399 |
results[GuardrailType.RELEVANCE] = relevance_result
|
| 400 |
results[GuardrailType.HALLUCINATION] = hallucination_result
|
| 401 |
results[GuardrailType.CORRECTNESS] = correctness_result
|
| 402 |
+
results[GuardrailType.TOXICITY] = toxicity_result
|
| 403 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
+
print("Result for query: ", query)
|
| 406 |
+
print("WITH CONTEXT: ", context)
|
| 407 |
+
print("Model output: ", response)
|
| 408 |
+
for guardrail_type, result in results.items():
|
| 409 |
+
print(f"\n Guardrail: {guardrail_type.value.upper()}")
|
| 410 |
+
print(f" Passed: {result.passed}")
|
| 411 |
+
print(f" Confidence: {result.confidence:.3f}")
|
| 412 |
+
|
| 413 |
+
if result.issues:
|
| 414 |
+
print(f" Issues: {', '.join(result.issues)}")
|
| 415 |
+
else:
|
| 416 |
+
print(f" Issues: None")
|
| 417 |
|
| 418 |
+
print(" Metrics:")
|
| 419 |
+
for metric, value in result.metrics.items():
|
| 420 |
+
print(f" - {metric}: {value:.4f}")
|
| 421 |
+
|
| 422 |
+
return results
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def format_guardrail_issues(self, gr_result: dict) -> str:
|
| 426 |
+
"""Formats the issues text to readable output
|
| 427 |
+
Parametes:
|
| 428 |
+
- gr_result (dict): The dictionary containing the different results from the output guardrail check
|
| 429 |
+
Returns:
|
| 430 |
+
Formatted string.
|
| 431 |
+
"""
|
| 432 |
+
|
| 433 |
+
lines = ["Output Issues:\n"]
|
| 434 |
+
|
| 435 |
+
for guardrail_type, result in gr_result.items():
|
| 436 |
+
issues = result.issues
|
| 437 |
+
title = f"- {guardrail_type.value.capitalize()} issues:"
|
| 438 |
+
if issues:
|
| 439 |
+
lines.append(title)
|
| 440 |
+
for issue in issues:
|
| 441 |
+
lines.append(f" • {issue}")
|
| 442 |
+
if guardrail_type == GuardrailType.TOXICITY and not result.passed:
|
| 443 |
+
lines.append(f" • {AUTO_ANSWERS.REPHRASE_SENTENCE.value}")
|
| 444 |
+
else:
|
| 445 |
+
lines.append(f"{title} None")
|
| 446 |
+
|
| 447 |
+
return "\n".join(lines)
|
requirements.txt
CHANGED
|
@@ -3,8 +3,9 @@ python-dotenv==1.0.1
|
|
| 3 |
transformers
|
| 4 |
sentence-transformers
|
| 5 |
scikit-learn
|
| 6 |
-
|
| 7 |
chromadb==1.0.21
|
| 8 |
sentence-transformers==5.1.0
|
| 9 |
numpy==1.26.4
|
| 10 |
huggingface-hub==0.34.4
|
|
|
|
|
|
| 3 |
transformers
|
| 4 |
sentence-transformers
|
| 5 |
scikit-learn
|
| 6 |
+
faker
|
| 7 |
chromadb==1.0.21
|
| 8 |
sentence-transformers==5.1.0
|
| 9 |
numpy==1.26.4
|
| 10 |
huggingface-hub==0.34.4
|
| 11 |
+
chromadb
|