benjamin5607's picture
Update app.py
7cf2014 verified
Raw
History Blame Contribute Delete
14.4 kB
import streamlit as st
import feedparser
import urllib.parse
from groq import Groq, RateLimitError
import pandas as pd
import json
import os
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime, timedelta
# --- 1. Page Config (Mobile Optimized) ---
st.set_page_config(page_title="Omni-Watch T&S Master", page_icon="๐Ÿ›ก๏ธ", layout="wide")
# --- 2. API Key Load ---
api_key = os.environ.get("GROQ_API_KEY")
if not api_key:
try: api_key = st.secrets["GROQ_API_KEY"]
except: pass
if not api_key:
st.error("๐Ÿ”‘ GROQ_API_KEY Missing. Please check your deployment settings.")
st.stop()
client = Groq(api_key=api_key)
# ๐Ÿšจ [CRITICAL] Robust Model Hierarchy (Auto-Switching)
# 429 ์—๋Ÿฌ๋‚˜ 400(๋ชจ๋ธ ์ข…๋ฃŒ) ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ, ์ž๋™์œผ๋กœ ๋‹ค์Œ ์ˆœ์œ„ ๋ชจ๋ธ๋กœ ์ „ํ™˜ํ•˜์—ฌ ์ค‘๋‹จ ์—†์ด ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.
MODEL_HIERARCHY = [
"llama-3.3-70b-versatile", # 1์ˆœ์œ„: ์ตœ์‹  SOTA ๋ชจ๋ธ
"llama-3.1-70b-versatile", # 2์ˆœ์œ„: ๊ณ ์„ฑ๋Šฅ ๋ฐฑ์—…
"mixtral-8x7b-32768", # 3์ˆœ์œ„: ์•ˆ์ •์„ฑ ์œ„์ฃผ
"llama-3.1-8b-instant" # 4์ˆœ์œ„: ์ดˆ๊ณ ์† ๋น„์ƒ์šฉ
]
# --- 3. Configuration ---
REGION_MAP = {
"Asia & Pacific": ["KR", "JP", "CN", "VN", "IN", "AU"],
"Europe": ["GB", "FR", "DE", "UA", "RU"],
"Americas": ["US", "CA", "BR", "MX"],
"ME & Africa": ["IL", "SA", "AE", "TR"]
}
# --- 4. Core Functions ---
# [Smart AI Wrapper] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ๋ชจ๋ธ ์ž๋™ ์ „ํ™˜ ๋กœ์ง
def get_ai_response(system_msg, user_content, json_mode=True):
for model in MODEL_HIERARCHY:
try:
kwargs = {
"model": model,
"messages": [{"role": "system", "content": system_msg}, {"role": "user", "content": user_content}],
}
if json_mode:
kwargs["response_format"] = {"type": "json_object"}
res = client.chat.completions.create(**kwargs)
return res
except RateLimitError:
continue # ํ•œ๋„ ์ดˆ๊ณผ ์‹œ ๋‹ค์Œ ๋ชจ๋ธ ์‹œ๋„
except Exception as e:
# ๋ชจ๋ธ ์ข…๋ฃŒ(400)๋‚˜ ์ฐพ์„ ์ˆ˜ ์—†์Œ(404) ์—๋Ÿฌ ์‹œ์—๋„ ๋‹ค์Œ ๋ชจ๋ธ ์‹œ๋„
if "model_decommissioned" in str(e) or "404" in str(e) or "400" in str(e):
continue
st.error(f"โš ๏ธ Error with {model}: {e}") # ๊ทธ ์™ธ ์น˜๋ช…์  ์—๋Ÿฌ๋Š” ์ถœ๋ ฅ
return None
st.error("๐Ÿšจ All AI models are currently unavailable. Please check API Status.")
return None
def fetch_extensive_news(query, geo="US", limit=60, period="7d"):
time_filter = f" when:{period}"
encoded = urllib.parse.quote(query + time_filter)
url = f"https://news.google.com/rss/search?q={encoded}&hl=en&gl={geo}&ceid={geo}:en"
feed = feedparser.parse(url)
articles = []
for e in feed.entries[:limit]:
articles.append({"title": e.title, "source": e.source.title if 'source' in e else "G-News", "link": e.link})
return articles
def render_gauge(score, title):
# ์ƒ์„ธ ๋ถ„์„์šฉ ๊ฒŒ์ด์ง€ ์ฐจํŠธ (์›Œ๋ฃธ ๋ฏธ์‚ฌ์šฉ)
fig = go.Figure(go.Indicator(
mode = "gauge+number",
value = score,
domain = {'x': [0, 1], 'y': [0, 1]},
title = {'text': title, 'font': {'size': 18, 'color': "#FF4B4B"}},
gauge = {
'axis': {'range': [0, 100], 'tickwidth': 1},
'bar': {'color': "#FF4B4B"},
'steps': [{'range': [0, 100], 'color': "#ffebee"}],
}
))
fig.update_layout(height=180, margin=dict(l=10, r=10, t=40, b=10), paper_bgcolor="rgba(0,0,0,0)")
return fig
# --- 5. ๐Ÿšจ GLOBAL WAR ROOM (Policy Focused, Numeric Only) ---
st.title("๐Ÿšจ OMNI-WATCH: T&S WAR ROOM")
st.caption(f"Policy Risk Monitoring: {datetime.now().strftime('%H:%M:%S')} UTC")
if st.button("๐Ÿ”„ Refresh"):
if 'hot_issues' in st.session_state: del st.session_state.hot_issues
if 'hot_issues' not in st.session_state:
with st.spinner("Scanning 12h Global Feeds for Violations..."):
# ๊ฒ€์ƒ‰์–ด: T&S ์œ„๋ฐ˜ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์€ ํ‚ค์›Œ๋“œ
raw_news = fetch_extensive_news("violence OR hate speech OR disinformation OR scandal OR protest", limit=40, period="12h")
if not raw_news:
st.warning("No critical incidents found in the last 12h.")
st.session_state.hot_issues = []
else:
news_context = "\n".join([f"Event: {n['title']}" for n in raw_news])
# PROMPT: ์‚ฌ๊ฑด ๊ฐœ์š”์™€ ์œ„๋ฐ˜ ์‚ฌํ•ญ์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„
system_msg = """
Identify TOP 3 incidents with highest 'Trust & Safety' risk.
The 'summary' array MUST follow this order:
1. "Event: [Brief summary of what happened]"
2. "Violation: [Specific Community Guideline breached]"
3. "Risk: [Potential offline harm]"
Return ONLY JSON:
{"issues": [{"title": "Short Title", "score": 85, "summary": ["Event: ...", "Violation: ...", "Risk: ..."], "link": ".."}]}
"""
res = get_ai_response(system_msg, news_context)
if res:
try:
st.session_state.hot_issues = json.loads(res.choices[0].message.content).get('issues', [])[:3]
except:
st.session_state.hot_issues = []
if st.session_state.hot_issues:
cols = st.columns(3)
for i, issue in enumerate(st.session_state.hot_issues):
with cols[i]:
with st.container(border=True):
# UI: ๊ฒŒ์ด์ง€ ๋Œ€์‹  ํฐ ์ˆซ์ž ์‚ฌ์šฉ (๋ชจ๋ฐ”์ผ ๊ฐ€๋…์„ฑ)
st.markdown(f"<h1 style='text-align: center; color: #FF4B4B; margin: 0;'>{issue.get('score', 50)}</h1>", unsafe_allow_html=True)
st.markdown("<p style='text-align: center; color: gray; font-size: 0.8em;'>Safety Risk Index</p>", unsafe_allow_html=True)
st.error(f"**{issue.get('title')}**")
for line in issue.get('summary', []): st.caption(f"โ€ข {line}")
st.markdown(f"[๐Ÿ”— Link]({issue.get('link')})")
st.divider()
# --- 6. Strategic Tabs ---
tab1, tab2, tab3 = st.tabs(["๐ŸŒ GLOBAL POLICY SCAN", "๐Ÿ” NATIONAL T&S FORENSICS", "๐Ÿ“ˆ RISK VELOCITY"])
# --- [Tab 1: Strategic Global Scan] ---
with tab1:
st.header("Strategic Policy & Impact Briefing")
keyword = st.text_input("Risk Category", "Election Integrity")
if st.button("Analyze Policy Impact", type="primary"):
with st.status("Auditing Global Content Compliance (20+ Sources)...", expanded=True):
all_news = []
for reg in REGION_MAP:
for geo in REGION_MAP[reg][:2]:
all_news.extend(fetch_extensive_news(keyword, geo=geo, limit=6, period="7d"))
if not all_news:
st.error("No news found for this keyword.")
else:
news_summary = "\n".join([n['title'] for n in all_news[:45]])
# PROMPT: ์‚ฌ๊ฑด ๊ฐœ์š”(Summary) ํ•„์ˆ˜ ํฌํ•จ
global_prompt = f"""
Analyze '{keyword}' focusing on 'Community Guidelines' and 'Social Impact'.
1. Executive Summary: Start with a clear **Event Summary** of what happened. Then, analyze the systemic policy risks and societal harm. (300+ words).
2. Risk Landscape: Map findings to specific violations.
Return ONLY JSON:
{{
"executive_summary": "Start with [The Incident Details], then move to [Policy Analysis].",
"risk_landscape": [
{{"Component": "Primary Violation", "Findings": "...", "Risk_Level": "High"}},
{{"Component": "Vulnerable Target", "Findings": "...", "Risk_Level": "Critical"}},
{{"Component": "Offline Harm", "Findings": "...", "Risk_Level": "High"}},
{{"Component": "Enforcement Gap", "Findings": "...", "Risk_Level": "Medium"}}
],
"platform_intelligence": [
{{"Platform": "TikTok", "Assessment": "...", "Strategy": "..."}},
{{"Platform": "YouTube", "Assessment": "...", "Strategy": "..."}},
{{"Platform": "Meta", "Assessment": "...", "Strategy": "..."}},
{{"Platform": "X", "Assessment": "...", "Strategy": "..."}}
],
"strategic_conclusion": "Final Trust & Safety recommendation."
}}
"""
res = get_ai_response(global_prompt, news_summary)
if res:
g_data = json.loads(res.choices[0].message.content)
with st.container(border=True):
st.subheader("1. Policy Impact Executive Summary")
# ๊ฐ€๋…์„ฑ์„ ์œ„ํ•œ ์ค„๋ฐ”๊ฟˆ ์ฒ˜๋ฆฌ
st.markdown(g_data.get('executive_summary').replace(". ", ".\n\n"))
st.subheader("2. Guideline Violation Matrix")
st.dataframe(pd.DataFrame(g_data.get('risk_landscape')), hide_index=True, use_container_width=True)
st.subheader("3. Platform Enforcement Strategy")
st.table(pd.DataFrame(g_data.get('platform_intelligence')))
st.success(f"**T&S Recommendation:** {g_data.get('strategic_conclusion')}")
# DOWNLOAD: Markdown Format (๋ชจ๋ฐ”์ผ ํ˜ธํ™˜)
report_md = f"# OMNI-WATCH POLICY REPORT: {keyword.upper()}\n\n"
report_md += f"## 1. EXECUTIVE SUMMARY\n{g_data.get('executive_summary')}\n\n"
report_md += "## 2. VIOLATION LANDSCAPE\n"
for item in g_data.get('risk_landscape', []):
report_md += f"- **{item['Component']}**: {item['Findings']} ({item['Risk_Level']})\n"
st.download_button("๐Ÿ“ฅ Download Policy Report (.md)", report_md, f"Policy_Intel_{keyword}.md")
# --- [Tab 2: Tactical Forensics] ---
with tab2:
st.header("National T&S Forensics")
target_geo = st.text_input("ISO Code", "US").upper()
if st.button("Analyze Violations"):
with st.status(f"Scanning {target_geo} for Policy Breaches (20+ Sources)...", expanded=True):
# ๊ฒ€์ƒ‰์–ด ์ตœ์ ํ™”: ์‹ค์ œ ์‚ฌ๊ฑด/์‚ฌ๊ณ  ์œ„์ฃผ
news = fetch_extensive_news(f"{target_geo} controversy OR scandal OR protest OR violence", geo=target_geo, limit=40, period="7d")
if not news:
st.error(f"No recent controversy news found for {target_geo}.")
else:
news_titles = "\n".join([n['title'] for n in news])
# PROMPT: Incident -> Policy Analysis ๊ตฌ์กฐ ๊ฐ•์ œ
system_prompt = f"""
Analyze Top 5 Risks in {target_geo} strictly through a 'Community Guidelines' lens.
Summary MUST start with **"The Incident:"** (What happened) followed by **"Policy Analysis:"** (Why it violates rules).
Return ONLY JSON:
{{
"risks": [
{{
"rank": 1, "title": "Event Title", "score": 90,
"summary": "1. The Incident: ... \n2. Policy Analysis: ...",
"forensic_grid": {{
"Guideline_Breached": "e.g. Dangerous Organizations Policy",
"Victim_Demographics": "e.g. Teenagers / Ethnic Minorities",
"Societal_Impact": "e.g. Incitement to Violence",
"Enforcement_Action": "e.g. Geo-blocking / Account Ban"
}}
}}
]
}}
"""
res = get_ai_response(system_prompt, news_titles)
if res:
report_data = json.loads(res.choices[0].message.content).get('risks', [])
full_report_md = f"# NATIONAL T&S FORENSICS: {target_geo}\n\n"
for i, r in enumerate(report_data):
full_report_md += f"## {r.get('rank')}. {r.get('title')} (Risk: {r.get('score')})\n"
full_report_md += f"{r.get('summary')}\n\n"
with st.expander(f"๐Ÿšฉ RISK {r.get('rank')}: {r.get('title')} (Score: {r.get('score')})", expanded=True):
c1, c2 = st.columns([1, 4])
with c1:
# ์ƒ์„ธ ๋ถ„์„ ํƒญ์—์„œ๋Š” ๊ฒŒ์ด์ง€ ์ฐจํŠธ ์‚ฌ์šฉ (Key ์ค‘๋ณต ๋ฐฉ์ง€ ์ ์šฉ)
st.plotly_chart(render_gauge(r.get('score'), "Risk Index"), use_container_width=True, key=f"fg_{i}")
with c2:
st.markdown("**Incident & Policy Analysis:**")
st.markdown(r.get('summary').replace(". ", ".\n\n"))
st.table(pd.DataFrame(r.get('forensic_grid', {}).items(), columns=["T&S Component", "Assessment"]))
st.download_button("๐Ÿ“ฅ Download Forensic Report (.md)", full_report_md, f"T&S_Forensics_{target_geo}.md")
st.divider()
st.caption(f"Evidence Base: {len(news)} articles")
st.dataframe(pd.DataFrame(news)[['title', 'source']], use_container_width=True)
# --- [Tab 3: Velocity] ---
with tab3:
st.header("Risk Velocity")
trend_key = st.text_input("Violation Type", "Hate Speech")
if st.button("Check Trend"):
dates = [(datetime.now() - timedelta(days=i)).strftime("%m-%d") for i in range(6, -1, -1)]
fig = px.area(x=dates, y=[15, 30, 50, 80, 95, 88, 92], title=f"Violation Surge: {trend_key}")
st.plotly_chart(fig, use_container_width=True)