Spaces:
Sleeping
Sleeping
Gamucopia-Creatives
refactor: update submission validator with strict log format checks, HF_TOKEN safety verification, and add FastAPI server for interactive task testing
1191a4e | import sys | |
| import os | |
| # Add parent directory to path so we can import 'envs' and 'inference' | |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from fastapi import FastAPI, HTTPException, Body, Query | |
| from fastapi.responses import HTMLResponse | |
| from pydantic import BaseModel, Field | |
| from typing import Optional, Dict, Any, List | |
| from enum import Enum | |
| from envs.social_stream_moderation.environment import SocialStreamModerationEnv | |
| from envs.social_stream_moderation.models import State, ModerationAction | |
| # Enums for Swagger Dropdowns | |
| class TaskName(str, Enum): | |
| TASK_1 = "Task 1: Basic Safety" | |
| TASK_2 = "Task 2: Context & Nuance" | |
| TASK_3 = "Task 3: Fairness & Bias" | |
| class PolicyModeChoice(str, Enum): | |
| NORMAL = "Standard Moderation" | |
| STRICT = "Strict Enforcement" | |
| LENIENT = "Lenient Privacy" | |
| class UserHistoryChoice(str, Enum): | |
| CLEAN = "Clean History" | |
| REPEATED = "Repeat Offender" | |
| class ContextTypeChoice(str, Enum): | |
| ROOT = "Main Post" | |
| COMMENT = "Comment" | |
| # Mapping UI labels back to backend IDs | |
| TASK_MAP = { | |
| TaskName.TASK_1: "clear_cut_moderation", | |
| TaskName.TASK_2: "nuanced_sarcastic", | |
| TaskName.TASK_3: "policy_fairness" | |
| } | |
| POLICY_MAP = { | |
| PolicyModeChoice.NORMAL: "normal", | |
| PolicyModeChoice.STRICT: "strict", | |
| PolicyModeChoice.LENIENT: "lenient" | |
| } | |
| HISTORY_MAP = { | |
| UserHistoryChoice.CLEAN: "no_prior_violations", | |
| UserHistoryChoice.REPEATED: "prior_violations" | |
| } | |
| CONTEXT_MAP = { | |
| ContextTypeChoice.ROOT: "root_post", | |
| ContextTypeChoice.COMMENT: "comment" | |
| } | |
| # API Metadata for Swagger | |
| TAGS_METADATA = [ | |
| { | |
| "name": "🤖 Automated Benchmarking", | |
| "description": "Autonomous evaluation loop. Sequence: **Reset** -> **Predict & Step** (Repeat). This tracks the official hackathon metrics.", | |
| }, | |
| { | |
| "name": "🧪 Interactive Lab", | |
| "description": "Manual testing endpoints. Perfect for testing specific edge cases with custom inputs and human overrides.", | |
| }, | |
| { | |
| "name": "📊 System Monitoring", | |
| "description": "Real-time state and status tracking for the moderation engine.", | |
| } | |
| ] | |
| app = FastAPI( | |
| title="🛡️ PolicyPulse AI | Intelligence Center", | |
| description=""" | |
| ### Evaluation Guide for Hackathon Judges: | |
| 1. **Automated Testing:** Use `[POST] /reset` then `[POST] /predict_and_step`. | |
| 2. **Fairness Testing (Task 3):** Start an episode with `task_name='policy_fairness'`. | |
| 3. **Internal Logic:** Use `[POST] /evaluate` to see the model's reasoning without advancing the environment. | |
| """, | |
| version="1.2.0", | |
| openapi_tags=TAGS_METADATA | |
| ) | |
| env = SocialStreamModerationEnv() | |
| class ResetRequest(BaseModel): | |
| task_name: TaskName = Field(TaskName.TASK_1, description="Select the benchmark level to initialize.") | |
| seed: Optional[int] = Field(42, description="Reproducibility seed for dataset sampling.") | |
| class EvaluateRequest(BaseModel): | |
| text: str = Field("I will kill you", description="The user content string to analyze.") | |
| api_base_url: Optional[str] = Field(None, description="Optional override. If blank, defaults to server's API_BASE_URL config.") | |
| model_name: Optional[str] = Field(None, description="Optional override. If blank, defaults to server's MODEL_NAME config.") | |
| api_key: Optional[str] = Field(None, description="Optional override. If blank, defaults to server's HF_TOKEN config.") | |
| class LLMConfigRequest(BaseModel): | |
| api_base_url: Optional[str] = Field(None, description="Optional override. If blank, defaults to server's API_BASE_URL config.") | |
| model_name: Optional[str] = Field(None, description="Optional override. If blank, defaults to server's MODEL_NAME config.") | |
| api_key: Optional[str] = Field(None, description="Optional override. If blank, defaults to server's HF_TOKEN config.") | |
| class StepRequest(BaseModel): | |
| action: ModerationAction = Field(ModerationAction.ALLOW, description="The action to apply to the current post.") | |
| def read_root(): | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PolicyPulse AI | Intelligence Center</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=JetBrains+Mono&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #030712; | |
| --sidebar: rgba(15, 23, 42, 0.6); | |
| --accent: #38bdf8; | |
| --danger: #f472b6; | |
| --success: #4ade80; | |
| --text: #f8fafc; | |
| --muted: #94a3b8; | |
| } | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { | |
| font-family:'Outfit', sans-serif; background: #030712; color:var(--text); | |
| height:100vh; overflow:hidden; display:flex; flex-direction:column; | |
| } | |
| main { flex:1; display:grid; grid-template-columns: 400px 1fr; gap:20px; padding:20px; max-height:calc(100vh - 60px); } | |
| header { height:60px; display:flex; align-items:center; justify-content:space-between; padding:0 30px; border-bottom:1px solid rgba(255,255,255,0.05); background:rgba(15, 23, 42, 0.4); } | |
| .logo { font-weight:800; font-size:1.4rem; letter-spacing:-0.03em; color:var(--accent); } | |
| .version { font-size:0.7rem; background:rgba(56, 189, 248, 0.1); padding:4px 10px; border-radius:6px; color:var(--accent); font-weight:600; } | |
| /* Panel Styling */ | |
| .panel { background:var(--sidebar); backdrop-filter:blur(20px); border-radius:24px; border:1px solid rgba(255,255,255,0.06); display:flex; flex-direction:column; overflow:hidden; } | |
| .panel-header { padding:25px; border-bottom:1px solid rgba(255,255,255,0.05); } | |
| .panel-title { font-size:0.9rem; font-weight:800; text-transform:uppercase; letter-spacing:0.05em; display:flex; align-items:center; gap:10px; } | |
| .panel-title::before { content:''; width:3px; height:14px; background:var(--accent); border-radius:10px; } | |
| .panel-content { padding:25px; flex:1; overflow-y:auto; } | |
| /* Tabs */ | |
| .mode-switch { display:flex; background:rgba(0,0,0,0.3); padding:4px; border-radius:12px; margin-bottom:25px; } | |
| .tab { flex:1; padding:10px; text-align:center; cursor:pointer; font-size:0.8rem; font-weight:700; border-radius:8px; transition:0.3s; color:var(--muted); } | |
| .tab.active { background:var(--accent); color:#020617; } | |
| /* Forms */ | |
| .field { margin-bottom:20px; } | |
| label { display:block; font-size:0.65rem; font-weight:700; color:var(--muted); text-transform:uppercase; margin-bottom:8px; } | |
| select, textarea { width:100%; background:rgba(0,0,0,0.4); border:1px solid rgba(255,255,255,0.1); border-radius:12px; padding:12px; color:#fff; font-family:'Outfit'; font-size:0.9rem; transition:0.3s; } | |
| textarea { resize:none; min-height:100px; } | |
| select:focus, textarea:focus { outline:none; border-color:var(--accent); } | |
| /* Buttons */ | |
| .btn { width:100%; padding:16px; border-radius:14px; border:none; font-weight:700; cursor:pointer; transition:0.3s; font-size:0.95rem; display:flex; align-items:center; justify-content:center; gap:10px; } | |
| .btn-primary { background:var(--accent); color:#020617; } | |
| .btn-primary:hover { background:#7dd3fc; transform:translateY(-2px); } | |
| .btn-secondary { background:rgba(255,255,255,0.05); color:#fff; border:1px solid rgba(255,255,255,0.1); margin-top:10px; } | |
| .btn-secondary:hover { background:rgba(255,255,255,0.08); } | |
| .btn:disabled { opacity:0.3; cursor:not-allowed; transform:none !important; } | |
| /* Right Column */ | |
| .stats-bar { display:grid; grid-template-columns: repeat(3, 1fr); gap:15px; margin-bottom:20px; } | |
| .stat-card { background:rgba(255,255,255,0.03); padding:15px; border-radius:16px; border:1px solid rgba(255,255,255,0.05); } | |
| .stat-label { font-size:0.6rem; color:var(--muted); font-weight:700; text-transform:uppercase; } | |
| .stat-value { font-size:1.1rem; font-weight:800; font-family:'JetBrains Mono'; margin-top:5px; color:var(--accent); } | |
| .log-container { background:rgba(0,0,0,0.2); border-radius:20px; border:1px solid rgba(255,255,255,0.05); flex:1; overflow-y:auto; padding:20px; display:flex; flex-direction:column; gap:12px; } | |
| .log-entry { background:rgba(255,255,255,0.02); padding:18px; border-radius:14px; border-left:3px solid var(--accent); animation:fadeIn 0.3s; } | |
| @keyframes fadeIn { from { opacity:0; transform:translateY(5px); } to { opacity:1; transform:translateY(0); } } | |
| .log-meta { display:flex; justify-content:space-between; font-size:0.7rem; color:var(--muted); margin-bottom:8px; font-weight:600; } | |
| .log-text { font-size:0.95rem; line-height:1.4; color:#e2e8f0; } | |
| .log-badge { font-size:0.6rem; font-weight:800; padding:2px 8px; border-radius:4px; text-transform:uppercase; margin-top:10px; display:inline-block; } | |
| /* Footer links icons */ | |
| .footer-links { display:flex; gap:15px; } | |
| .footer-links a { font-size:0.75rem; color:var(--muted); text-decoration:none; transition:0.3s; } | |
| .footer-links a:hover { color:var(--accent); } | |
| .empty-state { margin:auto; text-align:center; color:var(--muted); font-weight:300; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo">POLICYPULSE <span style="font-weight:300">AI</span></div> | |
| <div style="display:flex; align-items:center; gap:20px;"> | |
| <div class="footer-links"> | |
| <a href="/docs">API REFERENCE</a> | |
| <a href="/state">SYSTEM STATUS</a> | |
| </div> | |
| <div class="version">REVISION 1.0</div> | |
| </div> | |
| </header> | |
| <main> | |
| <!-- Left Panel: Orchestration --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <div class="panel-title">Operation Center</div> | |
| </div> | |
| <div class="panel-content"> | |
| <div class="mode-switch"> | |
| <div class="tab active" id="tab-lab">LIVE MODE</div> | |
| <div class="tab" id="tab-auto">GRADER MODE</div> | |
| </div> | |
| <!-- Lab Mode Form --> | |
| <div id="section-lab"> | |
| <div class="field"> | |
| <label>User Content</label> | |
| <textarea id="lab-input" placeholder="Type or paste text to test our agent's moderation logic..."></textarea> | |
| </div> | |
| <div class="field"> | |
| <label>Safety Policy</label> | |
| <select id="lab-policy"> | |
| <option value="NORMAL">Standard Moderation</option> | |
| <option value="STRICT">Strict Enforcement</option> | |
| <option value="LENIENT">Lenient Privacy</option> | |
| </select> | |
| </div> | |
| <div class="field" style="display:grid; grid-template-columns:1fr 1fr; gap:10px;"> | |
| <div> | |
| <label>User History</label> | |
| <select id="lab-history" style="font-size:0.75rem;"> | |
| <option value="no_prior_violations">Clean History</option> | |
| <option value="prior_violations">Repeat Offender</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Context Type</label> | |
| <select id="lab-context" style="font-size:0.75rem;"> | |
| <option value="root_post">Main Post</option> | |
| <option value="comment">Comment</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Auto Mode Form --> | |
| <div id="section-auto" style="display:none;"> | |
| <div class="field"> | |
| <label>Benchmark Level</label> | |
| <select id="auto-task"> | |
| <option value="clear_cut_moderation">Task 1: Basic Safety</option> | |
| <option value="nuanced_sarcastic">Task 2: Context & Nuance</option> | |
| <option value="policy_fairness">Task 3: Fairness & Bias</option> | |
| </select> | |
| </div> | |
| <button class="btn btn-primary" id="btn-auto-reset">START BENCHMARK</button> | |
| <button class="btn btn-secondary" id="btn-auto-step" disabled>PROCESS NEXT ITEM</button> | |
| </div> | |
| <div style="margin-top:20px; padding-top:20px; border-top:1px solid rgba(255,255,255,0.05);"> | |
| <div style="font-size:0.65rem; font-weight:700; color:var(--accent); text-transform:uppercase; margin-bottom:10px;">Optional: Custom LLM Override</div> | |
| <div class="field" style="margin-bottom:15px;"> | |
| <input type="text" id="config-base-url" placeholder="API Base URL (e.g., https://api.openai.com/v1)" style="width:100%; background:rgba(0,0,0,0.4); border:1px solid rgba(255,255,255,0.1); border-radius:8px; padding:10px; color:#fff; font-family:'Outfit'; font-size:0.8rem; margin-bottom:8px;"> | |
| <input type="text" id="config-model" placeholder="Model Name (e.g., gpt-4o-mini)" style="width:100%; background:rgba(0,0,0,0.4); border:1px solid rgba(255,255,255,0.1); border-radius:8px; padding:10px; color:#fff; font-family:'Outfit'; font-size:0.8rem; margin-bottom:8px;"> | |
| <input type="password" id="config-key" placeholder="API Key" style="width:100%; background:rgba(0,0,0,0.4); border:1px solid rgba(255,255,255,0.1); border-radius:8px; padding:10px; color:#fff; font-family:'Outfit'; font-size:0.8rem;"> | |
| </div> | |
| </div> | |
| <button class="btn btn-primary" id="btn-lab-run" style="margin-top:20px">RUN MODERATION</button> | |
| <button class="btn btn-secondary" id="btn-global-clear" style="margin-top:10px">PURGE LOGS</button> | |
| </div> | |
| </div> | |
| <!-- Right Panel: Intelligence Stream --> | |
| <div class="panel" style="background:transparent; border:none; backdrop-filter:none;"> | |
| <div class="stats-bar"> | |
| <div class="stat-card"> | |
| <div class="stat-label">Model Accuracy</div> | |
| <div class="stat-value" id="val-accuracy">--</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Aggregate Reward</div> | |
| <div class="stat-value" id="val-reward">0.000</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">System State</div> | |
| <div class="stat-value" id="val-state" style="color:var(--muted)">IDLE</div> | |
| </div> | |
| </div> | |
| <div class="log-container" id="log-viewport"> | |
| <div class="empty-state" id="empty-hint">Waiting for neural injection... Select a mode to begin.</div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| // Elements | |
| const tabs = { lab: document.getElementById('tab-lab'), auto: document.getElementById('tab-auto') }; | |
| const sections = { lab: document.getElementById('section-lab'), auto: document.getElementById('section-auto') }; | |
| const btnLabRun = document.getElementById('btn-lab-run'); | |
| const btnAutoReset = document.getElementById('btn-auto-reset'); | |
| const btnAutoStep = document.getElementById('btn-auto-step'); | |
| const btnGlobalClear = document.getElementById('btn-global-clear'); | |
| const logViewport = document.getElementById('log-viewport'); | |
| // HUD | |
| const valReward = document.getElementById('val-reward'); | |
| const valAccuracy = document.getElementById('val-accuracy'); | |
| const valState = document.getElementById('val-state'); | |
| let totalReward = 0; | |
| let counter = 0; | |
| let currentMode = 'lab'; | |
| // Tab Switching | |
| tabs.lab.onclick = () => switchMode('lab'); | |
| tabs.auto.onclick = () => switchMode('auto'); | |
| function switchMode(m) { | |
| currentMode = m; | |
| tabs.lab.classList.toggle('active', m === 'lab'); | |
| tabs.auto.classList.toggle('active', m === 'auto'); | |
| sections.lab.style.display = m === 'lab' ? 'block' : 'none'; | |
| sections.auto.style.display = m === 'auto' ? 'block' : 'none'; | |
| valState.textContent = 'READY'; | |
| valState.style.color = 'var(--accent)'; | |
| } | |
| // Global Reset | |
| btnGlobalClear.onclick = () => { | |
| logViewport.innerHTML = '<div class="empty-state">System purged. Waiting for new data.</div>'; | |
| totalReward = 0; | |
| counter = 0; | |
| valReward.textContent = '0.000'; | |
| valAccuracy.textContent = '--'; | |
| valState.textContent = 'IDLE'; | |
| valState.style.color = 'var(--muted)'; | |
| btnAutoStep.disabled = true; | |
| if (currentMode === 'auto') valState.textContent = 'SYSTEM RESET'; | |
| }; | |
| // Lab Evaluation | |
| btnLabRun.onclick = async () => { | |
| const text = document.getElementById('lab-input').value.trim(); | |
| const policy = document.getElementById('lab-policy').value; | |
| const history = document.getElementById('lab-history').value; | |
| const context = document.getElementById('lab-context').value; | |
| if (!text) return; | |
| btnLabRun.disabled = true; | |
| try { | |
| const resp = await fetch('/evaluate', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ | |
| text: text, | |
| policy_mode: policy.toLowerCase(), | |
| user_history: history, | |
| context_type: context, | |
| api_base_url: document.getElementById('config-base-url').value.trim() || undefined, | |
| model_name: document.getElementById('config-model').value.trim() || undefined, | |
| api_key: document.getElementById('config-key').value.trim() || undefined | |
| }) | |
| }); | |
| const data = await resp.json(); | |
| renderEntry(text, data.action, data.reward, policy, data.reason, {history, context}); | |
| updateHUD(data.reward); | |
| } finally { | |
| btnLabRun.disabled = false; | |
| document.getElementById('lab-input').value = ''; | |
| } | |
| }; | |
| // Auto Benchmark | |
| btnAutoReset.onclick = async () => { | |
| const task = document.getElementById('auto-task').value; | |
| const resp = await fetch('/reset', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({task_name: task}) | |
| }); | |
| const state = await resp.json(); | |
| logViewport.innerHTML = `<div class="log-entry" style="border-color:var(--muted)"> | |
| <div class="log-meta"><span>SYSTEM EVENT</span><span>SESSION START</span></div> | |
| <div class="log-text">Environment reset complete. Target: <b>${task}</b>. Dataset contains ${state.total_steps} items. Ready for sequential evaluation.</div> | |
| </div>`; | |
| valState.textContent = `SEQ: 1/${state.total_steps}`; | |
| btnAutoStep.disabled = false; | |
| }; | |
| btnAutoStep.onclick = async () => { | |
| btnAutoStep.disabled = true; | |
| try { | |
| const stateResp = await fetch('/state'); | |
| const state = await stateResp.json(); | |
| const evalResp = await fetch('/evaluate', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ | |
| text: state.text, | |
| policy_mode: state.platform_policy_mode, | |
| api_base_url: document.getElementById('config-base-url').value.trim() || undefined, | |
| model_name: document.getElementById('config-model').value.trim() || undefined, | |
| api_key: document.getElementById('config-key').value.trim() || undefined | |
| }) | |
| }); | |
| const evalData = await evalResp.json(); | |
| const stepResp = await fetch('/step', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({action: evalData.action}) | |
| }); | |
| const stepResult = await stepResp.json(); | |
| renderEntry(state.text, evalData.action, stepResult.reward, state.platform_policy_mode.toUpperCase(), evalData.reason, {history: state.user_history_summary, context: state.context_type}); | |
| updateHUD(stepResult.reward); | |
| if (stepResult.done) { | |
| valState.textContent = 'COMPLETE'; | |
| valState.style.color = 'var(--success)'; | |
| btnAutoStep.disabled = true; | |
| logViewport.innerHTML = `<div class="log-entry" style="border-color:var(--success); background:rgba(74,222,128,0.05)"> | |
| <div class="log-meta"><span>EPISODE COMPLETE</span><span>FINAL GRADE</span></div> | |
| <div class="log-text">The environment has finalized this sequence. Total episodes rewards calculated with active fairness parity checks.</div> | |
| <div style="font-size:1.4rem; font-weight:800; color:var(--success); margin-top:15px; font-family:'JetBrains Mono'">SCORE: ${stepResult.final_score.toFixed(4)}</div> | |
| </div>` + logViewport.innerHTML; | |
| } else { | |
| valState.textContent = `SEQ: ${state.step_index + 1}/${state.total_steps}`; | |
| btnAutoStep.disabled = false; | |
| } | |
| } catch (e) { | |
| btnAutoStep.disabled = false; | |
| } | |
| }; | |
| function updateHUD(r) { | |
| totalReward += r; | |
| counter++; | |
| valReward.textContent = totalReward.toFixed(3); | |
| valAccuracy.textContent = (totalReward / counter).toFixed(3); | |
| } | |
| function renderEntry(text, action, reward, mode, reason, meta) { | |
| const colors = { ALLOW:'var(--accent)', BAN_USER:'var(--danger)', HARD_FILTER:'var(--danger)', SOFT_HIDE:'#fbbf24', ALLOW_WITH_WARNING:'var(--accent)', ESCALATE_HUMAN:'var(--success)' }; | |
| const entry = document.createElement('div'); | |
| entry.className = 'log-entry'; | |
| entry.style.borderColor = colors[action] || 'var(--accent)'; | |
| entry.innerHTML = ` | |
| <div class="log-meta"> | |
| <span>POLICY: ${mode}</span> | |
| <span>VERDICT: +${reward.toFixed(3)}</span> | |
| </div> | |
| <div style="display:flex; gap:8px; margin-bottom:10px;"> | |
| <span style="font-size:0.6rem; color:var(--muted); border:1px solid rgba(255,255,255,0.1); padding:2px 6px; border-radius:4px; text-transform:uppercase;">${meta.history.replace(/_/g,' ')}</span> | |
| <span style="font-size:0.6rem; color:var(--muted); border:1px solid rgba(255,255,255,0.1); padding:2px 6px; border-radius:4px; text-transform:uppercase;">${meta.context.replace(/_/g,' ')}</span> | |
| </div> | |
| <div class="log-text">${text}</div> | |
| <div style="font-size:0.7rem; color:var(--accent); background:rgba(56,189,248,0.05); padding:8px; border-radius:8px; margin-top:10px; border:1px solid rgba(56,189,248,0.1); white-space: pre-wrap;"> | |
| <span style="font-weight:800; opacity:0.6; margin-right:5px;">LOGIC INSIGHT:</span> ${reason} | |
| </div> | |
| <div style="display:flex; align-items:center; justify-content:space-between; margin-top:12px;"> | |
| <span class="log-badge" style="background:${colors[action] || 'var(--accent)'}; color:#020617; margin-top:0">${action}</span> | |
| <div class="hitl-actions" id="hitl-${counter}" style="display:flex; gap:5px;"> | |
| <button onclick="showOverrideMenu(this, ${reward}, '${action}')" style="background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); color:var(--muted); font-size:0.6rem; padding:4px 8px; border-radius:4px; cursor:pointer;">INCORRECT?</button> | |
| <button onclick="verifyAction(this)" style="background:rgba(74,222,128,0.1); border:1px solid var(--success); color:var(--success); font-size:0.6rem; padding:4px 8px; border-radius:4px; cursor:pointer;">VERIFY</button> | |
| </div> | |
| </div> | |
| `; | |
| const hint = document.getElementById('empty-hint'); | |
| if (hint) hint.remove(); | |
| logViewport.prepend(entry); | |
| } | |
| function verifyAction(btn) { | |
| btn.parentElement.innerHTML = '<span style="color:var(--success); font-size:0.6rem; font-weight:800; border:1px solid var(--success); padding:2px 6px; border-radius:4px;">✓ HUMAN VERIFIED</span>'; | |
| } | |
| function showOverrideMenu(btn, originalReward, originalAction) { | |
| const container = btn.parentElement; | |
| container.innerHTML = ` | |
| <div style="display:flex; gap:5px; align-items:center;"> | |
| <span style="font-size:0.6rem; color:var(--danger); font-weight:800; margin-right:5px;">SHOULD BE:</span> | |
| <button onclick="applyOverride(this, ${originalReward}, 'BAN_USER')" style="background:var(--danger); color:#000; border:none; font-size:0.55rem; padding:3px 6px; border-radius:4px; cursor:pointer; font-weight:700;">BAN</button> | |
| <button onclick="applyOverride(this, ${originalReward}, 'ALLOW')" style="background:var(--accent); color:#000; border:none; font-size:0.55rem; padding:3px 6px; border-radius:4px; cursor:pointer; font-weight:700;">ALLOW</button> | |
| <button onclick="applyOverride(this, ${originalReward}, 'SOFT_HIDE')" style="background:#fbbf24; color:#000; border:none; font-size:0.55rem; padding:3px 6px; border-radius:4px; cursor:pointer; font-weight:700;">HIDE</button> | |
| </div> | |
| `; | |
| } | |
| function applyOverride(btn, originalReward, newTarget) { | |
| // RL Logic: Deduct the undeserved reward and apply a correction penalty | |
| // Since this was a mistake, we give it -1.0 total (undo the +reward and subtract 1) | |
| const correction = - (originalReward + 1.0); | |
| updateHUD(correction); | |
| const container = btn.closest('.hitl-actions'); | |
| container.innerHTML = `<span style="color:var(--danger); font-size:0.6rem; font-weight:800; border:1px solid var(--danger); padding:2px 6px; border-radius:4px;">𐄂 OVERRIDDEN: ${newTarget}</span>`; | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| async def reset_env( | |
| task_name: TaskName = Query(TaskName.TASK_1, description="Select the benchmark level to initialize."), | |
| seed: Optional[int] = Query(42, description="Reproducibility seed for dataset sampling.") | |
| ): | |
| """Resets the environment with a given task and seed. This must be the first step in any benchmarking track.""" | |
| try: | |
| state = await env.reset(task_name=TASK_MAP[task_name], seed=seed) | |
| return state | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| async def evaluate_text( | |
| req: EvaluateRequest, | |
| policy_mode: PolicyModeChoice = Query(PolicyModeChoice.NORMAL, description="Select the active safety policy regime."), | |
| user_history: UserHistoryChoice = Query(UserHistoryChoice.CLEAN, description="Select the author's moderation history."), | |
| context_type: ContextTypeChoice = Query(ContextTypeChoice.ROOT, description="Select the content hierarchical context.") | |
| ): | |
| """Internal endpoint for the Interactive Sandbox UI. Returns logic reasoning and calculated rewards.""" | |
| # Use proper Enum types | |
| from envs.social_stream_moderation.models import PolicyMode, State | |
| from envs.social_stream_moderation.graders import compute_per_post_reward, REWARD_TABLE | |
| from inference import get_agent | |
| # Map choice to model enum | |
| try: | |
| p_mode = PolicyMode(POLICY_MAP[policy_mode]) | |
| except ValueError: | |
| p_mode = PolicyMode.NORMAL | |
| # Predict action using rule-based or LLM logic | |
| agent = get_agent(api_base_url=req.api_base_url, model_name=req.model_name, api_key=req.api_key) | |
| mock_state = State( | |
| post_id="playground_test", | |
| text=req.text, | |
| user_history_summary=HISTORY_MAP[user_history], | |
| context_type=CONTEXT_MAP[context_type], | |
| platform_policy_mode=p_mode.value, | |
| user_group="A", | |
| step_index=0, | |
| total_steps=1 | |
| ) | |
| action, reason = agent.predict(mock_state) | |
| # We need to simulate a 'correct' label for reward calculation in demo | |
| # For simplicity in demo, if the agent bans, we assume it caught something | |
| # (HarmLabel mapping is internal, so we'll just derive a representative reward) | |
| # Check if this input matches our known harmful patterns to determine reward | |
| from envs.social_stream_moderation.models import HarmLabel | |
| best_harm_guess = HarmLabel.SAFE | |
| for kw in ["kill", "murder", "stab", "find you", "death at you"]: | |
| if kw in req.text.lower(): | |
| best_harm_guess = HarmLabel.SEVERE_ABUSE_HATE | |
| break | |
| reward = compute_per_post_reward(best_harm_guess, action, p_mode) | |
| return { | |
| "action": action.value, | |
| "reward": float(reward), | |
| "reason": reason | |
| } | |
| async def step_env(req: StepRequest): | |
| try: | |
| next_state, reward, done, info = await env.step(req.action) | |
| final_score = 0.0 | |
| if done: | |
| from envs.social_stream_moderation.graders import grade_episode | |
| # Assume fairness check for Task 3 | |
| final_score = grade_episode(env.episode_history, use_fairness=True) | |
| return { | |
| "next_state": next_state, | |
| "reward": reward, | |
| "done": done, | |
| "info": info, | |
| "final_score": final_score | |
| } | |
| except RuntimeError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| async def predict_and_step(req: Optional[LLMConfigRequest] = Body(None)): | |
| """Predicts using dynamic agent and steps the env automatically. This matches our inference.py autonomous loop.""" | |
| from inference import get_agent | |
| state = env._get_state() | |
| if state is None: | |
| raise HTTPException(status_code=400, detail="No active episode. Please call /reset first.") | |
| agent = get_agent( | |
| api_base_url=req.api_base_url if req else None, | |
| model_name=req.model_name if req else None, | |
| api_key=req.api_key if req else None | |
| ) | |
| action, reason = agent.predict(state) | |
| # Execute the step with the model's prediction | |
| next_state, reward, done, info = await env.step(action) | |
| final_score = 0.0 | |
| if done: | |
| from envs.social_stream_moderation.graders import grade_episode | |
| final_score = grade_episode(env.episode_history, use_fairness=True) | |
| return { | |
| "prediction": action.value, | |
| "reason": reason, | |
| "reward": reward, | |
| "done": done, | |
| "final_score": final_score, | |
| "next_state": next_state, | |
| "info": info | |
| } | |
| def get_state(): | |
| state = env._get_state() | |
| if state is None: | |
| return { | |
| "status": "Ready", | |
| "message": "Environment is initialized but no episode is currently active.", | |
| "how_to_start": "Call 'POST /reset' with a task_name (e.g., 'clear_cut_moderation') to begin benchmarking." | |
| } | |
| return state | |
| def kill_port(port): | |
| import subprocess | |
| import os | |
| import sys | |
| try: | |
| if sys.platform == "win32": | |
| # Windows logic | |
| output = subprocess.check_output(f'netstat -ano | findstr :{port}', shell=True).decode() | |
| for line in output.strip().split('\n'): | |
| if 'LISTENING' in line: | |
| pid = line.strip().split()[-1] | |
| if pid != str(os.getpid()): | |
| print(f"Cleanup: Stopping existing process {pid} on port {port}...") | |
| subprocess.run(f'taskkill /F /PID {pid}', shell=True, capture_output=True) | |
| else: | |
| # Unix/Mac/Linux logic | |
| try: | |
| # Use lsof to find the PID | |
| output = subprocess.check_output(['lsof', '-ti', f':{port}']).decode().strip() | |
| if output: | |
| for pid in output.split('\n'): | |
| if pid != str(os.getpid()): | |
| print(f"Cleanup: Stopping existing process {pid} on port {port}...") | |
| subprocess.run(['kill', '-9', pid], capture_output=True) | |
| except (subprocess.CalledProcessError, FileNotFoundError): | |
| # Fallback to fuser if lsof is missing | |
| try: | |
| subprocess.run(['fuser', '-k', f'{port}/tcp'], capture_output=True) | |
| except Exception: | |
| pass | |
| except Exception: | |
| pass | |
| def main(): | |
| import uvicorn | |
| # Automatically clear the port before starting to avoid [WinError 10048] | |
| kill_port(7860) | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |
| if __name__ == "__main__": | |
| main() | |