|
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=False, label="Policy PDF") |
|
|
|
|
|
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"> |
|
<!-- Content as HTML instead of trying to load PDF --> |
|
<div id="policy-content" style="height: 100%; overflow-y: auto; padding: 20px;"> |
|
<h1 style="font-size: 24px; margin-bottom: 20px;">Hate Speech Policy</h1> |
|
|
|
<h2 style="font-size: 18px; margin-top: 20px;">1. Definition of Hate Speech</h2> |
|
<p>Hate speech is defined as content that promotes violence against, threatens, or harasses individuals or groups based on protected characteristics including race, ethnicity, national origin, religion, sexual orientation, gender identity, disability, or serious disease.</p> |
|
|
|
<h2 style="font-size: 18px; margin-top: 20px;">2. Prohibited Content</h2> |
|
<p>The following types of content are prohibited:</p> |
|
<ul style="padding-left: 20px; margin-top: 10px;"> |
|
<li>Content that expresses, incites, or promotes hate based on identity</li> |
|
<li>Content that stereotypes, dehumanizes, or advocates for discrimination against protected groups</li> |
|
<li>Content that promotes harmful conspiracy theories about protected groups</li> |
|
<li>Content that denies well-documented historical atrocities</li> |
|
<li>Content that promotes or glorifies violence against individuals or groups</li> |
|
</ul> |
|
|
|
<h2 style="font-size: 18px; margin-top: 20px;">3. Content Moderation Guidelines</h2> |
|
<p>When evaluating content, moderators should consider:</p> |
|
<ul style="padding-left: 20px; margin-top: 10px;"> |
|
<li>Context and intent of the message</li> |
|
<li>Presence of slurs or derogatory terminology</li> |
|
<li>Whether content promotes hatred or violence</li> |
|
<li>Whether content targets individuals or groups based on protected characteristics</li> |
|
</ul> |
|
|
|
<h2 style="font-size: 18px; margin-top: 20px;">4. Enforcement</h2> |
|
<p>Content that violates this policy will be removed. Repeated or severe violations may result in account restrictions or termination.</p> |
|
|
|
<h2 style="font-size: 18px; margin-top: 20px;">5. Appeals Process</h2> |
|
<p>Users may appeal content moderation decisions by submitting evidence that:</p> |
|
<ul style="padding-left: 20px; margin-top: 10px;"> |
|
<li>The content was incorrectly identified as hate speech</li> |
|
<li>The content falls under an exception for educational, documentary, or artistic purposes</li> |
|
<li>The content serves a legitimate public interest purpose</li> |
|
</ul> |
|
|
|
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; font-style: italic; color: #666;"> |
|
<p>This policy document is provided as a reference for Contextual AI's hate speech classification model. The model evaluates content according to these guidelines.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
// Simple and reliable function to open the policy popup |
|
function openPolicyPopup() { |
|
// Just display the popup immediately - no PDF loading required |
|
document.getElementById('policy-popup').style.display = 'flex'; |
|
} |
|
|
|
// Make sure openPolicyPopup is globally accessible |
|
window.openPolicyPopup = openPolicyPopup; |
|
</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(share=True) |