|
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. |
|
""" |
|
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(): |
|
|
|
chunks.append(f"<strong>Evidence Document {i+1}</strong><br>{section.strip()}") |
|
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"<strong>Evidence Document {i+1}</strong><br>{chunk.strip()}" |
|
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 "<br><br>".join(highlighted_chunks) |
|
|
|
|
|
|
|
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 */ |
|
.doc-section { |
|
margin-bottom: 15px; |
|
padding-bottom: 15px; |
|
border-bottom: 1px solid #eee; |
|
} |
|
|
|
.doc-title { |
|
font-weight: bold; |
|
margin-bottom: 5px; |
|
color: #444; |
|
} |
|
|
|
.doc-content { |
|
padding-left: 10px; |
|
border-left: 3px solid #f0f0f0; |
|
} |
|
|
|
/* 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 = "ce5d18d6-41a9-4a36-a992-c543800f7045" |
|
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.") |
|
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) |
|
contextual_rating = re.sub(r'\.(?=\s+[A-Z])', '.\n', contextual_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">Retrieved Knowledge</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"> |
|
<!-- Primary method: Try Google PDF Viewer --> |
|
<iframe class="policy-iframe" id="policy-iframe"></iframe> |
|
|
|
<!-- Fallback content if iframe fails --> |
|
<div class="policy-fallback" id="policy-fallback" style="display:none;"> |
|
<p>The policy document couldn't be displayed in the preview.</p> |
|
<a href="#" id="policy-download-link" target="_blank">Download Policy PDF</a> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
// Function to handle opening the policy popup |
|
function openPolicyPopup() { |
|
// Set PDF URL - this approach is more reliable with Gradio |
|
const pdfFileName = "Hate Speech Policy.pdf"; |
|
|
|
// Try multiple approaches to display the PDF |
|
// 1. Google PDF viewer (works in most cases) |
|
const googleViewerUrl = "https://docs.google.com/viewer?embedded=true&url="; |
|
|
|
// 2. Direct link as fallback |
|
let directPdfUrl = ""; |
|
|
|
// Find the PDF link by looking for file links in the DOM |
|
const links = document.querySelectorAll("a"); |
|
for (const link of links) { |
|
if (link.href && link.href.includes(encodeURIComponent(pdfFileName))) { |
|
directPdfUrl = link.href; |
|
break; |
|
} |
|
} |
|
|
|
// Set the iframe source if we found a link |
|
const iframe = document.getElementById("policy-iframe"); |
|
const fallback = document.getElementById("policy-fallback"); |
|
const downloadLink = document.getElementById("policy-download-link"); |
|
|
|
if (directPdfUrl) { |
|
// Try Google Viewer first |
|
iframe.src = googleViewerUrl + encodeURIComponent(directPdfUrl); |
|
iframe.style.display = "block"; |
|
fallback.style.display = "none"; |
|
|
|
// Set the download link |
|
downloadLink.href = directPdfUrl; |
|
|
|
// Provide fallback in case Google Viewer fails |
|
iframe.onerror = function() { |
|
iframe.style.display = "none"; |
|
fallback.style.display = "block"; |
|
}; |
|
} else { |
|
// No direct URL found, show fallback |
|
iframe.style.display = "none"; |
|
fallback.style.display = "block"; |
|
downloadLink.href = "#"; |
|
downloadLink.textContent = "PDF not available"; |
|
} |
|
|
|
// Display the popup |
|
document.getElementById('policy-popup').style.display = 'flex'; |
|
} |
|
</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:[email protected]">[email protected]</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> |
|
<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> |
|
|
|
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. |
|
</p> |
|
""") |
|
|
|
with gr.Column(): |
|
|
|
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) |