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
Files changed (8) hide show
  1. .gitignore +1 -0
  2. app.py +39 -26
  3. helper.py +56 -3
  4. model/model.py +1 -3
  5. rag/retriever.py +4 -3
  6. rails/input.py +19 -4
  7. rails/output.py +96 -71
  8. 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
- def query_rag_pipeline(user_query: str, model: RAGModel, output_guardRails: OutputGuardrails) -> dict:
 
 
 
 
 
 
19
  start_time = time.time()
20
 
21
  # 1. Input Guardrails
22
- if not input_guard.is_valid(user_query):
23
- return {"answer": "Invalid query. Please try again.", "sources": []}
 
 
 
 
 
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
- return {
39
- "answer": response, #output_guardRails.sanitize(response),
40
- "sources": [{"title": str(doc), "relevance": 1.0} for doc in retrieved_docs], # TODO: relevance should be indicated by order in context list
41
- "processing_time": end_time - start_time
42
- }
 
43
 
44
  else:
45
- issues = [gr_result.issues for gr_result in gr_result.values()]
46
- return {
47
- "answer": "\n ".join(issues),
48
- "sources": [{"title": str(doc), "relevance": 1.0} for doc in retrieved_docs],
49
- "processing_time": end_time - start_time
50
- }
51
 
52
 
53
 
@@ -90,11 +104,11 @@ def main():
90
  st.write(message["content"])
91
 
92
  # Zeige Quellen wenn vorhanden
93
- if message.get("sources"):
94
- with st.expander("📚 Verwendete Quellen"):
95
- for source in message["sources"]:
96
- relevance_percent = source["relevance"] * 100
97
- st.write(f"• {source['title']} (Relevanz: {relevance_percent:.0f}%)")
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["answer"] if response["answer"] else AUTO_ANSWERS.UNEXPECTED_ERROR)
115
 
116
  # Antwort in Historie speichern
117
  st.session_state.messages.append({
118
  "role": ROLE_ASSISTANT,
119
- "content": response["answer"],
120
- "sources": response.get("sources", [])
121
  })
122
 
123
  # Quellen anzeigen wenn vorhanden
124
- if response.get("sources"):
125
  with st.expander("📚 Verwendete Quellen"):
126
- for source in response["sources"]:
127
- relevance_percent = source["relevance"] * 100
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
- # Initialize the embedding model
18
- model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
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
- def is_valid(query: str) -> bool:
 
 
 
 
 
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
- return True
 
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 SIMILARITY_MODEL_NAME, HALLUCINATION_MODEL_NAME, EMAIL_PATTERN
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
- """Ergebnis der Guardrail-Prüfung"""
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
- #TOXICITY = "toxicity"
29
 
30
  class OutputGuardrails:
31
- """Umfassende Output Guardrails für RAG Pipeline"""
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.hallucination_model = pipeline(
54
- "text-classification",
55
- model=HALLUCINATION_MODEL_NAME,
56
- trust_remote_code=True
57
- )
 
58
  except Exception as e:
59
  print(f"Could not retrieve hallucination check pipeline: {e}.")
60
- self.hallucination_model = None
61
-
62
- ''' # setup guard for guardrails - toxicity check - disabled for now, work in progress
63
- self.guard = Guard().use_many(
64
- ToxicLanguage(threshold=0.8),
65
- ValidLength(min=10, max=1000)
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
- input_text = f"Context: {context_text}\nClaim: {response}"
171
- result = self.hallucination_model(input_text)
172
- hallucination_score = next(
173
- (score['score'] for score in result if score['label'] == 'HALLUCINATION'),
174
- 0.0
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 # Invertiert für Hallucination Score
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
- # consistency with context
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 toxicity_check(self, response: str):
363
- """ Toxicity Check with Guardrails Library
364
  Args:
365
  response (str): response generated by model
 
 
366
  """
367
- try:
368
- guard_result = self.guard.validate(response)
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
- return EMAIL_PATTERN.sub(response, '[REDACTED_EMAIL]')
 
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
- else:
418
- # Todo: discuss whether how relevant this could be in our application
419
- print("WARNING: Output could be toxic.")
420
- '''
 
 
 
 
 
 
 
 
421
 
422
- return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- Faker==15.3.4
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