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"

{issue.get('score', 50)}

", unsafe_allow_html=True) st.markdown("

Safety Risk Index

", 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)