Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- server/app.py +83 -214
server/app.py
CHANGED
|
@@ -3,25 +3,24 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
|
|
|
| 6 |
import sys
|
| 7 |
-
import traceback
|
| 8 |
from pathlib import Path
|
| 9 |
|
| 10 |
-
# Allow importing sibling modules when running from server/ dir
|
| 11 |
sys.path.insert(0, str(Path(__file__).parent))
|
| 12 |
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 13 |
|
| 14 |
from fastapi import FastAPI, HTTPException
|
| 15 |
-
from fastapi.responses import HTMLResponse
|
| 16 |
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
from pydantic import BaseModel
|
| 18 |
-
from typing import Any, Dict,
|
| 19 |
|
| 20 |
from models import FakeGangAction, FakeGangObservation, FakeGangState, ActionType
|
| 21 |
from environment import FakeGangEnvironment
|
| 22 |
|
| 23 |
# ---------------------------------------------------------------------------
|
| 24 |
-
# App
|
| 25 |
# ---------------------------------------------------------------------------
|
| 26 |
|
| 27 |
app = FastAPI(
|
|
@@ -32,16 +31,14 @@ app = FastAPI(
|
|
| 32 |
|
| 33 |
app.add_middleware(
|
| 34 |
CORSMiddleware,
|
| 35 |
-
allow_origins=["*"],
|
| 36 |
-
allow_methods=["*"],
|
| 37 |
-
allow_headers=["*"],
|
| 38 |
)
|
| 39 |
|
| 40 |
_env = FakeGangEnvironment()
|
| 41 |
|
| 42 |
|
| 43 |
# ---------------------------------------------------------------------------
|
| 44 |
-
#
|
| 45 |
# ---------------------------------------------------------------------------
|
| 46 |
|
| 47 |
class ResetRequest(BaseModel):
|
|
@@ -49,7 +46,6 @@ class ResetRequest(BaseModel):
|
|
| 49 |
seed: Optional[int] = None
|
| 50 |
episode_id: Optional[str] = None
|
| 51 |
|
| 52 |
-
|
| 53 |
class StepResponse(BaseModel):
|
| 54 |
observation: Dict[str, Any]
|
| 55 |
done: bool
|
|
@@ -58,43 +54,29 @@ class StepResponse(BaseModel):
|
|
| 58 |
|
| 59 |
|
| 60 |
# ---------------------------------------------------------------------------
|
| 61 |
-
# API
|
| 62 |
# ---------------------------------------------------------------------------
|
| 63 |
|
| 64 |
@app.get("/health")
|
| 65 |
-
def health()
|
| 66 |
return {"status": "healthy"}
|
| 67 |
|
| 68 |
-
|
| 69 |
@app.post("/reset", response_model=StepResponse)
|
| 70 |
-
def reset(req: ResetRequest)
|
| 71 |
obs = _env.reset(task=req.task, seed=req.seed, episode_id=req.episode_id)
|
| 72 |
-
return StepResponse(
|
| 73 |
-
observation=obs.model_dump(),
|
| 74 |
-
done=obs.done,
|
| 75 |
-
reward=obs.reward,
|
| 76 |
-
message=obs.message,
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
|
| 80 |
@app.post("/step", response_model=StepResponse)
|
| 81 |
-
def step(action: FakeGangAction)
|
| 82 |
obs = _env.step(action)
|
| 83 |
-
return StepResponse(
|
| 84 |
-
observation=obs.model_dump(),
|
| 85 |
-
done=obs.done,
|
| 86 |
-
reward=obs.reward,
|
| 87 |
-
message=obs.message,
|
| 88 |
-
)
|
| 89 |
-
|
| 90 |
|
| 91 |
@app.get("/state")
|
| 92 |
-
def state()
|
| 93 |
return _env.state.model_dump()
|
| 94 |
|
| 95 |
-
|
| 96 |
@app.get("/tasks")
|
| 97 |
-
def list_tasks()
|
| 98 |
return {
|
| 99 |
"tasks": ["easy", "medium", "hard"],
|
| 100 |
"descriptions": {
|
|
@@ -109,159 +91,90 @@ def list_tasks() -> Dict[str, Any]:
|
|
| 109 |
"score_range": [0.0, 1.0],
|
| 110 |
}
|
| 111 |
|
| 112 |
-
|
| 113 |
@app.get("/grader")
|
| 114 |
-
def grader()
|
| 115 |
if not _env._done:
|
| 116 |
raise HTTPException(status_code=400, detail="Episode not complete. Call SUBMIT first.")
|
| 117 |
-
return {
|
| 118 |
-
"score": _env._last_grader_score,
|
| 119 |
-
"task": _env._task,
|
| 120 |
-
"episode_id": _env._episode_id,
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
|
| 124 |
@app.get("/metadata")
|
| 125 |
-
def metadata()
|
| 126 |
return {
|
| 127 |
"name": "graphstrike",
|
| 128 |
-
"description":
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
"hidden inside a synthetic social network before they evade detection."
|
| 132 |
-
),
|
| 133 |
-
"version": "1.0.0",
|
| 134 |
-
"author": "Pandago",
|
| 135 |
-
"tags": ["social-network", "fraud-detection", "graph", "rl", "fake-account-detection"],
|
| 136 |
}
|
| 137 |
|
| 138 |
-
|
| 139 |
@app.get("/schema")
|
| 140 |
-
def schema()
|
| 141 |
return {
|
| 142 |
"action": FakeGangAction.model_json_schema(),
|
| 143 |
"observation": FakeGangObservation.model_json_schema(),
|
| 144 |
"state": FakeGangState.model_json_schema(),
|
| 145 |
}
|
| 146 |
|
| 147 |
-
|
| 148 |
@app.post("/mcp")
|
| 149 |
-
def mcp(body: Dict[str, Any] = {})
|
| 150 |
-
"""Minimal MCP (Model Context Protocol) JSON-RPC 2.0 endpoint."""
|
| 151 |
method = body.get("method", "")
|
| 152 |
req_id = body.get("id", 1)
|
| 153 |
-
|
| 154 |
if method == "tools/list":
|
| 155 |
-
return {
|
| 156 |
-
"
|
| 157 |
-
|
| 158 |
-
"
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
"description": "Reset the environment with a task and seed",
|
| 163 |
-
"inputSchema": {
|
| 164 |
-
"type": "object",
|
| 165 |
-
"properties": {
|
| 166 |
-
"task": {"type": "string", "enum": ["easy", "medium", "hard"]},
|
| 167 |
-
"seed": {"type": "integer"},
|
| 168 |
-
},
|
| 169 |
-
},
|
| 170 |
-
},
|
| 171 |
-
{
|
| 172 |
-
"name": "step",
|
| 173 |
-
"description": "Take an action in the environment",
|
| 174 |
-
"inputSchema": FakeGangAction.model_json_schema(),
|
| 175 |
-
},
|
| 176 |
-
{
|
| 177 |
-
"name": "state",
|
| 178 |
-
"description": "Get current episode state",
|
| 179 |
-
"inputSchema": {"type": "object", "properties": {}},
|
| 180 |
-
},
|
| 181 |
-
]
|
| 182 |
-
},
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
return {
|
| 186 |
-
"jsonrpc": "2.0",
|
| 187 |
-
"id": req_id,
|
| 188 |
-
"result": {
|
| 189 |
-
"name": "graphstrike",
|
| 190 |
-
"version": "1.0.0",
|
| 191 |
-
"protocolVersion": "2024-11-05",
|
| 192 |
-
},
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
|
| 196 |
@app.post("/baseline")
|
| 197 |
-
def baseline()
|
| 198 |
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 199 |
-
from inference import run_rule_based_episode
|
| 200 |
-
scores
|
| 201 |
for task in ["easy", "medium", "hard"]:
|
| 202 |
scores[task] = run_rule_based_episode(_env, task=task, seed=0)
|
| 203 |
return {"scores": scores, "agent": "rule_based"}
|
| 204 |
|
| 205 |
|
| 206 |
# ---------------------------------------------------------------------------
|
| 207 |
-
# Gradio web interface
|
| 208 |
# ---------------------------------------------------------------------------
|
| 209 |
|
| 210 |
-
_gradio_mounted = False
|
| 211 |
-
|
| 212 |
try:
|
| 213 |
import gradio as gr
|
| 214 |
-
print("[GraphStrike] Gradio found, building web UI...", flush=True)
|
| 215 |
-
|
| 216 |
-
_demo_env = _env
|
| 217 |
|
| 218 |
-
def _fmt_obs(
|
| 219 |
lines = []
|
| 220 |
-
lines.append(
|
| 221 |
-
|
| 222 |
-
f"**
|
| 223 |
-
|
| 224 |
-
)
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
lines.append(f"**
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
lines.append(f"**
|
| 231 |
-
lines.append(f"**Inspected:** {len(obs_dict.get('inspected_ids', []))} accounts")
|
| 232 |
-
if obs_dict.get('evasion_triggered'):
|
| 233 |
-
lines.append(f"**Evasion events fired:** {obs_dict.get('evasion_count', 0)}")
|
| 234 |
-
lines.append(f"**Message:** {obs_dict.get('message', '')}")
|
| 235 |
return "\n\n".join(lines)
|
| 236 |
|
| 237 |
-
def _fmt_profiles(
|
| 238 |
-
|
| 239 |
-
if not
|
| 240 |
-
return "No accounts inspected yet. Use **INSPECT**
|
| 241 |
-
rows = [
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
f"| {a.get('status','?')} "
|
| 249 |
-
f"| {a.get('fake_risk_score',0):.3f} "
|
| 250 |
-
f"| {a.get('node_risk',0):.2f} "
|
| 251 |
-
f"| {a.get('behavior_risk',0):.2f} "
|
| 252 |
-
f"| {a.get('graph_risk',0):.2f} "
|
| 253 |
-
f"| {a.get('hub_legitimacy_score',0):.2f} "
|
| 254 |
-
f"| {a.get('photo_reuse_score',0):.2f} "
|
| 255 |
-
f"| {a.get('bio_template_score',0):.2f} "
|
| 256 |
-
f"| {a.get('flagged_neighbor_count',0)} |"
|
| 257 |
-
)
|
| 258 |
-
if len(accounts) > 25:
|
| 259 |
-
rows.append(f"| ... +{len(accounts) - 25} more | | | | | | | | | |")
|
| 260 |
return "\n".join(rows)
|
| 261 |
|
| 262 |
def gr_reset(task, seed):
|
| 263 |
try:
|
| 264 |
-
obs =
|
| 265 |
d = obs.model_dump()
|
| 266 |
return _fmt_obs(d), _fmt_profiles(d), json.dumps(d, indent=2, default=str)
|
| 267 |
except Exception as e:
|
|
@@ -271,27 +184,21 @@ try:
|
|
| 271 |
try:
|
| 272 |
acc = account_id.strip() if action_type != "submit" else None
|
| 273 |
action = FakeGangAction(action_type=ActionType(action_type), account_id=acc)
|
| 274 |
-
obs =
|
| 275 |
d = obs.model_dump()
|
| 276 |
return _fmt_obs(d), _fmt_profiles(d), json.dumps(d, indent=2, default=str)
|
| 277 |
except Exception as e:
|
| 278 |
return f"**Error:** {e}", "", "{}"
|
| 279 |
|
| 280 |
def gr_grader():
|
| 281 |
-
if not
|
| 282 |
return "Episode not complete. Call SUBMIT first."
|
| 283 |
-
return json.dumps({
|
| 284 |
-
"score": _demo_env._last_grader_score,
|
| 285 |
-
"task": _demo_env._task,
|
| 286 |
-
"episode_id": _demo_env._episode_id,
|
| 287 |
-
}, indent=2)
|
| 288 |
|
| 289 |
def gr_baseline():
|
| 290 |
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 291 |
from inference import run_rule_based_episode
|
| 292 |
-
scores = {}
|
| 293 |
-
for t in ["easy", "medium", "hard"]:
|
| 294 |
-
scores[t] = run_rule_based_episode(_demo_env, task=t, seed=0)
|
| 295 |
return json.dumps({"scores": scores, "agent": "rule_based"}, indent=2)
|
| 296 |
|
| 297 |
with gr.Blocks(title="GraphStrike") as demo:
|
|
@@ -300,99 +207,61 @@ try:
|
|
| 300 |
"### Coordinated Fake Account Ring Detection — OpenEnv RL Environment\n\n"
|
| 301 |
"Detect all 10 members of a coordinated fake account ring hidden in a social network.\n"
|
| 302 |
"Use **INSPECT** to reveal profiles, **FLAG** to mark fakes, **SUBMIT** to end.\n\n"
|
| 303 |
-
"
|
| 304 |
)
|
| 305 |
-
|
| 306 |
with gr.Row():
|
| 307 |
-
with gr.Column(
|
| 308 |
gr.Markdown("#### 1. Start Episode")
|
| 309 |
-
task_dd = gr.Dropdown(["easy",
|
| 310 |
-
|
| 311 |
reset_btn = gr.Button("Reset Episode", variant="primary", size="lg")
|
| 312 |
-
|
| 313 |
-
with gr.Column(scale=1):
|
| 314 |
gr.Markdown("#### 2. Take Actions")
|
| 315 |
-
action_dd = gr.Dropdown(
|
| 316 |
-
|
| 317 |
-
value="inspect", label="Action Type"
|
| 318 |
-
)
|
| 319 |
-
acc_input = gr.Textbox(label="Account ID", placeholder="e.g. acc_0012")
|
| 320 |
step_btn = gr.Button("Step", variant="primary", size="lg")
|
| 321 |
|
| 322 |
-
|
| 323 |
-
|
| 324 |
with gr.Accordion("Account Profiles (sorted by risk)", open=True):
|
| 325 |
-
|
| 326 |
-
|
| 327 |
with gr.Row():
|
| 328 |
grader_btn = gr.Button("Get Grader Score")
|
| 329 |
baseline_btn = gr.Button("Run Baseline (all 3 tasks)")
|
| 330 |
result_box = gr.Textbox(label="Result", lines=5, interactive=False)
|
| 331 |
-
|
| 332 |
-
with gr.Accordion("Raw JSON Observation", open=False):
|
| 333 |
raw_json = gr.Textbox(label="Raw JSON", lines=15, interactive=False)
|
| 334 |
|
| 335 |
-
reset_btn.click(gr_reset, [task_dd,
|
| 336 |
-
step_btn.click(gr_step, [action_dd,
|
| 337 |
grader_btn.click(gr_grader, [], result_box)
|
| 338 |
baseline_btn.click(gr_baseline, [], result_box)
|
| 339 |
|
| 340 |
app = gr.mount_gradio_app(app, demo, path="/")
|
| 341 |
-
_gradio_mounted = True
|
| 342 |
print("[GraphStrike] Gradio UI mounted at /", flush=True)
|
| 343 |
|
| 344 |
except Exception as exc:
|
| 345 |
-
print(f"[GraphStrike] Gradio
|
| 346 |
-
traceback.print_exc()
|
| 347 |
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
-
# ---------------------------------------------------------------------------
|
| 350 |
-
# Fallback: if Gradio didn't load, serve a simple HTML page at /
|
| 351 |
-
# ---------------------------------------------------------------------------
|
| 352 |
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
<html><head><title>GraphStrike</title>
|
| 358 |
-
<style>body{font-family:system-ui;max-width:800px;margin:40px auto;padding:0 20px;background:#0d1117;color:#c9d1d9}
|
| 359 |
-
a{color:#58a6ff}h1{color:#f0f6fc}code{background:#161b22;padding:2px 6px;border-radius:4px}
|
| 360 |
-
pre{background:#161b22;padding:16px;border-radius:8px;overflow-x:auto}
|
| 361 |
-
.ep{background:#161b22;padding:16px;border-radius:8px;margin:12px 0}</style></head>
|
| 362 |
-
<body>
|
| 363 |
-
<h1>GraphStrike</h1>
|
| 364 |
-
<p><strong>Coordinated Fake Account Ring Detection</strong> — OpenEnv RL Environment</p>
|
| 365 |
-
<h3>API Endpoints</h3>
|
| 366 |
-
<div class="ep"><pre>GET /health — liveness check
|
| 367 |
-
GET /tasks — task list + action schema
|
| 368 |
-
POST /reset — start episode {"task":"easy","seed":0}
|
| 369 |
-
POST /step — take action {"action_type":"inspect","account_id":"acc_0012"}
|
| 370 |
-
GET /state — episode state
|
| 371 |
-
GET /grader — score after SUBMIT
|
| 372 |
-
POST /baseline — rule-based agent on all 3 tasks
|
| 373 |
-
GET /metadata — environment metadata
|
| 374 |
-
GET /schema — action/observation/state schemas
|
| 375 |
-
POST /mcp — MCP JSON-RPC 2.0</pre></div>
|
| 376 |
-
<h3>Quick Test</h3>
|
| 377 |
-
<pre>curl -X POST /reset -H "Content-Type: application/json" -d '{"task":"easy","seed":0}'
|
| 378 |
-
curl -X POST /step -H "Content-Type: application/json" -d '{"action_type":"inspect","account_id":"acc_0000"}'
|
| 379 |
-
curl -X POST /step -H "Content-Type: application/json" -d '{"action_type":"submit"}'
|
| 380 |
-
curl /grader</pre>
|
| 381 |
-
<p><a href="/docs">Interactive API docs (Swagger)</a> | <a href="/redoc">ReDoc</a></p>
|
| 382 |
-
</body></html>"""
|
| 383 |
|
| 384 |
|
| 385 |
# ---------------------------------------------------------------------------
|
| 386 |
# Entry point
|
| 387 |
# ---------------------------------------------------------------------------
|
| 388 |
|
| 389 |
-
def main()
|
| 390 |
-
"""Start the environment server. PORT env var controls the port (default 7860)."""
|
| 391 |
-
import os
|
| 392 |
import uvicorn
|
| 393 |
port = int(os.environ.get("PORT", 7860))
|
|
|
|
| 394 |
uvicorn.run("server.app:app", host="0.0.0.0", port=port, log_level="info", workers=1)
|
| 395 |
|
| 396 |
-
|
| 397 |
if __name__ == "__main__":
|
| 398 |
main()
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
| 6 |
+
import os
|
| 7 |
import sys
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
|
|
|
|
| 10 |
sys.path.insert(0, str(Path(__file__).parent))
|
| 11 |
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 12 |
|
| 13 |
from fastapi import FastAPI, HTTPException
|
| 14 |
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
| 15 |
from fastapi.middleware.cors import CORSMiddleware
|
| 16 |
from pydantic import BaseModel
|
| 17 |
+
from typing import Any, Dict, Optional
|
| 18 |
|
| 19 |
from models import FakeGangAction, FakeGangObservation, FakeGangState, ActionType
|
| 20 |
from environment import FakeGangEnvironment
|
| 21 |
|
| 22 |
# ---------------------------------------------------------------------------
|
| 23 |
+
# App
|
| 24 |
# ---------------------------------------------------------------------------
|
| 25 |
|
| 26 |
app = FastAPI(
|
|
|
|
| 31 |
|
| 32 |
app.add_middleware(
|
| 33 |
CORSMiddleware,
|
| 34 |
+
allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
|
|
|
|
|
|
|
| 35 |
)
|
| 36 |
|
| 37 |
_env = FakeGangEnvironment()
|
| 38 |
|
| 39 |
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
+
# Schemas
|
| 42 |
# ---------------------------------------------------------------------------
|
| 43 |
|
| 44 |
class ResetRequest(BaseModel):
|
|
|
|
| 46 |
seed: Optional[int] = None
|
| 47 |
episode_id: Optional[str] = None
|
| 48 |
|
|
|
|
| 49 |
class StepResponse(BaseModel):
|
| 50 |
observation: Dict[str, Any]
|
| 51 |
done: bool
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
# ---------------------------------------------------------------------------
|
| 57 |
+
# OpenEnv API endpoints
|
| 58 |
# ---------------------------------------------------------------------------
|
| 59 |
|
| 60 |
@app.get("/health")
|
| 61 |
+
def health():
|
| 62 |
return {"status": "healthy"}
|
| 63 |
|
|
|
|
| 64 |
@app.post("/reset", response_model=StepResponse)
|
| 65 |
+
def reset(req: ResetRequest):
|
| 66 |
obs = _env.reset(task=req.task, seed=req.seed, episode_id=req.episode_id)
|
| 67 |
+
return StepResponse(observation=obs.model_dump(), done=obs.done, reward=obs.reward, message=obs.message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
@app.post("/step", response_model=StepResponse)
|
| 70 |
+
def step(action: FakeGangAction):
|
| 71 |
obs = _env.step(action)
|
| 72 |
+
return StepResponse(observation=obs.model_dump(), done=obs.done, reward=obs.reward, message=obs.message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
@app.get("/state")
|
| 75 |
+
def state():
|
| 76 |
return _env.state.model_dump()
|
| 77 |
|
|
|
|
| 78 |
@app.get("/tasks")
|
| 79 |
+
def list_tasks():
|
| 80 |
return {
|
| 81 |
"tasks": ["easy", "medium", "hard"],
|
| 82 |
"descriptions": {
|
|
|
|
| 91 |
"score_range": [0.0, 1.0],
|
| 92 |
}
|
| 93 |
|
|
|
|
| 94 |
@app.get("/grader")
|
| 95 |
+
def grader():
|
| 96 |
if not _env._done:
|
| 97 |
raise HTTPException(status_code=400, detail="Episode not complete. Call SUBMIT first.")
|
| 98 |
+
return {"score": _env._last_grader_score, "task": _env._task, "episode_id": _env._episode_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
@app.get("/metadata")
|
| 101 |
+
def metadata():
|
| 102 |
return {
|
| 103 |
"name": "graphstrike",
|
| 104 |
+
"description": "RL environment for detecting coordinated fake account rings in social networks.",
|
| 105 |
+
"version": "1.0.0", "author": "Pandago",
|
| 106 |
+
"tags": ["social-network", "fraud-detection", "graph", "rl"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
|
|
|
|
| 109 |
@app.get("/schema")
|
| 110 |
+
def schema():
|
| 111 |
return {
|
| 112 |
"action": FakeGangAction.model_json_schema(),
|
| 113 |
"observation": FakeGangObservation.model_json_schema(),
|
| 114 |
"state": FakeGangState.model_json_schema(),
|
| 115 |
}
|
| 116 |
|
|
|
|
| 117 |
@app.post("/mcp")
|
| 118 |
+
def mcp(body: Dict[str, Any] = {}):
|
|
|
|
| 119 |
method = body.get("method", "")
|
| 120 |
req_id = body.get("id", 1)
|
|
|
|
| 121 |
if method == "tools/list":
|
| 122 |
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": [
|
| 123 |
+
{"name": "reset", "description": "Reset the environment",
|
| 124 |
+
"inputSchema": {"type": "object", "properties": {"task": {"type": "string"}, "seed": {"type": "integer"}}}},
|
| 125 |
+
{"name": "step", "description": "Take an action", "inputSchema": FakeGangAction.model_json_schema()},
|
| 126 |
+
{"name": "state", "description": "Get episode state", "inputSchema": {"type": "object", "properties": {}}},
|
| 127 |
+
]}}
|
| 128 |
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"name": "graphstrike", "version": "1.0.0", "protocolVersion": "2024-11-05"}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
@app.post("/baseline")
|
| 131 |
+
def baseline():
|
| 132 |
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 133 |
+
from inference import run_rule_based_episode
|
| 134 |
+
scores = {}
|
| 135 |
for task in ["easy", "medium", "hard"]:
|
| 136 |
scores[task] = run_rule_based_episode(_env, task=task, seed=0)
|
| 137 |
return {"scores": scores, "agent": "rule_based"}
|
| 138 |
|
| 139 |
|
| 140 |
# ---------------------------------------------------------------------------
|
| 141 |
+
# Gradio web interface — mounted at /
|
| 142 |
# ---------------------------------------------------------------------------
|
| 143 |
|
|
|
|
|
|
|
| 144 |
try:
|
| 145 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
+
def _fmt_obs(d: dict) -> str:
|
| 148 |
lines = []
|
| 149 |
+
lines.append(f"**Task:** {d.get('task','?')} | **Done:** {d.get('done',False)} | **Steps remaining:** {d.get('steps_remaining','?')}")
|
| 150 |
+
if d.get('reward') is not None:
|
| 151 |
+
lines.append(f"**Reward:** {d['reward']:.2f}")
|
| 152 |
+
fl = d.get('flagged_ids', [])
|
| 153 |
+
lines.append(f"**Flagged ({len(fl)}/10):** {fl}")
|
| 154 |
+
su = d.get('suspect_ids', [])
|
| 155 |
+
lines.append(f"**Suspects ({len(su)}):** {su}")
|
| 156 |
+
lines.append(f"**Visible:** {len(d.get('visible_account_ids',[]))} IDs | **Inspected:** {len(d.get('inspected_ids',[]))} accounts")
|
| 157 |
+
if d.get('evasion_triggered'):
|
| 158 |
+
lines.append(f"**Evasion events:** {d.get('evasion_count',0)}")
|
| 159 |
+
lines.append(f"**Message:** {d.get('message','')}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
return "\n\n".join(lines)
|
| 161 |
|
| 162 |
+
def _fmt_profiles(d: dict) -> str:
|
| 163 |
+
accs = d.get("visible_accounts", [])
|
| 164 |
+
if not accs:
|
| 165 |
+
return "No accounts inspected yet. Use **INSPECT** to reveal profiles."
|
| 166 |
+
rows = ["| Account | Status | Risk | Node | Beh | Graph | Hub | Photo | Bio | F.Nbrs |",
|
| 167 |
+
"|---------|--------|------|------|-----|-------|-----|-------|-----|--------|"]
|
| 168 |
+
for a in sorted(accs, key=lambda x: x.get("fake_risk_score",0), reverse=True)[:25]:
|
| 169 |
+
rows.append(f"| {a.get('account_id','')} | {a.get('status','?')} | {a.get('fake_risk_score',0):.3f} "
|
| 170 |
+
f"| {a.get('node_risk',0):.2f} | {a.get('behavior_risk',0):.2f} | {a.get('graph_risk',0):.2f} "
|
| 171 |
+
f"| {a.get('hub_legitimacy_score',0):.2f} | {a.get('photo_reuse_score',0):.2f} "
|
| 172 |
+
f"| {a.get('bio_template_score',0):.2f} | {a.get('flagged_neighbor_count',0)} |")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
return "\n".join(rows)
|
| 174 |
|
| 175 |
def gr_reset(task, seed):
|
| 176 |
try:
|
| 177 |
+
obs = _env.reset(task=task, seed=int(seed))
|
| 178 |
d = obs.model_dump()
|
| 179 |
return _fmt_obs(d), _fmt_profiles(d), json.dumps(d, indent=2, default=str)
|
| 180 |
except Exception as e:
|
|
|
|
| 184 |
try:
|
| 185 |
acc = account_id.strip() if action_type != "submit" else None
|
| 186 |
action = FakeGangAction(action_type=ActionType(action_type), account_id=acc)
|
| 187 |
+
obs = _env.step(action)
|
| 188 |
d = obs.model_dump()
|
| 189 |
return _fmt_obs(d), _fmt_profiles(d), json.dumps(d, indent=2, default=str)
|
| 190 |
except Exception as e:
|
| 191 |
return f"**Error:** {e}", "", "{}"
|
| 192 |
|
| 193 |
def gr_grader():
|
| 194 |
+
if not _env._done:
|
| 195 |
return "Episode not complete. Call SUBMIT first."
|
| 196 |
+
return json.dumps({"score": _env._last_grader_score, "task": _env._task, "episode_id": _env._episode_id}, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
def gr_baseline():
|
| 199 |
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 200 |
from inference import run_rule_based_episode
|
| 201 |
+
scores = {t: run_rule_based_episode(_env, task=t, seed=0) for t in ["easy", "medium", "hard"]}
|
|
|
|
|
|
|
| 202 |
return json.dumps({"scores": scores, "agent": "rule_based"}, indent=2)
|
| 203 |
|
| 204 |
with gr.Blocks(title="GraphStrike") as demo:
|
|
|
|
| 207 |
"### Coordinated Fake Account Ring Detection — OpenEnv RL Environment\n\n"
|
| 208 |
"Detect all 10 members of a coordinated fake account ring hidden in a social network.\n"
|
| 209 |
"Use **INSPECT** to reveal profiles, **FLAG** to mark fakes, **SUBMIT** to end.\n\n"
|
| 210 |
+
"`/reset` `/step` `/state` `/grader` `/baseline` `/tasks` `/health` — [Swagger](/docs)"
|
| 211 |
)
|
|
|
|
| 212 |
with gr.Row():
|
| 213 |
+
with gr.Column():
|
| 214 |
gr.Markdown("#### 1. Start Episode")
|
| 215 |
+
task_dd = gr.Dropdown(["easy","medium","hard"], value="easy", label="Task")
|
| 216 |
+
seed_in = gr.Number(value=0, label="Seed", precision=0)
|
| 217 |
reset_btn = gr.Button("Reset Episode", variant="primary", size="lg")
|
| 218 |
+
with gr.Column():
|
|
|
|
| 219 |
gr.Markdown("#### 2. Take Actions")
|
| 220 |
+
action_dd = gr.Dropdown(["inspect","investigate_network","flag","unflag","submit"], value="inspect", label="Action Type")
|
| 221 |
+
acc_in = gr.Textbox(label="Account ID", placeholder="e.g. acc_0012")
|
|
|
|
|
|
|
|
|
|
| 222 |
step_btn = gr.Button("Step", variant="primary", size="lg")
|
| 223 |
|
| 224 |
+
obs_md = gr.Markdown(value="*Click 'Reset Episode' to begin.*")
|
|
|
|
| 225 |
with gr.Accordion("Account Profiles (sorted by risk)", open=True):
|
| 226 |
+
prof_md = gr.Markdown(value="")
|
|
|
|
| 227 |
with gr.Row():
|
| 228 |
grader_btn = gr.Button("Get Grader Score")
|
| 229 |
baseline_btn = gr.Button("Run Baseline (all 3 tasks)")
|
| 230 |
result_box = gr.Textbox(label="Result", lines=5, interactive=False)
|
| 231 |
+
with gr.Accordion("Raw JSON", open=False):
|
|
|
|
| 232 |
raw_json = gr.Textbox(label="Raw JSON", lines=15, interactive=False)
|
| 233 |
|
| 234 |
+
reset_btn.click(gr_reset, [task_dd, seed_in], [obs_md, prof_md, raw_json])
|
| 235 |
+
step_btn.click(gr_step, [action_dd, acc_in], [obs_md, prof_md, raw_json])
|
| 236 |
grader_btn.click(gr_grader, [], result_box)
|
| 237 |
baseline_btn.click(gr_baseline, [], result_box)
|
| 238 |
|
| 239 |
app = gr.mount_gradio_app(app, demo, path="/")
|
|
|
|
| 240 |
print("[GraphStrike] Gradio UI mounted at /", flush=True)
|
| 241 |
|
| 242 |
except Exception as exc:
|
| 243 |
+
print(f"[GraphStrike] Gradio unavailable: {exc}", flush=True)
|
|
|
|
| 244 |
|
| 245 |
+
@app.get("/", response_class=HTMLResponse)
|
| 246 |
+
def root_fallback():
|
| 247 |
+
return "<html><body><h1>GraphStrike</h1><p>API-only mode. <a href='/docs'>Swagger</a></p></body></html>"
|
| 248 |
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
+
# HF Spaces probes /web — redirect to root (Gradio UI)
|
| 251 |
+
@app.get("/web", response_class=RedirectResponse)
|
| 252 |
+
def web_redirect():
|
| 253 |
+
return RedirectResponse(url="/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
|
| 256 |
# ---------------------------------------------------------------------------
|
| 257 |
# Entry point
|
| 258 |
# ---------------------------------------------------------------------------
|
| 259 |
|
| 260 |
+
def main():
|
|
|
|
|
|
|
| 261 |
import uvicorn
|
| 262 |
port = int(os.environ.get("PORT", 7860))
|
| 263 |
+
print(f"[GraphStrike] Starting on port {port}", flush=True)
|
| 264 |
uvicorn.run("server.app:app", host="0.0.0.0", port=port, log_level="info", workers=1)
|
| 265 |
|
|
|
|
| 266 |
if __name__ == "__main__":
|
| 267 |
main()
|