|
|
import os |
|
|
import time |
|
|
import requests |
|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
import random |
|
|
import re |
|
|
from datetime import datetime |
|
|
from dotenv import load_dotenv |
|
|
from together import Together |
|
|
import openai |
|
|
import json |
|
|
|
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
|
|
|
|
|
|
def process_retrieval_text(retrieval_text, user_input): |
|
|
""" |
|
|
Process the retrieval text by identifying proper document boundaries |
|
|
and highlighting relevant keywords with improved formatting. |
|
|
""" |
|
|
if not retrieval_text or retrieval_text.strip() == "No retrieval text found.": |
|
|
return retrieval_text |
|
|
|
|
|
|
|
|
if retrieval_text.count("Doc:") > 0 and retrieval_text.count("Content:") > 0: |
|
|
|
|
|
chunks = [] |
|
|
|
|
|
|
|
|
doc_sections = re.split(r'\n\n(?=Doc:)', retrieval_text) |
|
|
|
|
|
for i, section in enumerate(doc_sections): |
|
|
if section.strip(): |
|
|
|
|
|
doc_info = section.strip() |
|
|
|
|
|
|
|
|
doc_match = re.search(r'Doc:\s*(.*?)(?:,\s*Page:\s*(.*?))?(?:\n|$)', doc_info) |
|
|
doc_name = doc_match.group(1) if doc_match else "Unknown" |
|
|
page = doc_match.group(2) if doc_match and doc_match.group(2) else "N/A" |
|
|
|
|
|
|
|
|
content_match = re.search(r'Content:\s*(.*)', doc_info, re.DOTALL) |
|
|
content = content_match.group(1).strip() if content_match else "No content available" |
|
|
|
|
|
|
|
|
formatted_html = f""" |
|
|
<div class='doc-section'> |
|
|
<strong>Evidence Document {i+1}</strong> |
|
|
<br> |
|
|
<strong>Document Title:</strong> {doc_name} |
|
|
<br> |
|
|
<strong>Section:</strong> Page {page} |
|
|
<br> |
|
|
<strong>Content:</strong> |
|
|
<div class='doc-content'>{content}</div> |
|
|
</div> |
|
|
""" |
|
|
chunks.append(formatted_html) |
|
|
else: |
|
|
|
|
|
|
|
|
raw_chunks = retrieval_text.strip().split("\n\n") |
|
|
chunks = [] |
|
|
current_chunk = "" |
|
|
|
|
|
for chunk in raw_chunks: |
|
|
|
|
|
|
|
|
if (len(chunk) < 50 and not re.search(r'doc|document|evidence', chunk.lower())) or \ |
|
|
not chunk.strip().startswith(("Doc", "Document", "Evidence", "Source", "Content")): |
|
|
if current_chunk: |
|
|
current_chunk += "\n\n" + chunk |
|
|
else: |
|
|
current_chunk = chunk |
|
|
else: |
|
|
|
|
|
if current_chunk: |
|
|
chunks.append(current_chunk) |
|
|
current_chunk = chunk |
|
|
|
|
|
|
|
|
if current_chunk: |
|
|
chunks.append(current_chunk) |
|
|
|
|
|
|
|
|
chunks = [f"<div class='doc-section'><div class='doc-title'>Evidence Document {i+1}</div><div class='doc-content'>{chunk.strip()}</div></div>" |
|
|
for i, chunk in enumerate(chunks)] |
|
|
|
|
|
|
|
|
keywords = re.findall(r'\b\w{4,}\b', user_input.lower()) |
|
|
keywords = [k for k in keywords if k not in ['what', 'when', 'where', 'which', 'would', 'could', |
|
|
'should', 'there', 'their', 'about', 'these', 'those', |
|
|
'them', 'from', 'have', 'this', 'that', 'will', 'with']] |
|
|
|
|
|
|
|
|
highlighted_chunks = [] |
|
|
for chunk in chunks: |
|
|
highlighted_chunk = chunk |
|
|
for keyword in keywords: |
|
|
|
|
|
pattern = r'\b(' + re.escape(keyword) + r')\b' |
|
|
highlighted_chunk = re.sub(pattern, r'<span class="highlight-match">\1</span>', highlighted_chunk, flags=re.IGNORECASE) |
|
|
|
|
|
highlighted_chunks.append(highlighted_chunk) |
|
|
|
|
|
return "<div class='knowledge-sections'>" + "".join(highlighted_chunks) + "</div>" |
|
|
|
|
|
|
|
|
|
|
|
ORACLE_API_KEY = os.environ.get("ORACLE_API_KEY", "") |
|
|
TOGETHER_API_KEY = os.environ.get("TOGETHER_API_KEY", "") |
|
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") |
|
|
PERSPECTIVE_API_KEY = os.environ.get("PERSPECTIVE_API_KEY", "") |
|
|
|
|
|
|
|
|
CUSTOM_CSS = """ |
|
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap'); |
|
|
|
|
|
body, .gradio-container { |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
} |
|
|
|
|
|
.rating-box { |
|
|
border-radius: 2px; |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
|
padding: 5px; |
|
|
margin-top: -10px; |
|
|
margin-bottom: 1px; |
|
|
transition: all 0.3s ease; |
|
|
background-color: #ffffff; |
|
|
position: relative; |
|
|
overflow-y: auto; |
|
|
white-space: pre-line; |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
} |
|
|
.rating-box:hover { |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
|
|
} |
|
|
.safe-rating { |
|
|
border-left: 5px solid #4CAF50; |
|
|
} |
|
|
.warning-rating { |
|
|
border-left: 5px solid #FCA539; |
|
|
} |
|
|
.unsafe-rating { |
|
|
border-left: 5px solid #F44336; |
|
|
} |
|
|
.empty-rating { |
|
|
border-left: 5px solid #FCA539; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-style: italic; |
|
|
color: #999; |
|
|
} |
|
|
|
|
|
/* Different heights for different rating boxes */ |
|
|
.contextual-box { |
|
|
min-height: 150px; |
|
|
} |
|
|
.secondary-box { |
|
|
min-height: 80px; |
|
|
} |
|
|
|
|
|
.result-header { |
|
|
font-size: 18px; |
|
|
font-weight: bold; |
|
|
margin-bottom: 0px; |
|
|
padding-bottom: 0px; |
|
|
border-bottom: 1px solid #eee; |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
} |
|
|
|
|
|
} |
|
|
.orange-button { |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
padding: 10px 15px !important; |
|
|
border-radius: 5px !important; |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
|
transition: all 0.3s ease; |
|
|
line-height: 1.2; |
|
|
text-align: center; |
|
|
display: inline-block; |
|
|
} |
|
|
.orange-button:hover { |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
|
|
|
/* Custom gray button style */ |
|
|
.gray-button { |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
background: #4285F4 !important; |
|
|
color: #000000 !important; |
|
|
border-radius: 5px; |
|
|
padding: 10px 15px; |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
|
transition: all 0.3s ease; |
|
|
line-height: 1.2; |
|
|
text-align: center; |
|
|
display: inline-block; |
|
|
} |
|
|
.gray-button:hover { |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
|
|
|
/* Input box styling with orange border */ |
|
|
textarea.svelte-1pie7s6 { |
|
|
border-left: 5px solid #FCA539 !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
#loading-spinner { |
|
|
display: none; |
|
|
margin: 10px auto; |
|
|
width: 100%; |
|
|
height: 4px; |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
background-color: #ddd; |
|
|
} |
|
|
#loading-spinner:before { |
|
|
content: ''; |
|
|
display: block; |
|
|
position: absolute; |
|
|
left: -50%; |
|
|
width: 50%; |
|
|
height: 100%; |
|
|
background-color: #FCA539; |
|
|
animation: loading 1s linear infinite; |
|
|
} |
|
|
@keyframes loading { |
|
|
from {left: -50%;} |
|
|
to {left: 100%;} |
|
|
} |
|
|
.loading-active { |
|
|
display: block !important; |
|
|
} |
|
|
.empty-box-message { |
|
|
color: #999; |
|
|
font-style: italic; |
|
|
text-align: center; |
|
|
margin-top: 30px; |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
} |
|
|
|
|
|
/* Knowledge Button Styling */ |
|
|
.knowledge-button { |
|
|
padding: 5px 10px; |
|
|
background-color: #222222; |
|
|
color: #ffffff !important; |
|
|
border: none; |
|
|
border-radius: 4px; |
|
|
cursor: pointer; |
|
|
font-weight: 500; |
|
|
font-size: 12px; |
|
|
margin: 0; /* β Remove the vertical spacing */ |
|
|
display: inline-block; |
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
|
|
transition: all 0.2s ease; |
|
|
text-decoration: none !important; |
|
|
} |
|
|
.knowledge-button:hover { |
|
|
background-color: #000000; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.15); |
|
|
} |
|
|
|
|
|
/* Knowledge popup styles - IMPROVED */ |
|
|
.knowledge-popup { |
|
|
display: block; |
|
|
padding: 20px; |
|
|
border: 2px solid #FCA539; |
|
|
background-color: white; |
|
|
border-radius: 8px; |
|
|
box-shadow: 0 5px 20px rgba(0,0,0,0.15); |
|
|
margin: 15px 0; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.knowledge-popup-header { |
|
|
font-weight: bold; |
|
|
border-bottom: 1px solid #eee; |
|
|
padding-bottom: 10px; |
|
|
margin-bottom: 15px; |
|
|
color: #222; |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.knowledge-popup-content { |
|
|
max-height: 400px; |
|
|
overflow-y: auto; |
|
|
line-height: 1.6; |
|
|
white-space: normal; |
|
|
} |
|
|
|
|
|
.knowledge-popup-content p { |
|
|
margin-bottom: 12px; |
|
|
} |
|
|
|
|
|
/* Document section formatting - IMPROVED */ |
|
|
.knowledge-sections { |
|
|
border-radius: 5px; |
|
|
background: #f9f9f9; |
|
|
padding: 10px; |
|
|
} |
|
|
|
|
|
.doc-section { |
|
|
margin-bottom: 20px; |
|
|
padding-bottom: 15px; |
|
|
border-bottom: 1px solid #e0e0e0; |
|
|
background: white; |
|
|
padding: 15px; |
|
|
border-radius: 5px; |
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05); |
|
|
} |
|
|
|
|
|
.doc-title { |
|
|
font-weight: bold; |
|
|
margin-bottom: 10px; |
|
|
color: #333; |
|
|
border-bottom: 1px solid #eee; |
|
|
padding-bottom: 5px; |
|
|
} |
|
|
|
|
|
.doc-content { |
|
|
padding-left: 10px; |
|
|
border-left: 3px solid #f0f0f0; |
|
|
line-height: 1.5; |
|
|
margin-top: 10px; |
|
|
background: #f9f9f9; |
|
|
padding: 10px; |
|
|
border-radius: 3px; |
|
|
} |
|
|
|
|
|
/* Matching text highlighting */ |
|
|
.highlight-match { |
|
|
background-color: #FCA539; |
|
|
color: black; |
|
|
font-weight: bold; |
|
|
padding: 0 2px; |
|
|
} |
|
|
|
|
|
/* Updated close button to match knowledge button */ |
|
|
.knowledge-popup-close { |
|
|
position: absolute; |
|
|
top: 15px; |
|
|
right: 15px; |
|
|
background-color: #222222; |
|
|
color: #ffffff !important; |
|
|
border: none; |
|
|
border-radius: 4px; |
|
|
padding: 5px 10px; |
|
|
cursor: pointer; |
|
|
font-size: 12px; |
|
|
font-weight: 500; |
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
|
|
} |
|
|
.knowledge-popup-close:hover { |
|
|
background-color: #000000; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.15); |
|
|
} |
|
|
|
|
|
h1, h2, h3, h4, h5, h6, p, span, div, button, input, textarea, label { |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
} |
|
|
|
|
|
/* Evidence button styling to match orange button */ |
|
|
.evidence-button { |
|
|
background: #FCA539 !important; |
|
|
color: #000000 !important; |
|
|
font-weight: bold; |
|
|
border-radius: 5px; |
|
|
padding: 10px 15px; |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
|
transition: all 0.3s ease; |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
cursor: pointer; |
|
|
display: inline-block; |
|
|
text-decoration: none !important; |
|
|
margin-top: 10px; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
.evidence-button:hover { |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
class ContextualAPIUtils: |
|
|
def __init__(self, api_key): |
|
|
self.api_key = api_key |
|
|
|
|
|
self.model_id = "92ab273b-378f-4b52-812b-7ec21506e49b" |
|
|
self.endpoint_url = f"https://api.contextual.ai/v1/agents/{self.model_id}/query" |
|
|
|
|
|
def chat(self, prompt): |
|
|
url = f"{self.endpoint_url}?retrievals_only=false&include_retrieval_content_text=true" |
|
|
headers = { |
|
|
"accept": "application/json", |
|
|
"content-type": "application/json", |
|
|
"authorization": f"Bearer {self.api_key}", |
|
|
} |
|
|
body = { |
|
|
"stream": False, |
|
|
"messages": [{"role": "user", "content": prompt}], |
|
|
} |
|
|
|
|
|
start_time = time.time() |
|
|
try: |
|
|
response = requests.post(url, headers=headers, json=body) |
|
|
response.raise_for_status() |
|
|
response_json = response.json() |
|
|
|
|
|
response_content = response_json.get("message", {}).get("content", "No content received.") |
|
|
|
|
|
response_content = response_content.replace("\n", " ") |
|
|
|
|
|
retrieved_texts = [ |
|
|
f"Doc: {item.get('doc_name', 'Unknown')}, Page: {item.get('page', 'N/A')}\n" |
|
|
f"Content: {item.get('content_text', 'No Content')}" |
|
|
for item in response_json.get("retrieval_contents", []) |
|
|
] |
|
|
retrieval_text = "\n\n".join(retrieved_texts) if retrieved_texts else "No retrieval text found." |
|
|
error_message = "" |
|
|
|
|
|
except requests.exceptions.RequestException as e: |
|
|
response_content = "API Request Failed" |
|
|
retrieval_text = "" |
|
|
error_message = str(e) |
|
|
|
|
|
response_time = round(time.time() - start_time, 4) |
|
|
return response_content, response_time, retrieval_text, error_message |
|
|
|
|
|
|
|
|
def get_contextual_rating(contextual_api, user_input): |
|
|
rating_prompt = f"Content to rate: {user_input}" |
|
|
response_text, response_time, retrieval_text, error = contextual_api.chat(rating_prompt) |
|
|
if error: |
|
|
return f"Error: {error}", "", "unsafe" |
|
|
|
|
|
|
|
|
safety_level = "safe" |
|
|
if "out of policy" in response_text.lower() or "unsafe" in response_text.lower(): |
|
|
safety_level = "unsafe" |
|
|
elif "caution" in response_text.lower() or "warning" in response_text.lower(): |
|
|
safety_level = "warning" |
|
|
|
|
|
return response_text, retrieval_text, safety_level |
|
|
|
|
|
|
|
|
LLAMA_HARM_CATEGORIES = { |
|
|
"S1": "Hate Speech", "S2": "Harassment", "S3": "Violence & Threats", |
|
|
"S4": "Sexual Content", "S5": "Self-Harm & Suicide", "S6": "Child Sexual Exploitation", |
|
|
"S7": "Dangerous & Illegal Activities", "S8": "Misinformation", "S9": "Political Content", |
|
|
"S10": "Spam & Scams", "S11": "Privacy Violations", "S12": "Defamation", "S13": "Other Unsafe Content" |
|
|
} |
|
|
|
|
|
def get_llama_guard_rating(together_client, user_input): |
|
|
chat_input = [{"role": "user", "content": user_input}] |
|
|
try: |
|
|
response = together_client.chat.completions.create( |
|
|
model="meta-llama/Meta-Llama-Guard-3-8B", messages=chat_input |
|
|
) |
|
|
raw_output = response.choices[0].message.content.strip() |
|
|
for code, full_name in LLAMA_HARM_CATEGORIES.items(): |
|
|
raw_output = raw_output.replace(code, full_name) |
|
|
|
|
|
|
|
|
safety_level = "safe" |
|
|
if "unsafe" in raw_output.lower(): |
|
|
safety_level = "unsafe" |
|
|
elif "caution" in raw_output.lower() or "warning" in raw_output.lower(): |
|
|
safety_level = "warning" |
|
|
|
|
|
return raw_output, safety_level |
|
|
except Exception as e: |
|
|
return f"Error: {str(e)}", "unsafe" |
|
|
|
|
|
|
|
|
def get_openai_moderation(openai_client, user_input): |
|
|
try: |
|
|
start_time = time.time() |
|
|
response = openai_client.moderations.create(input=user_input, model="omni-moderation-latest") |
|
|
end_time = time.time() |
|
|
|
|
|
moderation_result = response.results[0] |
|
|
flagged = moderation_result.flagged |
|
|
safety_status = "Unsafe" if flagged else "Safe" |
|
|
safety_level = "unsafe" if flagged else "safe" |
|
|
|
|
|
categories = moderation_result.categories |
|
|
high_risk_categories = { |
|
|
category: score |
|
|
for category, score in vars(categories).items() |
|
|
if isinstance(score, (int, float)) and score > 0.5 |
|
|
} |
|
|
|
|
|
if not flagged and any(score > 0.3 for score in vars(categories).values() if isinstance(score, (int, float))): |
|
|
safety_level = "warning" |
|
|
|
|
|
formatted_result = f"Safety Status: {safety_status}\n" |
|
|
if high_risk_categories: |
|
|
formatted_result += "Flagged Categories (Confidence > 0.5):\n" |
|
|
for category, score in high_risk_categories.items(): |
|
|
formatted_result += f" - {category}: {score:.2f}\n" |
|
|
else: |
|
|
formatted_result += "Flagged Categories: None\n" |
|
|
|
|
|
return formatted_result, safety_level |
|
|
except Exception as e: |
|
|
return f"Safety Status: Error\nError: {str(e)}", "unsafe" |
|
|
|
|
|
|
|
|
|
|
|
def get_perspective_rating(api_key, user_input): |
|
|
url = 'https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze' |
|
|
params = {'key': api_key} |
|
|
data = { |
|
|
'comment': {'text': user_input}, |
|
|
'requestedAttributes': { |
|
|
'TOXICITY': {}, 'SEVERE_TOXICITY': {}, 'IDENTITY_ATTACK': {}, |
|
|
'INSULT': {}, 'PROFANITY': {}, 'THREAT': {}, 'SEXUALLY_EXPLICIT': {} |
|
|
} |
|
|
} |
|
|
|
|
|
try: |
|
|
response = requests.post(url, params=params, data=json.dumps(data)) |
|
|
response.raise_for_status() |
|
|
response_json = response.json() |
|
|
|
|
|
scores = { |
|
|
attr: response_json['attributeScores'][attr]['summaryScore']['value'] |
|
|
for attr in data['requestedAttributes'] |
|
|
if attr in response_json.get('attributeScores', {}) |
|
|
} |
|
|
|
|
|
flagged_attrs = {k: v for k, v in scores.items() if v > 0.5} |
|
|
|
|
|
if flagged_attrs: |
|
|
rating_text = "Safety Status: Unsafe\nFlagged Attributes:\n" |
|
|
for attr, val in flagged_attrs.items(): |
|
|
rating_text += f" - {attr}: {val:.2f}\n" |
|
|
return rating_text, "unsafe" |
|
|
else: |
|
|
return "Safety Status: Safe", "safe" |
|
|
|
|
|
except Exception as e: |
|
|
return f"Safety Status: Error\nError: {str(e)}", "unsafe" |
|
|
|
|
|
|
|
|
|
|
|
def rate_user_input(user_input): |
|
|
|
|
|
contextual_api = ContextualAPIUtils(api_key=ORACLE_API_KEY) |
|
|
together_client = Together(api_key=TOGETHER_API_KEY) |
|
|
openai_client = openai.OpenAI(api_key=OPENAI_API_KEY) |
|
|
|
|
|
|
|
|
llama_rating, llama_safety = get_llama_guard_rating(together_client, user_input) |
|
|
contextual_rating, contextual_retrieval, contextual_safety = get_contextual_rating(contextual_api, user_input) |
|
|
openai_rating, openai_safety = get_openai_moderation(openai_client, user_input) |
|
|
perspective_rating, perspective_safety = get_perspective_rating(PERSPECTIVE_API_KEY, user_input) |
|
|
|
|
|
|
|
|
|
|
|
llama_rating = re.sub(r'\.(?=\s+[A-Z])', '.\n', llama_rating) |
|
|
|
|
|
|
|
|
|
|
|
processed_retrieval = process_retrieval_text(contextual_retrieval, user_input) |
|
|
|
|
|
|
|
|
llama_html = f"""<div class="rating-box secondary-box {llama_safety}-rating">{llama_rating}</div>""" |
|
|
openai_html = f"""<div class="rating-box secondary-box {openai_safety}-rating">{openai_rating}</div>""" |
|
|
perspective_html = f"""<div class="rating-box secondary-box {perspective_safety}-rating">{perspective_rating}</div>""" |
|
|
|
|
|
|
|
|
knowledge_html = "" |
|
|
knowledge_button = "" |
|
|
|
|
|
if processed_retrieval and processed_retrieval != "No retrieval text found.": |
|
|
|
|
|
import uuid |
|
|
popup_id = f"knowledge-popup-{uuid.uuid4().hex[:8]}" |
|
|
|
|
|
|
|
|
knowledge_html = f""" |
|
|
<div id="{popup_id}" class="knowledge-popup" style="display: none;"> |
|
|
<div class="knowledge-popup-header">Supporting evidence for Contextual Oracle</div> |
|
|
<button class="knowledge-popup-close" |
|
|
onclick="this.parentElement.style.display='none'; |
|
|
document.getElementById('btn-{popup_id}').style.display='inline-block'; |
|
|
return false;"> |
|
|
Close |
|
|
</button> |
|
|
<div class="knowledge-popup-content"> |
|
|
{processed_retrieval} |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
knowledge_button = f""" |
|
|
<div style="margin-top: 10px; margin-bottom: 5px;"> |
|
|
<a href="#" id="btn-{popup_id}" class="evidence-button" |
|
|
onclick="document.getElementById('{popup_id}').style.display='block'; this.style.display='none'; return false;"> |
|
|
Show supporting evidence |
|
|
</a> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
contextual_html = f""" |
|
|
<div class="rating-box contextual-box {contextual_safety}-rating"> |
|
|
{contextual_rating} |
|
|
</div> |
|
|
{knowledge_button} |
|
|
{knowledge_html} |
|
|
""" |
|
|
|
|
|
return contextual_html, llama_html, openai_html, perspective_html, "" |
|
|
|
|
|
def random_test_case(): |
|
|
try: |
|
|
df = pd.read_csv("hate_speech_test_cases.csv") |
|
|
sample = df.sample(1).iloc[0]["user input"] |
|
|
return sample |
|
|
except Exception as e: |
|
|
return f"Error: {e}" |
|
|
|
|
|
|
|
|
def create_gradio_app(): |
|
|
|
|
|
theme = gr.themes.Default().set( |
|
|
body_text_size="16px", |
|
|
body_text_color="#333333", |
|
|
button_primary_background_fill="#FCA539", |
|
|
button_primary_text_color="#000000", |
|
|
button_secondary_background_fill="#FCA539", |
|
|
button_secondary_text_color="#000000", |
|
|
background_fill_primary="#FFFFFF", |
|
|
background_fill_secondary="#F8F9FA", |
|
|
block_title_text_weight="600", |
|
|
block_border_width="1px", |
|
|
block_shadow="0 1px 3px rgba(0,0,0,0.1)", |
|
|
border_color_primary="#E0E0E0" |
|
|
) |
|
|
|
|
|
|
|
|
custom_css = CUSTOM_CSS + """ |
|
|
/* Policy preview popup styles */ |
|
|
.policy-popup { |
|
|
display: none; |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(0,0,0,0.7); |
|
|
z-index: 1000; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.policy-popup-content { |
|
|
background-color: white; |
|
|
width: 80%; |
|
|
height: 80%; |
|
|
border-radius: 8px; |
|
|
padding: 20px; |
|
|
position: relative; |
|
|
box-shadow: 0 5px 20px rgba(0,0,0,0.3); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.policy-popup-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 15px; |
|
|
border-bottom: 1px solid #eee; |
|
|
padding-bottom: 10px; |
|
|
} |
|
|
|
|
|
.policy-popup-title { |
|
|
font-weight: bold; |
|
|
font-size: 18px; |
|
|
} |
|
|
|
|
|
.policy-popup-close { |
|
|
background-color: #222222; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 4px; |
|
|
padding: 5px 10px; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.policy-popup-close:hover { |
|
|
background-color: #000000; |
|
|
} |
|
|
|
|
|
.policy-iframe-container { |
|
|
flex: 1; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.policy-iframe { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
border: 1px solid #eee; |
|
|
} |
|
|
|
|
|
/* Fallback for when PDF can't be displayed in iframe */ |
|
|
.policy-fallback { |
|
|
padding: 20px; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.policy-fallback a { |
|
|
display: inline-block; |
|
|
margin-top: 15px; |
|
|
padding: 10px 15px; |
|
|
background-color: #FCA539; |
|
|
color: #000000; |
|
|
text-decoration: none; |
|
|
border-radius: 4px; |
|
|
font-weight: bold; |
|
|
} |
|
|
|
|
|
|
|
|
""" |
|
|
|
|
|
with gr.Blocks(title="Hate Speech Rating Oracle", theme=theme, css=custom_css) as app: |
|
|
|
|
|
loading_spinner = gr.HTML('<div id="loading-spinner"></div>') |
|
|
|
|
|
|
|
|
pdf_file = gr.File("Hate Speech Policy.pdf", visible=True, label="Hate Speech Policy Document") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
policy_popup_html = """ |
|
|
<div id="policy-popup" class="policy-popup"> |
|
|
<div class="policy-popup-content"> |
|
|
<div class="policy-popup-header"> |
|
|
<div class="policy-popup-title">Hate Speech Policy</div> |
|
|
<button class="policy-popup-close" onclick="document.getElementById('policy-popup').style.display='none';">Close</button> |
|
|
</div> |
|
|
<div class="policy-iframe-container"> |
|
|
<!-- PDF.js viewer setup --> |
|
|
<div id="pdf-viewer" style="width: 100%; height: 100%; display: flex; flex-direction: column;"> |
|
|
<!-- We'll load PDF.js and then render the PDF here --> |
|
|
<iframe id="pdf-js-viewer" style="flex: 1; border: none; width: 100%; height: 100%;"></iframe> |
|
|
</div> |
|
|
|
|
|
<!-- Loading indicator --> |
|
|
<div id="pdf-loading" style="display: flex; justify-content: center; align-items: center; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.8);"> |
|
|
<div style="text-align: center;"> |
|
|
<div style="width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #FCA539; border-radius: 50%; margin: 0 auto 15px auto; animation: spin 1s linear infinite;"></div> |
|
|
<p>Loading document...</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Fallback for when viewer fails --> |
|
|
<div id="pdf-fallback" style="display: none; flex-direction: column; align-items: center; justify-content: center; height: 100%;"> |
|
|
<img src="https://img.icons8.com/color/96/000000/pdf.png" style="width: 96px; height: 96px; margin-bottom: 20px;"> |
|
|
<h3>PDF Viewer Unavailable</h3> |
|
|
<p style="text-align: center; max-width: 80%; margin: 15px auto;"> |
|
|
We couldn't display the PDF directly. Please download it using the button below. |
|
|
</p> |
|
|
<a id="policy-download-link" href="#" class="evidence-button" target="_blank" style="margin-top: 20px;"> |
|
|
Download Policy PDF |
|
|
</a> |
|
|
<button onclick="retryPdfLoad()" class="retry-button" style="margin-top: 15px;"> |
|
|
Try Again |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<style> |
|
|
@keyframes spin { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
.evidence-button, .retry-button { |
|
|
background: #FCA539 !important; |
|
|
color: #000000 !important; |
|
|
font-weight: bold; |
|
|
border-radius: 5px; |
|
|
padding: 10px 15px; |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
|
transition: all 0.3s ease; |
|
|
font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; |
|
|
cursor: pointer; |
|
|
display: inline-block; |
|
|
text-decoration: none !important; |
|
|
margin-top: 10px; |
|
|
margin-bottom: 5px; |
|
|
border: none; |
|
|
} |
|
|
.evidence-button:hover, .retry-button:hover { |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
.retry-button { |
|
|
background: #666 !important; |
|
|
color: white !important; |
|
|
} |
|
|
</style> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
// We need to load PDF.js dynamically |
|
|
let pdfViewerInitialized = false; |
|
|
let pdfUrl = null; |
|
|
|
|
|
// Function to find PDF URL from various Gradio components |
|
|
function findPdfUrl() { |
|
|
// Method 1: Try to find the PDF URL from file components |
|
|
const fileComponents = document.querySelectorAll(".file-component"); |
|
|
for (const comp of fileComponents) { |
|
|
const downloadBtn = comp.querySelector("a[download]"); |
|
|
if (downloadBtn && downloadBtn.href && |
|
|
(downloadBtn.href.includes("Hate Speech Policy.pdf") || |
|
|
downloadBtn.href.includes("Hate%20Speech%20Policy.pdf"))) { |
|
|
return downloadBtn.href; |
|
|
} |
|
|
} |
|
|
|
|
|
// Method 2: Try to find the URL from any element that might contain it |
|
|
const allLinks = document.querySelectorAll("a"); |
|
|
for (const link of allLinks) { |
|
|
if (link.href && |
|
|
(link.href.includes("Hate Speech Policy.pdf") || |
|
|
link.href.includes("Hate%20Speech%20Policy.pdf") || |
|
|
(link.href.includes("file=") && link.download))) { |
|
|
return link.href; |
|
|
} |
|
|
} |
|
|
|
|
|
// Method 3: Look for file inputs or elements |
|
|
const fileInputs = document.querySelectorAll("input[type='file']"); |
|
|
for (const input of fileInputs) { |
|
|
if (input.value && input.value.includes("Hate Speech Policy.pdf")) { |
|
|
// This means we found the file input, but need to find its download URL |
|
|
const parentContainer = input.closest("div[class*='component']"); |
|
|
if (parentContainer) { |
|
|
const downloadLinks = parentContainer.querySelectorAll("a[download]"); |
|
|
if (downloadLinks.length > 0) { |
|
|
return downloadLinks[0].href; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
// Method 4: Try hardcoded file paths |
|
|
const paths = [ |
|
|
"/file=Hate%20Speech%20Policy.pdf", |
|
|
"/file/Hate%20Speech%20Policy.pdf", |
|
|
"file=Hate%20Speech%20Policy.pdf", |
|
|
"file/Hate%20Speech%20Policy.pdf" |
|
|
]; |
|
|
|
|
|
// Return a default path that might work |
|
|
return paths[0]; |
|
|
} |
|
|
|
|
|
// Load PDF.js library |
|
|
function loadPdfJsLibrary() { |
|
|
if (typeof pdfjsLib !== 'undefined') { |
|
|
return Promise.resolve(); // Already loaded |
|
|
} |
|
|
|
|
|
return new Promise((resolve, reject) => { |
|
|
// Load PDF.js from CDN |
|
|
const pdfJsScript = document.createElement('script'); |
|
|
pdfJsScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js'; |
|
|
pdfJsScript.onload = resolve; |
|
|
pdfJsScript.onerror = reject; |
|
|
document.head.appendChild(pdfJsScript); |
|
|
}); |
|
|
} |
|
|
|
|
|
// Display the PDF using PDF.js viewer |
|
|
function displayPdfUsingViewer(pdfUrl) { |
|
|
// Get the iframe element |
|
|
const viewerIframe = document.getElementById('pdf-js-viewer'); |
|
|
|
|
|
// Mozilla's PDF.js viewer URL |
|
|
const viewerUrl = 'https://mozilla.github.io/pdf.js/web/viewer.html'; |
|
|
|
|
|
// Set the iframe source to the PDF.js viewer with our PDF URL as a parameter |
|
|
viewerIframe.src = `${viewerUrl}?file=${encodeURIComponent(pdfUrl)}`; |
|
|
|
|
|
// Hide loading indicator once iframe has loaded |
|
|
viewerIframe.onload = function() { |
|
|
document.getElementById('pdf-loading').style.display = 'none'; |
|
|
document.getElementById('pdf-viewer').style.display = 'flex'; |
|
|
}; |
|
|
|
|
|
// Show fallback if viewer fails to load after a timeout |
|
|
setTimeout(function() { |
|
|
if (document.getElementById('pdf-loading').style.display === 'flex') { |
|
|
showFallback(); |
|
|
} |
|
|
}, 8000); // 8 second timeout |
|
|
} |
|
|
|
|
|
// Show fallback download option |
|
|
function showFallback() { |
|
|
document.getElementById('pdf-loading').style.display = 'none'; |
|
|
document.getElementById('pdf-viewer').style.display = 'none'; |
|
|
document.getElementById('pdf-fallback').style.display = 'flex'; |
|
|
|
|
|
// Set download link |
|
|
const downloadLink = document.getElementById('policy-download-link'); |
|
|
if (downloadLink && pdfUrl) { |
|
|
downloadLink.href = pdfUrl; |
|
|
} |
|
|
} |
|
|
|
|
|
// Function to retry loading the PDF |
|
|
function retryPdfLoad() { |
|
|
// Show loading, hide fallback |
|
|
document.getElementById('pdf-loading').style.display = 'flex'; |
|
|
document.getElementById('pdf-fallback').style.display = 'none'; |
|
|
|
|
|
// Find the URL again (in case it changed) |
|
|
pdfUrl = findPdfUrl(); |
|
|
|
|
|
// Try displaying the PDF |
|
|
displayPdfUsingViewer(pdfUrl); |
|
|
} |
|
|
|
|
|
// Function to open the policy popup |
|
|
function openPolicyPopup() { |
|
|
// Display the popup |
|
|
document.getElementById('policy-popup').style.display = 'flex'; |
|
|
|
|
|
// Show loading indicator |
|
|
document.getElementById('pdf-loading').style.display = 'flex'; |
|
|
document.getElementById('pdf-viewer').style.display = 'none'; |
|
|
document.getElementById('pdf-fallback').style.display = 'none'; |
|
|
|
|
|
// If we've already found the PDF URL |
|
|
if (pdfUrl) { |
|
|
displayPdfUsingViewer(pdfUrl); |
|
|
} else { |
|
|
// Find the PDF URL |
|
|
pdfUrl = findPdfUrl(); |
|
|
|
|
|
// Set the download link for fallback |
|
|
const downloadLink = document.getElementById('policy-download-link'); |
|
|
if (downloadLink) { |
|
|
downloadLink.href = pdfUrl; |
|
|
} |
|
|
|
|
|
// Initialize PDF.js and display the PDF |
|
|
loadPdfJsLibrary() |
|
|
.then(() => { |
|
|
displayPdfUsingViewer(pdfUrl); |
|
|
}) |
|
|
.catch(error => { |
|
|
console.error('Error loading PDF.js:', error); |
|
|
showFallback(); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
// Make functions globally available |
|
|
window.openPolicyPopup = openPolicyPopup; |
|
|
window.retryPdfLoad = retryPdfLoad; |
|
|
|
|
|
// Initialize when the document is loaded |
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
// Find the PDF URL in advance |
|
|
pdfUrl = findPdfUrl(); |
|
|
|
|
|
// Load PDF.js in advance |
|
|
loadPdfJsLibrary().catch(err => { |
|
|
console.warn('PDF.js preload failed:', err); |
|
|
}); |
|
|
}); |
|
|
</script> |
|
|
""" |
|
|
|
|
|
gr.HTML(policy_popup_html) |
|
|
|
|
|
gr.Markdown("# Safety Oracle for Rating Hate Speech [BETA]") |
|
|
gr.HTML(""" |
|
|
<div style="margin-bottom: 20px;"> |
|
|
<p> |
|
|
<strong>Assess whether user-generated social content contains hate speech using Contextual AI's State-of-the-Art Agentic RAG system.</strong> |
|
|
</p> |
|
|
<p> |
|
|
Contextual's Safety Oracle classifications are steerable and explainable as they are based on a policy document rather than parametric knowledge. This app returns ratings from LlamaGuard 3.0, the OpenAI Moderation API and the Perspective API from Google Jigsaw for comparison. Feedback is welcome as we work with design partners to bring this to production. Reach out to Aravind Mohan, Head of Data Science, at <a href="mailto:aravind.mohan@contextual.ai">aravind.mohan@contextual.ai</a>. |
|
|
</p> |
|
|
|
|
|
<h2>Instructions</h2> |
|
|
<div> |
|
|
<p>Enter user-generated content to receive an assessment from all four models, or use the 'Random Test Case' button to generate an example. <strong> Safety warning: </strong> Some of the randomly generated test cases contain hateful language, which some readers may find offensive or upsetting.</p> |
|
|
</div> |
|
|
|
|
|
<h2>How it works</h2> |
|
|
<p> |
|
|
Our approach combines Contextual's state-of-the-art |
|
|
<a href='https://contextual.ai/blog/introducing-instruction-following-reranker/' target='_blank'>steerable reranker</a>, |
|
|
<a href='https://contextual.ai/blog/introducing-grounded-language-model/' target='_blank'>grounded language model</a>, and |
|
|
<a href='https://contextual.ai/blog/combining-rag-and-specialization/' target='_blank'>agent specialization</a> |
|
|
to deliver superhuman performance in content evaluation tasks. |
|
|
<br><br> |
|
|
<strong>Document-grounded evaluations</strong> ensure every rating is directly tied to our |
|
|
<a href="#" onclick="openPolicyPopup(); return false;">hate speech policy document</a>, making our system far superior to solutions that lack transparent decision criteria.<br> |
|
|
|
|
|
<strong>Adaptable policies</strong> mean the system can instantly evolve to match your requirements without retraining.<br> |
|
|
|
|
|
<strong>Clear rationales</strong> are provided with every decision, referencing specific policy sections to explain why content was approved or flagged.<br> |
|
|
|
|
|
<strong>Continuous improvement</strong> is achieved through feedback loops that enhance retrieval accuracy and reduce misclassifications over time.<br> |
|
|
</p> |
|
|
""") |
|
|
|
|
|
with gr.Column(): |
|
|
|
|
|
gr.HTML(""" |
|
|
<hr style="border-top: 1px solid #ddd; margin: 25px 0 20px 0;"> |
|
|
<h2 style="font-family: 'All Round Gothic Demi', 'Poppins', sans-serif !important; margin-bottom: 15px;">Try it out</h2> |
|
|
""") |
|
|
|
|
|
|
|
|
with gr.Row(equal_height=True) as button_row: |
|
|
random_test_btn = gr.Button("Random Test Case", elem_classes=["orange-button"], scale=1) |
|
|
rate_btn = gr.Button("Rate Content", elem_classes=["gray-button"], scale=1) |
|
|
|
|
|
user_input = gr.Textbox( |
|
|
placeholder="Type content to evaluate here...", |
|
|
lines=6, |
|
|
label="" |
|
|
) |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="result-header" style="display: flex; align-items: center; gap: 10px;"> |
|
|
<span>π Contextual Safety Oracle</span> |
|
|
<a href="#" class="knowledge-button" onclick="openPolicyPopup(); return false;">View policy</a> |
|
|
</div> |
|
|
""") |
|
|
contextual_results = gr.HTML('<div class="rating-box contextual-box empty-rating">Rating will appear here</div>') |
|
|
retrieved_knowledge = gr.HTML('', visible=False) |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="result-header" style="display: flex; align-items: center; gap: 10px;"> |
|
|
<span>LlamaGuard 3.0</span> |
|
|
<a href="https://github.com/meta-llama/PurpleLlama/blob/main/Llama-Guard3/8B/MODEL_CARD.md" |
|
|
target="_blank" class="knowledge-button">View model card</a> |
|
|
</div> |
|
|
""") |
|
|
llama_results = gr.HTML('<div class="rating-box secondary-box empty-rating">Rating will appear here</div>') |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="result-header" style="display: flex; align-items: center; gap: 10px;"> |
|
|
<span>OpenAI Moderation</span> |
|
|
<a href="https://platform.openai.com/docs/guides/moderation" |
|
|
target="_blank" class="knowledge-button">View model card</a> |
|
|
</div> |
|
|
""") |
|
|
openai_results = gr.HTML('<div class="rating-box secondary-box empty-rating">Rating will appear here</div>') |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="result-header" style="display: flex; align-items: center; gap: 10px;"> |
|
|
<span>Perspective API</span> |
|
|
<a href="https://developers.perspectiveapi.com/s/docs" |
|
|
target="_blank" class="knowledge-button">View docs</a> |
|
|
</div> |
|
|
""") |
|
|
perspective_results = gr.HTML('<div class="rating-box secondary-box empty-rating">Rating will appear here</div>') |
|
|
|
|
|
|
|
|
def show_loading(): |
|
|
return """<script> |
|
|
const spinner = document.getElementById('loading-spinner'); |
|
|
if (spinner) spinner.style.display = 'block'; |
|
|
</script>""" |
|
|
|
|
|
def hide_loading(): |
|
|
return """<script> |
|
|
const spinner = document.getElementById('loading-spinner'); |
|
|
if (spinner) spinner.style.display = 'none'; |
|
|
</script>""" |
|
|
|
|
|
|
|
|
random_test_btn.click( |
|
|
show_loading, |
|
|
inputs=None, |
|
|
outputs=loading_spinner |
|
|
).then( |
|
|
random_test_case, |
|
|
inputs=[], |
|
|
outputs=[user_input] |
|
|
).then( |
|
|
hide_loading, |
|
|
inputs=None, |
|
|
outputs=loading_spinner |
|
|
) |
|
|
|
|
|
|
|
|
rate_btn.click( |
|
|
show_loading, |
|
|
inputs=None, |
|
|
outputs=loading_spinner |
|
|
).then( |
|
|
rate_user_input, |
|
|
inputs=[user_input], |
|
|
outputs=[contextual_results, llama_results, openai_results, perspective_results] |
|
|
).then( |
|
|
hide_loading, |
|
|
inputs=None, |
|
|
outputs=loading_spinner |
|
|
) |
|
|
|
|
|
return app |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
app = create_gradio_app() |
|
|
app.launch() |