'
f'No significant external-data boost detected yet. '
f'The score will update as more attributes are inferred.'
f'
'
)
inferential_section = (
f''
# f'{driver_sentence}'
f'{inferential_section}'
f'
'
)
else:
tooltip_body = (
'Score is calculated from PII exposure, external data linkages, '
'differential privacy settings, and inference risk from sensitive attributes.'
)
why_anchor = (
f'']
# parts.append('
Privacy Settings
')
#
# # Tooltip content - formatted as HTML with !important for font sizes
# dp_tooltip = (
# '
What is Differential Privacy?
'
# '
'
# '
'
# 'Differential Privacy (DP) is a mathematical framework that adds calibrated noise '
# 'to data retrieval, ensuring individual records cannot be distinguished. '
# 'This protects sensitive information from being inferred when external data sources are used.'
# '
'
# '
'
# '
Understanding Epsilon (ε):
'
# '
'
# 'Lower values (0.1-1.0): Stronger privacy, more noise added.
'
# 'Higher values (5.0-10.0): Less privacy, better data utility.
'
# 'Epsilon = ∞: No privacy protection applied.'
# '
'
# )
#
# # Unique IDs for this tooltip instance
# tooltip_id = f"dp_tooltip_{abs(hash(str(eps)))}"
# anchor_id = f"dp_anchor_{abs(hash(str(eps)))}"
#
# # JavaScript to position tooltip above anchor
# # JavaScript to position tooltip next to the current mouse cursor
# position_script = f"""
#
# """
#
# # Add DP status with fixed-position tooltip
# if eps != float('inf'):
# parts.append(
# f'{position_script}'
# '
'
# f''
# 'Differential Privacy'
# ': '
# 'ENABLED'
# '
'
# f'
'
# f'Differential Privacy guarantee level (epsilon parameter) = {eps:.1f}'
# f'
'
# f'
'
# f'{dp_tooltip}'
# '
'
# )
# else:
# parts.append(
# f'{position_script}'
# '
'
# f''
# 'Differential Privacy'
# ': '
# 'DISABLED'
# '
'
# f'
'
# f'Differential Privacy Guarantee = ∞'
# f'
'
# f'
'
# f'Consider enabling DP for privacy protection.'
# f'
'
# f'
'
# f'{dp_tooltip}'
# '
'
# )
#
# parts.append("
")
# return "".join(parts)
def _build_privacy_settings_html(eps):
"""Build Privacy Settings HTML for left panel with a CSS-native expandable DP explainer."""
if eps != float('inf'):
status_html = ''
# f'Consider enabling privacy protection to reduce profiling risk.
'
)
return (
f'/avatars/:
male_teen.jpg female_teen.jpg (Age bin: 0-17)
male_young.jpg female_young.jpg (Age bin: 18-29)
male_adult.jpg female_adult.jpg (Age bin: 30-44)
male_middle.jpg female_middle.jpg (Age bin: 45-59)
male_senior.jpg female_senior.jpg (Age bin: 60+)
default.jpg (fallback when gender unknown)
Also accepts .png variants of every name above.
Fallback chain (tried in order):
1. avatars/{gender}_{age_label}.jpg/.png
2. avatars/{gender}_adult.jpg/.png ← same gender, adult age
3. avatars/default.jpg/.png ← universal fallback
4. Returns ("", False) → caller shows an initials circle
Returns: (data_uri_string, found_bool)
"""
import base64
gender_prefix = {"Male": "male", "Female": "female"}.get(gender, None)
age_suffix = {
"0-17": "teen",
"18-29": "young",
"30-44": "adult",
"45-59": "middle",
"60+": "senior",
}.get(age, "adult")
root = os.path.dirname(os.path.abspath(__file__))
avatars_dir = os.path.join(root, "avatars")
candidates = []
if gender_prefix:
candidates.append(os.path.join(avatars_dir, f"{gender_prefix}_{age_suffix}.jpg"))
candidates.append(os.path.join(avatars_dir, f"{gender_prefix}_{age_suffix}.png"))
candidates.append(os.path.join(avatars_dir, f"{gender_prefix}_adult.jpg"))
candidates.append(os.path.join(avatars_dir, f"{gender_prefix}_adult.png"))
candidates.append(os.path.join(avatars_dir, "default.jpg"))
candidates.append(os.path.join(avatars_dir, "default.png"))
for path in candidates:
if os.path.isfile(path):
try:
ext = os.path.splitext(path)[1].lower()
mime = "image/png" if ext == ".png" else "image/jpeg"
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
return f"data:{mime};base64,{b64}", True
except Exception:
continue
return "", False
def _build_inferred_avatar(probs_rag, inference_warning_html="", is_warning=False):
"""Build the 'How the AI sees you' card.
Layout (top → bottom inside the card):
• Header: title left, confidence badge right
• Subtitle
• Photo: 130×130 px circle, centred
• Attribute rows: one per inferred attribute, full card width.
Each row embeds the inner content of the matching verbatim from
inference_warning_html, preserving:
– the predicted value
– the Why? tt-anchor with its evidence tt-popup spans (hover works)
– the probability (p = X%)
– the red lift message if above threshold
Nothing is added or duplicated by this function.
Card colour:
is_warning=True → yellow (#FFFDE7) + amber border
is_warning=False → blue (#E3F2FD) + blue border
"""
# Always fill any missing attributes with a uniform distribution so the
# panel is always populated, even when the inference LLM call fails.
probs_rag = _fill_missing_with_uniform(probs_rag or {})
# ── Minimum-confidence gate ───────────────────────────────────────────
def _top(dist, min_p=0.15):
if not dist:
return None, 0.0
v = max(dist, key=dist.get)
p = dist[v]
return (v, p) if p >= min_p else (None, p)
gender, gender_p = _top(probs_rag.get("Gender", {}), 0.20)
age, age_p = _top(probs_rag.get("Age bin", {}), 0.15)
finance, finance_p = _top(probs_rag.get("Finance Status", {}), 0.15)
if max(gender_p, age_p, finance_p) < 0.15:
return ""
# ── Card colour scheme ────────────────────────────────────────────────
if is_warning:
card_bg = "#FFFDE7"
card_border = "#F9A825"
title_color = "#E65100"
title_icon = "⚠️"
# subtitle = "External data raised the risk of accurate profiling."
subtitle = "Based on your conversation so far."
else:
card_bg = "#E3F2FD"
card_border = "#1976D2"
title_color = "#0D47A1"
title_icon = "🤖"
subtitle = "Based on your conversation so far."
# ── Profile photo (or initials fallback) ──────────────────────────────
img_uri, img_found = _load_avatar_image_b64(gender, age)
if img_found:
photo_html = (
f'
'
)
else:
initials = "F" if gender == "Female" else ("M" if gender == "Male" else "?")
photo_html = (
f''
f'{initials}
'
)
# ── Per-attribute content extractor ──────────────────────────────────
# The structure produced by build_inference_warning (after Fix 1) is:
#
# ATTR: VALUE
# Why?
#
#
#
# (p = X%)
# [lift]
#
#
# We grab everything between "ATTR: " and "" and embed it
# verbatim — value, Why? anchor with evidence popups, probability, lift
# — all intact, with no duplication.
def _extract_li_content(html, attr):
"""Return inner content of the for attr (after the colon), or ''."""
marker = f'{attr}: '
idx = html.find(marker)
if idx == -1:
return ""
content_start = idx + len(marker)
li_close = html.find('', content_start)
if li_close == -1:
return ""
return html[content_start:li_close].strip()
# ── Build attribute rows ──────────────────────────────────────────────
ATTR_ORDER = [
"Gender", "Age bin", "Locale",
"Marital Status", "Finance Status", "Education",
]
rows = []
for attr in ATTR_ORDER:
dist = probs_rag.get(attr, {})
if not dist:
continue
top_val = max(dist, key=dist.get)
top_p = dist[top_val]
li_content = _extract_li_content(inference_warning_html, attr)
if li_content:
# Embed verbatim — Why? tooltip, probability, and lift all included
row_content = (
f''
f'{attr}: {li_content}'
)
else:
# Fallback: plain text when warning_html is absent or attr missing
row_content = (
f''
f'{attr}: '
f'{top_val} '
f''
f'(p = {top_p:.0%})'
)
rows.append(
f''
f'{row_content}'
f'
'
)
attrs_html = "".join(rows) if rows else (
''
'No attributes inferred yet.
'
)
# ── Confidence badge ──────────────────────────────────────────────────
conf = max(gender_p, age_p, finance_p)
conf_pct = int(conf * 100)
# ── Assemble card ─────────────────────────────────────────────────────
return (
f''
# Header
f'
'
f''
f'{title_icon} How the AI sees you'
f''
f''
f'confidence: {conf_pct}%'
f''
f'
'
# Subtitle
f'
'
f'{subtitle}'
f'
'
# Photo centred
f'{photo_html}'
# Attribute rows — full card width
f'
'
f'{attrs_html}'
f'
'
# Footer disclaimer
f'
'
f'Photo is illustrative only — selected to match inferred profile. '
f'These are AI predictions and may be incorrect.'
f'
'
f'
'
)
def _build_analysis(pii, rag, risk, use_rag, eps, pr, pnr, inference_warning_html="", is_warning=False, include_avatar=True, show_pii=True):
"""Build analysis HTML for the right panel.
Layout (top → bottom):
1. 'How the AI sees you' card (photo + inferred attrs + Why? tooltips)
— omitted when include_avatar=False (card rendered separately)
2. Divider
3. Detected PIIs (tags with severity)
"""
parts = ['']
# ── Section 1: Combined avatar + inferable attributes ────────────────
if include_avatar:
avatar_html = _build_inferred_avatar(pr or {}, inference_warning_html, is_warning)
if avatar_html:
parts.append(avatar_html)
parts.append('
')
# ── Section 2: Detected PIIs (last, as requested) ────────────────────
if show_pii:
parts.append('
Detected Personally Identifiable Information (PII) in user\'s messages:
')
if pii:
# De-duplicate by (text, fine_type)
seen = set()
unique_pii = []
for p in pii:
key = (p.text.strip().lower(), p.fine_type.lower())
if key not in seen:
seen.add(key)
unique_pii.append(p)
parts.append('
')
for p in unique_pii:
col = PII_COLORS.get(p.category, {"bg": "#f5f5f5", "border": "#999"})
sev_label, sev_color = _pii_severity(p.category)
cat_label = p.category.value.capitalize()
fine = p.fine_type.title() if p.fine_type else cat_label
text_disp = p.text[:28] + ("…" if len(p.text) > 28 else "")
parts.append(
f''
f'{text_disp}'
f'[{fine}]'
f'{sev_label}'
f''
)
parts.append('
')
# Category legend
cat_counts = {}
for p in unique_pii:
cat_counts[p.category.value] = cat_counts.get(p.category.value, 0) + 1
legend_parts = [f'
{v}× {k}' for k, v in cat_counts.items()]
parts.append(f'
'
f'Total: {len(unique_pii)} — {", ".join(legend_parts)}
')
else:
parts.append('
None detected
')
parts.append("
")
return "".join(parts)
# def build_inference_warning(probs_rag, probs_no_rag, use_rag):
# ============================================================
# SECTION 14.5 – ROBUST EVIDENCE SOURCE MATCHING
# ============================================================
def tokenize_for_matching(text):
"""
Tokenize text for matching, removing punctuation and common words.
Returns:
set: Set of significant tokens (lowercased, >3 chars)
"""
import re
# Remove punctuation and split
tokens = re.findall(r'\b\w+\b', text.lower())
# Keep only significant tokens (>3 chars, not common stop words)
significant_tokens = {t for t in tokens if len(t) > 3 and t not in _DP_STOPWORDS}
return significant_tokens
def calculate_token_overlap(quote_tokens, doc_tokens, bidirectional=False):
"""Calculate token overlap ratio between two token sets.
Parameters
----------
quote_tokens, doc_tokens : set
bidirectional : bool
If True, return max(forward, reverse) overlap so that a short quote
with a high fraction of user-input words is also detected, not only
a quote whose words are mostly covered by the user input.
Use True when matching against user input; False (default) for docs.
Returns
-------
float: overlap ratio in [0, 1]
"""
if not quote_tokens or not doc_tokens:
return 0.0
intersection = len(quote_tokens & doc_tokens)
forward = intersection / len(quote_tokens)
if not bidirectional:
return forward
reverse = intersection / len(doc_tokens) # doc_tokens is user_tokens here
return max(forward, reverse)
def extract_doc_source_label(doc):
"""
Extract human-readable source label from document metadata.
Args:
doc: Document object with metadata
Returns:
str: Source label (e.g., "From uploaded data", "From Twitter (Internet)")
"""
if not hasattr(doc, 'metadata') or not isinstance(doc.metadata, dict):
return "From system data"
source_type = doc.metadata.get('source', '')
platform = doc.metadata.get('platform', '').strip().lower()
if source_type == 'uploaded_csv':
return "From uploaded data"
elif source_type.startswith('social_media_'):
platform_label = platform.capitalize() if platform else "Social Media"
return f"From {platform_label} (Internet)" if "web" not in platform_label.lower() else f"From the {platform_label} (Internet)"
elif source_type == 'fallback_tweet_data':
return "From Twitter (Internet)"
else:
# Documents from the default background corpus (e.g. PANORAMA synthetic profiles)
return DEFAULT_CORPUS_SOURCE_LABEL
def _doc_source_url(doc):
"""Extract the source URL from a document's metadata, or '' if absent."""
if not hasattr(doc, "metadata") or not isinstance(doc.metadata, dict):
return ""
return (
doc.metadata.get("source_url") or
doc.metadata.get("tweet_url") or
doc.metadata.get("url") or
""
)
def find_evidence_source(quote, user_input, retrieved_docs, min_overlap_ratio=0.4,
perturbed_input=None, prior_turns=None):
"""
Find the source of an evidence quote using robust multi-strategy matching.
Matching priority:
Strategy 1 – Exact substring: user turns first, then docs (high precision)
Strategy 2 – Token overlap: docs first, then user turns (avoids topic-word FPs)
Strategy 3 – Phrase windows: docs first, then user turns
Evidence quotes are LLM-generated paraphrases. Because the LLM is asked
to explain inferences drawn from retrieved documents, most evidence quotes
describe external-data content even when they share topic words with the
user's message. Checking docs before user turns in the fuzzy strategies
prevents those shared topic words from causing false "From your input" labels.
When no source can be matched the quote is considered LLM internal reasoning
(rationale) rather than a verbatim excerpt from any document.
Returns:
tuple: (is_from_user, is_from_docs, doc_source, match_confidence,
matched_turn_dp_active, matched_turn_original_text, doc_url)
doc_url – URL of the matched document, or "" if not from a doc / unknown.
"""
import re as _re
quote_lower = str(quote).lower().strip()
user_input_lower = user_input.lower() if user_input else ""
perturbed_lower = perturbed_input.lower() if perturbed_input else ""
# Build the full list of user turns to search
current_dp_active = bool(perturbed_lower)
turns_to_search = [(user_input_lower, perturbed_lower,
current_dp_active, user_input or "")]
if prior_turns:
for pt in prior_turns:
orig_l = (pt.get("original") or "").lower()
pert_l = (pt.get("perturbed") or "").lower()
dp_on = bool(pt.get("dp_active", False))
orig_d = pt.get("original") or ""
turns_to_search.append((orig_l, pert_l, dp_on, orig_d))
# ──────────────────────────────────────────────────────────────────────
# Strategy 1: Exact substring — user turns first, then docs
# Rationale: if the evidence is a verbatim copy of text the user typed,
# it genuinely comes from the user.
# ──────────────────────────────────────────────────────────────────────
for orig_l, pert_l, dp_on, orig_disp in turns_to_search:
if quote_lower in orig_l:
return True, False, "From your input", "exact", dp_on, orig_disp, ""
if len(quote_lower) > 50 and quote_lower[:50] in orig_l:
return True, False, "From your input", "exact", dp_on, orig_disp, ""
if pert_l and quote_lower in pert_l:
return True, False, "From your input", "exact", dp_on, orig_disp, ""
if pert_l and len(quote_lower) > 50 and quote_lower[:50] in pert_l:
return True, False, "From your input", "exact", dp_on, orig_disp, ""
if retrieved_docs:
for doc in retrieved_docs:
doc_text = doc.page_content if hasattr(doc, "page_content") else str(doc)
doc_lower = doc_text.lower()
if quote_lower in doc_lower or (
len(quote_lower) > 50 and quote_lower[:50] in doc_lower):
doc_source = extract_doc_source_label(doc)
return False, True, doc_source, "exact", False, "", _doc_source_url(doc)
# ──────────────────────────────────────────────────────────────────────
# Strategy 2: Token overlap — docs first, then user turns
# Rationale: LLM-generated evidence paraphrases retrieved docs. Both
# the evidence and the user message discuss the same topic, so shared
# topic tokens do NOT indicate the evidence came from the user.
# We check docs first; user turns are only a fallback with a high bar.
# ──────────────────────────────────────────────────────────────────────
quote_tokens = tokenize_for_matching(quote)
DOC_OVERLAP_THRESHOLD = 0.35 # forward overlap: fraction of quote tokens in doc
USER_OVERLAP_THRESHOLD = 0.65 # bidirectional: raised from 0.25 to avoid FPs
if len(quote_tokens) >= 3:
# 2a: Check retrieved docs first
if retrieved_docs:
best_overlap, best_doc = 0.0, None
for doc in retrieved_docs:
doc_text = doc.page_content if hasattr(doc, "page_content") else str(doc)
doc_tokens = tokenize_for_matching(doc_text)
ov = calculate_token_overlap(quote_tokens, doc_tokens)
if ov > best_overlap:
best_overlap, best_doc = ov, doc
if best_overlap >= DOC_OVERLAP_THRESHOLD and best_doc:
doc_source = extract_doc_source_label(best_doc)
logger.info(
f" 🔍 Token overlap match with doc ({best_overlap:.0%}): '{quote[:50]}...'")
return False, True, doc_source, "token_overlap", False, "", _doc_source_url(best_doc)
# 2b: Fall back to user turns only with a high threshold
for orig_l, pert_l, dp_on, orig_disp in turns_to_search:
turn_tokens = tokenize_for_matching(orig_l)
if calculate_token_overlap(quote_tokens, turn_tokens,
bidirectional=True) >= USER_OVERLAP_THRESHOLD:
logger.info(
f" 🔍 Token overlap match with user turn: '{quote[:50]}...'")
return True, False, "From your input", "token_overlap", dp_on, orig_disp, ""
if pert_l:
pert_tokens = tokenize_for_matching(pert_l)
if calculate_token_overlap(quote_tokens, pert_tokens,
bidirectional=True) >= USER_OVERLAP_THRESHOLD:
logger.info(
f" 🔍 Token overlap match with perturbed user turn: '{quote[:50]}...'")
return True, False, "From your input", "token_overlap", dp_on, orig_disp, ""
# ──────────────────────────────────────────────────────────────────────
# Strategy 3: Short-phrase windows — docs first, then user turns
# 3a: 3-word windows against docs
# 3b: 3-word windows against user turns (stricter guard: phrase must NOT
# appear in any retrieved doc first)
# ──────────────────────────────────────────────────────────────────────
quote_words = _re.findall(r'\b\w+\b', quote_lower)
if len(quote_words) >= 3 and retrieved_docs:
doc_texts_lower = [
(doc.page_content if hasattr(doc, "page_content") else str(doc)).lower()
for doc in retrieved_docs
]
for i in range(len(quote_words) - 2):
phrase = ' '.join(quote_words[i:i+3])
for doc, doc_lower in zip(retrieved_docs, doc_texts_lower):
if phrase in doc_lower:
doc_source = extract_doc_source_label(doc)
logger.info(
f" 🔍 3-word phrase match with doc: '{phrase}' → {doc_source}")
return False, True, doc_source, "partial", False, "", _doc_source_url(doc)
# 3b: 3-word phrase scan against user turns — only attribute to user when
# the phrase does NOT appear in any retrieved doc (disambiguates shared
# topic phrases).
if len(quote_words) >= 3:
doc_texts_lower_flat = [
(doc.page_content if hasattr(doc, "page_content") else str(doc)).lower()
for doc in (retrieved_docs or [])
]
for i in range(len(quote_words) - 2):
phrase = ' '.join(quote_words[i:i+3])
phrase_in_doc = any(phrase in dt for dt in doc_texts_lower_flat)
if phrase_in_doc:
continue # ambiguous — skip, will fall through to "General"
for orig_l, pert_l, dp_on, orig_disp in turns_to_search:
if phrase in orig_l:
logger.info(
f" 🔍 3-word phrase (user-only) match: '{phrase}'")
return True, False, "From your input", "partial", dp_on, orig_disp, ""
if pert_l and phrase in pert_l:
logger.info(
f" 🔍 3-word phrase (perturbed, user-only) match: '{phrase}'")
return True, False, "From your input", "partial", dp_on, orig_disp, ""
# No match found — the text is LLM-internal reasoning, not a source quote.
logger.info(f" ⚠️ No source match for evidence (treated as rationale): '{quote[:50]}...'")
return False, False, "General", "none", False, "", ""
# ============================================================
# SECTION 15 – BUILD INFERENCE WARNING
# ============================================================
# def build_inference_warning(probs_rag, probs_no_rag, use_rag):
# """Build yellow warning banner with lift threshold and tooltips."""
# if not probs_rag:
# return ""
#
# items = []
# max_lift = 0.0 # Track highest lift across all attributes
#
# for attr in SENSITIVE_ATTRIBUTES:
# dist = probs_rag.get(attr, {})
# if not dist:
# continue
# top_val = max(dist, key=dist.get)
# top_p = dist[top_val]
#
# lift = 0.0
# extra = ""
# if use_rag and probs_no_rag:
# p_nr = probs_no_rag.get(attr, {}).get(top_val, 0.0)
# z = InferentialPrivacyMetrics.compute_z(top_p, p_nr)
# lift = InferentialPrivacyMetrics.compute_lift(z)
#
# # Track maximum lift
# if lift > max_lift:
# max_lift = lift
#
# # Only show attributes that meet the lift threshold
# if lift > INFERENCE_LIFT_THRESHOLD and not math.isinf(lift):
# extra = (f' '
# f'(+{lift:.0%} risk from external data)')
# else:
# # Skip this attribute if lift is below threshold
# continue
#
# # Add attribute with tooltip - wrap in container with tooltip class
# items.append(
# f'{attr}: '
# f''
# f'{top_val}'
# f''
# f' '
# f'(p = {top_p:.0%})'
# f'{extra}')
#
# # Only show warning if at least one attribute exceeds threshold
# if not items or (use_rag and max_lift <= INFERENCE_LIFT_THRESHOLD):
# return ""
#
# source_note = ("your input and external data sources"
# if use_rag else "your input alone")
#
# # Add tooltip to the warning title - wrap in container
# return (
# ''
# '
'
# '⚠ Privacy Warning – Inferable Attributes'
# ''
# ''
# ''
# f'
Based on {source_note}, the AI system '
# f'could potentially infer the following about you:'
# f'
'
# '
')
def build_inference_warning(user_input, probs_rag, probs_no_rag, use_rag,
evidence_rag=None, retrieved_docs=None,
input_dp_metadata=None, perturbed_user_input=None,
conversation_state=None):
"""Build yellow warning banner with lift threshold and explainability tooltips.
Inference metrics are ALWAYS collected for every attribute so they can be
logged regardless of whether the banner is ultimately shown to the user.
The warning banner is only shown when at least one attribute exceeds
INFERENCE_LIFT_THRESHOLD, but inference_metrics always captures everything.
evidence_rag may contain either:
- New format: {attr: [{"quote": "...", "type": "explicit"|"implicit"}, ...]}
- Legacy format: {attr: ["...", ...]} (plain strings)
Returns:
tuple: (warning_html, inference_metrics_dict, warning_shown_bool)
warning_html – HTML string (empty string if threshold not met)
inference_metrics – dict with ALL attributes, for logging
warning_shown – True iff the banner will be visible to the user
"""
# print("In build_inference_warning")
if not probs_rag:
return "", {}, False
if evidence_rag is None:
evidence_rag = {}
# Build prior_turns list: all user messages with original + perturbed text.
prior_turns = []
if conversation_state and conversation_state.messages:
for msg in conversation_state.messages:
if msg.role != "user":
continue
dp_m = getattr(msg, "dp_metadata", None)
prior_turns.append({
"original": msg.content,
"perturbed": dp_m.get("perturbed_text", "") if dp_m else "",
"dp_active": bool(dp_m and dp_m.get("num_substitutions", 0) > 0),
})
items = [] # list items for the HTML banner (only threshold-passing attrs)
max_lift = 0.0
inference_metrics = {} # collected for ALL attributes (logging)
for attr in SENSITIVE_ATTRIBUTES:
dist = probs_rag.get(attr, {})
if not dist:
continue
top_val = max(dist, key=dist.get)
top_p = dist[top_val]
lift = 0.0
p_nr = 0.0
p_pop = POPULATION_PRIORS.get(attr, {}).get(top_val, 0.0)
# Lift is always computed against the population prior so that
# ALL experimental conditions (with or without RAG) produce a
# non-zero score when the posterior exceeds the base rate.
if p_pop > 0:
z = InferentialPrivacyMetrics.compute_z(top_p, p_pop)
lift = InferentialPrivacyMetrics.compute_lift(z)
if lift > max_lift:
max_lift = lift
# Still track the no-RAG probability for logging / comparison.
if probs_no_rag:
p_nr = probs_no_rag.get(attr, {}).get(top_val, 0.0)
# ── Determine dominant evidence_type for this attribute ──────────
# evidence_rag[attr] is a list of either:
# new: {"quote": str, "type": "explicit"|"implicit"}
# legacy: plain string → treated as "implicit"
raw_evidence = evidence_rag.get(attr, [])
evidence_type = "unknown"
if raw_evidence:
for item in raw_evidence:
if isinstance(item, dict):
t = item.get("type", "implicit")
else:
t = "implicit"
# If any evidence item is explicit, mark the whole attribute explicit
if t == "explicit":
evidence_type = "explicit"
break
else:
evidence_type = "implicit"
# ── Always record in inference_metrics (for logging) ─────────────
if not (math.isinf(lift) or math.isnan(lift)):
inference_metrics[attr] = {
'top_value': top_val,
'confidence': round(float(top_p), 4),
'lift': round(float(lift), 4),
'prob_rag': round(float(top_p), 4),
'prob_no_rag': round(float(p_nr), 4),
'p_pop': round(float(p_pop), 4),
'evidence_type': evidence_type,
}
# ── Only add to the visible banner if threshold is met ───────────
# print(lift, INFERENCE_LIFT_THRESHOLD)
above_threshold = lift > INFERENCE_LIFT_THRESHOLD and not math.isinf(lift)
# if not above_threshold:
# continue
extra = (
f' (+{lift:.0%} increase based on external data)'
if above_threshold and use_rag else ""
)
# ── Build evidence tooltip ────────────────────────────────────────
# Normalise evidence items to dicts so we can extract quote text
normalised_evidence = []
for item in raw_evidence:
if isinstance(item, dict):
quote_text = str(item.get("quote", item.get("text", ""))).strip()
ev_type = item.get("type", "implicit")
else:
quote_text = str(item).strip()
ev_type = "implicit"
if (quote_text and
'no evidence' not in quote_text.lower() and
'not found' not in quote_text.lower() and
'no specific' not in quote_text.lower() and
'unavailable' not in quote_text.lower() and
len(quote_text) > 5):
normalised_evidence.append({"quote": quote_text, "type": ev_type})
if normalised_evidence:
evidence_html = 'Evidence for Prediction
'
evidence_html += 'The following evidence from your input or external data sources supports this prediction:
'
evidence_html += ''
for ev_item in normalised_evidence[:3]:
q = ev_item["quote"]
ev_badge = (
'explicit'
if ev_item["type"] == "explicit" else
'implicit'
)
q_esc = _esc(q)
(is_from_user, is_from_docs, doc_source, _conf,
matched_turn_dp, matched_turn_orig, doc_url) = find_evidence_source(
q, user_input, retrieved_docs, min_overlap_ratio=0.4,
perturbed_input=perturbed_user_input,
prior_turns=prior_turns,
)
# Determine whether this is a genuine source excerpt or LLM
# internal reasoning (rationale). When neither user input nor
# any retrieved document can be matched, the text is the model's
# own thinking and should NOT be displayed like a quoted source.
is_rationale = (not is_from_user) and (not is_from_docs)
if is_from_user:
source_label = "From your input"
source_color = "#a119d2"
elif is_from_docs:
source_label = doc_source
source_color = "#1976D2" if doc_source == "From uploaded data" else (
"#F57C00" if "(Internet)" in doc_source else "#666"
)
else:
source_label = "AI reasoning"
source_color = "#757575"
if len(q_esc) > ATTR_INFERENCE_EVIDENCE_STORED_EXCERPT_LENGTH:
q_esc = q_esc[:ATTR_INFERENCE_EVIDENCE_STORED_EXCERPT_LENGTH] + "..."
evidence_html += ''
evidence_html += f'
'
evidence_html += f'● {source_label}{ev_badge}'
evidence_html += '
'
if is_rationale:
# Display as plain italic reasoning text, not as a quoted excerpt.
evidence_html += (
f'
{q_esc}
'
)
else:
# Display as a quoted source excerpt.
evidence_html += f'
"{q_esc}"
'
# For doc sources, add a clickable URL if available.
if is_from_docs and doc_url:
url_esc = _esc(doc_url)
evidence_html += (
f'
'
)
# When DP was active for the matched turn, note that the AI saw
# a word-substituted version, not the original wording.
if is_from_user and matched_turn_dp and matched_turn_orig:
orig_display = matched_turn_orig[:400] + ("…" if len(matched_turn_orig) > 400 else "")
orig_esc = _esc(orig_display)
evidence_html += (
f'
'
f'🔒 Privacy protection was active for this message — the AI '
f'received a modified (word-substituted) version of your text, '
f'not your original wording.
'
f''
f'Your original message: “{orig_esc}”'
f''
f'
'
)
evidence_html += '
'
else:
evidence_html = 'Evidence for Prediction
'
evidence_html += ''
evidence_html += (''
'No specific evidence from external sources was found to '
'support this prediction. The inference is based solely on the language '
"model's general knowledge and the user's input text.
")
items.append(
f''
f'{attr}: {top_val} '
# tt-anchor is the hover target — evidence lives INSIDE it
f''
f'Why?'
f''
f''
f''
f' (p = {top_p:.0%})'
f'{"" + extra + "
" if extra else ""}'
f''
)
# ── Decide whether to show the warning banner ─────────────────────────
# print(max_lift, INFERENCE_LIFT_THRESHOLD)
# if not items or (use_rag and max_lift <= INFERENCE_LIFT_THRESHOLD):
if not probs_rag or not items:
return "", inference_metrics, False
is_warning = use_rag and max_lift > INFERENCE_LIFT_THRESHOLD and not math.isinf(max_lift)
source_note = ("your input and external data sources"
if use_rag else "your input alone")
if is_warning:
bg_color = "#FFF9C4"
border_color = "#F9A825"
title_color = "#E65100"
title_icon = "⚠"
title_text = "Privacy Warning – Inferable Attributes"
subtitle = (f'Based on {source_note}, the AI system '
f'could potentially infer the following about you:')
else:
bg_color = "#E3F2FD"
border_color = "#1976D2"
title_color = "#0D47A1"
title_icon = "ℹ"
title_text = "For your information – Inferable Attributes"
subtitle = (f'Based on {source_note}, the AI system '
f'may be able to infer the following about you. '
f'No significant lift from external data was detected.')
warning_html = (
f''
f'
'
f'{title_icon} {title_text}'
f''
f''
f'
{subtitle}'
f'
'
f'
'
)
return warning_html, inference_metrics, is_warning
# ============================================================
# SECTION 14 – CONVERSATION FORMATTING (WITH SMART TOOLTIPS)
# ============================================================
def _esc(text):
"""HTML escape."""
return (text.replace("&", "&").replace("<", "<")
.replace(">", ">").replace('"', """)
.replace("'", "'"))
def _merge_overlapping_annotations(anns):
"""
Split overlapping PII (pri=0) and RAG (pri=1) annotations into
non-overlapping sub-spans so that both highlights are always visible.
For each character range covered by both a PII and a RAG annotation a
"combined" sub-span is produced with:
• a diagonal gradient background blending both colours
• a box-shadow outline in the PII border colour
• a bottom-border in the RAG border colour
• a "P R" superscript label with each letter in its own colour
• both tooltips concatenated (PII first, divider, then RAG)
The non-overlapping tails of each annotation are preserved with their
original style.
"""
pii_list = [a for a in anns if a.get("pri", 1) == 0]
rag_list = [a for a in anns if a.get("pri", 1) == 1]
# If there is nothing to overlap, return as-is
if not pii_list or not rag_list:
return anns
out = []
# Track which (overlap_s, overlap_e) intervals belong to each source ann
used_pii = [] # list of (ov_s, ov_e, pii_ann_object)
used_rag = [] # list of (ov_s, ov_e, rag_ann_object)
for p in pii_list:
for r in rag_list:
ov_s = max(p["s"], r["s"])
ov_e = min(p["e"], r["e"])
if ov_s >= ov_e:
continue # no overlap
combined_tip = (
p.get("tip_html", "") +
''
'Also linked to external data:
' +
r.get("tip_html", "")
)
out.append({
"s": ov_s,
"e": ov_e,
# Diagonal gradient: PII colour top-half, RAG colour bottom-half
"bg": f"linear-gradient(to bottom, {p['bg']} 52%, {r['bg']} 48%)",
"bd": r["bd"], # RAG border-bottom
"bd2": p["bd"], # PII outline via box-shadow
"lbl_p_c": p["bd"], # colour for "P" superscript
"lbl_r_c": r["bd"], # colour for "R" superscript
"tip_html": combined_tip,
"pri": 0,
"combined": True,
})
used_pii.append((ov_s, ov_e, p))
used_rag.append((ov_s, ov_e, r))
# Subtract already-emitted overlap intervals from an annotation and
# return the remaining (non-overlapping) sub-spans.
def _remainders(ann, used_list):
intervals = sorted(
(ov_s, ov_e) for ov_s, ov_e, src in used_list if src is ann
)
if not intervals:
return [ann]
# Merge adjacent/touching intervals
merged = [list(intervals[0])]
for iv_s, iv_e in intervals[1:]:
if iv_s <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], iv_e)
else:
merged.append([iv_s, iv_e])
# Gaps between merged intervals → individual sub-spans
parts = []
cur = ann["s"]
for iv_s, iv_e in merged:
if cur < iv_s:
parts.append({**ann, "s": cur, "e": iv_s})
cur = iv_e
if cur < ann["e"]:
parts.append({**ann, "s": cur, "e": ann["e"]})
return parts
for p in pii_list:
out.extend(_remainders(p, used_pii))
for r in rag_list:
out.extend(_remainders(r, used_rag))
return out
def render_highlighted(text, pii_matches, rag_links, show_tips=True, show_rag_hl=True, show_pii_hl=True):
"""Render text with colored highlight spans and smart-positioned tooltips."""
if text is None:
text = ""
if not isinstance(text, str):
text = str(text)
anns = []
if show_pii_hl and pii_matches:
for m in pii_matches:
c = PII_COLORS[m.category]
tip_html = (
f'{c["label"]}
'
f'Detected: {_esc(m.text)}
'
f''
f'Type: {m.fine_type} | Confidence: {m.confidence:.0%}
'
)
anns.append({
"s": m.start,
"e": m.end,
"bg": c["bg"],
"bd": c["border"],
"lbl": "P",
"tip_html": tip_html,
"pri": 0,
})
if show_rag_hl and rag_links:
for lk in rag_links:
kw_txt = ", ".join(lk.overlap_keywords or ["N/A"])
# Determine source color based on source type
source = getattr(lk, 'source', 'From system data')
if source == "From uploaded data":
source_color = "#1976D2" # Blue
elif "Internet" in source:
source_color = "#F57C00" # Orange
else:
source_color = "#666" # Gray
# Build optional URL line for social media / web sources
lk_url = getattr(lk, 'url', '')
url_line = ""
if lk_url and "Internet" in source:
display_url = lk_url # if len(lk_url) <= 80 else lk_url[:77] + "..."
url_line = (
f''
)
tip_html = (
'External Data Linkage
'
f'Text: {_esc(lk.text)}
'
''
''
'
Top Retrieved Document:
'
f'
{_esc(lk.top_doc_text)}
'
'
'
''
# f'
Retrieval similarity: {float(lk.top_doc_score):.2f}
'
# f'
Matched keywords: {_esc(kw_txt)}
'
f'
Combined similarity: {float(lk.top_similarity):.2f}
'
f'
Retrieval score: {float(lk.top_doc_score):.2f}
'
f'
Matched keywords: {_esc(kw_txt)}
'
f'
Source: {source}
'
f'{url_line}'
'
'
''
'This linkage increases the chance that sensitive attributes can be inferred by combining your text with external data.
'
)
anns.append({
"s": lk.start,
"e": lk.end,
"bg": RAG_LINK_COLOR["bg"],
"bd": RAG_LINK_COLOR["border"],
"lbl": "R",
"tip_html": tip_html,
"pri": 1, # RAG yields to PII in overlaps
})
if not anns:
return _esc(text)
# return f"{_esc(text)}
"
# anns.sort(key=lambda a: (a["s"], -a["e"]))
anns = _merge_overlapping_annotations(anns)
anns.sort(key=lambda a: (a["s"], a.get("pri", 1), -a["e"]))
parts, last = [], 0
for a in anns:
if a["s"] < last:
continue
if a["s"] > last:
parts.append(_esc(text[last:a["s"]]))
st = _esc(text[a["s"] : a["e"]])
# CSS-based tooltip with smart positioning
cls = "hl"
tooltip_html = ""
if show_tips and a.get("tip_html"):
cls += " tt-anchor"
# Add both above and below variants - JS will choose
tooltip_html = (
f''
f''
f''
f''
f''
f''
)
if a.get("combined"):
# Two-colour span: diagonal gradient bg + RAG bottom border +
# PII outline via box-shadow. Label: "P" (PII colour) "R" (RAG colour).
label_html = (
f''
f'P'
f'R'
f''
)
parts.append(
f'{st}{label_html}{tooltip_html}'
)
else:
parts.append(
f'{st}'
f'{a["lbl"]}'
f'{tooltip_html}'
)
last = a["e"]
if last < len(text):
parts.append(_esc(text[last:]))
# Wrap in a div to ensure proper rendering
return "".join(parts)
# if last < len(text):
# parts.append(_esc(text[last:]))
# return "".join(parts)
# def fmt_conversation(messages, show_tips=True, show_rag_hl=True, show_pii_hl=True):
# """Format conversation with highlights and smart tooltips."""
# css = (
# ""
#
# # JavaScript to detect position and add 'near-top' class
# ""
# )
#
# h = [css, '']
# for m in messages:
# role_cls = "msg-user" if m.role == "user" else "msg-ai"
# role_label = "You" if m.role == "user" else "Assistant"
# # content_html = render_highlighted(m.content, m.pii_matches, m.rag_links, show_tips, show_rag_hl, show_pii_hl)
# if m.rag_links or m.pii_matches:
# content_html = render_highlighted(m.content, m.pii_matches, m.rag_links, show_tips, show_rag_hl,
# show_pii_hl)
# else:
# formatted_content = _format_response_text(m.content)
# content_html = render_highlighted(formatted_content, m.pii_matches, m.rag_links, show_tips, show_rag_hl,
# show_pii_hl)
# h.append(
# f'
'
# f'
{role_label}
'
# f'{content_html}
'
# )
# h.append('
')
# return "".join(h)
def fmt_conversation(messages, show_tips=True, show_rag_hl=True, show_pii_hl=True):
"""Format conversation with highlights and smart tooltips."""
css = (
""
)
h = [css, '']
for m in messages:
role_cls = "msg-user" if m.role == "user" else "msg-ai"
role_label = "You" if m.role == "user" else "Assistant"
# if m.rag_links or m.pii_matches:
# content_html = render_highlighted(m.content, m.pii_matches, m.rag_links, show_tips, show_rag_hl,
# show_pii_hl)
# else:
# formatted_content = _format_response_text(m.content)
# content_html = render_highlighted(formatted_content, m.pii_matches, m.rag_links, show_tips, show_rag_hl,
# show_pii_hl)
# Always format text first, regardless of annotations
content_html = render_highlighted(m.content, m.pii_matches, m.rag_links, show_tips, show_rag_hl, show_pii_hl)
# Append DP badge to user messages that were perturbed
badge_html = ""
if m.role == "user" and getattr(m, "dp_metadata", None):
badge_html = _dp_badge_html(m.dp_metadata)
if badge_html:
badge_row = (
f'
{badge_html}
'
)
else:
badge_row = ""
h.append(
f'
'
f'
{role_label}
'
f'{content_html}{badge_row}
'
)
h.append('
')
return "".join(h)
def clear_conversation(state, show_tips, show_rag_hl, show_pii_hl):
"""Clear conversation."""
state.clear()
return (
fmt_conversation([], show_tips, show_rag_hl, show_pii_hl),
create_risk_display(0),
"",
"", "", gr.update(visible=False), gr.update(visible=False)
)
# ============================================================
# SECTION 15 – LEGEND
# ============================================================
def create_legend_html(show_tips=True, show_rag_hl=True):
"""Create legend HTML."""
items = []
if show_tips:
for _c, col in PII_COLORS.items():
items.append(
f''
f''
f'{col["label"]}
')
if show_rag_hl:
items.append(
f''
f''
f'{RAG_LINK_COLOR["label"]}
')
return "".join(items)
# ============================================================
# SECTION 16 – GRADIO UI
# ============================================================
def create_ui(
model=None,
rag="1",
epsilon="inf",
show_risk="0",
show_tips="0",
show_rag_highlights="0",
show_pii_highlights="1",
show_settings="0",
demo="0",
enable_social_scraping="0",
show_social_scraping="0",
show_upload_data = "1",
scenario_mode="real",
retriever=None,
show_dp="1",
show_infr_attr_card="1",
):
"""Create Gradio UI with settings panel."""
# state = ConversationState()
def _to_bool(v):
if v is None or v == "":
return False
if isinstance(v, (int, float)):
return bool(int(v))
s = str(v).strip().lower()
return s in {"1", "true", "t", "yes", "y", "on"}
def _to_float(v, default=float("inf")):
if v is None or v == "":
return default
s = str(v).strip().lower()
if s in {"inf", "infty", "infinite", "infinity"}:
return float("inf")
try:
return float(s)
except Exception:
return default
def _make_demo_prompt():
tpl = random.choice(list(DATASET_PROMPTS_PANORAMA.values()))
return tpl.format(name="Raymond Phillips")
# Defaults
use_rag_default = _to_bool(rag)
eps_default = _to_float(epsilon, default=float("inf"))
model_default = str(model or DEFAULT_MODEL_NAME)
show_risk_default = _to_bool(show_risk)
show_tips_default = _to_bool(show_tips)
show_raghl_default = _to_bool(show_rag_highlights)
# show_pii_hl_default = _to_bool(highlight_pii)
show_pii_hl_default = _to_bool(show_pii_highlights)
show_settings_default = _to_bool(show_settings)
show_social_scraping_default = _to_bool(show_social_scraping)
show_upload_data_default = _to_bool(show_upload_data)
demo_default = _to_bool(demo)
social_scraping_default = _to_bool(enable_social_scraping)
scenario_mode_default = str(scenario_mode or "real").strip().lower()
# show_dp: 0=never, 1=after each turn (default), 2=beginning only, 3=on End button
# show_infr_attr_card: 0=never, 1=after each turn (default), 2=on End button
show_dp_default = int(_to_float(show_dp, default=1.0))
show_infr_default = int(_to_float(show_infr_attr_card, default=1.0))
# Initialize social scraper
initialize_social_scraper(enabled=social_scraping_default)
# vs = get_retriever()
_get_system_retriever_for_scenario = get_scenario_retriever
with gr.Blocks(
# css=css,
title="What can LLMs infer about me?",
theme=gr.themes.Default(spacing_size=gr.themes.sizes.spacing_sm, radius_size=gr.themes.sizes.radius_none),
) as app:
access_control_css = gr.HTML("")
if show_settings_default:
gr.Markdown(
"# 🔒 What can LLMs infer about me?\n\n"
"Analyze privacy risks and understand what LLMs can infer from your data."
)
# Session-scoped configuration
rag_st = gr.State(use_rag_default)
eps_st = gr.State(eps_default)
mdl_st = gr.State(model_default)
tips_st = gr.State(show_tips_default)
raghl_st = gr.State(show_raghl_default)
piihl_st = gr.State(show_pii_hl_default)
riskpanel_st = gr.State(show_risk_default)
demo_st = gr.State(demo_default)
demo_prompt_st = gr.State(_make_demo_prompt() if demo_default else "")
vs_st = gr.State(None)
social_scraping_st = gr.State(False) # Social media scraping toggle
custom_corpus_loaded_st = gr.State(False) # Track if custom corpus is loaded
uploaded_file_path_st = gr.State(None)
state_st = gr.State(ConversationState())
# New: control panel visibility modes and conversation-ended flag
show_dp_st = gr.State(show_dp_default)
show_infr_st = gr.State(show_infr_default)
conv_ended_st = gr.State(False)
with gr.Row():
# LEFT: legend + settings panel
with gr.Column(min_width=220, scale=1) as legend_col:
with gr.Group(visible=False) as legend_group:
# with gr.Row():
gr.Markdown("### 🎨 Legend")
legend_html = gr.HTML("")
# Settings panel (toggled by URL parameter)
with gr.Group(visible=show_settings_default) as settings_panel:
gr.Markdown("### ⚙️ Settings")
# Model selector
model_dropdown = gr.Dropdown(
choices=[name for _, _, name in MODEL_CONFIGS],
value=model_default,
label="Model",
interactive=True
)
# Epsilon slider - Differential Privacy Guarantee (Noise Level)
epsilon_slider = gr.Slider(
minimum=0.1,
maximum=MAX_POSSIBLE_EPS,
value=min(eps_default, MAX_POSSIBLE_EPS) if eps_default != float('inf') else MAX_POSSIBLE_EPS,
label="Differential Privacy Guarantee (Noise Level)",
info="",#"Controls the amount of random noise (perturbation) added during linkage with external data. Lower values = more noise = stronger privacy protection. Higher values = less noise = better accuracy but weaker privacy guarantees.",
interactive=True
)
# Infinite epsilon checkbox
eps_inf_checkbox = gr.Checkbox(
value=(eps_default == float('inf')),
label="Disable Differential Privacy",
interactive=True
)
with gr.Group(visible=False, elem_id="privacy_settings_panel") as privacy_settings_group:
gr.Markdown("### 🔒 Privacy Settings")
privacy_settings_html = gr.HTML("")
social_scraping_group = gr.Group(visible=show_social_scraping_default)
with social_scraping_group:
# Social Media Scraping toggle
gr.Markdown("#### 🌐 Access to Social Media")
social_scraping_checkbox = gr.Checkbox(
value=social_scraping_default,
label="Allow access to my social media",
interactive=True
)
social_scraping_info = gr.HTML(
"""
Extract data from social media platforms in real-time.
This may take up to a minute.
"""
)
social_platforms = gr.CheckboxGroup(
choices=[
"Twitter",
"Facebook",
"LinkedIn",
"Web",
],
value=["Twitter","Facebook", "LinkedIn", "Web"],
label="Platforms",
info=(
"Twitter | "
"Facebook | "
"LinkedIn | "
"Web"
),
visible=social_scraping_default,
interactive=True
)
upload_data_group = gr.Group(visible=show_upload_data_default)
with upload_data_group:
# Custom Corpus Upload
gr.Markdown("#### 📁 Upload My Data")
corpus_file = gr.File(
label="Upload CSV file",
file_types=None, #[".csv"],
type="filepath",
interactive=True
)
corpus_upload_info = gr.HTML(
"""
Download your textual social media data from Facebook, LinkedIn, Twitter (X).
CSV should have columns: text (required), user id (if known), First Name, Last Name
"""
)
corpus_upload_btn = gr.Button("Upload", variant="secondary", size="sm")
corpus_status = gr.Markdown(
"",
elem_id="corpus_status",
elem_classes="corpus-status-box"
)
# CENTER: conversation
with gr.Column(scale=8):
conversation_panel_title = "### 💬 Conversation"
if show_raghl_default:
conversation_panel_title += " | Hover over highlighted text to see why it was flagged."
gr.Markdown(conversation_panel_title)
conv_html = gr.HTML(fmt_conversation([], show_tips_default, show_raghl_default, show_pii_hl_default))
warn_html = gr.HTML("")
persona_hint = gr.HTML("")
user_tb = gr.Textbox(
value="",
placeholder="Type your message here…",
label="Your Message",
lines=3,
)
with gr.Row():
send_btn = gr.Button("Send", variant="primary", scale=1)
# clear_btn = gr.Button("Clear", scale=1)
reset_btn = gr.Button("Reset conversation", scale=1)
end_btn = gr.Button("End conversation", variant="stop", scale=1)
# RIGHT: risk panel
right_col_needed = show_risk_default or show_pii_hl_default or (show_infr_default != 0)
with gr.Column(scale=3, visible=right_col_needed) as risk_col:
risk_title = gr.Markdown("", visible=bool(show_risk_default))
risk_html = gr.HTML("", visible=bool(show_risk_default))
analysis_title = gr.Markdown("### 📝 Analysis", visible=False)
infer_card_html = gr.HTML("", visible=(show_infr_default != 0))
analysis_md = gr.HTML("", visible=False)
# Page-load: apply URL query parameters
# def _apply_query_params(request: gr.Request):
# qp = getattr(request, "query_params", {}) or {}
#
# access_token = qp.get("token", "")
# if not validate_access_token(access_token):
# # Invalid or missing token - return error state
# error_msg = """
#
#
⚠️ Access Denied
#
Invalid or missing access token.
#
Please contact the administrator for a valid access link.
#
# """
# return (
# error_msg, # legend_html - show error
# gr.update(visible=False), # settings_panel
# gr.update(visible=False), # risk_col
# False, # rag_st
# 1.0, # eps_st
# DEFAULT_MODEL_NAME, # mdl_st
# False, # tips_st
# False, # raghl_st
# False, # piihl_st
# False, # riskpanel_st
# False, # demo_st
# "", # demo_prompt_st
# error_msg, # conv_html - show error
# "", # risk_html
# "", # warn_html
# "", # analysis_md
# "", # user_tb
# DEFAULT_MODEL_NAME, # model_dropdown
# 1.0, # epsilon_slider
# False, # eps_inf_checkbox
# )
#
# mdl = str(qp.get("model", model_default))
# use_rag = _to_bool(qp.get("rag", use_rag_default))
# eps = _to_float(qp.get("epsilon", eps_default), default=eps_default)
#
# show_risk_v = _to_bool(qp.get("show_risk", show_risk_default))
# show_tips_v = _to_bool(qp.get("show_tips", show_tips_default))
# show_raghl_v = _to_bool(qp.get("show_rag_highlights", show_raghl_default))
# show_pii_hl_v = _to_bool(qp.get("highlight_pii", show_pii_hl_default))
# show_settings_v = _to_bool(qp.get("show_settings", show_settings_default))
# demo_v = _to_bool(qp.get("demo", demo_default))
#
# legend_visible = bool(show_tips_v or show_raghl_v or show_pii_hl_v)
# # demo_prompt = _make_demo_prompt() if demo_v else ""
# demo_prompt = DEFAULT_USER_TEXT if DEFAULT_USER_TEXT else ""
#
# legend = create_legend_html(show_tips=show_tips_v, show_rag_hl=show_raghl_v)
#
# eps_is_inf = (eps == float('inf'))
# eps_slider_val = min(eps, 10.0) if not eps_is_inf else 10.0
#
# return (
# legend,
# gr.update(visible=show_settings_v),
# gr.update(visible=show_risk_v),
# use_rag,
# eps,
# mdl,
# show_tips_v,
# show_raghl_v,
# show_pii_hl_v,
# show_risk_v,
# demo_v,
# demo_prompt,
# fmt_conversation([], show_tips_v, show_raghl_v, show_pii_hl_v),
# create_risk_display(0),
# "",
# "",
# demo_prompt,
# mdl,
# eps_slider_val,
# eps_is_inf,
# )
def _persona_hint_html(scenario_mode, persona):
"""Return an HTML hint banner for the given persona, or '' for 'real' mode."""
if scenario_mode == "real" or not persona.get("description"):
return (
''
'💡 Tip: Describe what you need help with. '
'For example: "I am looking for job suggestions in my field" '
'or "What health screenings should I consider?"'
'
'
)
attrs = persona.get("attributes", {})
attr_str = ", ".join(f"{k}: {v}" for k, v in attrs.items())
return (
''
f'🎭 Your persona: {persona["description"]}
'
# f'Profile — {attr_str}'
'
'
)
def _apply_query_params(request: gr.Request):
qp = getattr(request, "query_params", {}) or {}
access_token = qp.get("token", "")
# ★ SIMPLE: If invalid token, inject CSS to hide everything + show error
if not validate_access_token(access_token):
hide_all_css = """
🔒
Access Denied
Invalid or missing access token.
Please contact the administrator for a valid access link.
"""
# Return CSS that blocks everything
return (
hide_all_css, # 1. access_control_css
gr.update(visible=False), # 2. legend_html
gr.update(visible=False), # 3. settings_panel
gr.update(visible=False), # 4. risk_col
False, # 5. rag_st
1.0, # 6. eps_st
DEFAULT_MODEL_NAME, # 7. mdl_st
False, # 8. tips_st
False, # 9. raghl_st
False, # 10. piihl_st
False, # 11. riskpanel_st
False, # 12. demo_st
"", # 13. demo_prompt_st
"", # 14. conv_html
"", # 15. risk_html
"", # 16. warn_html
"", # 17. analysis_md
"", # 18. user_tb
DEFAULT_MODEL_NAME, # 19. model_dropdown
1.0, # 20. epsilon_slider
False, # 21. eps_inf_checkbox
False, # 22. social_scraping_checkbox
gr.update(visible=False), # 23. social_platforms
False, # 24. social_scraping_st
"", # 25. corpus_status
False, # 26. custom_corpus_loaded_st
gr.update(visible=False), # 27. social_scraping_group
gr.update(visible=False), # 28. upload_data_group
"", # 29. persona_hint
gr.update(visible=False), # 30. legend_group
ConversationState(), # 31. state_st – fresh per-session state
"", # 32. privacy_settings_html
gr.update(visible=False), # 33. privacy_settings_group
1, # 34. show_dp_st
1, # 35. show_infr_st
False, # 36. conv_ended_st
gr.update(visible=False), # 37. infer_card_html
_GLOBAL_RETRIEVER, # 38. vs_st – reset to system retriever
gr.update(visible=False), # 39. risk_title
gr.update(visible=False), # 40. analysis_title
)
# ★ Valid token - clear CSS, show normal UI
mdl = str(qp.get("model", model_default))
use_rag = _to_bool(qp.get("rag", use_rag_default))
eps = _to_float(qp.get("epsilon", eps_default), default=eps_default)
show_risk_v = _to_bool(qp.get("show_risk", show_risk_default))
show_tips_v = _to_bool(qp.get("show_tips", show_tips_default))
show_raghl_v = _to_bool(qp.get("show_rag_highlights", show_raghl_default))
# show_pii_hl_v = _to_bool(qp.get("highlight_pii", show_pii_hl_default))
show_pii_hl_v = _to_bool(qp.get("show_pii_highlights", show_pii_hl_default))
show_settings_v = _to_bool(qp.get("show_settings", show_settings_default))
demo_v = _to_bool(qp.get("demo", demo_default))
social_scraping_v = _to_bool(qp.get("enable_social_scraping", social_scraping_default))
show_social_scraping_v = _to_bool(qp.get("show_social_scraping", show_social_scraping_default))
show_upload_data_v = _to_bool(qp.get("show_upload_data", show_upload_data_default))
scenario_mode_v = str(qp.get("scenario_mode", scenario_mode_default)).strip().lower()
# Resolve the system retriever for this session's scenario.
# If the user later uploads their own CSV, vs_st will be overwritten
# with the user-uploaded retriever for their session only.
scenario_vs = _get_system_retriever_for_scenario(scenario_mode_v)
# New URL params
show_dp_v = int(_to_float(qp.get("show_dp", show_dp_default), default=1.0))
show_infr_v = int(_to_float(qp.get("show_infr_attr_card", show_infr_default), default=1.0))
persona = get_persona(scenario_mode_v)
# Create a fresh per-session ConversationState and stamp scenario/persona
# on it. This gives every browser session its own isolated state.
fresh_state = ConversationState()
fresh_state._scenario_mode = scenario_mode_v
fresh_state._persona_attributes = persona
fresh_state._show_dp = show_dp_v
fresh_state._show_infr_attr_card = show_infr_v
fresh_state._show_social_scraping = show_social_scraping_v
fresh_state._show_upload_data = show_upload_data_v
fresh_state._rag_corpus_path = SCENARIO_RETRIEVER_PATHS.get(scenario_mode_v) or ""
# Use persona description as the textbox placeholder (or fall back to DEFAULT_USER_TEXT)
demo_prompt = persona["description"] or (DEFAULT_USER_TEXT if DEFAULT_USER_TEXT else "")
# demo_prompt = DEFAULT_USER_TEXT if DEFAULT_USER_TEXT else ""
legend = create_legend_html(show_tips=show_tips_v, show_rag_hl=show_raghl_v)
eps_is_inf = (eps == float('inf'))
eps_slider_val = min(eps, MAX_POSSIBLE_EPS) if not eps_is_inf else MAX_POSSIBLE_EPS
hint_html = _persona_hint_html(scenario_mode_v, persona)
# Initialize social scraper with URL parameter
initialize_social_scraper(enabled=social_scraping_v)
# show_dp=2 means show Privacy Settings at the start (before any turn)
privacy_html_initial = _build_privacy_settings_html(eps) if show_dp_v == 2 else ""
privacy_visible_initial = (show_dp_v == 2)
return (
"", # 1. access_control_css - clear it
legend, # 2. legend_html
gr.update(visible=show_settings_v), # 3. settings_panel
gr.update(visible=show_risk_v or show_pii_hl_v or (show_infr_v != 0)), # 4. risk_col
use_rag, # 5. rag_st
eps, # 6. eps_st
mdl, # 7. mdl_st
show_tips_v, # 8. tips_st
show_raghl_v, # 9. raghl_st
show_pii_hl_v, # 10. piihl_st
show_risk_v, # 11. riskpanel_st
demo_v, # 12. demo_st
demo_prompt, # 13. demo_prompt_st
fmt_conversation([], show_tips_v, show_raghl_v, show_pii_hl_v), # 14. conv_html
gr.update(value="", visible=bool(show_risk_v)), # 15. risk_html — empty until first turn, visibility fixed at load
"", # 16. warn_html
gr.update(value="", visible=False), # 17. analysis_md — hidden until first turn
"", # 18. user_tb
mdl, # 19. model_dropdown
eps_slider_val, # 20. epsilon_slider
eps_is_inf, # 21. eps_inf_checkbox
social_scraping_v, # 22. social_scraping_checkbox
gr.update(visible=social_scraping_v), # 23. social_platforms
social_scraping_v, # 24. social_scraping_st
"", # 25. corpus_status
False, # 26. custom_corpus_loaded_st
gr.update(visible=show_social_scraping_v), # 27. social_scraping_group
gr.update(visible=show_upload_data_v), # 28. upload_data_group
hint_html, # 29. persona_hint
gr.update(visible=False), # 30. legend_group
fresh_state, # 31. state_st – fresh per-session state
gr.update(value=privacy_html_initial), # 32. privacy_settings_html (value only)
gr.update(visible=privacy_visible_initial), # 33. privacy_settings_group (visibility)
show_dp_v, # 34. show_dp_st
show_infr_v, # 35. show_infr_st
False, # 36. conv_ended_st
gr.update(visible=False), # 37. infer_card_html
scenario_vs, # 38. vs_st
gr.update(value="", visible=bool(show_risk_v)), # 39. risk_title — empty until first turn, visibility fixed at load
gr.update(visible=False), # 40. analysis_title — hidden until first turn
)
app.load(
fn=_apply_query_params,
inputs=None,
outputs=[
access_control_css,
legend_html,
settings_panel,
risk_col,
rag_st,
eps_st,
mdl_st,
tips_st,
raghl_st,
piihl_st,
riskpanel_st,
demo_st,
demo_prompt_st,
conv_html,
risk_html,
warn_html,
analysis_md,
user_tb,
model_dropdown,
epsilon_slider,
eps_inf_checkbox,
social_scraping_checkbox,
social_platforms,
social_scraping_st,
corpus_status,
custom_corpus_loaded_st,
social_scraping_group,
upload_data_group,
persona_hint,
legend_group,
state_st, # ← fresh per-session ConversationState
privacy_settings_html,
privacy_settings_group,
show_dp_st,
show_infr_st,
conv_ended_st,
infer_card_html,
vs_st,
risk_title,
analysis_title,
],
)
# Update epsilon when slider or checkbox changes
def _update_epsilon_from_checkbox(slider_val, is_inf):
"""Called when checkbox changes: respect checkbox state."""
if is_inf:
return float('inf'), gr.update()
return slider_val, gr.update()
def _update_epsilon_from_slider(slider_val):
"""Called when slider moves: always enable DP (uncheck the Disable checkbox)."""
return slider_val, gr.update(value=False)
epsilon_slider.input(
fn=_update_epsilon_from_slider,
inputs=[epsilon_slider],
outputs=[eps_st, eps_inf_checkbox]
)
eps_inf_checkbox.input(
fn=_update_epsilon_from_checkbox,
inputs=[epsilon_slider, eps_inf_checkbox],
outputs=[eps_st, eps_inf_checkbox],
)
# Update model when dropdown changes
model_dropdown.change(
fn=lambda m: m,
inputs=model_dropdown,
outputs=mdl_st
)
# Toggle social platforms visibility when scraping checkbox changes
def _toggle_social_platforms(enabled):
return gr.update(visible=enabled), enabled
social_scraping_checkbox.change(
fn=_toggle_social_platforms,
inputs=social_scraping_checkbox,
outputs=[social_platforms, social_scraping_st]
)
# Handle corpus file upload with progress tracking
def _upload_corpus(file_path, current_vs, state, progress=gr.Progress()):
if file_path is None:
yield "❌ No file uploaded", False, current_vs, None, state
return
try:
# Immediate status update
# ── File type guard: reject non-CSV files immediately ────────────
ext = os.path.splitext(file_path)[-1].lower()
if ext not in (".csv", ".tsv", ".txt"):
yield (
""
"❌ Unsupported file type: "
f"{ext or '(no extension)'}
"
"Please upload a CSV file "
"with at least a text column. "
"Other formats (PDF, DOCX, images, etc.) are not supported."
"
"
), False, current_vs, None, state
return
# Immediate status update
yield "", False, current_vs, None, state
progress(0, desc="Starting upload...")
logger.info(f"Attempting to upload corpus from: {file_path}")
yield "", False, current_vs, None, state
progress(0.2, desc="Reading CSV file...")
time.sleep(0.1) # Brief pause to ensure UI updates
new_retriever = build_retriever_from_csv(file_path, progress)
if new_retriever is None:
yield "❌ **Failed to build retriever** from uploaded file", False, current_vs, None, state
return
time.sleep(0.1) # Brief pause to ensure UI updates
yield "", False, current_vs, None, state
progress(0.9, desc="Finalizing...")
time.sleep(1) # Brief pause to ensure UI updates
# Update the global retriever
# set_retriever(new_retriever)
state._uploaded_file_path = file_path
progress(1.0, desc="Complete!")
time.sleep(1) # Brief pause to ensure UI updates
yield "✅ Custom data loaded successfully!
System is now ready to use.
", True, new_retriever, file_path, state
except Exception as e:
logger.error(f"Error uploading corpus: {str(e)}")
yield f"❌ Error:
{str(e)}
", False, current_vs, None, state
corpus_upload_btn.click(
fn=_upload_corpus,
inputs=[corpus_file, vs_st, state_st],
outputs=[corpus_status, custom_corpus_loaded_st, vs_st, uploaded_file_path_st, state_st]
)
# Send message
def _send(msg, rag_v, eps_v, mdl_v, tips_v, raghl_v, pii_v, demo_v,
vs_v, social_scraping_v, social_platforms_v, uploaded_file_path_v,
show_dp_v, show_infr_v, state, request: gr.Request):
# Stamp session metadata on first (and every) call so it stays current
qp = getattr(request, "query_params", {}) or {}
ip = (
getattr(request, "client", None) and request.client.host
or qp.get("x-forwarded-for", "")
or "unknown"
)
state._session_source = ip
state._access_token = qp.get("token", "")
state._show_rag_highlights = raghl_v
state._show_tips = tips_v
state._show_pii_hl = pii_v
state._uploaded_file_path = uploaded_file_path_v
state._show_risk = _to_bool(qp.get("show_risk", True))
state._show_settings = _to_bool(qp.get("show_settings", False))
if demo_v:
result = process_demo_message(msg, tips_v, raghl_v, pii_v, state, rag_v)
else:
result = process_message(msg, rag_v, eps_v, mdl_v, tips_v, raghl_v,
pii_v, state, vs_v, social_scraping_v, social_platforms_v)
# Reveal panels now that a response exists, respecting each flag
# result[1] is the risk display value; None signals an LLM error.
llm_errored = (result[1] is None)
show_risk_now = getattr(state, "_show_risk", False) and not llm_errored
show_legend_now = (raghl_v or pii_v) and not llm_errored
# ── Privacy Settings panel visibility (show_dp) ───────────────
# 0=never, 1=after each turn, 2=beginning only (no update after turns),
# 3=on End button (hide during turns)
if llm_errored or show_dp_v == 0 or show_dp_v == 2 or show_dp_v == 3:
privacy_html_update = gr.update()
privacy_group_update = gr.update(visible=False)
else:
privacy_html_update = gr.update(value=result[4])
privacy_group_update = gr.update(visible=True)
# ── Inference card visibility (show_infr_attr_card) ───────────
# 0=never, 1=after each turn, 2=on End button
avatar_html = getattr(state, "_last_avatar_html", "")
if llm_errored or show_infr_v == 0 or show_infr_v == 2:
infer_card_update = gr.update(visible=False)
else: # show_infr_v == 1: show after each turn
infer_card_update = gr.update(value=avatar_html) # node already visible, just set content
# ── Per-card visibility ───────────────────────────────────────────
risk_html_val = result[1] if not llm_errored else ""
show_pii_now = bool(pii_v) and not llm_errored
# analysis_title is shown when at least one of its child cards is visible this turn
infer_visible_now = (not llm_errored) and show_infr_v == 1 and bool(avatar_html)
show_analysis_title_now = infer_visible_now or show_pii_now
return (
result[0], # conv_html
gr.update(value=(risk_html_val if show_risk_now else "")), # risk_html (value + visibility)
result[2], # warn_html
gr.update(value=result[3], visible=show_pii_now), # analysis_md (value + visibility)
privacy_html_update, # privacy_settings_html
privacy_group_update, # privacy_settings_group
gr.update(), # risk_col — visibility decided once at load; do not re-toggle the parent
gr.update(visible=show_legend_now), # legend_group
infer_card_update, # infer_card_html
gr.update(value=("### 🛡️ Privacy Risk" if show_risk_now else "")), # risk_title
gr.update(visible=show_analysis_title_now), # analysis_title
state,
)
send_btn.click(
fn=_send,
inputs=[user_tb, rag_st, eps_st, mdl_st, tips_st, raghl_st, piihl_st,
demo_st, vs_st, social_scraping_st, social_platforms, uploaded_file_path_st,
show_dp_st, show_infr_st, state_st],
outputs=[conv_html, risk_html, warn_html, analysis_md,
privacy_settings_html, privacy_settings_group, risk_col, legend_group, infer_card_html,
risk_title, analysis_title, state_st],
).then(fn=lambda: "", outputs=user_tb)
user_tb.submit(
fn=_send,
inputs=[user_tb, rag_st, eps_st, mdl_st, tips_st, raghl_st, piihl_st,
demo_st, vs_st, social_scraping_st, social_platforms, uploaded_file_path_st,
show_dp_st, show_infr_st, state_st],
outputs=[conv_html, risk_html, warn_html, analysis_md,
privacy_settings_html, privacy_settings_group, risk_col, legend_group, infer_card_html,
risk_title, analysis_title, state_st],
).then(fn=lambda: "", outputs=user_tb)
# clear_btn.click(
# fn=lambda tv, rv, pv: clear_conversation(state, tv, rv, pv),
# inputs=[tips_st, raghl_st, piihl_st],
# outputs=[conv_html, risk_html, warn_html, analysis_md, privacy_settings_html, risk_col, legend_group],
# )
# End conversation button – disables input, reveals panels per mode
def _end_conversation(show_dp_v, show_infr_v, state):
privacy_html = getattr(state, "_last_privacy_html", "")
avatar_html = getattr(state, "_last_avatar_html", "")
append_session_end_log(state)
# Privacy Settings panel: show only if show_dp == 3
if show_dp_v == 3 and privacy_html:
privacy_html_update = gr.update(value=privacy_html)
privacy_group_update = gr.update(visible=True)
else:
privacy_html_update = gr.update()
privacy_group_update = gr.update()
# Inference card: show only if show_infr == 2
if show_infr_v == 2 and avatar_html:
infer_update = gr.update(value=avatar_html, visible=True)
title_update = gr.update(visible=True)
else:
infer_update = gr.update()
title_update = gr.update()
return (
gr.update(interactive=False), # send_btn
gr.update(interactive=False), # reset_btn
gr.update(interactive=False), # end_btn
gr.update(interactive=False), # user_tb
privacy_html_update,
privacy_group_update,
infer_update,
title_update, # analysis_title
True,
)
end_btn.click(
fn=_end_conversation,
inputs=[show_dp_st, show_infr_st, state_st],
outputs=[send_btn, reset_btn, end_btn, user_tb,
privacy_settings_html, privacy_settings_group, infer_card_html, analysis_title, conv_ended_st],
)
def _reset(tv, rv, pv, state):
state.clear()
return (
fmt_conversation([], tv, rv, pv),
gr.update(value=create_risk_display(0), visible=False), # risk_html — hidden
"",
gr.update(value="", visible=False), # analysis_md — hidden
"",
gr.update(value=""), # privacy_settings_html
gr.update(visible=False), # privacy_settings_group
gr.update(visible=True), # risk_col — always visible container
gr.update(visible=False), # legend_group hidden
gr.update(value="", visible=False), # infer_card_html hidden
gr.update(visible=False), # risk_title hidden
gr.update(visible=False), # analysis_title hidden
gr.update(interactive=True), # send_btn re-enabled
gr.update(interactive=True), # reset_btn re-enabled
gr.update(interactive=True), # end_btn re-enabled
gr.update(value="", interactive=True), # user_tb re-enabled
False, # conv_ended_st reset
state,
)
reset_btn.click(
fn=_reset,
inputs=[tips_st, raghl_st, piihl_st, state_st],
outputs=[conv_html, risk_html, warn_html, analysis_md, user_tb,
privacy_settings_html, privacy_settings_group, risk_col, legend_group,
infer_card_html, risk_title, analysis_title,
send_btn, reset_btn, end_btn, user_tb,
conv_ended_st, state_st],
)
return app
# ============================================================
# SECTION 17 – MAIN ENTRY POINT
# ============================================================
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--model", default=DEFAULT_MODEL_NAME)
p.add_argument("--rag", default="1")
p.add_argument("--epsilon", default="inf")
p.add_argument("--show_risk", default="1")
p.add_argument("--show_tips", default="1")
p.add_argument("--show_rag_highlights", default="1")
p.add_argument("--show_pii_highlights", default="1")
p.add_argument("--show_settings", default="0")
p.add_argument("--demo", default="0")
p.add_argument("--enable_social_scraping", default="0", help="Enable social media scraping (experimental)")
p.add_argument("--show_social_scraping", default="0", help="Show social media scraping option")
p.add_argument("--show_upload_data", default="0", help="Show upload data option")
p.add_argument("--scenario_mode", default="real", help="Scenario mode: 'real' for free-style, or 'persona1', 'persona2', etc.")
p.add_argument("--show_dp", default="1")
p.add_argument("--show_infr_attr_card", default="1")
p.add_argument("--retriever_path", default=None)
p.add_argument("--port", default="7860")
args = p.parse_args()
css = """
/* --- COMPREHENSIVE LIGHT THEME OVERRIDE --- */
:root, body, .gradio-container, .dark, .dark * {
/* Layout Backgrounds */
--body-background-fill: #ffffff !important;
--background-fill-primary: #ffffff !important;
--background-fill-secondary: #f8f9fa !important;
--block-background-fill: #ffffff !important;
--panel-background-fill: #ffffff !important;
/* Text Colors */
--text-color: #000000 !important;
--body-text-color: #000000 !important;
--block-title-text-color: #000000 !important;
--block-label-text-color: #000000 !important;
/* General Inputs */
--input-background-fill: #ffffff !important;
--input-background-fill-focus: #ffffff !important;
--input-text-color: #000000 !important;
--border-color-primary: #aaaaaa !important;
/* Checkboxes & Radios */
--checkbox-background-color: #ffffff !important;
--checkbox-background-color-selected: #4A90D9 !important;
--checkbox-border-color: #aaaaaa !important;
--checkbox-border-color-selected: #4A90D9 !important;
--checkbox-border-color-focus: #4A90D9 !important;
--checkbox-label-background-fill: #ffffff !important;
--checkbox-label-background-fill-selected: #f0f7ff !important;
--checkbox-label-text-color: #000000 !important;
--checkbox-label-text-color-selected: #000000 !important;
--radio-background-color: #ffffff !important;
/* Sliders */
--slider-color: #4A90D9 !important;
/* Buttons */
--button-primary-background-fill: #f97316 !important;
--button-primary-text-color: #ffffff !important;
--button-secondary-background-fill: #ffffff !important;
--button-secondary-text-color: #000000 !important;
color-scheme: light !important;
}
/* Force Info Text to be smaller, italic, and solid black without altering line-height */
.gradio-container .info,
.dark .info,
.checkbox-container .info,
.dark .checkbox-container .info {
font-size: 0.85em !important;
font-style: italic !important;
color: #000000 !important;
opacity: 1 !important;
}
/* Force standard fallback for stubborn elements, EXCLUDING checkboxes/radios */
.dark textarea,
.dark input:not([type="checkbox"]):not([type="radio"]),
.dark select,
.dark .gr-box,
.dark .gr-panel,
.dark input[type="range"] {
background-color: #ffffff !important;
color: #000000 !important;
}
/* Hardcode the checkmark icon just in case Hugging Face wipes it */
.dark input[type="checkbox"]:checked {
background-color: #4A90D9 !important;
border-color: #4A90D9 !important;
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e") !important;
}
/* ------------------------------------------- */
/* Your existing custom styling */
.gradio-container, .gradio-container * { font-size: calc(1em + 0px) !important; }
body, .gradio-container { padding-top: 0 !important; margin-top: 0 !important; }
.main { padding-top: 0 !important; }
.contain { padding-top: 0 !important; }
textarea, input[type="text"], input[type="number"], select,
.gr-box, .gr-group, .gr-panel, .block.gr-group, .gr-input,
.gr-text-input, .input-text, .border, div[class*="border"] {
border-width: 2px !important;
border-style: solid !important;
border-color: #aaaaaa !important;
}
textarea:focus, input:focus, select:focus {
border-color: #4A90D9 !important;
border-width: 2.5px !important;
}
button { border-width: 2px !important; border-style: solid !important; }
.msg-ai {
background: #f5f5f5;
margin-right: 18%;
line-height: 1.5;
white-space: pre-wrap;
color: #000000 !important;
}
.chat-box { padding-left: 0px; padding-top: 0px; padding-right: 0px; }
/* Corpus status box styling */
#corpus_status, .corpus-status-box {
min-height: 60px !important;
padding: 8px !important;
margin: 8px 0 !important;
font-size: 1.05em !important;
line-height: 1.4 !important;
background-color: #ffffff !important;
color: #000000 !important;
}
/* Force Privacy Settings panel to white background */
#privacy_settings_panel,
.dark #privacy_settings_panel,
#privacy_settings_panel > div,
.dark #privacy_settings_panel > div {
background: #ffffff !important;
background-color: #ffffff !important;
color: #000000 !important;
}
"""
js = """
"""
# Initialize LLM clients
initialize_llm_clients()
# Load retriever from pickle (generic corpus)
# retriever_path = args.retriever_path or RETRIEVAL_PICKLE_PATH
# load_retriever(retriever_path)
load_retriever(args.retriever_path or DEFAULT_RETRIEVAL_PICKLE_PATH)
for _scenario_key in SCENARIO_RETRIEVER_PATHS:
get_scenario_retriever(_scenario_key) # loads and caches each one
app = create_ui(
model=args.model if args.model in [name for _, _, name in MODEL_CONFIGS] else DEFAULT_MODEL_NAME,
rag=args.rag,
epsilon=args.epsilon if str(args.epsilon) == "inf" or float(args.epsilon) > 0 else "inf",
show_risk=args.show_risk,
show_tips=args.show_tips,
show_rag_highlights=args.show_rag_highlights,
show_pii_highlights=args.show_pii_highlights,
show_settings=args.show_settings,
demo=args.demo if str(args.demo) in ["0", "1"] else "0",
enable_social_scraping=args.enable_social_scraping ,
show_social_scraping=args.show_social_scraping,
show_upload_data = args.show_upload_data,
scenario_mode=args.scenario_mode,
show_dp=args.show_dp,
show_infr_attr_card=args.show_infr_attr_card
)
app.queue(max_size=600, default_concurrency_limit=20) # required for concurrent multi-user support
app.launch(
css=css,
head=js,
server_name="0.0.0.0", # Local only - change to "0.0.0.0" for network access
server_port=int(args.port), # Default 7860
share=False, # Keep this as False for local
show_error=True,
ssr_mode=False
)
# Hi! I am Catherine from Jamesfort. I'm looking for inspiration from people who've had to adapt to physical challenges while keeping their career going. I work in a fast-paced environment where mistakes aren't really an option, and sometimes I feel like I'm fighting an uphill battle. Anyone else dealing with similar situations? How do you stay motivated?
# I am Raymond Phillips. I live in the Philippines. I am searching for new job. Do you have any suggestions that would work for me?