File size: 14,367 Bytes
386c921
4e66bcd
 
7cf2014
386c921
 
fe9ebf1
231e179
969519b
adfef13
386c921
f50adf2
 
386c921
e5ff858
df3a179
d7ade7a
df3a179
b57e706
11f5974
26cc62c
7cf2014
26cc62c
 
 
9d0be3c
7cf2014
 
9d0be3c
7cf2014
 
 
 
9d0be3c
23aad06
dc8071d
e5ff858
dc8071d
 
 
 
e5ff858
 
f50adf2
7cf2014
 
9d0be3c
 
 
 
 
 
 
 
 
 
 
7cf2014
9d0be3c
7cf2014
9d0be3c
7cf2014
 
 
 
9d0be3c
 
7cf2014
9d0be3c
 
c0b64b1
ed17923
42eb666
91b6080
231e179
ed17923
 
969519b
ed17923
2edfdfa
969519b
7cf2014
969519b
 
 
3b1954a
781a11b
969519b
3b1954a
969519b
dc8071d
969519b
 
2d6d3d4
969519b
 
7cf2014
f50adf2
 
fa1297d
f50adf2
fa1297d
3cc37d8
adfef13
7cf2014
 
f50adf2
f3b0f37
9d0be3c
7cf2014
9d0be3c
 
 
 
7cf2014
9d0be3c
 
 
7cf2014
 
 
9d0be3c
 
 
 
 
 
 
 
 
 
 
8ab9bfc
969519b
dc8071d
969519b
 
 
7cf2014
dc8071d
f50adf2
969519b
 
f50adf2
06690f0
21cf7f9
adfef13
2d6d3d4
f50adf2
e5ff858
f50adf2
e5ff858
f50adf2
 
 
 
969519b
 
 
f50adf2
969519b
9d0be3c
 
 
 
 
7cf2014
9d0be3c
 
 
 
 
 
 
 
7cf2014
9d0be3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7cf2014
9d0be3c
 
 
 
 
 
 
 
 
 
7cf2014
9d0be3c
 
 
 
 
 
 
d131605
f50adf2
8ab9bfc
f50adf2
 
 
 
7cf2014
f50adf2
f3b0f37
9d0be3c
 
 
 
 
7cf2014
9d0be3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969519b
9d0be3c
 
 
 
 
f50adf2
9d0be3c
 
f50adf2
9d0be3c
 
 
 
 
 
 
 
 
7cf2014
9d0be3c
 
 
 
 
 
 
 
 
 
 
 
969519b
dc8071d
e5ff858
dc8071d
f50adf2
dc8071d
c246c73
f50adf2
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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)