Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +50 -0
- README.md +273 -12
- __init__.py +29 -0
- baseline_scores.json +27 -0
- client.py +84 -0
- convo_logging/task_1_conversation.md +666 -0
- convo_logging/task_2_conversation.md +0 -0
- convo_logging/task_3_conversation.md +0 -0
- data/__init__.py +18 -0
- data/ad_generator.py +561 -0
- data/advertiser_profiles.py +160 -0
- data/fraud_patterns.py +432 -0
- data/landing_pages.py +187 -0
- data/network_generator.py +194 -0
- graders/__init__.py +14 -0
- graders/base_grader.py +113 -0
- graders/task1_grader.py +49 -0
- graders/task2_grader.py +89 -0
- graders/task3_grader.py +133 -0
- inference.py +451 -0
- models.py +100 -0
- openenv.yaml +7 -0
- openenv_ad_fraud_env.egg-info/PKG-INFO +18 -0
- openenv_ad_fraud_env.egg-info/SOURCES.txt +33 -0
- openenv_ad_fraud_env.egg-info/dependency_links.txt +1 -0
- openenv_ad_fraud_env.egg-info/entry_points.txt +2 -0
- openenv_ad_fraud_env.egg-info/requires.txt +14 -0
- openenv_ad_fraud_env.egg-info/top_level.txt +1 -0
- outputs/task_1_conversation.md +840 -0
- outputs/task_2_conversation.md +1251 -0
- outputs/task_3_conversation.md +1057 -0
- pyproject.toml +44 -0
- server/__init__.py +1 -0
- server/app.py +113 -0
- server/environment.py +559 -0
- server/requirements.txt +9 -0
- tests/__init__.py +0 -0
- tests/test_data_generation.py +141 -0
- tests/test_environment.py +178 -0
- tests/test_graders.py +203 -0
- uv.lock +0 -0
Dockerfile
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 2 |
+
FROM ${BASE_IMAGE} AS builder
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
ARG BUILD_MODE=in-repo
|
| 7 |
+
|
| 8 |
+
COPY . /app/env
|
| 9 |
+
|
| 10 |
+
WORKDIR /app/env
|
| 11 |
+
|
| 12 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 13 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 14 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 15 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 16 |
+
fi
|
| 17 |
+
|
| 18 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 19 |
+
git \
|
| 20 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
+
|
| 22 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 23 |
+
if [ -f uv.lock ]; then \
|
| 24 |
+
uv sync --frozen --no-install-project --no-editable; \
|
| 25 |
+
else \
|
| 26 |
+
uv sync --no-install-project --no-editable; \
|
| 27 |
+
fi
|
| 28 |
+
|
| 29 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 30 |
+
if [ -f uv.lock ]; then \
|
| 31 |
+
uv sync --frozen --no-editable; \
|
| 32 |
+
else \
|
| 33 |
+
uv sync --no-editable; \
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
FROM ${BASE_IMAGE}
|
| 37 |
+
|
| 38 |
+
WORKDIR /app
|
| 39 |
+
|
| 40 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 41 |
+
COPY --from=builder /app/env /app/env
|
| 42 |
+
|
| 43 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 44 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 45 |
+
|
| 46 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 47 |
+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
| 48 |
+
|
| 49 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 50 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -1,12 +1,273 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Ad Fraud Investigation Environment
|
| 3 |
+
emoji: "\U0001F575\uFE0F"
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
- ad-fraud
|
| 13 |
+
- reinforcement-learning
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Ad Fraud Investigation Environment
|
| 17 |
+
|
| 18 |
+
An OpenEnv environment that simulates ad fraud review - a real-world task where AI agents investigate queues of advertisements, uncover fraud signals, and render verdicts under budget constraints.
|
| 19 |
+
|
| 20 |
+
Ad fraud costs the digital advertising industry over **$100 billion annually**. Platforms like Meta process billions of ads daily and ban advertisers only at high confidence thresholds. Unlike simple classification, real ad review is a **sequential decision-making** problem: a reviewer starts with limited surface-level signals, actively chooses what to investigate within a constrained budget, and must decide when enough evidence exists to commit to a verdict. This environment captures that workflow and provides a training ground for agents to learn it.
|
| 21 |
+
|
| 22 |
+
## Quick Start
|
| 23 |
+
|
| 24 |
+
### Install
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
pip install -e .
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### Run the server
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### Use the client
|
| 37 |
+
|
| 38 |
+
```python
|
| 39 |
+
from ad_fraud_env import AdFraudEnv, AdReviewAction
|
| 40 |
+
|
| 41 |
+
with AdFraudEnv(base_url="http://localhost:8000").sync() as env:
|
| 42 |
+
result = env.reset(seed=42, task_id="task_1")
|
| 43 |
+
print(result.observation.queue_summary)
|
| 44 |
+
|
| 45 |
+
# Investigate an ad
|
| 46 |
+
result = env.step(AdReviewAction(
|
| 47 |
+
action_type="investigate",
|
| 48 |
+
ad_id="ad_001",
|
| 49 |
+
investigation_target="landing_page",
|
| 50 |
+
))
|
| 51 |
+
print(result.observation.feedback)
|
| 52 |
+
|
| 53 |
+
# Render a verdict
|
| 54 |
+
result = env.step(AdReviewAction(
|
| 55 |
+
action_type="verdict",
|
| 56 |
+
ad_id="ad_001",
|
| 57 |
+
verdict="reject",
|
| 58 |
+
confidence=0.9,
|
| 59 |
+
))
|
| 60 |
+
print(f"Reward: {result.reward}, Done: {result.done}")
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
### Run with Docker
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
docker build -t ad-fraud-env .
|
| 67 |
+
docker run -p 8000:8000 ad-fraud-env
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
## Environment Design
|
| 71 |
+
|
| 72 |
+
### Episode flow
|
| 73 |
+
|
| 74 |
+
Each episode is a review session. The agent receives a queue of ads and must process them within a limited action budget:
|
| 75 |
+
|
| 76 |
+
```
|
| 77 |
+
reset(task_id, seed)
|
| 78 |
+
|
|
| 79 |
+
v
|
| 80 |
+
+----------------------------------+<----------------------+
|
| 81 |
+
| Observe queue + first ad info | |
|
| 82 |
+
+------------------+---------------+ |
|
| 83 |
+
| |
|
| 84 |
+
v |
|
| 85 |
+
+-------------+ +------------------+ |
|
| 86 |
+
| investigate |---->| Reveal one signal |----------+
|
| 87 |
+
+-------------+ | (costs 1 budget) |
|
| 88 |
+
| +------------------+
|
| 89 |
+
v
|
| 90 |
+
+-------------+ +------------------+
|
| 91 |
+
| verdict |---->| approve / reject |----------+
|
| 92 |
+
+-------------+ | / escalate | |
|
| 93 |
+
| +------------------+ |
|
| 94 |
+
v |
|
| 95 |
+
+--------------+ +------------------+ |
|
| 96 |
+
| link_accounts|---->| Flag fraud ring |----------+
|
| 97 |
+
+--------------+ | (Task 3 only) |
|
| 98 |
+
| +------------------+
|
| 99 |
+
v
|
| 100 |
+
Budget exhausted or all ads reviewed -> episode ends
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### Tasks
|
| 104 |
+
|
| 105 |
+
Three tasks with increasing difficulty test different capabilities:
|
| 106 |
+
|
| 107 |
+
| Task | Name | Ads | Budget | Composition | Challenge |
|
| 108 |
+
|---|---|---:|---:|---|---|
|
| 109 |
+
| 1 | Basic Ad Triage | 5 | 25 | 2 legit, 3 obvious fraud | Learn the investigate -> verdict loop |
|
| 110 |
+
| 2 | Sophisticated Fraud | 12 | 30 | 5 legit, 5 sophisticated scams, 2 gray-area | Triage under budget pressure (~2.5 actions/ad) |
|
| 111 |
+
| 3 | Fraud Network Detection | 20 | 35 | 6 legit, 10 fraud (3 hidden rings), 4 gray-area | Cross-ad reasoning to detect coordinated networks (~1.75 actions/ad) |
|
| 112 |
+
|
| 113 |
+
Task 3 introduces **fraud rings** - clusters of 3-5 ads controlled by the same actor, using varied topologies (cliques, chains, hub-and-spoke). Individual ring members look borderline; the fraud signal is only visible by cross-referencing investigation data across ads (shared payment IDs, matching template hashes, overlapping targeting fingerprints).
|
| 114 |
+
|
| 115 |
+
### Action Space
|
| 116 |
+
|
| 117 |
+
Actions are JSON objects. Three types:
|
| 118 |
+
|
| 119 |
+
**`investigate`** - spend one budget point to reveal a signal about an ad.
|
| 120 |
+
|
| 121 |
+
```json
|
| 122 |
+
{
|
| 123 |
+
"action_type": "investigate",
|
| 124 |
+
"ad_id": "ad_001",
|
| 125 |
+
"investigation_target": "landing_page"
|
| 126 |
+
}
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
Each ad has six investigation dimensions:
|
| 130 |
+
|
| 131 |
+
| Target | What it reveals |
|
| 132 |
+
|---|---|
|
| 133 |
+
| `advertiser_history` | Account age, spend history, violation record, verification status |
|
| 134 |
+
| `landing_page` | Domain age, SSL, registrar, redirect chains, scam template similarity |
|
| 135 |
+
| `payment_method` | Payment type, chargeback history, cross-account velocity |
|
| 136 |
+
| `targeting_overlap` | Targeting fingerprint, audience overlap percentages |
|
| 137 |
+
| `creative_similarity` | Template hash, image dimensions, scam template similarity score |
|
| 138 |
+
| `campaign_structure` | Objective, bid strategy, budget/age ratio, placement distribution |
|
| 139 |
+
|
| 140 |
+
**`verdict`** - render a final decision on an ad.
|
| 141 |
+
|
| 142 |
+
```json
|
| 143 |
+
{
|
| 144 |
+
"action_type": "verdict",
|
| 145 |
+
"ad_id": "ad_001",
|
| 146 |
+
"verdict": "reject",
|
| 147 |
+
"confidence": 0.9
|
| 148 |
+
}
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
`verdict` options: `approve`, `reject`, `escalate`. `confidence`: 0.0-1.0.
|
| 152 |
+
|
| 153 |
+
**`link_accounts`** - flag two ads as part of the same fraud network (Task 3).
|
| 154 |
+
|
| 155 |
+
```json
|
| 156 |
+
{
|
| 157 |
+
"action_type": "link_accounts",
|
| 158 |
+
"ad_id": "ad_003",
|
| 159 |
+
"linked_ad_id": "ad_007",
|
| 160 |
+
"link_reason": "shared payment ID pmt_ring_48231 and matching template hash"
|
| 161 |
+
}
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
### Observation Space
|
| 165 |
+
|
| 166 |
+
Observations are text-heavy by design so LLM agents can reason naturally:
|
| 167 |
+
|
| 168 |
+
| Field | Type | Description |
|
| 169 |
+
|---|---|---|
|
| 170 |
+
| `queue_summary` | `str` | Task name, total/reviewed/pending counts, budget remaining |
|
| 171 |
+
| `current_ad_info` | `str` | Ad copy, category, targeting, risk signals for the focused ad |
|
| 172 |
+
| `investigation_findings` | `str` | Accumulated findings from all investigations so far |
|
| 173 |
+
| `verdict_history_summary` | `str` | Verdicts rendered so far |
|
| 174 |
+
| `feedback` | `str` | Natural language feedback on the last action |
|
| 175 |
+
| `available_ads` | `list[str]` | Ad IDs still pending review |
|
| 176 |
+
| `queue_status` | `dict` | Structured status for programmatic access |
|
| 177 |
+
| `done` | `bool` | Whether the episode is complete |
|
| 178 |
+
| `reward` | `float` | Step reward |
|
| 179 |
+
|
| 180 |
+
## Reward Design
|
| 181 |
+
|
| 182 |
+
| Action | Reward | Rationale |
|
| 183 |
+
|---|---:|---|
|
| 184 |
+
| Investigation | -0.02 | Simulates time/latency cost |
|
| 185 |
+
| Correct rejection (fraud -> reject) | +0.30 to +0.40 | Scaled by fraud severity |
|
| 186 |
+
| Correct approval (legit -> approve) | +0.10 | Revenue preserved |
|
| 187 |
+
| Correct escalation | +0.15 | Appropriate caution |
|
| 188 |
+
| False positive (legit -> reject) | -0.35 | Lost advertiser revenue |
|
| 189 |
+
| False negative (fraud -> approve) | -0.50 | Worst outcome - fraud goes live |
|
| 190 |
+
| Escalate (when wrong) | -0.05 | Human reviewer cost |
|
| 191 |
+
| Correct network link | +0.40 | High-value coordinated fraud detection |
|
| 192 |
+
| Incorrect network link | -0.25 | False accusation cost |
|
| 193 |
+
|
| 194 |
+
Unreviewed ads are auto-approved at episode end - missed fraud incurs the full -0.50 false-negative penalty.
|
| 195 |
+
|
| 196 |
+
## Grading & Scoring
|
| 197 |
+
|
| 198 |
+
Each task has a dedicated grader that produces a normalized **0.0-1.0 score**. Raw reward is normalized between theoretical worst-case (every decision wrong + full budget wasted) and best-case (every decision correct + efficient budget use).
|
| 199 |
+
|
| 200 |
+
| Component | Task 1 | Task 2 | Task 3 |
|
| 201 |
+
|---|:---:|:---:|:---:|
|
| 202 |
+
| Verdict accuracy | Yes | Yes | Yes |
|
| 203 |
+
| Budget efficiency bonus | Yes | Yes | Yes |
|
| 204 |
+
| Calibration bonus | - | Yes | Yes |
|
| 205 |
+
| Network detection (edge coverage) | - | - | Yes |
|
| 206 |
+
| Investigation coverage bonus | - | - | Yes |
|
| 207 |
+
|
| 208 |
+
**Calibration bonus** rewards agents whose stated confidence correlates with actual accuracy - high confidence on correct verdicts and low confidence on uncertain ones.
|
| 209 |
+
|
| 210 |
+
**Network detection** uses edge coverage: what fraction of ground-truth fraud ring connections did the agent discover via `link_accounts`?
|
| 211 |
+
|
| 212 |
+
**Coverage bonus** rewards breadth over depth - agents that review more ads (rather than deep-diving a single one) score higher on Task 3.
|
| 213 |
+
|
| 214 |
+
## Baseline Scores
|
| 215 |
+
|
| 216 |
+
Generated with `seed=42` using `meta-llama/Llama-3.1-8B-Instruct`. Reproducible via `python inference.py`.
|
| 217 |
+
|
| 218 |
+
| Task | Score | Steps | Verdicts |
|
| 219 |
+
|---|---:|---:|---:|
|
| 220 |
+
| Task 1 (Easy) | 0.953 | 10 | 5/5 |
|
| 221 |
+
| Task 2 (Medium) | 0.882 | 23 | 12/12 |
|
| 222 |
+
| Task 3 (Hard) | 0.415 | 35 | 20/20 |
|
| 223 |
+
|
| 224 |
+
The sharp drop on Task 3 reflects the difficulty of cross-ad reasoning under tight budget - the baseline agent investigates and renders verdicts well but struggles to detect coordinated fraud rings.
|
| 225 |
+
|
| 226 |
+
## Project Structure
|
| 227 |
+
|
| 228 |
+
```
|
| 229 |
+
ad_fraud_env/
|
| 230 |
+
+-- __init__.py # Package exports
|
| 231 |
+
+-- client.py # WebSocket client (extends EnvClient)
|
| 232 |
+
+-- models.py # Action, Observation, State types
|
| 233 |
+
+-- inference.py # Baseline LLM agent with mandatory stdout logging
|
| 234 |
+
+-- openenv.yaml # OpenEnv manifest
|
| 235 |
+
+-- pyproject.toml # Dependencies and package config
|
| 236 |
+
+-- Dockerfile # Multi-stage Docker build
|
| 237 |
+
+-- baseline_scores.json # Cached baseline results
|
| 238 |
+
+-- data/
|
| 239 |
+
| +-- ad_generator.py # Episode generation, task configs, campaign profiles
|
| 240 |
+
| +-- advertiser_profiles.py # Synthetic advertiser history
|
| 241 |
+
| +-- fraud_patterns.py # Fraud + legit ad templates (easy/medium/hard)
|
| 242 |
+
| +-- landing_pages.py # Simulated landing page investigation data
|
| 243 |
+
| +-- network_generator.py # Fraud ring topologies via networkx
|
| 244 |
+
+-- graders/
|
| 245 |
+
| +-- base_grader.py # Shared normalization and reward logic
|
| 246 |
+
| +-- task1_grader.py # Verdict accuracy only
|
| 247 |
+
| +-- task2_grader.py # + calibration bonus
|
| 248 |
+
| +-- task3_grader.py # + network detection + coverage bonus
|
| 249 |
+
+-- server/
|
| 250 |
+
| +-- app.py # FastAPI app with /tasks, /baseline, /grader endpoints
|
| 251 |
+
| +-- environment.py # Core environment (reset/step/state)
|
| 252 |
+
| +-- requirements.txt # Server dependencies
|
| 253 |
+
+-- tests/
|
| 254 |
+
+-- test_data_generation.py # Determinism, cross-ref checks, decoy validation
|
| 255 |
+
+-- test_environment.py # Step logic, state tracking, anti-exploit
|
| 256 |
+
+-- test_graders.py # Score ranges, calibration, network scoring
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
## API Endpoints
|
| 260 |
+
|
| 261 |
+
| Endpoint | Method | Description |
|
| 262 |
+
|---|---|---|
|
| 263 |
+
| `/health` | GET | Health check |
|
| 264 |
+
| `/schema` | GET | Action/Observation JSON schemas |
|
| 265 |
+
| `/ws` | WS | WebSocket for `step()` / `reset()` / `state()` |
|
| 266 |
+
| `/tasks` | GET | Task list with configs and action schema |
|
| 267 |
+
| `/baseline` | GET | Baseline scores (cached or live) |
|
| 268 |
+
| `/grader` | GET | Last episode's grader result |
|
| 269 |
+
| `/web` | GET | Auto-generated Gradio UI |
|
| 270 |
+
|
| 271 |
+
## License
|
| 272 |
+
|
| 273 |
+
BSD 3-Clause License
|
__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Ad Fraud Investigation Environment for OpenEnv.
|
| 3 |
+
|
| 4 |
+
A real-world simulation where an AI agent investigates a queue of ads
|
| 5 |
+
for fraud, making sequential investigation and verdict decisions under
|
| 6 |
+
a limited action budget.
|
| 7 |
+
|
| 8 |
+
Example:
|
| 9 |
+
>>> from ad_fraud_env import AdFraudEnv, AdReviewAction
|
| 10 |
+
>>>
|
| 11 |
+
>>> with AdFraudEnv(base_url="http://localhost:8000").sync() as env:
|
| 12 |
+
... result = env.reset(seed=42, task_id="task_1")
|
| 13 |
+
... result = env.step(AdReviewAction(
|
| 14 |
+
... action_type="investigate",
|
| 15 |
+
... ad_id="ad_001",
|
| 16 |
+
... investigation_target="landing_page",
|
| 17 |
+
... ))
|
| 18 |
+
... print(result.observation.feedback)
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from .client import AdFraudEnv
|
| 22 |
+
from .models import AdFraudState, AdReviewAction, AdReviewObservation
|
| 23 |
+
|
| 24 |
+
__all__ = [
|
| 25 |
+
"AdFraudEnv",
|
| 26 |
+
"AdReviewAction",
|
| 27 |
+
"AdReviewObservation",
|
| 28 |
+
"AdFraudState",
|
| 29 |
+
]
|
baseline_scores.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"baseline_model": "llama3.1:8b",
|
| 3 |
+
"seed": 42,
|
| 4 |
+
"tasks": {
|
| 5 |
+
"task_1": {
|
| 6 |
+
"task_id": "task_1",
|
| 7 |
+
"score": 0.9603547186637474,
|
| 8 |
+
"steps": 9,
|
| 9 |
+
"verdicts": 5,
|
| 10 |
+
"total_ads": 5
|
| 11 |
+
},
|
| 12 |
+
"task_2": {
|
| 13 |
+
"task_id": "task_2",
|
| 14 |
+
"score": 0.8877459920214088,
|
| 15 |
+
"steps": 24,
|
| 16 |
+
"verdicts": 12,
|
| 17 |
+
"total_ads": 12
|
| 18 |
+
},
|
| 19 |
+
"task_3": {
|
| 20 |
+
"task_id": "task_3",
|
| 21 |
+
"score": 0.4817339313969324,
|
| 22 |
+
"steps": 35,
|
| 23 |
+
"verdicts": 20,
|
| 24 |
+
"total_ads": 20
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
}
|
client.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Ad Fraud Investigation Environment Client.
|
| 3 |
+
|
| 4 |
+
Provides programmatic access via WebSocket. Users and evaluators interact
|
| 5 |
+
with the environment using:
|
| 6 |
+
|
| 7 |
+
env = AdFraudEnv.from_hub("your-org/ad-fraud-env")
|
| 8 |
+
# or
|
| 9 |
+
env = AdFraudEnv(base_url="http://localhost:8000")
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from typing import Any, Dict
|
| 15 |
+
|
| 16 |
+
from openenv.core.client_types import StepResult
|
| 17 |
+
from openenv.core.env_client import EnvClient
|
| 18 |
+
|
| 19 |
+
from .models import AdFraudState, AdReviewAction, AdReviewObservation
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class AdFraudEnv(EnvClient[AdReviewAction, AdReviewObservation, AdFraudState]):
|
| 23 |
+
"""
|
| 24 |
+
WebSocket client for the Ad Fraud Investigation Environment.
|
| 25 |
+
|
| 26 |
+
Example:
|
| 27 |
+
>>> with AdFraudEnv(base_url="http://localhost:8000").sync() as env:
|
| 28 |
+
... result = env.reset(seed=42, task_id="task_1")
|
| 29 |
+
... print(result.observation.queue_summary)
|
| 30 |
+
... result = env.step(AdReviewAction(
|
| 31 |
+
... action_type="investigate",
|
| 32 |
+
... ad_id="ad_001",
|
| 33 |
+
... investigation_target="advertiser_history",
|
| 34 |
+
... ))
|
| 35 |
+
... print(result.observation.feedback)
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def _step_payload(self, action: AdReviewAction) -> Dict[str, Any]:
|
| 39 |
+
"""Convert action to JSON payload for the WebSocket step message."""
|
| 40 |
+
return action.model_dump(exclude_none=True, exclude={"metadata"})
|
| 41 |
+
|
| 42 |
+
def _parse_result(
|
| 43 |
+
self, payload: Dict[str, Any]
|
| 44 |
+
) -> StepResult[AdReviewObservation]:
|
| 45 |
+
"""Parse server response into a typed StepResult.
|
| 46 |
+
|
| 47 |
+
OpenEnv serializes as {"observation": {...}, "reward": float, "done": bool}
|
| 48 |
+
with reward/done at the top level, excluded from the observation dict.
|
| 49 |
+
"""
|
| 50 |
+
obs_data = payload.get("observation", {})
|
| 51 |
+
reward = payload.get("reward", 0.0) or 0.0
|
| 52 |
+
done = payload.get("done", False)
|
| 53 |
+
|
| 54 |
+
observation = AdReviewObservation(
|
| 55 |
+
done=done,
|
| 56 |
+
reward=reward,
|
| 57 |
+
queue_summary=obs_data.get("queue_summary", ""),
|
| 58 |
+
current_ad_info=obs_data.get("current_ad_info", ""),
|
| 59 |
+
investigation_findings=obs_data.get("investigation_findings", ""),
|
| 60 |
+
verdict_history_summary=obs_data.get("verdict_history_summary", ""),
|
| 61 |
+
feedback=obs_data.get("feedback", ""),
|
| 62 |
+
available_ads=obs_data.get("available_ads", []),
|
| 63 |
+
queue_status=obs_data.get("queue_status", {}),
|
| 64 |
+
metadata=obs_data.get("metadata", {}),
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
return StepResult(
|
| 68 |
+
observation=observation,
|
| 69 |
+
reward=reward,
|
| 70 |
+
done=done,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def _parse_state(self, payload: Dict[str, Any]) -> AdFraudState:
|
| 74 |
+
"""Parse server state response into AdFraudState."""
|
| 75 |
+
return AdFraudState(
|
| 76 |
+
episode_id=payload.get("episode_id"),
|
| 77 |
+
step_count=payload.get("step_count", 0),
|
| 78 |
+
task_id=payload.get("task_id", ""),
|
| 79 |
+
total_ads=payload.get("total_ads", 0),
|
| 80 |
+
reviewed_count=payload.get("reviewed_count", 0),
|
| 81 |
+
remaining_budget=payload.get("remaining_budget", 0),
|
| 82 |
+
verdicts=payload.get("verdicts", {}),
|
| 83 |
+
grader_score=payload.get("grader_score"),
|
| 84 |
+
)
|
convo_logging/task_1_conversation.md
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Episode Log — task_1
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
## Step 0
|
| 5 |
+
|
| 6 |
+
### Observation (sent to LLM)
|
| 7 |
+
```
|
| 8 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 9 |
+
Total ads: 5 | Reviewed: 0 | Pending: 5 | Steps remaining: 25/25 | Investigation budget: 25 | Step: 0
|
| 10 |
+
|
| 11 |
+
Current Ad: === Ad in Focus: ad_001 ===
|
| 12 |
+
Category: local_service
|
| 13 |
+
Ad copy: "Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria."
|
| 14 |
+
Targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 15 |
+
Initial risk signals: None
|
| 16 |
+
Advertiser country: Germany
|
| 17 |
+
Account age: 1132 days
|
| 18 |
+
Investigations completed: None yet
|
| 19 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 20 |
+
|
| 21 |
+
Feedback: Episode started. Review the ad queue and begin your investigation.
|
| 22 |
+
|
| 23 |
+
Available ads: ad_001, ad_002, ad_003, ad_004, ad_005
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
### LLM Response
|
| 27 |
+
```json
|
| 28 |
+
{
|
| 29 |
+
"action_type": "investigate",
|
| 30 |
+
"ad_id": "ad_001",
|
| 31 |
+
"investigation_target": "targeting_overlap"
|
| 32 |
+
}
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Parsed Action
|
| 36 |
+
```json
|
| 37 |
+
{
|
| 38 |
+
"action_type": "investigate",
|
| 39 |
+
"ad_id": "ad_001",
|
| 40 |
+
"investigation_target": "targeting_overlap"
|
| 41 |
+
}
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### Environment Response
|
| 45 |
+
- **Reward:** `-0.02`
|
| 46 |
+
- **Done:** `False`
|
| 47 |
+
|
| 48 |
+
- **Feedback:** Investigation complete: targeting_overlap for ad_001.
|
| 49 |
+
--- Findings ---
|
| 50 |
+
Targeting Analysis for ad_001:
|
| 51 |
+
Declared targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 52 |
+
Targeting fingerprint: seg_20328
|
| 53 |
+
Audience overlap with platform average for category: 27%
|
| 54 |
+
Geographic regions targeted: 2
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
## Step 1
|
| 58 |
+
|
| 59 |
+
### Observation (sent to LLM)
|
| 60 |
+
```
|
| 61 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 62 |
+
Total ads: 5 | Reviewed: 0 | Pending: 5 | Steps remaining: 24/25 | Investigation budget: 24 | Step: 1
|
| 63 |
+
|
| 64 |
+
Current Ad: === Ad in Focus: ad_001 ===
|
| 65 |
+
Category: local_service
|
| 66 |
+
Ad copy: "Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria."
|
| 67 |
+
Targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 68 |
+
Initial risk signals: None
|
| 69 |
+
Advertiser country: Germany
|
| 70 |
+
Account age: 1132 days
|
| 71 |
+
Investigations completed: targeting_overlap
|
| 72 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 73 |
+
|
| 74 |
+
Feedback: Investigation complete: targeting_overlap for ad_001.
|
| 75 |
+
--- Findings ---
|
| 76 |
+
Targeting Analysis for ad_001:
|
| 77 |
+
Declared targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 78 |
+
Targeting fingerprint: seg_20328
|
| 79 |
+
Audience overlap with platform average for category: 27%
|
| 80 |
+
Geographic regions targeted: 2
|
| 81 |
+
|
| 82 |
+
Available ads: ad_001, ad_002, ad_003, ad_004, ad_005
|
| 83 |
+
|
| 84 |
+
Findings:
|
| 85 |
+
|
| 86 |
+
[ad_001 / targeting_overlap]
|
| 87 |
+
Targeting Analysis for ad_001:
|
| 88 |
+
Declared targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 89 |
+
Targeting fingerprint: seg_20328
|
| 90 |
+
Audience overlap with platform average for category: 27%
|
| 91 |
+
Geographic regions targeted: 2
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
### LLM Response
|
| 95 |
+
```json
|
| 96 |
+
{
|
| 97 |
+
"action_type": "verdict",
|
| 98 |
+
"ad_id": "ad_001",
|
| 99 |
+
"verdict": "approve",
|
| 100 |
+
"confidence": 0.9
|
| 101 |
+
}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Parsed Action
|
| 105 |
+
```json
|
| 106 |
+
{
|
| 107 |
+
"action_type": "verdict",
|
| 108 |
+
"ad_id": "ad_001",
|
| 109 |
+
"verdict": "approve",
|
| 110 |
+
"confidence": 0.9
|
| 111 |
+
}
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### Environment Response
|
| 115 |
+
- **Reward:** `+0.10`
|
| 116 |
+
- **Done:** `False`
|
| 117 |
+
|
| 118 |
+
- **Feedback:** Verdict recorded for ad_001: approve (confidence: 0.90). 4 ad(s) remaining in queue.
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
## Step 2
|
| 122 |
+
|
| 123 |
+
### Observation (sent to LLM)
|
| 124 |
+
```
|
| 125 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 126 |
+
Total ads: 5 | Reviewed: 1 | Pending: 4 | Steps remaining: 23/25 | Investigation budget: 24 | Step: 2
|
| 127 |
+
|
| 128 |
+
Current Ad: === Ad in Focus: ad_002 ===
|
| 129 |
+
Category: counterfeit_goods
|
| 130 |
+
Ad copy: "Rolex watches starting at $49.99! 100% genuine! Free worldwide shipping! LuxWatchDeals.net"
|
| 131 |
+
Targeting: Men 25-55, interests: watches, luxury
|
| 132 |
+
Initial risk signals: impossibly low prices for luxury goods, unfamiliar domain, claims of authenticity with extreme discounts
|
| 133 |
+
Advertiser country: Estonia
|
| 134 |
+
Account age: 6 days
|
| 135 |
+
Flag: New account (< 30 days)
|
| 136 |
+
Investigations completed: None yet
|
| 137 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 138 |
+
|
| 139 |
+
Feedback: Verdict recorded for ad_001: approve (confidence: 0.90). 4 ad(s) remaining in queue.
|
| 140 |
+
|
| 141 |
+
Available ads: ad_002, ad_003, ad_004, ad_005
|
| 142 |
+
|
| 143 |
+
Verdicts: Reviewed 1 ad(s): 1 approve.
|
| 144 |
+
approve: ad_001
|
| 145 |
+
|
| 146 |
+
Findings:
|
| 147 |
+
[ad_001 / targeting_overlap] Declared targeting: Adults 18-55, Brooklyn, interests: food delivery | Targeting fingerprint: seg_20328 | Audience overlap with platform average for category: 27% | Geographic regions targeted: 2
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
### LLM Response
|
| 151 |
+
```json
|
| 152 |
+
{
|
| 153 |
+
"action_type": "investigate",
|
| 154 |
+
"ad_id": "ad_002",
|
| 155 |
+
"investigation_target": "landing_page"
|
| 156 |
+
}
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
### Parsed Action
|
| 160 |
+
```json
|
| 161 |
+
{
|
| 162 |
+
"action_type": "investigate",
|
| 163 |
+
"ad_id": "ad_002",
|
| 164 |
+
"investigation_target": "landing_page"
|
| 165 |
+
}
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
### Environment Response
|
| 169 |
+
- **Reward:** `-0.02`
|
| 170 |
+
- **Done:** `False`
|
| 171 |
+
|
| 172 |
+
- **Feedback:** Investigation complete: landing_page for ad_002.
|
| 173 |
+
--- Findings ---
|
| 174 |
+
Domain: elite2813.buzz
|
| 175 |
+
Domain age: 35 days
|
| 176 |
+
SSL: NO SSL / expired certificate
|
| 177 |
+
Registrar: Epik
|
| 178 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 179 |
+
Redirect chain: elite2813.buzz -> redirect948.xyz -> redirect705.xyz -> redirect225.xyz -> final-333.site
|
| 180 |
+
Similarity to known scam templates: 71%
|
| 181 |
+
Key claims on page:
|
| 182 |
+
- Authentic products
|
| 183 |
+
- Direct from manufacturer
|
| 184 |
+
- 90% below retail price
|
| 185 |
+
Suspicious elements:
|
| 186 |
+
- Product images appear stolen from official brand site
|
| 187 |
+
- Terms of service link returns 404
|
| 188 |
+
Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
## Step 3
|
| 192 |
+
|
| 193 |
+
### Observation (sent to LLM)
|
| 194 |
+
```
|
| 195 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 196 |
+
Total ads: 5 | Reviewed: 1 | Pending: 4 | Steps remaining: 22/25 | Investigation budget: 23 | Step: 3
|
| 197 |
+
|
| 198 |
+
Current Ad: === Ad in Focus: ad_002 ===
|
| 199 |
+
Category: counterfeit_goods
|
| 200 |
+
Ad copy: "Rolex watches starting at $49.99! 100% genuine! Free worldwide shipping! LuxWatchDeals.net"
|
| 201 |
+
Targeting: Men 25-55, interests: watches, luxury
|
| 202 |
+
Initial risk signals: impossibly low prices for luxury goods, unfamiliar domain, claims of authenticity with extreme discounts
|
| 203 |
+
Advertiser country: Estonia
|
| 204 |
+
Account age: 6 days
|
| 205 |
+
Flag: New account (< 30 days)
|
| 206 |
+
Investigations completed: landing_page
|
| 207 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 208 |
+
|
| 209 |
+
Feedback: Investigation complete: landing_page for ad_002.
|
| 210 |
+
--- Findings ---
|
| 211 |
+
Domain: elite2813.buzz
|
| 212 |
+
Domain age: 35 days
|
| 213 |
+
SSL: NO SSL / expired certificate
|
| 214 |
+
Registrar: Epik
|
| 215 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 216 |
+
Redirect chain: elite2813.buzz -> redirect948.xyz -> redirect705.xyz -> redirect225.xyz -> final-333.site
|
| 217 |
+
Similarity to known scam templates: 71%
|
| 218 |
+
Key claims on page:
|
| 219 |
+
- Authentic products
|
| 220 |
+
- Direct from manufacturer
|
| 221 |
+
- 90% below retail price
|
| 222 |
+
Suspicious elements:
|
| 223 |
+
- Product images appear stolen from official brand site
|
| 224 |
+
- Terms of service link returns 404
|
| 225 |
+
Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 226 |
+
|
| 227 |
+
Available ads: ad_002, ad_003, ad_004, ad_005
|
| 228 |
+
|
| 229 |
+
Verdicts: Reviewed 1 ad(s): 1 approve.
|
| 230 |
+
approve: ad_001
|
| 231 |
+
|
| 232 |
+
Findings:
|
| 233 |
+
[ad_001 / targeting_overlap] Declared targeting: Adults 18-55, Brooklyn, interests: food delivery | Targeting fingerprint: seg_20328 | Audience overlap with platform average for category: 27% | Geographic regions targeted: 2
|
| 234 |
+
|
| 235 |
+
[ad_002 / landing_page]
|
| 236 |
+
Domain: elite2813.buzz
|
| 237 |
+
Domain age: 35 days
|
| 238 |
+
SSL: NO SSL / expired certificate
|
| 239 |
+
Registrar: Epik
|
| 240 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 241 |
+
Redirect chain: elite2813.buzz -> redirect948.xyz -> redirect705.xyz -> redirect225.xyz -> final-333.site
|
| 242 |
+
Similarity to known scam templates: 71%
|
| 243 |
+
Key claims on page:
|
| 244 |
+
- Authentic products
|
| 245 |
+
- Direct from manufacturer
|
| 246 |
+
- 90% below retail price
|
| 247 |
+
Suspicious elements:
|
| 248 |
+
- Product images appear stolen from official brand site
|
| 249 |
+
- Terms of service link returns 404
|
| 250 |
+
Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
### LLM Response
|
| 254 |
+
```json
|
| 255 |
+
{
|
| 256 |
+
"action_type": "verdict",
|
| 257 |
+
"ad_id": "ad_002",
|
| 258 |
+
"verdict": "reject",
|
| 259 |
+
"confidence": 0.95
|
| 260 |
+
}
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
### Parsed Action
|
| 264 |
+
```json
|
| 265 |
+
{
|
| 266 |
+
"action_type": "verdict",
|
| 267 |
+
"ad_id": "ad_002",
|
| 268 |
+
"verdict": "reject",
|
| 269 |
+
"confidence": 0.95
|
| 270 |
+
}
|
| 271 |
+
```
|
| 272 |
+
|
| 273 |
+
### Environment Response
|
| 274 |
+
- **Reward:** `+0.38`
|
| 275 |
+
- **Done:** `False`
|
| 276 |
+
|
| 277 |
+
- **Feedback:** Verdict recorded for ad_002: reject (confidence: 0.95). 3 ad(s) remaining in queue.
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
## Step 4
|
| 281 |
+
|
| 282 |
+
### Observation (sent to LLM)
|
| 283 |
+
```
|
| 284 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 285 |
+
Total ads: 5 | Reviewed: 2 | Pending: 3 | Steps remaining: 21/25 | Investigation budget: 23 | Step: 4
|
| 286 |
+
|
| 287 |
+
Current Ad: === Ad in Focus: ad_003 ===
|
| 288 |
+
Category: fake_giveaway
|
| 289 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 290 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 291 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 292 |
+
Advertiser country: Estonia
|
| 293 |
+
Account age: 19 days
|
| 294 |
+
Flag: New account (< 30 days)
|
| 295 |
+
Investigations completed: None yet
|
| 296 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 297 |
+
|
| 298 |
+
Feedback: Verdict recorded for ad_002: reject (confidence: 0.95). 3 ad(s) remaining in queue.
|
| 299 |
+
|
| 300 |
+
Available ads: ad_003, ad_004, ad_005
|
| 301 |
+
|
| 302 |
+
Verdicts: Reviewed 2 ad(s): 1 approve, 1 reject.
|
| 303 |
+
reject: ad_002
|
| 304 |
+
approve: ad_001
|
| 305 |
+
|
| 306 |
+
Findings:
|
| 307 |
+
[ad_001 / targeting_overlap] Declared targeting: Adults 18-55, Brooklyn, interests: food delivery | Targeting fingerprint: seg_20328 | Audience overlap with platform average for category: 27% | Geographic regions targeted: 2
|
| 308 |
+
[ad_002 / landing_page] Domain: elite2813.buzz | Domain age: 35 days | SSL: NO SSL / expired certificate | Registrar: Epik | WHOIS privacy: Disabled (registrant info public) | Redirect chain: elite2813.buzz -> redirect948.xyz -> redirect705.xyz -> redirect225.xyz -> final-333.site | Similarity to known scam templates: 71% | Authentic products | Direct from manufacturer | 90% below retail price | Product images appear stolen from official brand site | Terms of service link returns 404 | Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 309 |
+
```
|
| 310 |
+
|
| 311 |
+
### LLM Response
|
| 312 |
+
```json
|
| 313 |
+
{
|
| 314 |
+
"action_type": "investigate",
|
| 315 |
+
"ad_id": "ad_003",
|
| 316 |
+
"investigation_target": "landing_page"
|
| 317 |
+
}
|
| 318 |
+
```
|
| 319 |
+
|
| 320 |
+
### Parsed Action
|
| 321 |
+
```json
|
| 322 |
+
{
|
| 323 |
+
"action_type": "investigate",
|
| 324 |
+
"ad_id": "ad_003",
|
| 325 |
+
"investigation_target": "landing_page"
|
| 326 |
+
}
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
### Environment Response
|
| 330 |
+
- **Reward:** `-0.02`
|
| 331 |
+
- **Done:** `False`
|
| 332 |
+
|
| 333 |
+
- **Feedback:** Investigation complete: landing_page for ad_003.
|
| 334 |
+
--- Findings ---
|
| 335 |
+
Domain: shop2631.buzz
|
| 336 |
+
Domain age: 51 days
|
| 337 |
+
SSL: NO SSL / expired certificate
|
| 338 |
+
Registrar: Openprovider
|
| 339 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 340 |
+
Redirect chain: shop2631.buzz -> redirect611.top -> redirect542.top -> redirect995.xyz -> redirect882.click -> final-697.site
|
| 341 |
+
Similarity to known scam templates: 53%
|
| 342 |
+
Key claims on page:
|
| 343 |
+
- 100% free, no purchase necessary
|
| 344 |
+
- Guaranteed winner
|
| 345 |
+
- Act now — expires in 2 hours
|
| 346 |
+
Suspicious elements:
|
| 347 |
+
- No physical address listed
|
| 348 |
+
- No contact phone number
|
| 349 |
+
Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
## Step 5
|
| 353 |
+
|
| 354 |
+
### Observation (sent to LLM)
|
| 355 |
+
```
|
| 356 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 357 |
+
Total ads: 5 | Reviewed: 2 | Pending: 3 | Steps remaining: 20/25 | Investigation budget: 22 | Step: 5
|
| 358 |
+
|
| 359 |
+
Current Ad: === Ad in Focus: ad_003 ===
|
| 360 |
+
Category: fake_giveaway
|
| 361 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 362 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 363 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 364 |
+
Advertiser country: Estonia
|
| 365 |
+
Account age: 19 days
|
| 366 |
+
Flag: New account (< 30 days)
|
| 367 |
+
Investigations completed: landing_page
|
| 368 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 369 |
+
|
| 370 |
+
Feedback: Investigation complete: landing_page for ad_003.
|
| 371 |
+
--- Findings ---
|
| 372 |
+
Domain: shop2631.buzz
|
| 373 |
+
Domain age: 51 days
|
| 374 |
+
SSL: NO SSL / expired certificate
|
| 375 |
+
Registrar: Openprovider
|
| 376 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 377 |
+
Redirect chain: shop2631.buzz -> redirect611.top -> redirect542.top -> redirect995.xyz -> redirect882.click -> final-697.site
|
| 378 |
+
Similarity to known scam templates: 53%
|
| 379 |
+
Key claims on page:
|
| 380 |
+
- 100% free, no purchase necessary
|
| 381 |
+
- Guaranteed winner
|
| 382 |
+
- Act now — expires in 2 hours
|
| 383 |
+
Suspicious elements:
|
| 384 |
+
- No physical address listed
|
| 385 |
+
- No contact phone number
|
| 386 |
+
Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 387 |
+
|
| 388 |
+
Available ads: ad_003, ad_004, ad_005
|
| 389 |
+
|
| 390 |
+
Verdicts: Reviewed 2 ad(s): 1 approve, 1 reject.
|
| 391 |
+
reject: ad_002
|
| 392 |
+
approve: ad_001
|
| 393 |
+
|
| 394 |
+
Findings:
|
| 395 |
+
[ad_001 / targeting_overlap] Declared targeting: Adults 18-55, Brooklyn, interests: food delivery | Targeting fingerprint: seg_20328 | Audience overlap with platform average for category: 27% | Geographic regions targeted: 2
|
| 396 |
+
[ad_002 / landing_page] Domain: elite2813.buzz | Domain age: 35 days | SSL: NO SSL / expired certificate | Registrar: Epik | WHOIS privacy: Disabled (registrant info public) | Redirect chain: elite2813.buzz -> redirect948.xyz -> redirect705.xyz -> redirect225.xyz -> final-333.site | Similarity to known scam templates: 71% | Authentic products | Direct from manufacturer | 90% below retail price | Product images appear stolen from official brand site | Terms of service link returns 404 | Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 397 |
+
|
| 398 |
+
[ad_003 / landing_page]
|
| 399 |
+
Domain: shop2631.buzz
|
| 400 |
+
Domain age: 51 days
|
| 401 |
+
SSL: NO SSL / expired certificate
|
| 402 |
+
Registrar: Openprovider
|
| 403 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 404 |
+
Redirect chain: shop2631.buzz -> redirect611.top -> redirect542.top -> redirect995.xyz -> redirect882.click -> final-697.site
|
| 405 |
+
Similarity to known scam templates: 53%
|
| 406 |
+
Key claims on page:
|
| 407 |
+
- 100% free, no purchase necessary
|
| 408 |
+
- Guaranteed winner
|
| 409 |
+
- Act now — expires in 2 hours
|
| 410 |
+
Suspicious elements:
|
| 411 |
+
- No physical address listed
|
| 412 |
+
- No contact phone number
|
| 413 |
+
Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 414 |
+
```
|
| 415 |
+
|
| 416 |
+
### LLM Response
|
| 417 |
+
```json
|
| 418 |
+
{
|
| 419 |
+
"action_type": "verdict",
|
| 420 |
+
"ad_id": "ad_003",
|
| 421 |
+
"verdict": "reject",
|
| 422 |
+
"confidence": 0.85
|
| 423 |
+
}
|
| 424 |
+
```
|
| 425 |
+
|
| 426 |
+
### Parsed Action
|
| 427 |
+
```json
|
| 428 |
+
{
|
| 429 |
+
"action_type": "verdict",
|
| 430 |
+
"ad_id": "ad_003",
|
| 431 |
+
"verdict": "reject",
|
| 432 |
+
"confidence": 0.85
|
| 433 |
+
}
|
| 434 |
+
```
|
| 435 |
+
|
| 436 |
+
### Environment Response
|
| 437 |
+
- **Reward:** `+0.39`
|
| 438 |
+
- **Done:** `False`
|
| 439 |
+
|
| 440 |
+
- **Feedback:** Verdict recorded for ad_003: reject (confidence: 0.85). 2 ad(s) remaining in queue.
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
## Step 6
|
| 444 |
+
|
| 445 |
+
### Observation (sent to LLM)
|
| 446 |
+
```
|
| 447 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 448 |
+
Total ads: 5 | Reviewed: 3 | Pending: 2 | Steps remaining: 19/25 | Investigation budget: 22 | Step: 6
|
| 449 |
+
|
| 450 |
+
Current Ad: === Ad in Focus: ad_004 ===
|
| 451 |
+
Category: ecommerce
|
| 452 |
+
Ad copy: "Spring Sale — Up to 30% off home essentials. Free shipping on orders over $50. Shop now at HomeNest."
|
| 453 |
+
Targeting: Women 25-54, interests: home decor, shopping
|
| 454 |
+
Initial risk signals: None
|
| 455 |
+
Advertiser country: Germany
|
| 456 |
+
Account age: 615 days
|
| 457 |
+
Investigations completed: None yet
|
| 458 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 459 |
+
|
| 460 |
+
Feedback: Verdict recorded for ad_003: reject (confidence: 0.85). 2 ad(s) remaining in queue.
|
| 461 |
+
|
| 462 |
+
Available ads: ad_004, ad_005
|
| 463 |
+
|
| 464 |
+
Verdicts: Reviewed 3 ad(s): 1 approve, 2 reject.
|
| 465 |
+
reject: ad_002, ad_003
|
| 466 |
+
approve: ad_001
|
| 467 |
+
|
| 468 |
+
Findings:
|
| 469 |
+
[ad_001 / targeting_overlap] Declared targeting: Adults 18-55, Brooklyn, interests: food delivery | Targeting fingerprint: seg_20328 | Audience overlap with platform average for category: 27% | Geographic regions targeted: 2
|
| 470 |
+
[ad_002 / landing_page] Domain: elite2813.buzz | Domain age: 35 days | SSL: NO SSL / expired certificate | Registrar: Epik | WHOIS privacy: Disabled (registrant info public) | Redirect chain: elite2813.buzz -> redirect948.xyz -> redirect705.xyz -> redirect225.xyz -> final-333.site | Similarity to known scam templates: 71% | Authentic products | Direct from manufacturer | 90% below retail price | Product images appear stolen from official brand site | Terms of service link returns 404 | Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 471 |
+
[ad_003 / landing_page] Domain: shop2631.buzz | Domain age: 51 days | SSL: NO SSL / expired certificate | Registrar: Openprovider | WHOIS privacy: Disabled (registrant info public) | Redirect chain: shop2631.buzz -> redirect611.top -> redirect542.top -> redirect995.xyz -> redirect882.click -> final-697.site | Similarity to known scam templates: 53% | 100% free, no purchase necessary | Guaranteed winner | Act now — expires in 2 hours | No physical address listed | No contact phone number | Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 472 |
+
```
|
| 473 |
+
|
| 474 |
+
### LLM Response
|
| 475 |
+
```json
|
| 476 |
+
{
|
| 477 |
+
"action_type": "investigate",
|
| 478 |
+
"ad_id": "ad_004",
|
| 479 |
+
"investigation_target": "landing_page"
|
| 480 |
+
}
|
| 481 |
+
```
|
| 482 |
+
|
| 483 |
+
### Parsed Action
|
| 484 |
+
```json
|
| 485 |
+
{
|
| 486 |
+
"action_type": "investigate",
|
| 487 |
+
"ad_id": "ad_004",
|
| 488 |
+
"investigation_target": "landing_page"
|
| 489 |
+
}
|
| 490 |
+
```
|
| 491 |
+
|
| 492 |
+
### Environment Response
|
| 493 |
+
- **Reward:** `-0.02`
|
| 494 |
+
- **Done:** `False`
|
| 495 |
+
|
| 496 |
+
- **Feedback:** Investigation complete: landing_page for ad_004.
|
| 497 |
+
--- Findings ---
|
| 498 |
+
Domain: homenest.com
|
| 499 |
+
Domain age: 3338 days
|
| 500 |
+
SSL: Valid SSL certificate
|
| 501 |
+
Registrar: Google Domains
|
| 502 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 503 |
+
Redirect chain: No redirects (direct)
|
| 504 |
+
Similarity to known scam templates: 4%
|
| 505 |
+
Key claims on page:
|
| 506 |
+
- 14-day free trial
|
| 507 |
+
- Money-back guarantee
|
| 508 |
+
Suspicious elements:
|
| 509 |
+
None detected
|
| 510 |
+
Content summary: SaaS landing page with feature breakdown, pricing tiers, integration docs, and live demo option.
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
## Step 7
|
| 514 |
+
|
| 515 |
+
### Observation (sent to LLM)
|
| 516 |
+
```
|
| 517 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 518 |
+
Total ads: 5 | Reviewed: 3 | Pending: 2 | Steps remaining: 18/25 | Investigation budget: 21 | Step: 7
|
| 519 |
+
|
| 520 |
+
Current Ad: === Ad in Focus: ad_004 ===
|
| 521 |
+
Category: ecommerce
|
| 522 |
+
Ad copy: "Spring Sale — Up to 30% off home essentials. Free shipping on orders over $50. Shop now at HomeNest."
|
| 523 |
+
Targeting: Women 25-54, interests: home decor, shopping
|
| 524 |
+
Initial risk signals: None
|
| 525 |
+
Advertiser country: Germany
|
| 526 |
+
Account age: 615 days
|
| 527 |
+
Investigations completed: landing_page
|
| 528 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 529 |
+
|
| 530 |
+
Feedback: Investigation complete: landing_page for ad_004.
|
| 531 |
+
--- Findings ---
|
| 532 |
+
Domain: homenest.com
|
| 533 |
+
Domain age: 3338 days
|
| 534 |
+
SSL: Valid SSL certificate
|
| 535 |
+
Registrar: Google Domains
|
| 536 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 537 |
+
Redirect chain: No redirects (direct)
|
| 538 |
+
Similarity to known scam templates: 4%
|
| 539 |
+
Key claims on page:
|
| 540 |
+
- 14-day free trial
|
| 541 |
+
- Money-back guarantee
|
| 542 |
+
Suspicious elements:
|
| 543 |
+
None detected
|
| 544 |
+
Content summary: SaaS landing page with feature breakdown, pricing tiers, integration docs, and live demo option.
|
| 545 |
+
|
| 546 |
+
Available ads: ad_004, ad_005
|
| 547 |
+
|
| 548 |
+
Verdicts: Reviewed 3 ad(s): 1 approve, 2 reject.
|
| 549 |
+
reject: ad_002, ad_003
|
| 550 |
+
approve: ad_001
|
| 551 |
+
|
| 552 |
+
Findings:
|
| 553 |
+
[ad_001 / targeting_overlap] Declared targeting: Adults 18-55, Brooklyn, interests: food delivery | Targeting fingerprint: seg_20328 | Audience overlap with platform average for category: 27% | Geographic regions targeted: 2
|
| 554 |
+
[ad_002 / landing_page] Domain: elite2813.buzz | Domain age: 35 days | SSL: NO SSL / expired certificate | Registrar: Epik | WHOIS privacy: Disabled (registrant info public) | Redirect chain: elite2813.buzz -> redirect948.xyz -> redirect705.xyz -> redirect225.xyz -> final-333.site | Similarity to known scam templates: 71% | Authentic products | Direct from manufacturer | 90% below retail price | Product images appear stolen from official brand site | Terms of service link returns 404 | Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 555 |
+
[ad_003 / landing_page] Domain: shop2631.buzz | Domain age: 51 days | SSL: NO SSL / expired certificate | Registrar: Openprovider | WHOIS privacy: Disabled (registrant info public) | Redirect chain: shop2631.buzz -> redirect611.top -> redirect542.top -> redirect995.xyz -> redirect882.click -> final-697.site | Similarity to known scam templates: 53% | 100% free, no purchase necessary | Guaranteed winner | Act now — expires in 2 hours | No physical address listed | No contact phone number | Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 556 |
+
|
| 557 |
+
[ad_004 / landing_page]
|
| 558 |
+
Domain: homenest.com
|
| 559 |
+
Domain age: 3338 days
|
| 560 |
+
SSL: Valid SSL certificate
|
| 561 |
+
Registrar: Google Domains
|
| 562 |
+
WHOIS privacy: Disabled (registrant info public)
|
| 563 |
+
Redirect chain: No redirects (direct)
|
| 564 |
+
Similarity to known scam templates: 4%
|
| 565 |
+
Key claims on page:
|
| 566 |
+
- 14-day free trial
|
| 567 |
+
- Money-back guarantee
|
| 568 |
+
Suspicious elements:
|
| 569 |
+
None detected
|
| 570 |
+
Content summary: SaaS landing page with feature breakdown, pricing tiers, integration docs, and live demo option.
|
| 571 |
+
```
|
| 572 |
+
|
| 573 |
+
### LLM Response
|
| 574 |
+
```json
|
| 575 |
+
{
|
| 576 |
+
"action_type": "verdict",
|
| 577 |
+
"ad_id": "ad_004",
|
| 578 |
+
"verdict": "approve",
|
| 579 |
+
"confidence": 0.95
|
| 580 |
+
}
|
| 581 |
+
```
|
| 582 |
+
|
| 583 |
+
### Parsed Action
|
| 584 |
+
```json
|
| 585 |
+
{
|
| 586 |
+
"action_type": "verdict",
|
| 587 |
+
"ad_id": "ad_004",
|
| 588 |
+
"verdict": "approve",
|
| 589 |
+
"confidence": 0.95
|
| 590 |
+
}
|
| 591 |
+
```
|
| 592 |
+
|
| 593 |
+
### Environment Response
|
| 594 |
+
- **Reward:** `+0.10`
|
| 595 |
+
- **Done:** `False`
|
| 596 |
+
|
| 597 |
+
- **Feedback:** Verdict recorded for ad_004: approve (confidence: 0.95). 1 ad(s) remaining in queue.
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
## Step 8
|
| 601 |
+
|
| 602 |
+
### Observation (sent to LLM)
|
| 603 |
+
```
|
| 604 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 605 |
+
Total ads: 5 | Reviewed: 4 | Pending: 1 | Steps remaining: 17/25 | Investigation budget: 21 | Step: 8
|
| 606 |
+
|
| 607 |
+
Current Ad: === Ad in Focus: ad_005 ===
|
| 608 |
+
Category: fake_giveaway
|
| 609 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 610 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 611 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 612 |
+
Advertiser country: Singapore
|
| 613 |
+
Account age: 40 days
|
| 614 |
+
Investigations completed: None yet
|
| 615 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity, campaign_structure
|
| 616 |
+
|
| 617 |
+
Feedback: Verdict recorded for ad_004: approve (confidence: 0.95). 1 ad(s) remaining in queue.
|
| 618 |
+
|
| 619 |
+
Available ads: ad_005
|
| 620 |
+
|
| 621 |
+
Verdicts: Reviewed 4 ad(s): 2 approve, 2 reject.
|
| 622 |
+
reject: ad_002, ad_003
|
| 623 |
+
approve: ad_001, ad_004
|
| 624 |
+
|
| 625 |
+
Findings:
|
| 626 |
+
[ad_001 / targeting_overlap] Declared targeting: Adults 18-55, Brooklyn, interests: food delivery | Targeting fingerprint: seg_20328 | Audience overlap with platform average for category: 27% | Geographic regions targeted: 2
|
| 627 |
+
[ad_002 / landing_page] Domain: elite2813.buzz | Domain age: 35 days | SSL: NO SSL / expired certificate | Registrar: Epik | WHOIS privacy: Disabled (registrant info public) | Redirect chain: elite2813.buzz -> redirect948.xyz -> redirect705.xyz -> redirect225.xyz -> final-333.site | Similarity to known scam templates: 71% | Authentic products | Direct from manufacturer | 90% below retail price | Product images appear stolen from official brand site | Terms of service link returns 404 | Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 628 |
+
[ad_003 / landing_page] Domain: shop2631.buzz | Domain age: 51 days | SSL: NO SSL / expired certificate | Registrar: Openprovider | WHOIS privacy: Disabled (registrant info public) | Redirect chain: shop2631.buzz -> redirect611.top -> redirect542.top -> redirect995.xyz -> redirect882.click -> final-697.site | Similarity to known scam templates: 53% | 100% free, no purchase necessary | Guaranteed winner | Act now — expires in 2 hours | No physical address listed | No contact phone number | Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 629 |
+
[ad_004 / landing_page] Domain: homenest.com | Domain age: 3338 days | SSL: Valid SSL certificate | Registrar: Google Domains | WHOIS privacy: Disabled (registrant info public) | Redirect chain: No redirects (direct) | Similarity to known scam templates: 4% | 14-day free trial | Money-back guarantee | None detected | Content summary: SaaS landing page with feature breakdown, pricing tiers, integration docs, and live demo option.
|
| 630 |
+
```
|
| 631 |
+
|
| 632 |
+
### LLM Response
|
| 633 |
+
```json
|
| 634 |
+
{
|
| 635 |
+
"action_type": "verdict",
|
| 636 |
+
"ad_id": "ad_005",
|
| 637 |
+
"verdict": "reject",
|
| 638 |
+
"confidence": 0.9
|
| 639 |
+
}
|
| 640 |
+
```
|
| 641 |
+
|
| 642 |
+
### Parsed Action
|
| 643 |
+
```json
|
| 644 |
+
{
|
| 645 |
+
"action_type": "verdict",
|
| 646 |
+
"ad_id": "ad_005",
|
| 647 |
+
"verdict": "reject",
|
| 648 |
+
"confidence": 0.9
|
| 649 |
+
}
|
| 650 |
+
```
|
| 651 |
+
|
| 652 |
+
### Environment Response
|
| 653 |
+
- **Reward:** `+0.39`
|
| 654 |
+
- **Done:** `True`
|
| 655 |
+
|
| 656 |
+
- **Feedback:** Episode complete. Grader score: 0.960/1.000
|
| 657 |
+
Verdicts rendered: 5/5
|
| 658 |
+
Correct decisions: 5/5
|
| 659 |
+
False positives (legit rejected): 0
|
| 660 |
+
False negatives (fraud approved): 0
|
| 661 |
+
Unreviewed ads auto-approved: 0
|
| 662 |
+
|
| 663 |
+
|
| 664 |
+
---
|
| 665 |
+
## Result
|
| 666 |
+
**Score: 0.960 | Steps: 9 | Verdicts: 5/5**
|
convo_logging/task_2_conversation.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
convo_logging/task_3_conversation.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synthetic data generation for the Ad Fraud Investigation Environment."""
|
| 2 |
+
|
| 3 |
+
from .ad_generator import TASK_CONFIGS, CampaignProfile, generate_episode
|
| 4 |
+
from .advertiser_profiles import generate_advertiser_profile
|
| 5 |
+
from .fraud_patterns import FRAUD_TEMPLATES, LEGIT_TEMPLATES
|
| 6 |
+
from .landing_pages import generate_landing_page
|
| 7 |
+
from .network_generator import generate_fraud_networks
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"generate_episode",
|
| 11 |
+
"generate_advertiser_profile",
|
| 12 |
+
"generate_landing_page",
|
| 13 |
+
"generate_fraud_networks",
|
| 14 |
+
"CampaignProfile",
|
| 15 |
+
"FRAUD_TEMPLATES",
|
| 16 |
+
"LEGIT_TEMPLATES",
|
| 17 |
+
"TASK_CONFIGS",
|
| 18 |
+
]
|
data/ad_generator.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Synthetic ad queue generation.
|
| 3 |
+
|
| 4 |
+
Generates a complete queue of ads for a given task configuration,
|
| 5 |
+
including all pre-generated investigation data. When the agent
|
| 6 |
+
investigates, the environment just reveals pre-computed data.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import random
|
| 12 |
+
from dataclasses import dataclass, field
|
| 13 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 14 |
+
|
| 15 |
+
from .advertiser_profiles import AdvertiserProfile, generate_advertiser_profile
|
| 16 |
+
from .fraud_patterns import FRAUD_TEMPLATES, LEGIT_TEMPLATES, AdTemplate
|
| 17 |
+
from .landing_pages import LandingPageData, generate_landing_page
|
| 18 |
+
from .network_generator import FraudRing, generate_fraud_networks
|
| 19 |
+
|
| 20 |
+
# Decoy pools: values that can appear in both legit and fraud ads,
|
| 21 |
+
# making naive pattern-matching unreliable.
|
| 22 |
+
_DECOY_REGISTRARS = ["NameSilo", "Cloudflare Registrar", "GoDaddy", "Tucows (privacy proxy)"]
|
| 23 |
+
_DECOY_PAYMENT_TYPES = ["credit_card", "prepaid_card", "corporate_card"]
|
| 24 |
+
_COMMON_TARGETING_SEGMENTS = [
|
| 25 |
+
"Adults 25-54, interests: shopping, lifestyle",
|
| 26 |
+
"Adults 18-45, interests: technology, gadgets",
|
| 27 |
+
"Adults 30-55, interests: finance, investing",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class TaskConfig:
|
| 33 |
+
task_id: str
|
| 34 |
+
name: str
|
| 35 |
+
difficulty: str
|
| 36 |
+
queue_size: int
|
| 37 |
+
action_budget: int
|
| 38 |
+
n_legit: int
|
| 39 |
+
n_fraud: int
|
| 40 |
+
n_escalate: int
|
| 41 |
+
include_networks: bool
|
| 42 |
+
n_fraud_rings: int
|
| 43 |
+
allowed_difficulties: List[str]
|
| 44 |
+
description: str
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
TASK_CONFIGS: Dict[str, TaskConfig] = {
|
| 48 |
+
"task_1": TaskConfig(
|
| 49 |
+
task_id="task_1",
|
| 50 |
+
name="Basic Ad Triage",
|
| 51 |
+
difficulty="easy",
|
| 52 |
+
queue_size=5,
|
| 53 |
+
action_budget=25,
|
| 54 |
+
n_legit=2,
|
| 55 |
+
n_fraud=3,
|
| 56 |
+
n_escalate=0,
|
| 57 |
+
include_networks=False,
|
| 58 |
+
n_fraud_rings=0,
|
| 59 |
+
allowed_difficulties=["easy"],
|
| 60 |
+
description=(
|
| 61 |
+
"Learn the investigation loop. Queue of 5 ads with obviously "
|
| 62 |
+
"fraudulent or clearly legitimate signals. Generous budget of 25 "
|
| 63 |
+
"actions (5 per ad)."
|
| 64 |
+
),
|
| 65 |
+
),
|
| 66 |
+
"task_2": TaskConfig(
|
| 67 |
+
task_id="task_2",
|
| 68 |
+
name="Sophisticated Fraud Under Budget Pressure",
|
| 69 |
+
difficulty="medium",
|
| 70 |
+
queue_size=12,
|
| 71 |
+
action_budget=30,
|
| 72 |
+
n_legit=5,
|
| 73 |
+
n_fraud=5,
|
| 74 |
+
n_escalate=2,
|
| 75 |
+
include_networks=False,
|
| 76 |
+
n_fraud_rings=0,
|
| 77 |
+
allowed_difficulties=["easy", "medium"],
|
| 78 |
+
description=(
|
| 79 |
+
"Triage under budget constraints. Mix of legit ads, sophisticated "
|
| 80 |
+
"scams, and gray-area cases. 12 ads but only 30 actions (~2.5 per ad). "
|
| 81 |
+
"Agent must prioritize which ads to investigate deeply."
|
| 82 |
+
),
|
| 83 |
+
),
|
| 84 |
+
"task_3": TaskConfig(
|
| 85 |
+
task_id="task_3",
|
| 86 |
+
name="Coordinated Fraud Network Detection",
|
| 87 |
+
difficulty="hard",
|
| 88 |
+
queue_size=20,
|
| 89 |
+
action_budget=35,
|
| 90 |
+
n_legit=6,
|
| 91 |
+
n_fraud=10,
|
| 92 |
+
n_escalate=4,
|
| 93 |
+
include_networks=True,
|
| 94 |
+
n_fraud_rings=3,
|
| 95 |
+
allowed_difficulties=["easy", "medium", "hard"],
|
| 96 |
+
description=(
|
| 97 |
+
"Full challenge including coordinated fraud rings. 20 ads with 3 "
|
| 98 |
+
"hidden fraud networks using varied topologies (cliques, chains, "
|
| 99 |
+
"hub-and-spoke). Budget of 35 actions (~1.75 per ad). Ring member "
|
| 100 |
+
"ads look borderline individually — the agent must cross-reference "
|
| 101 |
+
"investigation data across ads to detect shared signals."
|
| 102 |
+
),
|
| 103 |
+
),
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@dataclass
|
| 108 |
+
class CampaignProfile:
|
| 109 |
+
"""Campaign-level metadata associated with an ad."""
|
| 110 |
+
objective: str # e.g. "conversions", "traffic", "awareness", "app_installs"
|
| 111 |
+
bid_strategy: str # e.g. "lowest_cost", "cost_cap", "bid_cap"
|
| 112 |
+
daily_budget_usd: float
|
| 113 |
+
ad_set_count: int
|
| 114 |
+
placements: List[str]
|
| 115 |
+
|
| 116 |
+
def to_investigation_text(self, account_age_days: int) -> str:
|
| 117 |
+
budget_age_ratio = (
|
| 118 |
+
self.daily_budget_usd / max(account_age_days, 1)
|
| 119 |
+
)
|
| 120 |
+
placements_str = ", ".join(self.placements)
|
| 121 |
+
|
| 122 |
+
lines = [
|
| 123 |
+
f"Campaign Objective: {self.objective}",
|
| 124 |
+
f"Bid Strategy: {self.bid_strategy}",
|
| 125 |
+
f"Daily Budget: ${self.daily_budget_usd:,.2f} "
|
| 126 |
+
f"(account is {account_age_days} days old — "
|
| 127 |
+
f"budget/age ratio: ${budget_age_ratio:,.2f}/day)",
|
| 128 |
+
f"Active Ad Sets: {self.ad_set_count}",
|
| 129 |
+
f"Placements: {placements_str}",
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
warnings = []
|
| 133 |
+
if budget_age_ratio > 50:
|
| 134 |
+
warnings.append(
|
| 135 |
+
"Budget-to-account-age ratio exceeds typical thresholds."
|
| 136 |
+
)
|
| 137 |
+
if self.ad_set_count > 15:
|
| 138 |
+
warnings.append(
|
| 139 |
+
f"High ad set count ({self.ad_set_count}) — "
|
| 140 |
+
"possible policy evasion testing via creative variation."
|
| 141 |
+
)
|
| 142 |
+
if self.objective in ("traffic", "awareness") and self.bid_strategy == "lowest_cost":
|
| 143 |
+
warnings.append(
|
| 144 |
+
f"Optimizing for {self.objective} with lowest-cost bidding "
|
| 145 |
+
"— common in spray-and-pray fraud campaigns."
|
| 146 |
+
)
|
| 147 |
+
if "Audience Network" in self.placements and len(self.placements) <= 2:
|
| 148 |
+
warnings.append(
|
| 149 |
+
"Heavy reliance on Audience Network placement — "
|
| 150 |
+
"higher bot traffic exposure."
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
if warnings:
|
| 154 |
+
for w in warnings:
|
| 155 |
+
lines.append(f" WARNING: {w}")
|
| 156 |
+
else:
|
| 157 |
+
lines.append("Budget and pacing consistent with historical account behavior.")
|
| 158 |
+
|
| 159 |
+
return "\n".join(lines)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@dataclass
|
| 163 |
+
class Ad:
|
| 164 |
+
ad_id: str
|
| 165 |
+
ad_copy: str
|
| 166 |
+
category: str
|
| 167 |
+
targeting_summary: str
|
| 168 |
+
initial_risk_signals: List[str]
|
| 169 |
+
ground_truth_label: str # "fraud", "legit", or "escalate"
|
| 170 |
+
fraud_type: str
|
| 171 |
+
severity: float
|
| 172 |
+
difficulty: str
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@dataclass
|
| 176 |
+
class GeneratedEpisode:
|
| 177 |
+
"""All pre-generated data for one episode."""
|
| 178 |
+
task_config: TaskConfig
|
| 179 |
+
ads: List[Ad]
|
| 180 |
+
advertiser_profiles: Dict[str, AdvertiserProfile]
|
| 181 |
+
campaign_profiles: Dict[str, CampaignProfile]
|
| 182 |
+
landing_pages: Dict[str, LandingPageData]
|
| 183 |
+
fraud_rings: List[FraudRing]
|
| 184 |
+
ad_to_rings: Dict[str, List[str]]
|
| 185 |
+
investigation_data: Dict[str, Dict[str, str]]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def generate_episode(seed: int, task_id: str = "task_1") -> GeneratedEpisode:
|
| 189 |
+
"""Generate a complete episode with all pre-computed investigation data."""
|
| 190 |
+
rng = random.Random(seed)
|
| 191 |
+
config = TASK_CONFIGS[task_id]
|
| 192 |
+
|
| 193 |
+
ads = _generate_ad_queue(rng, config)
|
| 194 |
+
|
| 195 |
+
fraud_ad_ids = [a.ad_id for a in ads if a.ground_truth_label == "fraud"]
|
| 196 |
+
|
| 197 |
+
fraud_rings: List[FraudRing] = []
|
| 198 |
+
ad_to_rings: Dict[str, List[str]] = {}
|
| 199 |
+
ring_shared_payments: Dict[str, str] = {}
|
| 200 |
+
|
| 201 |
+
if config.include_networks and config.n_fraud_rings > 0:
|
| 202 |
+
fraud_rings, ad_to_rings = generate_fraud_networks(
|
| 203 |
+
rng, config.n_fraud_rings, fraud_ad_ids
|
| 204 |
+
)
|
| 205 |
+
for ring in fraud_rings:
|
| 206 |
+
if "payment_method" in ring.shared_signals:
|
| 207 |
+
for ad_id in ring.member_ad_ids:
|
| 208 |
+
ring_shared_payments[ad_id] = ring.shared_signals["payment_method"]
|
| 209 |
+
|
| 210 |
+
advertiser_profiles: Dict[str, AdvertiserProfile] = {}
|
| 211 |
+
campaign_profiles: Dict[str, CampaignProfile] = {}
|
| 212 |
+
landing_pages: Dict[str, LandingPageData] = {}
|
| 213 |
+
investigation_data: Dict[str, Dict[str, str]] = {}
|
| 214 |
+
|
| 215 |
+
ring_campaign_overrides: Dict[str, Dict[str, Any]] = {}
|
| 216 |
+
ring_created_dates: Dict[str, str] = {}
|
| 217 |
+
for ring in fraud_rings:
|
| 218 |
+
shared_objective = rng.choice(["traffic", "awareness"])
|
| 219 |
+
shared_bid = "lowest_cost"
|
| 220 |
+
# Ring members share account creation dates within the same week
|
| 221 |
+
from datetime import date, timedelta
|
| 222 |
+
base_date = date(2026, 4, 6) - timedelta(days=rng.randint(5, 45))
|
| 223 |
+
for ad_id in ring.member_ad_ids:
|
| 224 |
+
ring_campaign_overrides[ad_id] = {
|
| 225 |
+
"objective": shared_objective,
|
| 226 |
+
"bid_strategy": shared_bid,
|
| 227 |
+
}
|
| 228 |
+
offset = timedelta(days=rng.randint(0, 6))
|
| 229 |
+
ring_created_dates[ad_id] = (base_date + offset).isoformat()
|
| 230 |
+
|
| 231 |
+
for ad in ads:
|
| 232 |
+
is_fraud = ad.ground_truth_label in ("fraud", "escalate")
|
| 233 |
+
|
| 234 |
+
profile = generate_advertiser_profile(
|
| 235 |
+
rng, ad.ad_id, is_fraud,
|
| 236 |
+
payment_method_id=ring_shared_payments.get(ad.ad_id),
|
| 237 |
+
ring_created_date=ring_created_dates.get(ad.ad_id),
|
| 238 |
+
)
|
| 239 |
+
advertiser_profiles[ad.ad_id] = profile
|
| 240 |
+
|
| 241 |
+
campaign = _generate_campaign_profile(
|
| 242 |
+
rng, ad, is_fraud,
|
| 243 |
+
ring_overrides=ring_campaign_overrides.get(ad.ad_id),
|
| 244 |
+
)
|
| 245 |
+
campaign_profiles[ad.ad_id] = campaign
|
| 246 |
+
|
| 247 |
+
landing_page_kwargs = {}
|
| 248 |
+
if ad.ad_id in ad_to_rings:
|
| 249 |
+
ring = next(r for r in fraud_rings if ad.ad_id in r.member_ad_ids)
|
| 250 |
+
if "domain_registrar" in ring.shared_signals:
|
| 251 |
+
landing_page_kwargs["registrar_override"] = ring.shared_signals["domain_registrar"]
|
| 252 |
+
elif not is_fraud and rng.random() < 0.25:
|
| 253 |
+
landing_page_kwargs["registrar_override"] = rng.choice(_DECOY_REGISTRARS)
|
| 254 |
+
|
| 255 |
+
lp = generate_landing_page(
|
| 256 |
+
rng, ad.ad_id, is_fraud, ad.fraud_type, **landing_page_kwargs
|
| 257 |
+
)
|
| 258 |
+
landing_pages[ad.ad_id] = lp
|
| 259 |
+
|
| 260 |
+
inv = {}
|
| 261 |
+
inv["advertiser_history"] = profile.to_investigation_text()
|
| 262 |
+
inv["landing_page"] = lp.to_investigation_text()
|
| 263 |
+
inv["payment_method"] = _generate_payment_investigation(rng, profile, ad.ad_id, ad_to_rings, fraud_rings)
|
| 264 |
+
inv["targeting_overlap"] = _generate_targeting_investigation(rng, ad, ads, ad_to_rings, fraud_rings)
|
| 265 |
+
inv["creative_similarity"] = _generate_creative_investigation(rng, ad, ads, ad_to_rings, fraud_rings)
|
| 266 |
+
inv["campaign_structure"] = _generate_campaign_investigation(
|
| 267 |
+
rng, ad, campaign, profile, ad_to_rings, fraud_rings,
|
| 268 |
+
)
|
| 269 |
+
investigation_data[ad.ad_id] = inv
|
| 270 |
+
|
| 271 |
+
return GeneratedEpisode(
|
| 272 |
+
task_config=config,
|
| 273 |
+
ads=ads,
|
| 274 |
+
advertiser_profiles=advertiser_profiles,
|
| 275 |
+
campaign_profiles=campaign_profiles,
|
| 276 |
+
landing_pages=landing_pages,
|
| 277 |
+
fraud_rings=fraud_rings,
|
| 278 |
+
ad_to_rings=ad_to_rings,
|
| 279 |
+
investigation_data=investigation_data,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def _generate_ad_queue(rng: random.Random, config: TaskConfig) -> List[Ad]:
|
| 284 |
+
"""Build the ad queue by sampling from templates."""
|
| 285 |
+
ads: List[Ad] = []
|
| 286 |
+
ad_counter = 0
|
| 287 |
+
|
| 288 |
+
legit_templates = [t for t in LEGIT_TEMPLATES]
|
| 289 |
+
fraud_templates = [
|
| 290 |
+
t for t in FRAUD_TEMPLATES
|
| 291 |
+
if t.difficulty in config.allowed_difficulties and t.label == "fraud"
|
| 292 |
+
]
|
| 293 |
+
escalate_templates = [
|
| 294 |
+
t for t in FRAUD_TEMPLATES
|
| 295 |
+
if t.difficulty in config.allowed_difficulties and t.label == "escalate"
|
| 296 |
+
]
|
| 297 |
+
|
| 298 |
+
if not escalate_templates:
|
| 299 |
+
escalate_templates = [
|
| 300 |
+
t for t in FRAUD_TEMPLATES if t.label == "escalate"
|
| 301 |
+
]
|
| 302 |
+
|
| 303 |
+
for _ in range(config.n_legit):
|
| 304 |
+
template = rng.choice(legit_templates)
|
| 305 |
+
idx = rng.randint(0, len(template.ad_copies) - 1)
|
| 306 |
+
ad_counter += 1
|
| 307 |
+
ads.append(Ad(
|
| 308 |
+
ad_id=f"ad_{ad_counter:03d}",
|
| 309 |
+
ad_copy=template.ad_copies[idx],
|
| 310 |
+
category=template.category,
|
| 311 |
+
targeting_summary=template.targeting_hints[idx % len(template.targeting_hints)],
|
| 312 |
+
initial_risk_signals=list(template.risk_signals),
|
| 313 |
+
ground_truth_label=template.label,
|
| 314 |
+
fraud_type=template.fraud_type,
|
| 315 |
+
severity=template.severity,
|
| 316 |
+
difficulty=template.difficulty,
|
| 317 |
+
))
|
| 318 |
+
|
| 319 |
+
for _ in range(config.n_fraud):
|
| 320 |
+
if fraud_templates:
|
| 321 |
+
template = rng.choice(fraud_templates)
|
| 322 |
+
else:
|
| 323 |
+
template = rng.choice(FRAUD_TEMPLATES)
|
| 324 |
+
idx = rng.randint(0, len(template.ad_copies) - 1)
|
| 325 |
+
ad_counter += 1
|
| 326 |
+
ads.append(Ad(
|
| 327 |
+
ad_id=f"ad_{ad_counter:03d}",
|
| 328 |
+
ad_copy=template.ad_copies[idx],
|
| 329 |
+
category=template.category,
|
| 330 |
+
targeting_summary=template.targeting_hints[idx % len(template.targeting_hints)],
|
| 331 |
+
initial_risk_signals=list(template.risk_signals),
|
| 332 |
+
ground_truth_label="fraud",
|
| 333 |
+
fraud_type=template.fraud_type,
|
| 334 |
+
severity=template.severity,
|
| 335 |
+
difficulty=template.difficulty,
|
| 336 |
+
))
|
| 337 |
+
|
| 338 |
+
for _ in range(config.n_escalate):
|
| 339 |
+
if escalate_templates:
|
| 340 |
+
template = rng.choice(escalate_templates)
|
| 341 |
+
idx = rng.randint(0, len(template.ad_copies) - 1)
|
| 342 |
+
ad_counter += 1
|
| 343 |
+
ads.append(Ad(
|
| 344 |
+
ad_id=f"ad_{ad_counter:03d}",
|
| 345 |
+
ad_copy=template.ad_copies[idx],
|
| 346 |
+
category=template.category,
|
| 347 |
+
targeting_summary=template.targeting_hints[idx % len(template.targeting_hints)],
|
| 348 |
+
initial_risk_signals=list(template.risk_signals),
|
| 349 |
+
ground_truth_label="escalate",
|
| 350 |
+
fraud_type=template.fraud_type,
|
| 351 |
+
severity=template.severity,
|
| 352 |
+
difficulty=template.difficulty,
|
| 353 |
+
))
|
| 354 |
+
|
| 355 |
+
rng.shuffle(ads)
|
| 356 |
+
|
| 357 |
+
renumbered = []
|
| 358 |
+
for i, ad in enumerate(ads):
|
| 359 |
+
ad.ad_id = f"ad_{i + 1:03d}"
|
| 360 |
+
renumbered.append(ad)
|
| 361 |
+
|
| 362 |
+
return renumbered
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def _generate_payment_investigation(
|
| 366 |
+
rng: random.Random,
|
| 367 |
+
profile: AdvertiserProfile,
|
| 368 |
+
ad_id: str,
|
| 369 |
+
ad_to_rings: Dict[str, List[str]],
|
| 370 |
+
fraud_rings: List[FraudRing],
|
| 371 |
+
) -> str:
|
| 372 |
+
"""Generate payment method investigation text.
|
| 373 |
+
|
| 374 |
+
Ring signals are embedded as raw data values (shared payment IDs) without
|
| 375 |
+
explicitly naming other ads. The agent must cross-reference across ads.
|
| 376 |
+
"""
|
| 377 |
+
lines = [
|
| 378 |
+
f"Payment Method Analysis for {ad_id}:",
|
| 379 |
+
f" Method type: {profile.payment_method_type}",
|
| 380 |
+
f" Payment ID: {profile.payment_method_id}",
|
| 381 |
+
]
|
| 382 |
+
|
| 383 |
+
if profile.payment_method_type in ("prepaid_card", "crypto", "virtual_card"):
|
| 384 |
+
lines.append(f" Note: {profile.payment_method_type} payments have elevated fraud correlation in platform data.")
|
| 385 |
+
|
| 386 |
+
if profile.previous_violations > 0:
|
| 387 |
+
lines.append(f" Chargeback/dispute history: {profile.previous_violations} incident(s) on record.")
|
| 388 |
+
else:
|
| 389 |
+
lines.append(" Chargeback/dispute history: Clean record.")
|
| 390 |
+
|
| 391 |
+
velocity = rng.randint(1, 5) if ad_id not in ad_to_rings else rng.randint(3, 12)
|
| 392 |
+
lines.append(f" Payment method added to {velocity} advertiser account(s) in the last 90 days.")
|
| 393 |
+
|
| 394 |
+
if profile.account_age_days < 30:
|
| 395 |
+
lines.append(f" First charge on this method: {profile.account_age_days} days ago.")
|
| 396 |
+
|
| 397 |
+
return "\n".join(lines)
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def _generate_targeting_investigation(
|
| 401 |
+
rng: random.Random,
|
| 402 |
+
ad: Ad,
|
| 403 |
+
all_ads: List[Ad],
|
| 404 |
+
ad_to_rings: Dict[str, List[str]],
|
| 405 |
+
fraud_rings: List[FraudRing],
|
| 406 |
+
) -> str:
|
| 407 |
+
"""Generate targeting overlap investigation text.
|
| 408 |
+
|
| 409 |
+
Ring members share an exact targeting fingerprint, presented as raw data.
|
| 410 |
+
The agent must compare fingerprints across ads to detect collusion.
|
| 411 |
+
"""
|
| 412 |
+
lines = [
|
| 413 |
+
f"Targeting Analysis for {ad.ad_id}:",
|
| 414 |
+
f" Declared targeting: {ad.targeting_summary}",
|
| 415 |
+
]
|
| 416 |
+
|
| 417 |
+
if ad.ad_id in ad_to_rings:
|
| 418 |
+
ring = next(r for r in fraud_rings if ad.ad_id in r.member_ad_ids)
|
| 419 |
+
if "targeting_overlap" in ring.shared_signals:
|
| 420 |
+
lines.append(f" Targeting fingerprint: {ring.shared_signals['targeting_overlap']}")
|
| 421 |
+
overlap_pct = rng.randint(85, 98)
|
| 422 |
+
lines.append(f" Audience overlap with platform average for category: {overlap_pct}%")
|
| 423 |
+
else:
|
| 424 |
+
fingerprint = f"seg_{rng.randint(10000, 99999)}"
|
| 425 |
+
lines.append(f" Targeting fingerprint: {fingerprint}")
|
| 426 |
+
overlap_pct = rng.randint(20, 55)
|
| 427 |
+
lines.append(f" Audience overlap with platform average for category: {overlap_pct}%")
|
| 428 |
+
else:
|
| 429 |
+
fingerprint = f"seg_{rng.randint(10000, 99999)}"
|
| 430 |
+
lines.append(f" Targeting fingerprint: {fingerprint}")
|
| 431 |
+
similar = [a for a in all_ads if a.ad_id != ad.ad_id and a.category == ad.category]
|
| 432 |
+
if similar:
|
| 433 |
+
overlap_pct = rng.randint(30, 65)
|
| 434 |
+
lines.append(f" {len(similar)} other ad(s) in same category ({ad.category}) in queue.")
|
| 435 |
+
lines.append(f" Audience overlap with platform average for category: {overlap_pct}%")
|
| 436 |
+
else:
|
| 437 |
+
overlap_pct = rng.randint(10, 40)
|
| 438 |
+
lines.append(f" Audience overlap with platform average for category: {overlap_pct}%")
|
| 439 |
+
|
| 440 |
+
geo_regions = rng.randint(1, 8) if ad.ground_truth_label != "legit" else rng.randint(1, 3)
|
| 441 |
+
lines.append(f" Geographic regions targeted: {geo_regions}")
|
| 442 |
+
|
| 443 |
+
return "\n".join(lines)
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def _generate_creative_investigation(
|
| 447 |
+
rng: random.Random,
|
| 448 |
+
ad: Ad,
|
| 449 |
+
all_ads: List[Ad],
|
| 450 |
+
ad_to_rings: Dict[str, List[str]],
|
| 451 |
+
fraud_rings: List[FraudRing],
|
| 452 |
+
) -> str:
|
| 453 |
+
"""Generate creative similarity investigation text.
|
| 454 |
+
|
| 455 |
+
Ring members share a template hash, presented as raw data. The agent
|
| 456 |
+
must compare hashes across ads to detect reuse.
|
| 457 |
+
"""
|
| 458 |
+
lines = [
|
| 459 |
+
f"Creative Analysis for {ad.ad_id}:",
|
| 460 |
+
]
|
| 461 |
+
|
| 462 |
+
if ad.ad_id in ad_to_rings:
|
| 463 |
+
ring = next(r for r in fraud_rings if ad.ad_id in r.member_ad_ids)
|
| 464 |
+
if "creative_template" in ring.shared_signals:
|
| 465 |
+
lines.append(f" Template hash: {ring.shared_signals['creative_template']}")
|
| 466 |
+
lines.append(f" Image dimensions: {rng.choice(['1200x628', '1080x1080', '1200x1200'])} px")
|
| 467 |
+
lines.append(f" Color palette hash: pal_{rng.randint(100, 999)}")
|
| 468 |
+
lines.append(f" Text-to-image ratio: {rng.randint(18, 25)}%")
|
| 469 |
+
else:
|
| 470 |
+
lines.append(f" Template hash: tmpl_{rng.randint(10000, 99999)}")
|
| 471 |
+
lines.append(f" Image dimensions: {rng.choice(['1200x628', '1080x1080', '1200x1200', '600x600'])} px")
|
| 472 |
+
lines.append(f" Text-to-image ratio: {rng.randint(10, 30)}%")
|
| 473 |
+
else:
|
| 474 |
+
lines.append(f" Template hash: tmpl_{rng.randint(10000, 99999)}")
|
| 475 |
+
lines.append(f" Image dimensions: {rng.choice(['1200x628', '1080x1080'])} px")
|
| 476 |
+
lines.append(f" Text-to-image ratio: {rng.randint(8, 22)}%")
|
| 477 |
+
|
| 478 |
+
if ad.ground_truth_label == "fraud" and ad.fraud_type:
|
| 479 |
+
similarity = rng.uniform(0.3, 0.7)
|
| 480 |
+
else:
|
| 481 |
+
similarity = rng.uniform(0.0, 0.15)
|
| 482 |
+
lines.append(f" Similarity to known scam templates: {similarity:.0%}")
|
| 483 |
+
|
| 484 |
+
return "\n".join(lines)
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
_LEGIT_OBJECTIVES = ["conversions", "leads", "sales", "app_installs"]
|
| 488 |
+
_FRAUD_OBJECTIVES = ["traffic", "awareness", "reach", "engagement"]
|
| 489 |
+
_LEGIT_BID_STRATEGIES = ["cost_cap", "bid_cap", "target_cost"]
|
| 490 |
+
_FRAUD_BID_STRATEGIES = ["lowest_cost", "lowest_cost", "lowest_cost", "cost_cap"]
|
| 491 |
+
|
| 492 |
+
_LEGIT_PLACEMENTS = [
|
| 493 |
+
["Facebook Feed", "Instagram Feed"],
|
| 494 |
+
["Facebook Feed", "Instagram Feed", "Instagram Stories"],
|
| 495 |
+
["Facebook Feed"],
|
| 496 |
+
["Facebook Feed", "Instagram Feed", "Instagram Reels"],
|
| 497 |
+
]
|
| 498 |
+
_FRAUD_PLACEMENTS = [
|
| 499 |
+
["Audience Network", "Facebook Feed"],
|
| 500 |
+
["Audience Network", "Facebook Feed", "Instagram Stories"],
|
| 501 |
+
["Facebook Feed", "Instagram Feed", "Audience Network", "Messenger"],
|
| 502 |
+
["Audience Network"],
|
| 503 |
+
]
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
def _generate_campaign_profile(
|
| 507 |
+
rng: random.Random,
|
| 508 |
+
ad: Ad,
|
| 509 |
+
is_fraud: bool,
|
| 510 |
+
*,
|
| 511 |
+
ring_overrides: Optional[Dict[str, Any]] = None,
|
| 512 |
+
) -> CampaignProfile:
|
| 513 |
+
"""Generate campaign-level metadata for an ad."""
|
| 514 |
+
if is_fraud:
|
| 515 |
+
objective = rng.choice(_FRAUD_OBJECTIVES)
|
| 516 |
+
bid_strategy = rng.choice(_FRAUD_BID_STRATEGIES)
|
| 517 |
+
daily_budget = round(rng.uniform(500, 5000), 2)
|
| 518 |
+
ad_set_count = rng.randint(8, 50)
|
| 519 |
+
placements = rng.choice(_FRAUD_PLACEMENTS)
|
| 520 |
+
else:
|
| 521 |
+
objective = rng.choice(_LEGIT_OBJECTIVES)
|
| 522 |
+
bid_strategy = rng.choice(_LEGIT_BID_STRATEGIES)
|
| 523 |
+
daily_budget = round(rng.uniform(20, 500), 2)
|
| 524 |
+
ad_set_count = rng.randint(1, 5)
|
| 525 |
+
placements = rng.choice(_LEGIT_PLACEMENTS)
|
| 526 |
+
|
| 527 |
+
if ring_overrides:
|
| 528 |
+
objective = ring_overrides.get("objective", objective)
|
| 529 |
+
bid_strategy = ring_overrides.get("bid_strategy", bid_strategy)
|
| 530 |
+
|
| 531 |
+
return CampaignProfile(
|
| 532 |
+
objective=objective,
|
| 533 |
+
bid_strategy=bid_strategy,
|
| 534 |
+
daily_budget_usd=daily_budget,
|
| 535 |
+
ad_set_count=ad_set_count,
|
| 536 |
+
placements=list(placements),
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def _generate_campaign_investigation(
|
| 541 |
+
rng: random.Random,
|
| 542 |
+
ad: Ad,
|
| 543 |
+
campaign: CampaignProfile,
|
| 544 |
+
profile: AdvertiserProfile,
|
| 545 |
+
ad_to_rings: Dict[str, List[str]],
|
| 546 |
+
fraud_rings: List[FraudRing],
|
| 547 |
+
) -> str:
|
| 548 |
+
"""Generate campaign structure investigation text.
|
| 549 |
+
|
| 550 |
+
Ring members share campaign configurations but no explicit cross-references.
|
| 551 |
+
The agent must compare objective/bid/budget patterns across ads.
|
| 552 |
+
"""
|
| 553 |
+
lines = [
|
| 554 |
+
f"Campaign Structure Analysis for {ad.ad_id}:",
|
| 555 |
+
campaign.to_investigation_text(profile.account_age_days),
|
| 556 |
+
]
|
| 557 |
+
|
| 558 |
+
config_hash = f"cfg_{hash((campaign.objective, campaign.bid_strategy)) & 0xFFFF:04x}"
|
| 559 |
+
lines.append(f" Campaign configuration fingerprint: {config_hash}")
|
| 560 |
+
|
| 561 |
+
return "\n".join(lines)
|
data/advertiser_profiles.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Synthetic advertiser profile generation.
|
| 3 |
+
|
| 4 |
+
Each ad has an associated advertiser with history data
|
| 5 |
+
that becomes available when the agent investigates 'advertiser_history'.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import random
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from typing import List
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class AdvertiserProfile:
|
| 17 |
+
advertiser_id: str
|
| 18 |
+
account_name: str
|
| 19 |
+
account_age_days: int
|
| 20 |
+
total_spend_usd: float
|
| 21 |
+
previous_violations: int
|
| 22 |
+
previous_bans: int
|
| 23 |
+
ad_volume_last_30d: int
|
| 24 |
+
historical_approval_rate: float
|
| 25 |
+
payment_method_id: str
|
| 26 |
+
payment_method_type: str
|
| 27 |
+
country: str
|
| 28 |
+
verified_business: bool
|
| 29 |
+
account_created_date: str = ""
|
| 30 |
+
spend_velocity: str = ""
|
| 31 |
+
ad_submission_pattern: str = ""
|
| 32 |
+
|
| 33 |
+
def to_investigation_text(self) -> str:
|
| 34 |
+
status = "Verified Business" if self.verified_business else "Unverified"
|
| 35 |
+
violation_note = ""
|
| 36 |
+
if self.previous_violations > 0:
|
| 37 |
+
violation_note = f" ({self.previous_violations} policy violations on record, {self.previous_bans} previous bans)"
|
| 38 |
+
elif self.previous_bans > 0:
|
| 39 |
+
violation_note = f" ({self.previous_bans} previous bans on record)"
|
| 40 |
+
|
| 41 |
+
lines = [
|
| 42 |
+
f"Advertiser: {self.account_name} ({status})",
|
| 43 |
+
f"Account age: {self.account_age_days} days",
|
| 44 |
+
f"Account created: {self.account_created_date}" if self.account_created_date else None,
|
| 45 |
+
f"Country: {self.country}",
|
| 46 |
+
f"Total historical spend: ${self.total_spend_usd:,.2f}",
|
| 47 |
+
f"Ads submitted in last 30 days: {self.ad_volume_last_30d}",
|
| 48 |
+
f"Historical approval rate: {self.historical_approval_rate:.0%}{violation_note}",
|
| 49 |
+
f"Payment method: {self.payment_method_type} (ID: {self.payment_method_id})",
|
| 50 |
+
f"Spend velocity: {self.spend_velocity}" if self.spend_velocity else None,
|
| 51 |
+
f"Submission pattern: {self.ad_submission_pattern}" if self.ad_submission_pattern else None,
|
| 52 |
+
]
|
| 53 |
+
return "\n".join(l for l in lines if l is not None)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
_LEGIT_NAMES = [
|
| 57 |
+
"HomeNest LLC", "StyleHaven Inc", "Chef's Choice Store", "GlowNatural Co",
|
| 58 |
+
"ArtisanHide Crafts", "TaskFlow Pro", "InvoiceSimple Inc", "PeopleFirst HR",
|
| 59 |
+
"VaultBackup Solutions", "MailCraft Inc", "Mike's Plumbing", "Sal's Pizzeria",
|
| 60 |
+
"SparkleHome Services", "Williams Tax Group", "PawPals Chicago",
|
| 61 |
+
"CodeAcademy Pro", "StateU Online", "PrepMaster Tutoring",
|
| 62 |
+
"LensArt Studio", "LinguaViva Education", "FitLife Fitness",
|
| 63 |
+
"ZenFlow Yoga", "NutriPlan Co", "SwiftStride Athletics", "FitVisit",
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
_SCAM_NAMES = [
|
| 67 |
+
"Digital Marketing Solutions LLC", "Global Deals Marketplace",
|
| 68 |
+
"Premium Offers International", "Quick Rewards Corp",
|
| 69 |
+
"NextGen Trading Ltd", "Elite Ventures Group",
|
| 70 |
+
"Horizon Brands LLC", "TrustPoint Commerce",
|
| 71 |
+
"Alpha Innovations Inc", "PrimeEdge Solutions",
|
| 72 |
+
"Quantum Returns Ltd", "FuturePath Holdings",
|
| 73 |
+
"BlueChip Partners", "ClearView Enterprises",
|
| 74 |
+
"Apex Growth Strategies", "SilverLine Dynamics",
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
_COUNTRIES_LEGIT = ["United States", "United Kingdom", "Canada", "Australia", "Germany"]
|
| 78 |
+
_COUNTRIES_MIXED = [
|
| 79 |
+
"United States", "United Kingdom", "Netherlands", "Singapore",
|
| 80 |
+
"Hong Kong", "United Arab Emirates", "Estonia", "Georgia",
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def generate_advertiser_profile(
|
| 85 |
+
rng: random.Random,
|
| 86 |
+
ad_id: str,
|
| 87 |
+
is_fraud: bool,
|
| 88 |
+
*,
|
| 89 |
+
payment_method_id: str | None = None,
|
| 90 |
+
ring_created_date: str | None = None,
|
| 91 |
+
) -> AdvertiserProfile:
|
| 92 |
+
"""Generate a synthetic advertiser profile for a single ad."""
|
| 93 |
+
from datetime import date, timedelta
|
| 94 |
+
|
| 95 |
+
if is_fraud:
|
| 96 |
+
account_name = rng.choice(_SCAM_NAMES)
|
| 97 |
+
account_age = rng.randint(1, 90)
|
| 98 |
+
total_spend = round(rng.uniform(0, 500), 2)
|
| 99 |
+
violations = rng.choices([0, 1, 2, 3], weights=[40, 30, 20, 10])[0]
|
| 100 |
+
bans = rng.choices([0, 1, 2], weights=[60, 30, 10])[0]
|
| 101 |
+
ad_volume = rng.randint(5, 80)
|
| 102 |
+
approval_rate = round(rng.uniform(0.3, 0.75), 2)
|
| 103 |
+
country = rng.choice(_COUNTRIES_MIXED)
|
| 104 |
+
verified = rng.random() < 0.15
|
| 105 |
+
pmt_type = rng.choice(["prepaid_card", "crypto", "virtual_card", "wire_transfer", "credit_card"])
|
| 106 |
+
else:
|
| 107 |
+
account_name = rng.choice(_LEGIT_NAMES)
|
| 108 |
+
account_age = rng.randint(180, 2500)
|
| 109 |
+
total_spend = round(rng.uniform(5000, 500000), 2)
|
| 110 |
+
violations = 0
|
| 111 |
+
bans = 0
|
| 112 |
+
ad_volume = rng.randint(1, 20)
|
| 113 |
+
approval_rate = round(rng.uniform(0.9, 1.0), 2)
|
| 114 |
+
country = rng.choice(_COUNTRIES_LEGIT)
|
| 115 |
+
verified = rng.random() < 0.85
|
| 116 |
+
pmt_type = rng.choice(["credit_card", "bank_account", "corporate_card"])
|
| 117 |
+
|
| 118 |
+
if payment_method_id is None:
|
| 119 |
+
payment_method_id = f"pmt_{rng.randint(100000, 999999)}"
|
| 120 |
+
|
| 121 |
+
# Temporal signals
|
| 122 |
+
if ring_created_date:
|
| 123 |
+
created_date = ring_created_date
|
| 124 |
+
else:
|
| 125 |
+
created = date(2026, 4, 6) - timedelta(days=account_age)
|
| 126 |
+
created_date = created.isoformat()
|
| 127 |
+
|
| 128 |
+
if is_fraud:
|
| 129 |
+
spend_per_day = total_spend / max(account_age, 1)
|
| 130 |
+
if spend_per_day > 20:
|
| 131 |
+
spend_velocity = f"${spend_per_day:,.0f}/day avg — ramped from $0 to ${total_spend:,.0f} in {account_age} days"
|
| 132 |
+
else:
|
| 133 |
+
spend_velocity = f"${spend_per_day:,.0f}/day avg over account lifetime"
|
| 134 |
+
|
| 135 |
+
if ad_volume > 20:
|
| 136 |
+
submission_pattern = f"{ad_volume} ads in 30 days (burst: {rng.randint(8, ad_volume)} in a single 24h window)"
|
| 137 |
+
else:
|
| 138 |
+
submission_pattern = f"{ad_volume} ads in 30 days (steady cadence)"
|
| 139 |
+
else:
|
| 140 |
+
spend_per_day = total_spend / max(account_age, 1)
|
| 141 |
+
spend_velocity = f"${spend_per_day:,.0f}/day avg — consistent growth over {account_age} days"
|
| 142 |
+
submission_pattern = f"{ad_volume} ads in 30 days (steady cadence)"
|
| 143 |
+
|
| 144 |
+
return AdvertiserProfile(
|
| 145 |
+
advertiser_id=f"adv_{ad_id}",
|
| 146 |
+
account_name=account_name,
|
| 147 |
+
account_age_days=account_age,
|
| 148 |
+
total_spend_usd=total_spend,
|
| 149 |
+
previous_violations=violations,
|
| 150 |
+
previous_bans=bans,
|
| 151 |
+
ad_volume_last_30d=ad_volume,
|
| 152 |
+
historical_approval_rate=approval_rate,
|
| 153 |
+
payment_method_id=payment_method_id,
|
| 154 |
+
payment_method_type=pmt_type,
|
| 155 |
+
country=country,
|
| 156 |
+
verified_business=verified,
|
| 157 |
+
account_created_date=created_date,
|
| 158 |
+
spend_velocity=spend_velocity,
|
| 159 |
+
ad_submission_pattern=submission_pattern,
|
| 160 |
+
)
|
data/fraud_patterns.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fraud and legitimate ad templates for synthetic data generation.
|
| 3 |
+
|
| 4 |
+
Templates are organized by difficulty: obvious scams for Task 1,
|
| 5 |
+
sophisticated scams for Task 2, and network-ready scams for Task 3.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from typing import List
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class AdTemplate:
|
| 16 |
+
category: str
|
| 17 |
+
ad_copies: List[str]
|
| 18 |
+
targeting_hints: List[str]
|
| 19 |
+
risk_signals: List[str]
|
| 20 |
+
label: str # "fraud", "legit", or "escalate"
|
| 21 |
+
fraud_type: str = ""
|
| 22 |
+
severity: float = 0.0 # 0.0-1.0
|
| 23 |
+
difficulty: str = "easy" # easy, medium, hard
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
LEGIT_TEMPLATES: List[AdTemplate] = [
|
| 27 |
+
AdTemplate(
|
| 28 |
+
category="ecommerce",
|
| 29 |
+
ad_copies=[
|
| 30 |
+
"Spring Sale — Up to 30% off home essentials. Free shipping on orders over $50. Shop now at HomeNest.",
|
| 31 |
+
"New arrivals in women's fashion. Curated styles for every occasion. Visit StyleHaven today.",
|
| 32 |
+
"Upgrade your kitchen with premium cookware. Professional-grade, lifetime warranty. Chef's Choice Store.",
|
| 33 |
+
"Organic skincare products made with natural ingredients. Dermatologist tested. GlowNatural.com",
|
| 34 |
+
"Handcrafted leather bags — designed to last. Each piece unique. ArtisanHide.com",
|
| 35 |
+
],
|
| 36 |
+
targeting_hints=[
|
| 37 |
+
"Women 25-54, interests: home decor, shopping",
|
| 38 |
+
"Women 18-45, interests: fashion, lifestyle",
|
| 39 |
+
"Adults 30-60, interests: cooking, kitchen",
|
| 40 |
+
"Women 22-50, interests: beauty, wellness",
|
| 41 |
+
"Adults 25-55, interests: fashion, luxury goods",
|
| 42 |
+
],
|
| 43 |
+
risk_signals=[],
|
| 44 |
+
label="legit",
|
| 45 |
+
severity=0.0,
|
| 46 |
+
),
|
| 47 |
+
AdTemplate(
|
| 48 |
+
category="saas",
|
| 49 |
+
ad_copies=[
|
| 50 |
+
"Streamline your project management. 14-day free trial, no credit card required. Try TaskFlow Pro.",
|
| 51 |
+
"Automate your invoicing in minutes. Trusted by 50,000+ small businesses. InvoiceSimple.",
|
| 52 |
+
"HR management made easy. Onboarding, payroll, compliance — all in one platform. PeopleFirst.",
|
| 53 |
+
"Secure cloud backup for your business. 256-bit encryption. Plans from $9/mo. VaultBackup.",
|
| 54 |
+
"Email marketing that converts. AI-powered subject lines, drag-and-drop editor. MailCraft.",
|
| 55 |
+
],
|
| 56 |
+
targeting_hints=[
|
| 57 |
+
"Professionals 25-55, interests: business, productivity",
|
| 58 |
+
"Small business owners, interests: finance, accounting",
|
| 59 |
+
"HR managers 30-50, interests: management, recruiting",
|
| 60 |
+
"IT professionals 25-55, interests: cloud, security",
|
| 61 |
+
"Marketers 25-45, interests: digital marketing, email",
|
| 62 |
+
],
|
| 63 |
+
risk_signals=[],
|
| 64 |
+
label="legit",
|
| 65 |
+
severity=0.0,
|
| 66 |
+
),
|
| 67 |
+
AdTemplate(
|
| 68 |
+
category="local_service",
|
| 69 |
+
ad_copies=[
|
| 70 |
+
"Need a plumber? Same-day service, licensed & insured. Call Mike's Plumbing — serving Portland since 2008.",
|
| 71 |
+
"Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria.",
|
| 72 |
+
"Professional house cleaning. Eco-friendly products, background-checked staff. SparkleHome Services.",
|
| 73 |
+
"Expert tax preparation. CPA with 15 years experience. Free consultation. Williams Tax Group.",
|
| 74 |
+
"Dog walking and pet sitting. GPS-tracked walks, daily photo updates. PawPals Chicago.",
|
| 75 |
+
],
|
| 76 |
+
targeting_hints=[
|
| 77 |
+
"Homeowners 30-65, Portland metro area",
|
| 78 |
+
"Adults 18-55, Brooklyn, interests: food delivery",
|
| 79 |
+
"Homeowners 25-60, interests: home services",
|
| 80 |
+
"Adults 25-65, tax season, interests: finance",
|
| 81 |
+
"Pet owners 25-50, Chicago metro area",
|
| 82 |
+
],
|
| 83 |
+
risk_signals=[],
|
| 84 |
+
label="legit",
|
| 85 |
+
severity=0.0,
|
| 86 |
+
),
|
| 87 |
+
AdTemplate(
|
| 88 |
+
category="education",
|
| 89 |
+
ad_copies=[
|
| 90 |
+
"Learn Python programming in 12 weeks. Live instructor, project-based curriculum. CodeAcademy Pro.",
|
| 91 |
+
"MBA programs online — AACSB accredited. Flexible schedule for working professionals. StateU Online.",
|
| 92 |
+
"SAT prep that works. Average 150-point score improvement. Guaranteed. PrepMaster Tutoring.",
|
| 93 |
+
"Professional photography workshops. Small class sizes, hands-on practice. LensArt Studio.",
|
| 94 |
+
"Spanish fluency in 6 months. Native instructors, conversation-first approach. LinguaViva.",
|
| 95 |
+
],
|
| 96 |
+
targeting_hints=[
|
| 97 |
+
"Adults 18-35, interests: programming, tech careers",
|
| 98 |
+
"Professionals 25-45, interests: MBA, business education",
|
| 99 |
+
"Parents of teens, interests: college prep, SAT",
|
| 100 |
+
"Adults 25-55, interests: photography, creative arts",
|
| 101 |
+
"Adults 18-50, interests: language learning, travel",
|
| 102 |
+
],
|
| 103 |
+
risk_signals=[],
|
| 104 |
+
label="legit",
|
| 105 |
+
severity=0.0,
|
| 106 |
+
),
|
| 107 |
+
AdTemplate(
|
| 108 |
+
category="fitness",
|
| 109 |
+
ad_copies=[
|
| 110 |
+
"Join our community gym. No contracts, cancel anytime. First month $19.99. FitLife Fitness.",
|
| 111 |
+
"Yoga for beginners — online classes with certified instructors. 7-day free trial. ZenFlow Yoga.",
|
| 112 |
+
"Custom meal plans designed by registered dietitians. Achieve your health goals. NutriPlan.",
|
| 113 |
+
"Running shoes engineered for comfort. 90-day return policy. SwiftStride Athletics.",
|
| 114 |
+
"Personal training at home. Certified trainers, flexible scheduling. FitVisit.",
|
| 115 |
+
],
|
| 116 |
+
targeting_hints=[
|
| 117 |
+
"Adults 18-55, interests: fitness, gym",
|
| 118 |
+
"Adults 25-50, interests: yoga, wellness, mindfulness",
|
| 119 |
+
"Health-conscious adults 22-55, interests: nutrition, diet",
|
| 120 |
+
"Runners 18-55, interests: running, athletics",
|
| 121 |
+
"Adults 25-50, interests: personal training, fitness",
|
| 122 |
+
],
|
| 123 |
+
risk_signals=[],
|
| 124 |
+
label="legit",
|
| 125 |
+
severity=0.0,
|
| 126 |
+
),
|
| 127 |
+
]
|
| 128 |
+
|
| 129 |
+
FRAUD_TEMPLATES: List[AdTemplate] = [
|
| 130 |
+
# === OBVIOUS SCAMS (Task 1) ===
|
| 131 |
+
AdTemplate(
|
| 132 |
+
category="fake_giveaway",
|
| 133 |
+
ad_copies=[
|
| 134 |
+
"CONGRATULATIONS!! You've been selected to win a FREE iPhone 16 Pro!! Click NOW before offer expires!!!",
|
| 135 |
+
"🎉 WINNER ALERT 🎉 Claim your FREE $1000 Amazon Gift Card! Limited time only! Act NOW!",
|
| 136 |
+
"You are the 1,000,000th visitor! Click here to claim your FREE MacBook Pro! Don't miss out!!!",
|
| 137 |
+
"FREE PS5 GIVEAWAY! Just complete a quick survey! Only 3 left! Hurry!!!",
|
| 138 |
+
"EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!",
|
| 139 |
+
],
|
| 140 |
+
targeting_hints=[
|
| 141 |
+
"All adults 18+, broad targeting, worldwide",
|
| 142 |
+
"Adults 18-65, interests: shopping, deals, coupons",
|
| 143 |
+
"Adults 18-35, interests: technology, gaming",
|
| 144 |
+
"Young adults 16-30, interests: gaming, PlayStation",
|
| 145 |
+
"Adults 18-55, interests: cars, Tesla, electric vehicles",
|
| 146 |
+
],
|
| 147 |
+
risk_signals=[
|
| 148 |
+
"excessive urgency language",
|
| 149 |
+
"too-good-to-be-true offer",
|
| 150 |
+
"excessive punctuation/caps",
|
| 151 |
+
"no purchase necessary claims",
|
| 152 |
+
],
|
| 153 |
+
label="fraud",
|
| 154 |
+
fraud_type="fake_giveaway",
|
| 155 |
+
severity=0.9,
|
| 156 |
+
difficulty="easy",
|
| 157 |
+
),
|
| 158 |
+
AdTemplate(
|
| 159 |
+
category="counterfeit_goods",
|
| 160 |
+
ad_copies=[
|
| 161 |
+
"Designer handbags 90% OFF! Gucci, Louis Vuitton, Prada — all authentic! Limited stock! BargainLux.shop",
|
| 162 |
+
"Rolex watches starting at $49.99! 100% genuine! Free worldwide shipping! LuxWatchDeals.net",
|
| 163 |
+
"Nike Air Jordan CLEARANCE — $29.99! All sizes available! Original quality guaranteed! SneakerVault.store",
|
| 164 |
+
"Ray-Ban sunglasses factory direct! 95% discount! Premium quality! SunDealz.com",
|
| 165 |
+
"Canada Goose jackets $79.99! Authentic merchandise, warehouse clearance! ArcticDeals.shop",
|
| 166 |
+
],
|
| 167 |
+
targeting_hints=[
|
| 168 |
+
"Adults 18-45, interests: luxury brands, fashion deals",
|
| 169 |
+
"Men 25-55, interests: watches, luxury",
|
| 170 |
+
"Young adults 16-30, interests: sneakers, streetwear",
|
| 171 |
+
"Adults 18-50, interests: fashion, sunglasses, deals",
|
| 172 |
+
"Adults 20-45, interests: outdoor gear, fashion",
|
| 173 |
+
],
|
| 174 |
+
risk_signals=[
|
| 175 |
+
"impossibly low prices for luxury goods",
|
| 176 |
+
"unfamiliar domain",
|
| 177 |
+
"claims of authenticity with extreme discounts",
|
| 178 |
+
],
|
| 179 |
+
label="fraud",
|
| 180 |
+
fraud_type="counterfeit",
|
| 181 |
+
severity=0.8,
|
| 182 |
+
difficulty="easy",
|
| 183 |
+
),
|
| 184 |
+
AdTemplate(
|
| 185 |
+
category="miracle_cure",
|
| 186 |
+
ad_copies=[
|
| 187 |
+
"DOCTORS HATE THIS! One simple trick cures diabetes in 7 days! Big Pharma doesn't want you to know!",
|
| 188 |
+
"Lose 30 pounds in 2 weeks with this ancient herb! No diet, no exercise! MiracleSlim pills!",
|
| 189 |
+
"CURE CANCER NATURALLY! This secret remedy has a 99% success rate! Order now before it's banned!",
|
| 190 |
+
"Reverse aging in 14 days! Scientists discover fountain of youth pill! Limited supply!",
|
| 191 |
+
"Grow 3 inches taller at ANY age! Patented growth formula! 100% guaranteed results!",
|
| 192 |
+
],
|
| 193 |
+
targeting_hints=[
|
| 194 |
+
"Adults 35-70, interests: health, diabetes, natural remedies",
|
| 195 |
+
"Adults 25-60, interests: weight loss, diet, fitness",
|
| 196 |
+
"Adults 40-75, interests: cancer, health, alternative medicine",
|
| 197 |
+
"Adults 35-65, interests: anti-aging, beauty, health",
|
| 198 |
+
"Men 18-40, interests: fitness, height, self-improvement",
|
| 199 |
+
],
|
| 200 |
+
risk_signals=[
|
| 201 |
+
"unsubstantiated medical claims",
|
| 202 |
+
"conspiracy language ('they don't want you to know')",
|
| 203 |
+
"impossible health promises",
|
| 204 |
+
"urgency/scarcity tactics",
|
| 205 |
+
],
|
| 206 |
+
label="fraud",
|
| 207 |
+
fraud_type="miracle_cure",
|
| 208 |
+
severity=0.95,
|
| 209 |
+
difficulty="easy",
|
| 210 |
+
),
|
| 211 |
+
AdTemplate(
|
| 212 |
+
category="advance_fee",
|
| 213 |
+
ad_copies=[
|
| 214 |
+
"URGENT: I am Prince Adebayo of Nigeria. I need your help to transfer $4.5 million. You keep 30%.",
|
| 215 |
+
"Congratulations! You've inherited $2.8 million from a distant relative. Pay $200 processing fee to claim.",
|
| 216 |
+
"WORK FROM HOME! Earn $5000/week stuffing envelopes! No experience needed! Send $50 registration fee.",
|
| 217 |
+
"URGENT BUSINESS PROPOSAL: Oil company executive needs foreign partner. $10M deal. Reply immediately.",
|
| 218 |
+
"You've won the UK National Lottery! Send $100 for tax processing to receive your $1.5M prize.",
|
| 219 |
+
],
|
| 220 |
+
targeting_hints=[
|
| 221 |
+
"All adults 18+, broad targeting",
|
| 222 |
+
"Adults 30-70, interests: finance, inheritance",
|
| 223 |
+
"Adults 18-65, interests: work from home, jobs",
|
| 224 |
+
"Adults 30-65, interests: business, investment",
|
| 225 |
+
"Adults 25-70, interests: lottery, gambling, luck",
|
| 226 |
+
],
|
| 227 |
+
risk_signals=[
|
| 228 |
+
"advance fee request",
|
| 229 |
+
"unsolicited money offer",
|
| 230 |
+
"urgency pressure",
|
| 231 |
+
"request for personal information",
|
| 232 |
+
],
|
| 233 |
+
label="fraud",
|
| 234 |
+
fraud_type="advance_fee_scam",
|
| 235 |
+
severity=0.85,
|
| 236 |
+
difficulty="easy",
|
| 237 |
+
),
|
| 238 |
+
|
| 239 |
+
# === SOPHISTICATED SCAMS (Task 2) ===
|
| 240 |
+
AdTemplate(
|
| 241 |
+
category="fake_crypto",
|
| 242 |
+
ad_copies=[
|
| 243 |
+
"Invest in DeFi with confidence. Our AI-powered trading platform delivers 12-15% monthly returns. Audited smart contracts. Join CryptoYield Pro.",
|
| 244 |
+
"Stake your crypto with institutional-grade security. 8% APY on BTC, 12% on ETH. Regulated in Singapore. VaultStake Finance.",
|
| 245 |
+
"New token launch: GreenChain ($GRNC) — carbon credit blockchain. Pre-sale live. Backed by major VCs. Read our whitepaper.",
|
| 246 |
+
"Automated crypto arbitrage — our algorithm exploits price differences across exchanges. Average 20% monthly. QuantBot Trading.",
|
| 247 |
+
"Web3 real estate tokenization. Own fractions of premium properties. Starting at $100. Licensed broker-dealer. PropChain.io",
|
| 248 |
+
],
|
| 249 |
+
targeting_hints=[
|
| 250 |
+
"Men 25-45, interests: cryptocurrency, DeFi, investing",
|
| 251 |
+
"Adults 25-55, interests: crypto staking, passive income",
|
| 252 |
+
"Tech-savvy adults 20-40, interests: blockchain, Web3, tokens",
|
| 253 |
+
"Adults 25-50, interests: algo trading, crypto, finance",
|
| 254 |
+
"Adults 30-55, interests: real estate, investing, crypto",
|
| 255 |
+
],
|
| 256 |
+
risk_signals=[
|
| 257 |
+
"high guaranteed returns",
|
| 258 |
+
"complex financial product",
|
| 259 |
+
],
|
| 260 |
+
label="fraud",
|
| 261 |
+
fraud_type="fake_crypto",
|
| 262 |
+
severity=0.7,
|
| 263 |
+
difficulty="medium",
|
| 264 |
+
),
|
| 265 |
+
AdTemplate(
|
| 266 |
+
category="celebrity_endorsement_fraud",
|
| 267 |
+
ad_copies=[
|
| 268 |
+
"Elon Musk reveals his #1 investment for 2026. 'This will be bigger than Tesla.' Exclusive interview inside.",
|
| 269 |
+
"Warren Buffett's secret stock pick leaked! Financial advisors are scrambling. See the details before it's removed.",
|
| 270 |
+
"Oprah's favorite weight loss secret finally revealed. 'I lost 40 pounds without dieting.' Doctor-approved method.",
|
| 271 |
+
"Mark Cuban: 'I'm putting everything into this AI company.' Find out which stock billionaires are buying.",
|
| 272 |
+
"Taylor Swift partners with new wellness brand. 'It changed my life.' Limited celebrity edition available.",
|
| 273 |
+
],
|
| 274 |
+
targeting_hints=[
|
| 275 |
+
"Adults 25-55, interests: investing, Elon Musk, technology",
|
| 276 |
+
"Adults 35-70, interests: stocks, Warren Buffett, investing",
|
| 277 |
+
"Women 25-60, interests: weight loss, Oprah, wellness",
|
| 278 |
+
"Adults 25-50, interests: AI, investing, Mark Cuban",
|
| 279 |
+
"Adults 18-45, interests: Taylor Swift, wellness, beauty",
|
| 280 |
+
],
|
| 281 |
+
risk_signals=[
|
| 282 |
+
"celebrity name used without verification",
|
| 283 |
+
"urgency/exclusivity language",
|
| 284 |
+
],
|
| 285 |
+
label="fraud",
|
| 286 |
+
fraud_type="fake_endorsement",
|
| 287 |
+
severity=0.65,
|
| 288 |
+
difficulty="medium",
|
| 289 |
+
),
|
| 290 |
+
AdTemplate(
|
| 291 |
+
category="clone_brand",
|
| 292 |
+
ad_copies=[
|
| 293 |
+
"Amaz0n Warehouse Clearance — Electronics up to 70% off. Official overstock sale. Limited time. amaz0n-deals.com",
|
| 294 |
+
"PayPaI Security Alert: Your account needs verification. Update your information to avoid suspension. Secure portal.",
|
| 295 |
+
"Walmrt+ Members: Exclusive flash sale this weekend only. Save an extra 25% on groceries. Members-only link.",
|
| 296 |
+
"App1e Trade-In Event: Get up to $800 for your old device. Upgrade to iPhone 16. Authorized reseller.",
|
| 297 |
+
"Netfliix Premium — Get 1 year for $29.99! Special promotional price. Activate through our partner link.",
|
| 298 |
+
],
|
| 299 |
+
targeting_hints=[
|
| 300 |
+
"Adults 18-65, interests: Amazon, deals, electronics",
|
| 301 |
+
"Adults 18-65, interests: online payments, PayPal",
|
| 302 |
+
"Adults 25-60, interests: Walmart, grocery shopping",
|
| 303 |
+
"Adults 18-55, interests: Apple, iPhone, technology",
|
| 304 |
+
"Adults 18-50, interests: Netflix, streaming, entertainment",
|
| 305 |
+
],
|
| 306 |
+
risk_signals=[
|
| 307 |
+
"brand name misspelling",
|
| 308 |
+
"unofficial domain",
|
| 309 |
+
],
|
| 310 |
+
label="fraud",
|
| 311 |
+
fraud_type="brand_impersonation",
|
| 312 |
+
severity=0.75,
|
| 313 |
+
difficulty="medium",
|
| 314 |
+
),
|
| 315 |
+
AdTemplate(
|
| 316 |
+
category="gray_area_supplements",
|
| 317 |
+
ad_copies=[
|
| 318 |
+
"NeuroPeak — clinically studied nootropic for enhanced focus and memory. 30-day money-back guarantee. FDA-registered facility.",
|
| 319 |
+
"TestoMax Elite — natural testosterone support for men over 40. Doctor-formulated. Published clinical trial results.",
|
| 320 |
+
"SlimBiome Pro — proprietary probiotic blend for healthy weight management. Featured in Health Magazine. GMP certified.",
|
| 321 |
+
"Joint Restore Plus — advanced collagen peptide formula. Thousands of 5-star reviews. Orthopedist recommended.",
|
| 322 |
+
"SleepDeep Ultra — melatonin-free natural sleep aid. Non-habit forming. Money-back guarantee. PharmGrade Labs.",
|
| 323 |
+
],
|
| 324 |
+
targeting_hints=[
|
| 325 |
+
"Adults 25-55, interests: nootropics, productivity, brain health",
|
| 326 |
+
"Men 40-65, interests: men's health, testosterone, fitness",
|
| 327 |
+
"Adults 30-60, interests: weight loss, probiotics, gut health",
|
| 328 |
+
"Adults 45-75, interests: joint health, supplements, arthritis",
|
| 329 |
+
"Adults 30-65, interests: sleep, insomnia, wellness",
|
| 330 |
+
],
|
| 331 |
+
risk_signals=[
|
| 332 |
+
"supplement with strong efficacy claims",
|
| 333 |
+
"medical-sounding language",
|
| 334 |
+
],
|
| 335 |
+
label="escalate",
|
| 336 |
+
fraud_type="gray_area",
|
| 337 |
+
severity=0.4,
|
| 338 |
+
difficulty="medium",
|
| 339 |
+
),
|
| 340 |
+
|
| 341 |
+
# === NETWORK SCAMS (Task 3 — individual ads that belong to fraud rings) ===
|
| 342 |
+
# These templates look almost entirely legitimate on the surface.
|
| 343 |
+
# The fraud is only detectable through cross-ad investigation.
|
| 344 |
+
AdTemplate(
|
| 345 |
+
category="network_crypto",
|
| 346 |
+
ad_copies=[
|
| 347 |
+
"Earn passive income with our DeFi staking pool. Community-driven, transparent governance. BlockYield.io",
|
| 348 |
+
"Next-gen DEX with lowest fees. Trade with zero slippage on major pairs. SwapNova Protocol.",
|
| 349 |
+
"NFT marketplace for digital artists. Mint, list, and sell with 2% fees. Creator-first platform. ArtBlock.exchange",
|
| 350 |
+
"Cross-chain bridge with military-grade security. Move assets between chains in seconds. ChainLink Pro.",
|
| 351 |
+
"Crypto tax software — auto-import from 50+ exchanges. CPA-approved reports. TaxChain Solutions.",
|
| 352 |
+
],
|
| 353 |
+
targeting_hints=[
|
| 354 |
+
"Adults 20-40, interests: DeFi, crypto staking",
|
| 355 |
+
"Adults 20-45, interests: crypto trading, DEX",
|
| 356 |
+
"Adults 20-40, interests: NFT, digital art, crypto",
|
| 357 |
+
"Adults 25-45, interests: cross-chain, crypto bridges",
|
| 358 |
+
"Adults 25-55, interests: crypto, tax, finance",
|
| 359 |
+
],
|
| 360 |
+
risk_signals=[],
|
| 361 |
+
label="fraud",
|
| 362 |
+
fraud_type="coordinated_network",
|
| 363 |
+
severity=0.6,
|
| 364 |
+
difficulty="hard",
|
| 365 |
+
),
|
| 366 |
+
AdTemplate(
|
| 367 |
+
category="network_ecommerce",
|
| 368 |
+
ad_copies=[
|
| 369 |
+
"Premium wireless earbuds — ANC, 40hr battery. Compare to AirPods Pro at half the price. SoundElite Store.",
|
| 370 |
+
"Smart home security camera — 2K resolution, night vision, cloud storage. TechGuard Home.",
|
| 371 |
+
"Portable power station for camping. 1000W, solar-ready. Adventure-proof design. PowerTrail Gear.",
|
| 372 |
+
"Ergonomic office chair — lumbar support, breathable mesh. 5-year warranty. WorkComfort Direct.",
|
| 373 |
+
"Robot vacuum with LiDAR mapping. Self-emptying dock. Smart home compatible. CleanBot Pro.",
|
| 374 |
+
],
|
| 375 |
+
targeting_hints=[
|
| 376 |
+
"Adults 18-45, interests: tech, audio, headphones",
|
| 377 |
+
"Adults 25-55, interests: home security, smart home",
|
| 378 |
+
"Adults 25-50, interests: camping, outdoor gear",
|
| 379 |
+
"Adults 25-55, interests: home office, ergonomics",
|
| 380 |
+
"Adults 25-55, interests: smart home, cleaning",
|
| 381 |
+
],
|
| 382 |
+
risk_signals=[],
|
| 383 |
+
label="fraud",
|
| 384 |
+
fraud_type="coordinated_network",
|
| 385 |
+
severity=0.5,
|
| 386 |
+
difficulty="hard",
|
| 387 |
+
),
|
| 388 |
+
AdTemplate(
|
| 389 |
+
category="network_fintech",
|
| 390 |
+
ad_copies=[
|
| 391 |
+
"Simplify your bookkeeping. AI-powered categorization, bank sync, tax-ready reports. LedgerFlow.",
|
| 392 |
+
"Expense management for growing teams. Real-time tracking, receipt scanning, approval workflows. SpendWise.",
|
| 393 |
+
"Invoicing made simple. Send professional invoices in 30 seconds. Accept 40+ payment methods. BillStream.",
|
| 394 |
+
"Payroll for startups — automated tax filings, direct deposit, compliance included. PayRun HR.",
|
| 395 |
+
"Business credit monitoring. Real-time alerts, score tracking, credit-building tips. BizCredit Insights.",
|
| 396 |
+
],
|
| 397 |
+
targeting_hints=[
|
| 398 |
+
"Small business owners 28-55, interests: accounting, bookkeeping",
|
| 399 |
+
"Finance managers 30-50, interests: expense management, SaaS",
|
| 400 |
+
"Freelancers 22-45, interests: invoicing, self-employment",
|
| 401 |
+
"Startup founders 25-45, interests: payroll, HR tech",
|
| 402 |
+
"Entrepreneurs 30-55, interests: business credit, finance",
|
| 403 |
+
],
|
| 404 |
+
risk_signals=[],
|
| 405 |
+
label="fraud",
|
| 406 |
+
fraud_type="coordinated_network",
|
| 407 |
+
severity=0.55,
|
| 408 |
+
difficulty="hard",
|
| 409 |
+
),
|
| 410 |
+
AdTemplate(
|
| 411 |
+
category="network_health",
|
| 412 |
+
ad_copies=[
|
| 413 |
+
"Telehealth appointments in minutes. Board-certified physicians, prescriptions delivered. MediConnect Health.",
|
| 414 |
+
"Mental wellness platform. Licensed therapists, flexible scheduling, insurance accepted. MindBridge Care.",
|
| 415 |
+
"At-home lab testing. Comprehensive panels, fast results, doctor-reviewed. HealthCheck Direct.",
|
| 416 |
+
"Nutrition coaching backed by science. Personalized meal plans from registered dietitians. NutriGuide Pro.",
|
| 417 |
+
"Fitness tracking reimagined. AI-powered workout plans, progress analytics, community support. FitMetrics.",
|
| 418 |
+
],
|
| 419 |
+
targeting_hints=[
|
| 420 |
+
"Adults 25-60, interests: telemedicine, healthcare",
|
| 421 |
+
"Adults 25-50, interests: mental health, therapy, wellness",
|
| 422 |
+
"Health-conscious adults 30-55, interests: lab testing, preventive health",
|
| 423 |
+
"Adults 25-50, interests: nutrition, diet planning, health",
|
| 424 |
+
"Fitness enthusiasts 20-45, interests: fitness tracking, wearables",
|
| 425 |
+
],
|
| 426 |
+
risk_signals=[],
|
| 427 |
+
label="fraud",
|
| 428 |
+
fraud_type="coordinated_network",
|
| 429 |
+
severity=0.5,
|
| 430 |
+
difficulty="hard",
|
| 431 |
+
),
|
| 432 |
+
]
|
data/landing_pages.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simulated landing page investigation data.
|
| 3 |
+
|
| 4 |
+
When the agent investigates 'landing_page' for an ad, this module
|
| 5 |
+
generates the findings text based on the ad's ground truth label.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import random
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from typing import List
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class LandingPageData:
|
| 17 |
+
domain: str
|
| 18 |
+
domain_age_days: int
|
| 19 |
+
ssl_valid: bool
|
| 20 |
+
registrar: str
|
| 21 |
+
content_summary: str
|
| 22 |
+
redirect_chain: List[str]
|
| 23 |
+
similarity_to_known_scams: float # 0.0-1.0
|
| 24 |
+
key_claims: List[str]
|
| 25 |
+
suspicious_elements: List[str]
|
| 26 |
+
|
| 27 |
+
def to_investigation_text(self) -> str:
|
| 28 |
+
ssl_status = "Valid SSL certificate" if self.ssl_valid else "NO SSL / expired certificate"
|
| 29 |
+
redirects = " -> ".join(self.redirect_chain) if self.redirect_chain else "No redirects (direct)"
|
| 30 |
+
suspicious = "\n".join(f" - {s}" for s in self.suspicious_elements) if self.suspicious_elements else " None detected"
|
| 31 |
+
claims = "\n".join(f" - {c}" for c in self.key_claims) if self.key_claims else " None"
|
| 32 |
+
|
| 33 |
+
lines = [
|
| 34 |
+
f"Domain: {self.domain}",
|
| 35 |
+
f"Domain age: {self.domain_age_days} days",
|
| 36 |
+
f"SSL: {ssl_status}",
|
| 37 |
+
f"Registrar: {self.registrar}",
|
| 38 |
+
f"WHOIS privacy: {'Enabled' if 'privacy' in self.registrar.lower() or 'proxy' in self.registrar.lower() else 'Disabled (registrant info public)'}",
|
| 39 |
+
f"Redirect chain: {redirects}",
|
| 40 |
+
f"Similarity to known scam templates: {self.similarity_to_known_scams:.0%}",
|
| 41 |
+
f"Key claims on page:\n{claims}",
|
| 42 |
+
f"Suspicious elements:\n{suspicious}",
|
| 43 |
+
f"Content summary: {self.content_summary}",
|
| 44 |
+
]
|
| 45 |
+
return "\n".join(lines)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
_LEGIT_REGISTRARS = ["GoDaddy", "Namecheap", "Google Domains", "Cloudflare Registrar", "AWS Route 53"]
|
| 49 |
+
_SUSPICIOUS_REGISTRARS = ["NameSilo", "Epik", "Njalla (privacy)", "Tucows (privacy proxy)", "Openprovider"]
|
| 50 |
+
|
| 51 |
+
_LEGIT_DOMAINS_SUFFIXES = [".com", ".co", ".org", ".io", ".net"]
|
| 52 |
+
_SCAM_DOMAINS_SUFFIXES = [".shop", ".store", ".xyz", ".click", ".top", ".buzz", ".site"]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def generate_landing_page(
|
| 56 |
+
rng: random.Random,
|
| 57 |
+
ad_id: str,
|
| 58 |
+
is_fraud: bool,
|
| 59 |
+
fraud_type: str = "",
|
| 60 |
+
*,
|
| 61 |
+
domain_override: str | None = None,
|
| 62 |
+
registrar_override: str | None = None,
|
| 63 |
+
) -> LandingPageData:
|
| 64 |
+
"""Generate simulated landing page investigation data."""
|
| 65 |
+
|
| 66 |
+
base_word = rng.choice(["deal", "offer", "shop", "store", "buy", "get", "best", "top", "pro", "elite"])
|
| 67 |
+
|
| 68 |
+
if is_fraud:
|
| 69 |
+
suffix = rng.choice(_SCAM_DOMAINS_SUFFIXES)
|
| 70 |
+
domain = domain_override or f"{base_word}{rng.randint(10, 9999)}{suffix}"
|
| 71 |
+
domain_age = rng.randint(1, 60)
|
| 72 |
+
ssl_valid = rng.random() < 0.4
|
| 73 |
+
registrar = registrar_override or rng.choice(_SUSPICIOUS_REGISTRARS)
|
| 74 |
+
similarity = round(rng.uniform(0.5, 0.95), 2)
|
| 75 |
+
|
| 76 |
+
redirect_count = rng.randint(1, 4)
|
| 77 |
+
redirect_chain = [domain]
|
| 78 |
+
for _ in range(redirect_count):
|
| 79 |
+
redirect_chain.append(f"redirect{rng.randint(1, 999)}{rng.choice(['.click', '.top', '.xyz'])}")
|
| 80 |
+
redirect_chain.append(f"final-{rng.randint(1, 999)}{rng.choice(['.site', '.store'])}")
|
| 81 |
+
|
| 82 |
+
suspicious = _get_fraud_suspicious_elements(rng, fraud_type)
|
| 83 |
+
claims = _get_fraud_claims(rng, fraud_type)
|
| 84 |
+
content = _get_fraud_content_summary(rng, fraud_type)
|
| 85 |
+
else:
|
| 86 |
+
suffix = rng.choice(_LEGIT_DOMAINS_SUFFIXES)
|
| 87 |
+
brand = rng.choice(["homenest", "stylehaven", "taskflow", "fitlife", "sparkle", "codeacademy", "nutri"])
|
| 88 |
+
domain = domain_override or f"{brand}{suffix}"
|
| 89 |
+
domain_age = rng.randint(365, 5000)
|
| 90 |
+
ssl_valid = True
|
| 91 |
+
registrar = registrar_override or rng.choice(_LEGIT_REGISTRARS)
|
| 92 |
+
similarity = round(rng.uniform(0.0, 0.15), 2)
|
| 93 |
+
redirect_chain = []
|
| 94 |
+
suspicious = []
|
| 95 |
+
claims = _get_legit_claims(rng)
|
| 96 |
+
content = _get_legit_content_summary(rng)
|
| 97 |
+
|
| 98 |
+
return LandingPageData(
|
| 99 |
+
domain=domain,
|
| 100 |
+
domain_age_days=domain_age,
|
| 101 |
+
ssl_valid=ssl_valid,
|
| 102 |
+
registrar=registrar,
|
| 103 |
+
content_summary=content,
|
| 104 |
+
redirect_chain=redirect_chain,
|
| 105 |
+
similarity_to_known_scams=similarity,
|
| 106 |
+
key_claims=claims,
|
| 107 |
+
suspicious_elements=suspicious,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _get_fraud_suspicious_elements(rng: random.Random, fraud_type: str) -> List[str]:
|
| 112 |
+
common = [
|
| 113 |
+
"No physical address listed",
|
| 114 |
+
"No contact phone number",
|
| 115 |
+
"Privacy policy copied from template",
|
| 116 |
+
"Terms of service link returns 404",
|
| 117 |
+
]
|
| 118 |
+
type_specific = {
|
| 119 |
+
"fake_giveaway": ["Countdown timer with fake urgency", "Requests personal info before any value provided"],
|
| 120 |
+
"counterfeit": ["Product images appear stolen from official brand site", "Price comparison shows 90%+ discount vs retail"],
|
| 121 |
+
"miracle_cure": ["FDA disclaimer buried in footer", "Before/after photos appear digitally altered"],
|
| 122 |
+
"advance_fee_scam": ["Wire transfer requested upfront", "No company registration found"],
|
| 123 |
+
"fake_crypto": ["Smart contract not verified on-chain", "Team photos are stock images"],
|
| 124 |
+
"fake_endorsement": ["Celebrity quote not found in any verified source", "Affiliate tracking parameters in URL"],
|
| 125 |
+
"brand_impersonation": ["Domain mimics well-known brand with character substitution", "Logo appears edited from official assets"],
|
| 126 |
+
"gray_area": ["Citations reference unpublished or retracted studies", "Testimonials lack verifiable details"],
|
| 127 |
+
"coordinated_network": ["Identical page template used across multiple domains", "Contact form submits to third-party aggregator"],
|
| 128 |
+
}
|
| 129 |
+
elements = list(common)
|
| 130 |
+
elements.extend(type_specific.get(fraud_type, []))
|
| 131 |
+
rng.shuffle(elements)
|
| 132 |
+
return elements[: rng.randint(2, min(5, len(elements)))]
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _get_fraud_claims(rng: random.Random, fraud_type: str) -> List[str]:
|
| 136 |
+
by_type = {
|
| 137 |
+
"fake_giveaway": ["100% free, no purchase necessary", "Guaranteed winner", "Act now — expires in 2 hours"],
|
| 138 |
+
"counterfeit": ["Authentic products", "Direct from manufacturer", "90% below retail price"],
|
| 139 |
+
"miracle_cure": ["Clinically proven results", "Works in 7 days or less", "Endorsed by doctors"],
|
| 140 |
+
"advance_fee_scam": ["Guaranteed returns", "Low processing fee required", "Confidential opportunity"],
|
| 141 |
+
"fake_crypto": ["12-15% monthly returns guaranteed", "Audited by top security firms", "Risk-free investment"],
|
| 142 |
+
"fake_endorsement": ["As seen on TV", "Celebrity recommended", "Limited exclusive offer"],
|
| 143 |
+
"brand_impersonation": ["Official authorized retailer", "Factory direct pricing", "Same quality, lower price"],
|
| 144 |
+
"gray_area": ["Clinically studied ingredients", "Doctor recommended", "30-day money-back guarantee"],
|
| 145 |
+
"coordinated_network": ["Limited stock available", "Thousands of 5-star reviews", "Fast free shipping"],
|
| 146 |
+
}
|
| 147 |
+
return by_type.get(fraud_type, ["Special limited offer", "Act now"])
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def _get_fraud_content_summary(rng: random.Random, fraud_type: str) -> str:
|
| 151 |
+
summaries = {
|
| 152 |
+
"fake_giveaway": "Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.",
|
| 153 |
+
"counterfeit": "Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.",
|
| 154 |
+
"miracle_cure": "Long-form sales page with testimonials, before/after images, and pseudoscientific explanations. Multiple urgency CTAs.",
|
| 155 |
+
"advance_fee_scam": "Simple page with a letter-style appeal requesting wire transfer. Grammar errors. No company details.",
|
| 156 |
+
"fake_crypto": "Professional-looking platform with dashboard screenshots. Whitepaper link leads to a generic PDF. Team bios use stock photos.",
|
| 157 |
+
"fake_endorsement": "News article-style page with celebrity photos. Comments section appears pre-populated with positive feedback.",
|
| 158 |
+
"brand_impersonation": "Near-replica of the official brand website. URL uses character substitution. Cart and checkout flow functional but data destination unclear.",
|
| 159 |
+
"gray_area": "Well-designed supplement product page. Ingredient list present but efficacy claims exceed scientific evidence. Has a real return policy.",
|
| 160 |
+
"coordinated_network": "Standard e-commerce template. Products appear legitimate but company details are vague. Identical template to other flagged sites.",
|
| 161 |
+
}
|
| 162 |
+
return summaries.get(fraud_type, "Generic landing page with minimal content and heavy use of urgency language.")
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def _get_legit_claims(rng: random.Random) -> List[str]:
|
| 166 |
+
options = [
|
| 167 |
+
"Free shipping on orders over $50",
|
| 168 |
+
"14-day free trial",
|
| 169 |
+
"Money-back guarantee",
|
| 170 |
+
"Established since 2010",
|
| 171 |
+
"4.5-star average customer rating",
|
| 172 |
+
"Licensed and insured",
|
| 173 |
+
"BBB accredited",
|
| 174 |
+
"Certified organic ingredients",
|
| 175 |
+
]
|
| 176 |
+
rng.shuffle(options)
|
| 177 |
+
return options[: rng.randint(2, 4)]
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _get_legit_content_summary(rng: random.Random) -> str:
|
| 181 |
+
options = [
|
| 182 |
+
"Well-structured business website with clear product catalog, about page, contact information, and shipping policy.",
|
| 183 |
+
"Professional service website with team bios, case studies, client testimonials, and clear pricing.",
|
| 184 |
+
"E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.",
|
| 185 |
+
"SaaS landing page with feature breakdown, pricing tiers, integration docs, and live demo option.",
|
| 186 |
+
]
|
| 187 |
+
return rng.choice(options)
|
data/network_generator.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fraud network (ring) generation for Task 3 using networkx.
|
| 3 |
+
|
| 4 |
+
Generates complex fraud ring topologies:
|
| 5 |
+
- Simple cliques (3-5 ads fully connected)
|
| 6 |
+
- Chain structures (A-B via payment, B-C via creative — transitive reasoning required)
|
| 7 |
+
- Hub-and-spoke (one master account linked to satellites)
|
| 8 |
+
|
| 9 |
+
Individual ads in a ring may look borderline; the signal is in the connections.
|
| 10 |
+
Each edge in the graph carries the signal type that connects the two ads.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import random
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from typing import Dict, List, Set, Tuple
|
| 18 |
+
|
| 19 |
+
import networkx as nx
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class FraudRing:
|
| 24 |
+
ring_id: str
|
| 25 |
+
member_ad_ids: List[str]
|
| 26 |
+
shared_signals: Dict[str, str] # signal_type -> shared_value
|
| 27 |
+
topology: str = "clique" # clique, chain, hub_spoke
|
| 28 |
+
|
| 29 |
+
@property
|
| 30 |
+
def size(self) -> int:
|
| 31 |
+
return len(self.member_ad_ids)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
_RING_TOPOLOGIES = ["clique", "chain", "hub_spoke"]
|
| 35 |
+
|
| 36 |
+
_SIGNAL_POOL_KEYS = ["payment_method", "domain_registrar", "creative_template", "targeting_overlap"]
|
| 37 |
+
|
| 38 |
+
_REGISTRAR_CHOICES = ["Njalla (privacy)", "Epik", "NameSilo", "Tucows (privacy proxy)"]
|
| 39 |
+
_TARGETING_CHOICES = [
|
| 40 |
+
"Men 25-45, crypto+investing, US+UK+AU",
|
| 41 |
+
"Adults 18-35, tech+gaming, worldwide",
|
| 42 |
+
"Women 25-55, health+beauty, US+CA",
|
| 43 |
+
"Adults 30-60, finance+real-estate, US+UK",
|
| 44 |
+
"Adults 20-40, e-commerce+dropshipping, US+EU",
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _make_signal_pool(rng: random.Random, ring_index: int) -> Dict[str, str]:
|
| 49 |
+
"""Generate a pool of shared signal values for one ring."""
|
| 50 |
+
return {
|
| 51 |
+
"payment_method": f"pmt_ring_{rng.randint(10000, 99999)}",
|
| 52 |
+
"domain_registrar": rng.choice(_REGISTRAR_CHOICES),
|
| 53 |
+
"creative_template": f"tmpl_{rng.randint(1000, 9999)}",
|
| 54 |
+
"targeting_overlap": rng.choice(_TARGETING_CHOICES),
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def generate_fraud_networks(
|
| 59 |
+
rng: random.Random,
|
| 60 |
+
n_rings: int,
|
| 61 |
+
available_fraud_ad_ids: List[str],
|
| 62 |
+
) -> Tuple[List[FraudRing], Dict[str, List[str]]]:
|
| 63 |
+
"""
|
| 64 |
+
Generate fraud ring structures with complex topologies.
|
| 65 |
+
|
| 66 |
+
Returns:
|
| 67 |
+
rings: list of FraudRing objects
|
| 68 |
+
ad_to_rings: mapping from ad_id to list of ring_ids it belongs to
|
| 69 |
+
"""
|
| 70 |
+
G = nx.Graph()
|
| 71 |
+
rings: List[FraudRing] = []
|
| 72 |
+
ad_to_rings: Dict[str, List[str]] = {}
|
| 73 |
+
|
| 74 |
+
remaining = list(available_fraud_ad_ids)
|
| 75 |
+
rng.shuffle(remaining)
|
| 76 |
+
|
| 77 |
+
for i in range(n_rings):
|
| 78 |
+
if len(remaining) < 3:
|
| 79 |
+
break
|
| 80 |
+
ring_size = rng.randint(3, min(5, len(remaining)))
|
| 81 |
+
|
| 82 |
+
members = remaining[:ring_size]
|
| 83 |
+
remaining = remaining[ring_size:]
|
| 84 |
+
|
| 85 |
+
topology = rng.choice(_RING_TOPOLOGIES)
|
| 86 |
+
signal_pool = _make_signal_pool(rng, i)
|
| 87 |
+
|
| 88 |
+
signal_keys = list(_SIGNAL_POOL_KEYS)
|
| 89 |
+
rng.shuffle(signal_keys)
|
| 90 |
+
n_shared = rng.randint(2, len(signal_keys))
|
| 91 |
+
shared_signals = {k: signal_pool[k] for k in signal_keys[:n_shared]}
|
| 92 |
+
|
| 93 |
+
_add_edges_for_topology(G, members, shared_signals, topology, rng)
|
| 94 |
+
|
| 95 |
+
ring_id = f"ring_{i}"
|
| 96 |
+
ring = FraudRing(
|
| 97 |
+
ring_id=ring_id,
|
| 98 |
+
member_ad_ids=members,
|
| 99 |
+
shared_signals=shared_signals,
|
| 100 |
+
topology=topology,
|
| 101 |
+
)
|
| 102 |
+
rings.append(ring)
|
| 103 |
+
|
| 104 |
+
for ad_id in members:
|
| 105 |
+
ad_to_rings.setdefault(ad_id, []).append(ring_id)
|
| 106 |
+
G.add_node(ad_id, ring_id=ring_id)
|
| 107 |
+
|
| 108 |
+
# Optionally create bridge nodes between rings for extra complexity
|
| 109 |
+
if len(rings) >= 2 and remaining:
|
| 110 |
+
_add_bridge_ads(G, rings, remaining, ad_to_rings, rng)
|
| 111 |
+
|
| 112 |
+
return rings, ad_to_rings
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _add_edges_for_topology(
|
| 116 |
+
G: nx.Graph,
|
| 117 |
+
members: List[str],
|
| 118 |
+
shared_signals: Dict[str, str],
|
| 119 |
+
topology: str,
|
| 120 |
+
rng: random.Random,
|
| 121 |
+
) -> None:
|
| 122 |
+
"""Add edges to the graph based on the ring topology."""
|
| 123 |
+
signal_types = list(shared_signals.keys())
|
| 124 |
+
|
| 125 |
+
if topology == "clique":
|
| 126 |
+
for i, a in enumerate(members):
|
| 127 |
+
for b in members[i + 1:]:
|
| 128 |
+
signal = rng.choice(signal_types)
|
| 129 |
+
G.add_edge(a, b, signal_type=signal, signal_value=shared_signals[signal])
|
| 130 |
+
|
| 131 |
+
elif topology == "chain":
|
| 132 |
+
for idx in range(len(members) - 1):
|
| 133 |
+
signal = signal_types[idx % len(signal_types)]
|
| 134 |
+
G.add_edge(
|
| 135 |
+
members[idx], members[idx + 1],
|
| 136 |
+
signal_type=signal, signal_value=shared_signals[signal],
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
elif topology == "hub_spoke":
|
| 140 |
+
hub = members[0]
|
| 141 |
+
for spoke in members[1:]:
|
| 142 |
+
signal = rng.choice(signal_types)
|
| 143 |
+
G.add_edge(hub, spoke, signal_type=signal, signal_value=shared_signals[signal])
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _add_bridge_ads(
|
| 147 |
+
G: nx.Graph,
|
| 148 |
+
rings: List[FraudRing],
|
| 149 |
+
remaining: List[str],
|
| 150 |
+
ad_to_rings: Dict[str, List[str]],
|
| 151 |
+
rng: random.Random,
|
| 152 |
+
) -> None:
|
| 153 |
+
"""Optionally link two rings via a shared bridge ad from the remaining pool."""
|
| 154 |
+
if len(remaining) < 1 or len(rings) < 2:
|
| 155 |
+
return
|
| 156 |
+
|
| 157 |
+
bridge_ad = remaining.pop(0)
|
| 158 |
+
r1, r2 = rings[0], rings[1]
|
| 159 |
+
|
| 160 |
+
bridge_to_r1 = rng.choice(r1.member_ad_ids)
|
| 161 |
+
bridge_to_r2 = rng.choice(r2.member_ad_ids)
|
| 162 |
+
|
| 163 |
+
r1.member_ad_ids.append(bridge_ad)
|
| 164 |
+
ad_to_rings.setdefault(bridge_ad, []).extend([r1.ring_id, r2.ring_id])
|
| 165 |
+
|
| 166 |
+
sig_key = rng.choice(list(r1.shared_signals.keys()))
|
| 167 |
+
G.add_edge(bridge_ad, bridge_to_r1, signal_type=sig_key, signal_value=r1.shared_signals[sig_key])
|
| 168 |
+
|
| 169 |
+
sig_key2 = rng.choice(list(r2.shared_signals.keys()))
|
| 170 |
+
G.add_edge(bridge_ad, bridge_to_r2, signal_type=sig_key2, signal_value=r2.shared_signals[sig_key2])
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def get_ring_shared_signal_text(ring: FraudRing) -> str:
|
| 174 |
+
"""Describe the shared signals in a ring (for grader/debug use)."""
|
| 175 |
+
lines = [f"Fraud Ring {ring.ring_id} ({ring.size} members, topology={ring.topology}):"]
|
| 176 |
+
lines.append(f" Members: {', '.join(ring.member_ad_ids)}")
|
| 177 |
+
lines.append(" Shared signals:")
|
| 178 |
+
for signal_type, value in ring.shared_signals.items():
|
| 179 |
+
lines.append(f" - {signal_type}: {value}")
|
| 180 |
+
return "\n".join(lines)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def build_ground_truth_graph(rings: List[FraudRing]) -> nx.Graph:
|
| 184 |
+
"""Reconstruct the full ground truth network graph from rings.
|
| 185 |
+
|
| 186 |
+
Used by graders to compute the expected set of edges.
|
| 187 |
+
"""
|
| 188 |
+
G = nx.Graph()
|
| 189 |
+
for ring in rings:
|
| 190 |
+
for i, a in enumerate(ring.member_ad_ids):
|
| 191 |
+
G.add_node(a, ring_id=ring.ring_id)
|
| 192 |
+
for b in ring.member_ad_ids[i + 1:]:
|
| 193 |
+
G.add_edge(a, b, ring_id=ring.ring_id)
|
| 194 |
+
return G
|
graders/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Graders for scoring agent performance on each task."""
|
| 2 |
+
|
| 3 |
+
from .base_grader import grade_episode
|
| 4 |
+
from .task1_grader import Task1Grader
|
| 5 |
+
from .task2_grader import Task2Grader
|
| 6 |
+
from .task3_grader import Task3Grader
|
| 7 |
+
|
| 8 |
+
GRADERS = {
|
| 9 |
+
"task_1": Task1Grader(),
|
| 10 |
+
"task_2": Task2Grader(),
|
| 11 |
+
"task_3": Task3Grader(),
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
__all__ = ["grade_episode", "Task1Grader", "Task2Grader", "Task3Grader", "GRADERS"]
|
graders/base_grader.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shared grading logic for all tasks.
|
| 3 |
+
|
| 4 |
+
Each grader produces a 0.0-1.0 score by normalizing raw reward
|
| 5 |
+
between theoretical worst and best cases.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from abc import ABC, abstractmethod
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from typing import Any, Dict, List, Optional
|
| 13 |
+
|
| 14 |
+
_NORM_EPS = 1e-8 # For numerical stability
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class VerdictResult:
|
| 19 |
+
ad_id: str
|
| 20 |
+
verdict: str # approve, reject, escalate
|
| 21 |
+
confidence: float
|
| 22 |
+
ground_truth: str # fraud, legit, escalate
|
| 23 |
+
auto_approved: bool = False
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class LinkResult:
|
| 28 |
+
ad_id_1: str
|
| 29 |
+
ad_id_2: str
|
| 30 |
+
correct: bool
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class EpisodeRecord:
|
| 35 |
+
"""All data needed for grading a completed episode."""
|
| 36 |
+
task_id: str
|
| 37 |
+
total_steps: int
|
| 38 |
+
action_budget: int
|
| 39 |
+
verdicts: List[VerdictResult]
|
| 40 |
+
links: List[LinkResult]
|
| 41 |
+
ads_metadata: List[Dict[str, Any]] # [{ad_id, ground_truth, severity, ...}]
|
| 42 |
+
n_fraud_rings: int = 0
|
| 43 |
+
ring_sizes: List[int] = None
|
| 44 |
+
|
| 45 |
+
def __post_init__(self):
|
| 46 |
+
if self.ring_sizes is None:
|
| 47 |
+
self.ring_sizes = []
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class BaseGrader(ABC):
|
| 51 |
+
"""Abstract grader that scores an episode 0.0-1.0."""
|
| 52 |
+
|
| 53 |
+
@abstractmethod
|
| 54 |
+
def grade(self, record: EpisodeRecord) -> float:
|
| 55 |
+
"""Return a score in [0.0, 1.0]."""
|
| 56 |
+
...
|
| 57 |
+
|
| 58 |
+
def _count_correct_verdicts(self, verdicts: List[VerdictResult]) -> int:
|
| 59 |
+
return sum(1 for v in verdicts if self._is_correct(v))
|
| 60 |
+
|
| 61 |
+
def _count_false_positives(self, verdicts: List[VerdictResult]) -> int:
|
| 62 |
+
return sum(
|
| 63 |
+
1 for v in verdicts
|
| 64 |
+
if v.verdict == "reject" and v.ground_truth == "legit"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
def _count_false_negatives(self, verdicts: List[VerdictResult]) -> int:
|
| 68 |
+
return sum(
|
| 69 |
+
1 for v in verdicts
|
| 70 |
+
if v.verdict == "approve" and v.ground_truth == "fraud"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def _is_correct(self, v: VerdictResult) -> bool:
|
| 74 |
+
return (
|
| 75 |
+
(v.verdict == "reject" and v.ground_truth == "fraud")
|
| 76 |
+
or (v.verdict == "approve" and v.ground_truth == "legit")
|
| 77 |
+
or (v.verdict == "escalate" and v.ground_truth == "escalate")
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
def _verdict_reward(self, v: VerdictResult, severity: float = 0.5) -> float:
|
| 81 |
+
if v.verdict == "reject" and v.ground_truth == "fraud":
|
| 82 |
+
return 0.3 + 0.1 * severity
|
| 83 |
+
elif v.verdict == "approve" and v.ground_truth == "legit":
|
| 84 |
+
return 0.1
|
| 85 |
+
elif v.verdict == "escalate" and v.ground_truth == "escalate":
|
| 86 |
+
return 0.15
|
| 87 |
+
elif v.verdict == "reject" and v.ground_truth == "legit":
|
| 88 |
+
return -0.35
|
| 89 |
+
elif v.verdict == "approve" and v.ground_truth == "fraud":
|
| 90 |
+
return -0.5
|
| 91 |
+
elif v.verdict == "escalate":
|
| 92 |
+
return -0.05
|
| 93 |
+
elif v.verdict == "approve" and v.ground_truth == "escalate":
|
| 94 |
+
return -0.15
|
| 95 |
+
elif v.verdict == "reject" and v.ground_truth == "escalate":
|
| 96 |
+
return -0.1
|
| 97 |
+
return -0.05
|
| 98 |
+
|
| 99 |
+
def _normalize(self, raw: float, best: float, worst: float) -> float:
|
| 100 |
+
score_range = best - worst
|
| 101 |
+
if score_range <= 0:
|
| 102 |
+
return 0.5
|
| 103 |
+
normalized = (raw - worst) / (score_range + _NORM_EPS)
|
| 104 |
+
return max(0.0, min(1.0, normalized))
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def grade_episode(record: EpisodeRecord) -> float:
|
| 108 |
+
"""Grade an episode using the appropriate task grader."""
|
| 109 |
+
from . import GRADERS
|
| 110 |
+
grader = GRADERS.get(record.task_id)
|
| 111 |
+
if grader is None:
|
| 112 |
+
raise ValueError(f"Unknown task_id: {record.task_id}")
|
| 113 |
+
return grader.grade(record)
|
graders/task1_grader.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task 1 Grader: Basic Ad Triage (Easy).
|
| 3 |
+
|
| 4 |
+
Scores based on verdict accuracy only. No network or calibration bonuses.
|
| 5 |
+
5 ads, 25 budget — a decent LLM should score 0.6-0.8.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from .base_grader import BaseGrader, EpisodeRecord
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Task1Grader(BaseGrader):
|
| 14 |
+
def grade(self, record: EpisodeRecord) -> float:
|
| 15 |
+
raw = 0.0
|
| 16 |
+
best = 0.0
|
| 17 |
+
worst = 0.0
|
| 18 |
+
|
| 19 |
+
severity_map = {
|
| 20 |
+
m["ad_id"]: m.get("severity", 0.5) for m in record.ads_metadata
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
for v in record.verdicts:
|
| 24 |
+
severity = severity_map.get(v.ad_id, 0.5)
|
| 25 |
+
raw += self._verdict_reward(v, severity)
|
| 26 |
+
|
| 27 |
+
if v.ground_truth == "fraud":
|
| 28 |
+
best += 0.3 + 0.1 * severity
|
| 29 |
+
worst -= 0.5
|
| 30 |
+
elif v.ground_truth == "legit":
|
| 31 |
+
best += 0.1
|
| 32 |
+
worst -= 0.35
|
| 33 |
+
elif v.ground_truth == "escalate":
|
| 34 |
+
best += 0.15
|
| 35 |
+
worst -= 0.15
|
| 36 |
+
|
| 37 |
+
n_investigations = max(0, record.total_steps - len([
|
| 38 |
+
v for v in record.verdicts if not v.auto_approved
|
| 39 |
+
]))
|
| 40 |
+
raw -= n_investigations * 0.02
|
| 41 |
+
worst -= record.action_budget * 0.02
|
| 42 |
+
|
| 43 |
+
best += 0.2
|
| 44 |
+
|
| 45 |
+
if record.total_steps > 0:
|
| 46 |
+
correct = self._count_correct_verdicts(record.verdicts)
|
| 47 |
+
raw += (correct / record.total_steps) * 0.2
|
| 48 |
+
|
| 49 |
+
return self._normalize(raw, best, worst)
|
graders/task2_grader.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task 2 Grader: Sophisticated Fraud Under Budget Pressure (Medium).
|
| 3 |
+
|
| 4 |
+
Adds budget efficiency bonus and calibration scoring on top of verdict accuracy.
|
| 5 |
+
12 ads, 30 budget — requires triage, expected baseline 0.3-0.5.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from .base_grader import BaseGrader, EpisodeRecord
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Task2Grader(BaseGrader):
|
| 14 |
+
def grade(self, record: EpisodeRecord) -> float:
|
| 15 |
+
raw = 0.0
|
| 16 |
+
best = 0.0
|
| 17 |
+
worst = 0.0
|
| 18 |
+
|
| 19 |
+
severity_map = {
|
| 20 |
+
m["ad_id"]: m.get("severity", 0.5) for m in record.ads_metadata
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
for v in record.verdicts:
|
| 24 |
+
severity = severity_map.get(v.ad_id, 0.5)
|
| 25 |
+
raw += self._verdict_reward(v, severity)
|
| 26 |
+
|
| 27 |
+
if v.ground_truth == "fraud":
|
| 28 |
+
best += 0.3 + 0.1 * severity
|
| 29 |
+
worst -= 0.5
|
| 30 |
+
elif v.ground_truth == "legit":
|
| 31 |
+
best += 0.1
|
| 32 |
+
worst -= 0.35
|
| 33 |
+
elif v.ground_truth == "escalate":
|
| 34 |
+
best += 0.15
|
| 35 |
+
worst -= 0.15
|
| 36 |
+
|
| 37 |
+
n_investigations = max(0, record.total_steps - len([
|
| 38 |
+
v for v in record.verdicts if not v.auto_approved
|
| 39 |
+
]))
|
| 40 |
+
raw -= n_investigations * 0.02
|
| 41 |
+
worst -= record.action_budget * 0.02
|
| 42 |
+
|
| 43 |
+
# Budget efficiency bonus
|
| 44 |
+
best += 0.2
|
| 45 |
+
if record.total_steps > 0:
|
| 46 |
+
correct = self._count_correct_verdicts(record.verdicts)
|
| 47 |
+
raw += (correct / record.total_steps) * 0.2
|
| 48 |
+
|
| 49 |
+
# Calibration bonus: reward agents whose stated confidence
|
| 50 |
+
# correlates with actual accuracy
|
| 51 |
+
calibration = self._compute_calibration(record)
|
| 52 |
+
raw += calibration * 0.15
|
| 53 |
+
best += 0.15
|
| 54 |
+
|
| 55 |
+
return self._normalize(raw, best, worst)
|
| 56 |
+
|
| 57 |
+
def _compute_calibration(self, record: EpisodeRecord) -> float:
|
| 58 |
+
"""
|
| 59 |
+
Measure how well confidence tracks correctness.
|
| 60 |
+
|
| 61 |
+
Bins verdicts by confidence and checks if the fraction correct
|
| 62 |
+
in each bin roughly matches the stated confidence.
|
| 63 |
+
"""
|
| 64 |
+
manual = [v for v in record.verdicts if not v.auto_approved]
|
| 65 |
+
if len(manual) < 3:
|
| 66 |
+
return 0.5
|
| 67 |
+
|
| 68 |
+
bins = {"low": [], "mid": [], "high": []}
|
| 69 |
+
for v in manual:
|
| 70 |
+
if v.confidence < 0.4:
|
| 71 |
+
bins["low"].append(v)
|
| 72 |
+
elif v.confidence < 0.7:
|
| 73 |
+
bins["mid"].append(v)
|
| 74 |
+
else:
|
| 75 |
+
bins["high"].append(v)
|
| 76 |
+
|
| 77 |
+
errors = []
|
| 78 |
+
for _label, group in bins.items():
|
| 79 |
+
if not group:
|
| 80 |
+
continue
|
| 81 |
+
avg_conf = sum(v.confidence for v in group) / len(group)
|
| 82 |
+
avg_acc = sum(1 for v in group if self._is_correct(v)) / len(group)
|
| 83 |
+
errors.append(abs(avg_conf - avg_acc))
|
| 84 |
+
|
| 85 |
+
if not errors:
|
| 86 |
+
return 0.5
|
| 87 |
+
|
| 88 |
+
avg_error = sum(errors) / len(errors)
|
| 89 |
+
return max(0.0, 1.0 - 2.0 * avg_error)
|
graders/task3_grader.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task 3 Grader: Coordinated Fraud Network Detection (Hard).
|
| 3 |
+
|
| 4 |
+
Adds graph-based network detection scoring on top of verdict accuracy,
|
| 5 |
+
budget efficiency, and calibration. 20 ads, 35 budget, 3 fraud rings
|
| 6 |
+
with varied topologies. Expected baseline 0.1-0.3.
|
| 7 |
+
|
| 8 |
+
Network scoring uses ground truth edge coverage: what fraction of
|
| 9 |
+
ground-truth ring connections did the agent discover via link_accounts?
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from .base_grader import BaseGrader, EpisodeRecord
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class Task3Grader(BaseGrader):
|
| 18 |
+
def grade(self, record: EpisodeRecord) -> float:
|
| 19 |
+
raw = 0.0
|
| 20 |
+
best = 0.0
|
| 21 |
+
worst = 0.0
|
| 22 |
+
|
| 23 |
+
severity_map = {
|
| 24 |
+
m["ad_id"]: m.get("severity", 0.5) for m in record.ads_metadata
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
for v in record.verdicts:
|
| 28 |
+
severity = severity_map.get(v.ad_id, 0.5)
|
| 29 |
+
raw += self._verdict_reward(v, severity)
|
| 30 |
+
|
| 31 |
+
if v.ground_truth == "fraud":
|
| 32 |
+
best += 0.3 + 0.1 * severity
|
| 33 |
+
worst -= 0.5
|
| 34 |
+
elif v.ground_truth == "legit":
|
| 35 |
+
best += 0.1
|
| 36 |
+
worst -= 0.35
|
| 37 |
+
elif v.ground_truth == "escalate":
|
| 38 |
+
best += 0.15
|
| 39 |
+
worst -= 0.15
|
| 40 |
+
|
| 41 |
+
n_investigations = max(0, record.total_steps - len([
|
| 42 |
+
v for v in record.verdicts if not v.auto_approved
|
| 43 |
+
]))
|
| 44 |
+
raw -= n_investigations * 0.02
|
| 45 |
+
worst -= record.action_budget * 0.02
|
| 46 |
+
|
| 47 |
+
# Budget efficiency bonus
|
| 48 |
+
best += 0.2
|
| 49 |
+
if record.total_steps > 0:
|
| 50 |
+
correct = self._count_correct_verdicts(record.verdicts)
|
| 51 |
+
raw += (correct / record.total_steps) * 0.2
|
| 52 |
+
|
| 53 |
+
# Calibration bonus
|
| 54 |
+
calibration = self._compute_calibration(record)
|
| 55 |
+
raw += calibration * 0.15
|
| 56 |
+
best += 0.15
|
| 57 |
+
|
| 58 |
+
# Graph-based network detection scoring
|
| 59 |
+
network_reward = self._compute_network_score(record)
|
| 60 |
+
raw += network_reward
|
| 61 |
+
# Best case: discover all ground truth edges
|
| 62 |
+
total_gt_edges = self._count_ground_truth_edges(record)
|
| 63 |
+
best += max(total_gt_edges, 1) * 0.4
|
| 64 |
+
worst -= max(len(record.links), 1) * 0.2
|
| 65 |
+
|
| 66 |
+
# Investigation coverage bonus: reward breadth over depth
|
| 67 |
+
coverage = self._compute_coverage_bonus(record)
|
| 68 |
+
raw += coverage * 0.1
|
| 69 |
+
best += 0.1
|
| 70 |
+
|
| 71 |
+
return self._normalize(raw, best, worst)
|
| 72 |
+
|
| 73 |
+
def _compute_calibration(self, record: EpisodeRecord) -> float:
|
| 74 |
+
manual = [v for v in record.verdicts if not v.auto_approved]
|
| 75 |
+
if len(manual) < 3:
|
| 76 |
+
return 0.5
|
| 77 |
+
|
| 78 |
+
bins = {"low": [], "mid": [], "high": []}
|
| 79 |
+
for v in manual:
|
| 80 |
+
if v.confidence < 0.4:
|
| 81 |
+
bins["low"].append(v)
|
| 82 |
+
elif v.confidence < 0.7:
|
| 83 |
+
bins["mid"].append(v)
|
| 84 |
+
else:
|
| 85 |
+
bins["high"].append(v)
|
| 86 |
+
|
| 87 |
+
errors = []
|
| 88 |
+
for _label, group in bins.items():
|
| 89 |
+
if not group:
|
| 90 |
+
continue
|
| 91 |
+
avg_conf = sum(v.confidence for v in group) / len(group)
|
| 92 |
+
avg_acc = sum(1 for v in group if self._is_correct(v)) / len(group)
|
| 93 |
+
errors.append(abs(avg_conf - avg_acc))
|
| 94 |
+
|
| 95 |
+
if not errors:
|
| 96 |
+
return 0.5
|
| 97 |
+
|
| 98 |
+
avg_error = sum(errors) / len(errors)
|
| 99 |
+
return max(0.0, 1.0 - 2.0 * avg_error)
|
| 100 |
+
|
| 101 |
+
def _count_ground_truth_edges(self, record: EpisodeRecord) -> int:
|
| 102 |
+
"""Count the total number of ground truth edges across all rings."""
|
| 103 |
+
total = 0
|
| 104 |
+
for ring_size in record.ring_sizes:
|
| 105 |
+
total += ring_size * (ring_size - 1) // 2
|
| 106 |
+
return total
|
| 107 |
+
|
| 108 |
+
def _compute_network_score(self, record: EpisodeRecord) -> float:
|
| 109 |
+
"""Score link_accounts actions against ground truth fraud rings.
|
| 110 |
+
|
| 111 |
+
Uses edge-coverage: correct links earn +0.4 each, incorrect
|
| 112 |
+
links incur -0.25 each (heavier penalty since decoys exist).
|
| 113 |
+
"""
|
| 114 |
+
correct_links = sum(1 for l in record.links if l.correct)
|
| 115 |
+
incorrect_links = sum(1 for l in record.links if not l.correct)
|
| 116 |
+
|
| 117 |
+
reward = correct_links * 0.4
|
| 118 |
+
reward -= incorrect_links * 0.25
|
| 119 |
+
|
| 120 |
+
return reward
|
| 121 |
+
|
| 122 |
+
def _compute_coverage_bonus(self, record: EpisodeRecord) -> float:
|
| 123 |
+
"""Reward agents that investigate across multiple ads (breadth).
|
| 124 |
+
|
| 125 |
+
Agents that only deep-dive a single ad miss network signals.
|
| 126 |
+
"""
|
| 127 |
+
if not record.ads_metadata:
|
| 128 |
+
return 0.0
|
| 129 |
+
|
| 130 |
+
total_ads = len(record.ads_metadata)
|
| 131 |
+
reviewed = sum(1 for v in record.verdicts if not v.auto_approved)
|
| 132 |
+
coverage_ratio = reviewed / total_ads if total_ads > 0 else 0.0
|
| 133 |
+
return min(1.0, coverage_ratio)
|
inference.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inference Script — Ad Fraud Investigation Environment
|
| 3 |
+
===================================
|
| 4 |
+
MANDATORY
|
| 5 |
+
- Before submitting, ensure the following variables are defined in your environment configuration:
|
| 6 |
+
API_BASE_URL The API endpoint for the LLM.
|
| 7 |
+
MODEL_NAME The model identifier to use for inference.
|
| 8 |
+
HF_TOKEN Your Hugging Face / API key.
|
| 9 |
+
- The inference script must be named `inference.py` and placed in the root directory of the project
|
| 10 |
+
- Participants must use OpenAI Client for all LLM calls using above variables
|
| 11 |
+
|
| 12 |
+
STDOUT FORMAT
|
| 13 |
+
- The script must emit exactly three line types to stdout, in this order:
|
| 14 |
+
|
| 15 |
+
[START] task=<task_name> env=<benchmark> model=<model_name>
|
| 16 |
+
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
|
| 17 |
+
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import json
|
| 23 |
+
import logging
|
| 24 |
+
import os
|
| 25 |
+
import re
|
| 26 |
+
import sys
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Any, Dict, List, Optional
|
| 29 |
+
|
| 30 |
+
from openai import OpenAI
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
from .client import AdFraudEnv
|
| 34 |
+
from .models import AdReviewAction
|
| 35 |
+
from .data.ad_generator import TASK_CONFIGS
|
| 36 |
+
except ImportError:
|
| 37 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 38 |
+
from ad_fraud_env.client import AdFraudEnv
|
| 39 |
+
from ad_fraud_env.models import AdReviewAction
|
| 40 |
+
from ad_fraud_env.data.ad_generator import TASK_CONFIGS
|
| 41 |
+
|
| 42 |
+
from dotenv import load_dotenv
|
| 43 |
+
load_dotenv()
|
| 44 |
+
|
| 45 |
+
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 46 |
+
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 47 |
+
MODEL_NAME = os.getenv("MODEL_NAME") or "meta-llama/Llama-3.1-8B-Instruct"
|
| 48 |
+
ENV_URL = os.getenv("AD_FRAUD_ENV_URL", "https://QuantumTransformer-AdArena.hf.space")
|
| 49 |
+
BENCHMARK = "ad_fraud_env"
|
| 50 |
+
TEMPERATURE = 0.1
|
| 51 |
+
MAX_TOKENS = 256
|
| 52 |
+
FALLBACK_VERDICT = "escalate"
|
| 53 |
+
|
| 54 |
+
logger = logging.getLogger(__name__)
|
| 55 |
+
LOG_DIR = Path(__file__).resolve().parent / "convo_logging"
|
| 56 |
+
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
# Mandatory structured stdout logging
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
|
| 61 |
+
def log_start(task: str, env: str, model: str) -> None:
|
| 62 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 66 |
+
error_val = error if error else "null"
|
| 67 |
+
done_val = str(done).lower()
|
| 68 |
+
print(
|
| 69 |
+
f"[STEP] step={step} action={action} reward={reward:.2f} "
|
| 70 |
+
f"done={done_val} error={error_val}",
|
| 71 |
+
flush=True,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 76 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 77 |
+
print(
|
| 78 |
+
f"[END] success={str(success).lower()} steps={steps} "
|
| 79 |
+
f"score={score:.2f} rewards={rewards_str}",
|
| 80 |
+
flush=True,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _format_action(action: AdReviewAction) -> str:
|
| 85 |
+
"""Compact action string for [STEP] log line."""
|
| 86 |
+
if action.action_type == "investigate":
|
| 87 |
+
return f"investigate({action.ad_id},{action.investigation_target})"
|
| 88 |
+
elif action.action_type == "verdict":
|
| 89 |
+
conf = action.confidence if action.confidence is not None else 0.5
|
| 90 |
+
return f"verdict({action.ad_id},{action.verdict},{conf:.2f})"
|
| 91 |
+
elif action.action_type == "link_accounts":
|
| 92 |
+
return f"link_accounts({action.ad_id},{action.linked_ad_id})"
|
| 93 |
+
return f"unknown({action.ad_id})"
|
| 94 |
+
|
| 95 |
+
# ---------------------------------------------------------------------------
|
| 96 |
+
# System prompt & helpers
|
| 97 |
+
# ---------------------------------------------------------------------------
|
| 98 |
+
|
| 99 |
+
SYSTEM_PROMPT = """\
|
| 100 |
+
You are an ad fraud investigator reviewing a queue of advertisements.
|
| 101 |
+
Your job is to investigate suspicious ads and render verdicts (approve, reject, or escalate).
|
| 102 |
+
|
| 103 |
+
For each step, you must output a single JSON action. The action schema is:
|
| 104 |
+
|
| 105 |
+
{
|
| 106 |
+
"action_type": "investigate" | "verdict" | "link_accounts",
|
| 107 |
+
"ad_id": "<ad ID, e.g. ad_001>",
|
| 108 |
+
|
| 109 |
+
// For investigate actions:
|
| 110 |
+
"investigation_target": "advertiser_history" | "landing_page" | "payment_method" | "targeting_overlap" | "creative_similarity" | "campaign_structure",
|
| 111 |
+
|
| 112 |
+
// For verdict actions:
|
| 113 |
+
"verdict": "approve" | "reject" | "escalate",
|
| 114 |
+
"confidence": <float 0.0-1.0>,
|
| 115 |
+
|
| 116 |
+
// For link_accounts actions:
|
| 117 |
+
"linked_ad_id": "<other ad ID>",
|
| 118 |
+
"link_reason": "<reason>"
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
Strategy:
|
| 122 |
+
1. Start by reading the queue summary and the first ad's information.
|
| 123 |
+
2. For obviously suspicious ads, investigate 1-2 signals then reject.
|
| 124 |
+
3. For clearly legitimate ads, approve quickly with high confidence.
|
| 125 |
+
4. For ambiguous ads, investigate more deeply before deciding.
|
| 126 |
+
5. Manage your budget — you cannot investigate everything.
|
| 127 |
+
6. For link_accounts, only flag connections when you see shared signals across ads (same payment method, similar creative template, targeting overlap).
|
| 128 |
+
|
| 129 |
+
Output ONLY the JSON action, no other text.
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
JSON_BLOCK_RE = re.compile(r"```(?:json)?\s*\n(.*?)```", re.DOTALL)
|
| 133 |
+
_FINDING_BLOCK_RE = re.compile(r"\[(ad_\d+)\s*/\s*([a-z_]+)\]")
|
| 134 |
+
_ANALYSIS_HEADER_RE = re.compile(
|
| 135 |
+
r"^(?:Payment Method|Targeting|Creative|Campaign Structure) Analysis for ad_\d+:$"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _extract_json(text: str) -> Dict[str, Any]:
|
| 140 |
+
"""Extract JSON from LLM response, handling markdown code blocks."""
|
| 141 |
+
text = text.strip()
|
| 142 |
+
m = JSON_BLOCK_RE.search(text)
|
| 143 |
+
if m:
|
| 144 |
+
text = m.group(1).strip()
|
| 145 |
+
elif text.startswith("```"):
|
| 146 |
+
lines = text.split("\n")
|
| 147 |
+
lines = [l for l in lines if not l.strip().startswith("```")]
|
| 148 |
+
text = "\n".join(lines).strip()
|
| 149 |
+
return json.loads(text)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _compact_finding(text: str) -> str:
|
| 153 |
+
"""Compress one investigation finding block into a compact key-value line.
|
| 154 |
+
|
| 155 |
+
Strips analysis headers and section labels but preserves ALL data fields
|
| 156 |
+
(both signal and noise) so the agent must still reason about relevance.
|
| 157 |
+
"""
|
| 158 |
+
parts: List[str] = []
|
| 159 |
+
for line in text.strip().split("\n"):
|
| 160 |
+
stripped = line.strip()
|
| 161 |
+
if not stripped:
|
| 162 |
+
continue
|
| 163 |
+
if _ANALYSIS_HEADER_RE.match(stripped):
|
| 164 |
+
continue
|
| 165 |
+
if stripped in ("Key claims on page:", "Suspicious elements:"):
|
| 166 |
+
continue
|
| 167 |
+
if stripped.startswith("- "):
|
| 168 |
+
stripped = stripped[2:]
|
| 169 |
+
parts.append(stripped)
|
| 170 |
+
return " | ".join(parts)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _build_compact_findings(raw: str, focused_ad: Optional[str]) -> str:
|
| 174 |
+
"""Parse findings blocks; full prose for focused ad, compact lines for others.
|
| 175 |
+
|
| 176 |
+
Each block in the raw findings string is delimited by [ad_XXX / target].
|
| 177 |
+
The focused ad keeps full investigation prose for deep reasoning.
|
| 178 |
+
Other ads are compressed to single-line key-value summaries that still
|
| 179 |
+
contain all extracted fields (payment type, IDs, template hashes,
|
| 180 |
+
fingerprints, domain ages, etc.) mixed with noise fields — the agent
|
| 181 |
+
must determine which values are meaningful for cross-ad comparison.
|
| 182 |
+
"""
|
| 183 |
+
blocks: List[tuple] = []
|
| 184 |
+
current_ad: Optional[str] = None
|
| 185 |
+
current_header: Optional[str] = None
|
| 186 |
+
current_lines: List[str] = []
|
| 187 |
+
|
| 188 |
+
for line in raw.split("\n"):
|
| 189 |
+
m = _FINDING_BLOCK_RE.match(line.strip())
|
| 190 |
+
if m:
|
| 191 |
+
if current_ad is not None:
|
| 192 |
+
blocks.append((current_ad, current_header, "\n".join(current_lines)))
|
| 193 |
+
current_ad = m.group(1)
|
| 194 |
+
current_header = line.strip()
|
| 195 |
+
current_lines = []
|
| 196 |
+
else:
|
| 197 |
+
current_lines.append(line)
|
| 198 |
+
|
| 199 |
+
if current_ad is not None:
|
| 200 |
+
blocks.append((current_ad, current_header, "\n".join(current_lines)))
|
| 201 |
+
|
| 202 |
+
result: List[str] = []
|
| 203 |
+
for ad_id, header, text in blocks:
|
| 204 |
+
if focused_ad and ad_id == focused_ad:
|
| 205 |
+
result.append(f"\n{header}\n{text}")
|
| 206 |
+
else:
|
| 207 |
+
compact = _compact_finding(text)
|
| 208 |
+
if compact:
|
| 209 |
+
result.append(f"{header} {compact}")
|
| 210 |
+
|
| 211 |
+
return "\n".join(result)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def build_obs_prompt(obs: Any) -> str:
|
| 215 |
+
"""Format an observation into the user prompt for the LLM.
|
| 216 |
+
|
| 217 |
+
The focused ad gets full investigation prose for deep reasoning.
|
| 218 |
+
Other investigated ads are compressed to compact key-value summaries
|
| 219 |
+
that preserve all fields (signal + noise) for cross-ad comparison.
|
| 220 |
+
"""
|
| 221 |
+
focused_ad: Optional[str] = None
|
| 222 |
+
if obs.current_ad_info:
|
| 223 |
+
m = re.search(r"Ad in Focus:\s*(ad_\d+)", obs.current_ad_info)
|
| 224 |
+
if m:
|
| 225 |
+
focused_ad = m.group(1)
|
| 226 |
+
|
| 227 |
+
parts = [
|
| 228 |
+
f"Queue: {obs.queue_summary}",
|
| 229 |
+
f"Current Ad: {obs.current_ad_info}",
|
| 230 |
+
f"Feedback: {obs.feedback}",
|
| 231 |
+
f"Available ads: {', '.join(obs.available_ads)}",
|
| 232 |
+
]
|
| 233 |
+
if obs.verdict_history_summary and obs.verdict_history_summary != "No verdicts yet.":
|
| 234 |
+
parts.append(f"Verdicts: {obs.verdict_history_summary}")
|
| 235 |
+
if obs.investigation_findings:
|
| 236 |
+
findings = _build_compact_findings(obs.investigation_findings, focused_ad)
|
| 237 |
+
if findings:
|
| 238 |
+
parts.append(f"Findings:\n{findings}")
|
| 239 |
+
return "\n\n".join(parts)
|
| 240 |
+
|
| 241 |
+
# ---------------------------------------------------------------------------
|
| 242 |
+
# Episode logger (markdown, for debugging — separate from mandatory stdout)
|
| 243 |
+
# ---------------------------------------------------------------------------
|
| 244 |
+
|
| 245 |
+
class EpisodeLogger:
|
| 246 |
+
"""Logs the full agent-environment conversation to a markdown file."""
|
| 247 |
+
|
| 248 |
+
def __init__(self, task_id: str, log_dir: Path) -> None:
|
| 249 |
+
self.task_id = task_id
|
| 250 |
+
self.lines: List[str] = []
|
| 251 |
+
log_dir.mkdir(parents=True, exist_ok=True)
|
| 252 |
+
self.path = log_dir / f"{task_id}_conversation.md"
|
| 253 |
+
self._md(f"# Episode Log — {task_id}\n")
|
| 254 |
+
|
| 255 |
+
def step_start(self, step: int, obs_prompt: str) -> None:
|
| 256 |
+
self._md(f"\n## Step {step}\n")
|
| 257 |
+
self._md(f"### Observation (sent to LLM)\n```\n{obs_prompt}\n```\n")
|
| 258 |
+
|
| 259 |
+
def llm_response(self, step: int, raw: str, action: AdReviewAction | None, fallback: bool) -> None:
|
| 260 |
+
tag = " [FALLBACK]" if fallback else ""
|
| 261 |
+
self._md(f"### LLM Response\n```json\n{raw.strip()}\n```\n")
|
| 262 |
+
if action:
|
| 263 |
+
act_dict = action.model_dump(exclude_none=True, exclude={"metadata"})
|
| 264 |
+
self._md(f"### Parsed Action{tag}\n```json\n{json.dumps(act_dict, indent=2)}\n```\n")
|
| 265 |
+
|
| 266 |
+
def env_feedback(self, step: int, reward: float, done: bool, feedback: str) -> None:
|
| 267 |
+
self._md(f"### Environment Response\n- **Reward:** `{reward:+.2f}`\n- **Done:** `{done}`\n")
|
| 268 |
+
self._md(f"- **Feedback:** {feedback}\n")
|
| 269 |
+
|
| 270 |
+
def episode_end(self, score: float, steps: int, verdicts: int, total: int) -> None:
|
| 271 |
+
summary = f"Score: {score:.3f} | Steps: {steps} | Verdicts: {verdicts}/{total}"
|
| 272 |
+
self._md(f"\n---\n## Result\n**{summary}**\n")
|
| 273 |
+
self._flush()
|
| 274 |
+
|
| 275 |
+
def _md(self, text: str) -> None:
|
| 276 |
+
self.lines.append(text)
|
| 277 |
+
|
| 278 |
+
def _flush(self) -> None:
|
| 279 |
+
with open(self.path, "w", encoding="utf-8") as f:
|
| 280 |
+
f.write("\n".join(self.lines))
|
| 281 |
+
|
| 282 |
+
# ---------------------------------------------------------------------------
|
| 283 |
+
# Core task runner
|
| 284 |
+
# ---------------------------------------------------------------------------
|
| 285 |
+
|
| 286 |
+
def run_single_task(
|
| 287 |
+
task_id: str,
|
| 288 |
+
seed: int = 42,
|
| 289 |
+
env_base_url: str = "https://QuantumTransformer-AdArena.hf.space",
|
| 290 |
+
) -> Dict[str, Any]:
|
| 291 |
+
"""Run the baseline agent on a single task with mandatory [START]/[STEP]/[END] logging."""
|
| 292 |
+
base_url = os.getenv("API_BASE_URL") or API_BASE_URL
|
| 293 |
+
api_key = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or API_KEY
|
| 294 |
+
model = os.getenv("MODEL_NAME") or MODEL_NAME
|
| 295 |
+
|
| 296 |
+
client = OpenAI(base_url=base_url, api_key=api_key, timeout=60.0)
|
| 297 |
+
env = AdFraudEnv(base_url=env_base_url).sync()
|
| 298 |
+
elog = EpisodeLogger(task_id, LOG_DIR)
|
| 299 |
+
|
| 300 |
+
config = TASK_CONFIGS.get(task_id)
|
| 301 |
+
max_steps = config.action_budget if config else 25
|
| 302 |
+
|
| 303 |
+
rewards: List[float] = []
|
| 304 |
+
steps_taken = 0
|
| 305 |
+
score = 0.0
|
| 306 |
+
success = False
|
| 307 |
+
|
| 308 |
+
log_start(task=task_id, env=BENCHMARK, model=model)
|
| 309 |
+
|
| 310 |
+
try:
|
| 311 |
+
env.connect()
|
| 312 |
+
result = env.reset(seed=seed, task_id=task_id)
|
| 313 |
+
|
| 314 |
+
messages: List[Dict[str, str]] = [
|
| 315 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 316 |
+
]
|
| 317 |
+
|
| 318 |
+
while not result.done and steps_taken < max_steps:
|
| 319 |
+
obs = result.observation
|
| 320 |
+
user_prompt = build_obs_prompt(obs)
|
| 321 |
+
messages.append({"role": "user", "content": user_prompt})
|
| 322 |
+
|
| 323 |
+
elog.step_start(steps_taken, user_prompt)
|
| 324 |
+
|
| 325 |
+
try:
|
| 326 |
+
completion = client.chat.completions.create(
|
| 327 |
+
model=model,
|
| 328 |
+
messages=messages,
|
| 329 |
+
temperature=TEMPERATURE,
|
| 330 |
+
max_tokens=MAX_TOKENS,
|
| 331 |
+
stream=False,
|
| 332 |
+
)
|
| 333 |
+
response_text = completion.choices[0].message.content or "{}"
|
| 334 |
+
except Exception as exc:
|
| 335 |
+
logger.warning("Model request failed on step %d: %s", steps_taken, exc)
|
| 336 |
+
response_text = "{}"
|
| 337 |
+
|
| 338 |
+
messages.append({"role": "assistant", "content": response_text})
|
| 339 |
+
|
| 340 |
+
error_msg = None
|
| 341 |
+
fallback = False
|
| 342 |
+
try:
|
| 343 |
+
action_data = _extract_json(response_text)
|
| 344 |
+
action = AdReviewAction(**action_data)
|
| 345 |
+
except Exception as e:
|
| 346 |
+
logger.warning("Failed to parse action on step %d: %s", steps_taken, e)
|
| 347 |
+
fallback = True
|
| 348 |
+
error_msg = str(e)
|
| 349 |
+
if obs.available_ads:
|
| 350 |
+
action = AdReviewAction(
|
| 351 |
+
action_type="verdict",
|
| 352 |
+
ad_id=obs.available_ads[0],
|
| 353 |
+
verdict=FALLBACK_VERDICT,
|
| 354 |
+
confidence=0.3,
|
| 355 |
+
)
|
| 356 |
+
else:
|
| 357 |
+
elog.llm_response(steps_taken, response_text, None, True)
|
| 358 |
+
break
|
| 359 |
+
|
| 360 |
+
elog.llm_response(steps_taken, response_text, action, fallback)
|
| 361 |
+
|
| 362 |
+
result = env.step(action)
|
| 363 |
+
steps_taken += 1
|
| 364 |
+
reward = result.reward or 0.0
|
| 365 |
+
rewards.append(reward)
|
| 366 |
+
|
| 367 |
+
log_step(
|
| 368 |
+
step=steps_taken,
|
| 369 |
+
action=_format_action(action),
|
| 370 |
+
reward=reward,
|
| 371 |
+
done=result.done,
|
| 372 |
+
error=error_msg,
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
elog.env_feedback(steps_taken, reward, result.done, result.observation.feedback)
|
| 376 |
+
|
| 377 |
+
# Each observation is self-contained (all findings + verdict summary),
|
| 378 |
+
# so we only keep system prompt + last 2 exchanges to stay within
|
| 379 |
+
# context limits while preserving the agent's reasoning continuity.
|
| 380 |
+
if len(messages) > 6:
|
| 381 |
+
messages = messages[:1] + messages[-4:]
|
| 382 |
+
|
| 383 |
+
state = env.state()
|
| 384 |
+
score = state.grader_score if state.grader_score is not None else 0.0
|
| 385 |
+
score = max(0.0, min(1.0, score))
|
| 386 |
+
success = score > 0.0
|
| 387 |
+
|
| 388 |
+
elog.episode_end(score, steps_taken, state.reviewed_count, state.total_ads)
|
| 389 |
+
|
| 390 |
+
return {
|
| 391 |
+
"task_id": task_id,
|
| 392 |
+
"score": score,
|
| 393 |
+
"steps": steps_taken,
|
| 394 |
+
"verdicts": state.reviewed_count,
|
| 395 |
+
"total_ads": state.total_ads,
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
except Exception as e:
|
| 399 |
+
logger.error("Task %s failed: %s", task_id, e)
|
| 400 |
+
return {"task_id": task_id, "score": 0.0, "error": str(e)}
|
| 401 |
+
|
| 402 |
+
finally:
|
| 403 |
+
try:
|
| 404 |
+
env.close()
|
| 405 |
+
except Exception as e:
|
| 406 |
+
print(f"[DEBUG] env.close() error: {e}", flush=True)
|
| 407 |
+
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
def run_baseline(
|
| 411 |
+
env_base_url: str = "https://QuantumTransformer-AdArena.hf.space",
|
| 412 |
+
) -> Dict[str, Any]:
|
| 413 |
+
"""Run baseline inference on all 3 tasks."""
|
| 414 |
+
model = os.getenv("MODEL_NAME") or MODEL_NAME
|
| 415 |
+
results: Dict[str, Any] = {}
|
| 416 |
+
for task_id in ["task_1", "task_2", "task_3"]:
|
| 417 |
+
logger.info("Running baseline for %s...", task_id)
|
| 418 |
+
try:
|
| 419 |
+
task_result = run_single_task(
|
| 420 |
+
task_id, seed=42, env_base_url=env_base_url,
|
| 421 |
+
)
|
| 422 |
+
results[task_id] = task_result
|
| 423 |
+
logger.info(" %s score: %.3f", task_id, task_result["score"])
|
| 424 |
+
except Exception as e:
|
| 425 |
+
logger.error(" %s failed: %s", task_id, e)
|
| 426 |
+
results[task_id] = {"task_id": task_id, "score": 0.0, "error": str(e)}
|
| 427 |
+
|
| 428 |
+
return {"baseline_model": model, "seed": 42, "tasks": results}
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def main() -> None:
|
| 432 |
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
| 433 |
+
|
| 434 |
+
if not API_KEY:
|
| 435 |
+
print("Error: HF_TOKEN (or API_KEY) environment variable is required.", file=sys.stderr)
|
| 436 |
+
sys.exit(1)
|
| 437 |
+
env_base_url = os.getenv("AD_FRAUD_ENV_URL", "https://QuantumTransformer-AdArena.hf.space")
|
| 438 |
+
|
| 439 |
+
print(f"Running baseline inference against {env_base_url} with model {MODEL_NAME}...", file=sys.stderr)
|
| 440 |
+
scores = run_baseline(env_base_url=env_base_url)
|
| 441 |
+
|
| 442 |
+
output_path = Path(__file__).resolve().parent / "baseline_scores.json"
|
| 443 |
+
with open(output_path, "w") as f:
|
| 444 |
+
json.dump(scores, f, indent=2)
|
| 445 |
+
|
| 446 |
+
print(f"\nBaseline scores saved to {output_path}", file=sys.stderr)
|
| 447 |
+
print(json.dumps(scores, indent=2), file=sys.stderr)
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
if __name__ == "__main__":
|
| 451 |
+
main()
|
models.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data models for the Ad Fraud Investigation Environment.
|
| 3 |
+
|
| 4 |
+
Defines the Action, Observation, and State types used by the OpenEnv interface.
|
| 5 |
+
The agent interacts with a queue of ads, investigating them and rendering verdicts.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Dict, List, Literal, Optional
|
| 11 |
+
|
| 12 |
+
from openenv.core.env_server.types import Action, Observation, State
|
| 13 |
+
from pydantic import Field
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class AdReviewAction(Action):
|
| 17 |
+
"""
|
| 18 |
+
Action space for the ad fraud investigation agent.
|
| 19 |
+
|
| 20 |
+
Three action types:
|
| 21 |
+
- investigate: Spend budget to reveal information about an ad
|
| 22 |
+
- verdict: Approve, reject, or escalate an ad
|
| 23 |
+
- link_accounts: Flag two ads as part of the same fraud network
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
action_type: Literal["investigate", "verdict", "link_accounts"]
|
| 27 |
+
ad_id: str = Field(..., description="Target ad identifier (e.g. 'ad_001')")
|
| 28 |
+
|
| 29 |
+
investigation_target: Optional[
|
| 30 |
+
Literal[
|
| 31 |
+
"advertiser_history",
|
| 32 |
+
"landing_page",
|
| 33 |
+
"payment_method",
|
| 34 |
+
"targeting_overlap",
|
| 35 |
+
"creative_similarity",
|
| 36 |
+
"campaign_structure",
|
| 37 |
+
]
|
| 38 |
+
] = Field(None, description="What to investigate (required for action_type='investigate')")
|
| 39 |
+
|
| 40 |
+
verdict: Optional[Literal["approve", "reject", "escalate"]] = Field(
|
| 41 |
+
None, description="Verdict decision (required for action_type='verdict')"
|
| 42 |
+
)
|
| 43 |
+
confidence: Optional[float] = Field(
|
| 44 |
+
None, ge=0.0, le=1.0, description="Agent's confidence in verdict (0.0-1.0)"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
linked_ad_id: Optional[str] = Field(
|
| 48 |
+
None, description="Other ad in suspected fraud ring (required for action_type='link_accounts')"
|
| 49 |
+
)
|
| 50 |
+
link_reason: Optional[str] = Field(
|
| 51 |
+
None, description="Why the agent believes these ads are connected"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class AdReviewObservation(Observation):
|
| 56 |
+
"""
|
| 57 |
+
Observation returned after each step.
|
| 58 |
+
|
| 59 |
+
Text-heavy by design so LLM agents can reason about the content naturally.
|
| 60 |
+
Structured data is in queue_status for programmatic access.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
queue_summary: str = Field(
|
| 64 |
+
default="", description="Natural language overview of the ad queue"
|
| 65 |
+
)
|
| 66 |
+
current_ad_info: str = Field(
|
| 67 |
+
default="", description="Details of the ad currently in focus"
|
| 68 |
+
)
|
| 69 |
+
investigation_findings: str = Field(
|
| 70 |
+
default="", description="Accumulated investigation results"
|
| 71 |
+
)
|
| 72 |
+
verdict_history_summary: str = Field(
|
| 73 |
+
default="", description="Summary of verdicts rendered so far"
|
| 74 |
+
)
|
| 75 |
+
feedback: str = Field(
|
| 76 |
+
default="", description="Natural language feedback on the last action taken"
|
| 77 |
+
)
|
| 78 |
+
available_ads: List[str] = Field(
|
| 79 |
+
default_factory=list, description="Ad IDs still pending review"
|
| 80 |
+
)
|
| 81 |
+
queue_status: Dict = Field(
|
| 82 |
+
default_factory=dict,
|
| 83 |
+
description="Structured status: total_ads, reviewed, pending, budget, step",
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class AdFraudState(State):
|
| 88 |
+
"""
|
| 89 |
+
Internal environment state exposed via the state() property.
|
| 90 |
+
|
| 91 |
+
Inherits episode_id and step_count from State.
|
| 92 |
+
Uses extra='allow' so custom fields are permitted.
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
task_id: str = ""
|
| 96 |
+
total_ads: int = 0
|
| 97 |
+
reviewed_count: int = 0
|
| 98 |
+
remaining_budget: int = 0
|
| 99 |
+
verdicts: Dict = Field(default_factory=dict)
|
| 100 |
+
grader_score: Optional[float] = None
|
openenv.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: ad_fraud_env
|
| 3 |
+
type: space
|
| 4 |
+
sdk: docker
|
| 5 |
+
runtime: fastapi
|
| 6 |
+
app: server.app:app
|
| 7 |
+
port: 8000
|
openenv_ad_fraud_env.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: openenv-ad-fraud-env
|
| 3 |
+
Version: 0.1.0
|
| 4 |
+
Summary: Ad Fraud Investigation Environment for OpenEnv — real-world ad fraud detection and investigation simulation
|
| 5 |
+
Requires-Python: >=3.11
|
| 6 |
+
Requires-Dist: openenv-core[core]>=0.2.2
|
| 7 |
+
Requires-Dist: fastapi>=0.115.0
|
| 8 |
+
Requires-Dist: pydantic>=2.0.0
|
| 9 |
+
Requires-Dist: uvicorn[standard]>=0.24.0
|
| 10 |
+
Requires-Dist: requests>=2.31.0
|
| 11 |
+
Requires-Dist: faker==33.1.0
|
| 12 |
+
Requires-Dist: networkx>=3.2
|
| 13 |
+
Requires-Dist: openai>=1.0.0
|
| 14 |
+
Requires-Dist: python-dotenv>=1.0.0
|
| 15 |
+
Provides-Extra: dev
|
| 16 |
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 17 |
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
| 18 |
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
openenv_ad_fraud_env.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
README.md
|
| 2 |
+
__init__.py
|
| 3 |
+
client.py
|
| 4 |
+
inference.py
|
| 5 |
+
models.py
|
| 6 |
+
pyproject.toml
|
| 7 |
+
./__init__.py
|
| 8 |
+
./client.py
|
| 9 |
+
./inference.py
|
| 10 |
+
./models.py
|
| 11 |
+
data/__init__.py
|
| 12 |
+
data/ad_generator.py
|
| 13 |
+
data/advertiser_profiles.py
|
| 14 |
+
data/fraud_patterns.py
|
| 15 |
+
data/landing_pages.py
|
| 16 |
+
data/network_generator.py
|
| 17 |
+
graders/__init__.py
|
| 18 |
+
graders/base_grader.py
|
| 19 |
+
graders/task1_grader.py
|
| 20 |
+
graders/task2_grader.py
|
| 21 |
+
graders/task3_grader.py
|
| 22 |
+
openenv_ad_fraud_env.egg-info/PKG-INFO
|
| 23 |
+
openenv_ad_fraud_env.egg-info/SOURCES.txt
|
| 24 |
+
openenv_ad_fraud_env.egg-info/dependency_links.txt
|
| 25 |
+
openenv_ad_fraud_env.egg-info/entry_points.txt
|
| 26 |
+
openenv_ad_fraud_env.egg-info/requires.txt
|
| 27 |
+
openenv_ad_fraud_env.egg-info/top_level.txt
|
| 28 |
+
server/__init__.py
|
| 29 |
+
server/app.py
|
| 30 |
+
server/environment.py
|
| 31 |
+
tests/test_data_generation.py
|
| 32 |
+
tests/test_environment.py
|
| 33 |
+
tests/test_graders.py
|
openenv_ad_fraud_env.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
openenv_ad_fraud_env.egg-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
server = ad_fraud_env.server.app:main
|
openenv_ad_fraud_env.egg-info/requires.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core[core]>=0.2.2
|
| 2 |
+
fastapi>=0.115.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
uvicorn[standard]>=0.24.0
|
| 5 |
+
requests>=2.31.0
|
| 6 |
+
faker==33.1.0
|
| 7 |
+
networkx>=3.2
|
| 8 |
+
openai>=1.0.0
|
| 9 |
+
python-dotenv>=1.0.0
|
| 10 |
+
|
| 11 |
+
[dev]
|
| 12 |
+
pytest>=8.0.0
|
| 13 |
+
pytest-cov>=4.0.0
|
| 14 |
+
pytest-asyncio>=0.23.0
|
openenv_ad_fraud_env.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ad_fraud_env
|
outputs/task_1_conversation.md
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Episode Log — task_1
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
## Step 0
|
| 5 |
+
|
| 6 |
+
### Observation (sent to LLM)
|
| 7 |
+
```
|
| 8 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 9 |
+
Total ads: 5 | Reviewed: 0 | Pending: 5 | Budget remaining: 25 actions | Step: 0
|
| 10 |
+
|
| 11 |
+
Current Ad: === Ad in Focus: ad_001 ===
|
| 12 |
+
Category: local_service
|
| 13 |
+
Ad copy: "Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria."
|
| 14 |
+
Targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 15 |
+
Initial risk signals: None
|
| 16 |
+
Investigations completed: None yet
|
| 17 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 18 |
+
|
| 19 |
+
Feedback: Episode started. Review the ad queue and begin your investigation.
|
| 20 |
+
|
| 21 |
+
Available ads: ad_001, ad_002, ad_003, ad_004, ad_005
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### LLM Response
|
| 25 |
+
```json
|
| 26 |
+
{
|
| 27 |
+
"action_type": "investigate",
|
| 28 |
+
"ad_id": "ad_001",
|
| 29 |
+
"investigation_target": "landing_page"
|
| 30 |
+
}
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### Parsed Action
|
| 34 |
+
```json
|
| 35 |
+
{
|
| 36 |
+
"action_type": "investigate",
|
| 37 |
+
"ad_id": "ad_001",
|
| 38 |
+
"investigation_target": "landing_page"
|
| 39 |
+
}
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
### Environment Response
|
| 43 |
+
- **Reward:** `+0.00`
|
| 44 |
+
- **Done:** `False`
|
| 45 |
+
|
| 46 |
+
- **Feedback:** Investigation complete: landing_page for ad_001.
|
| 47 |
+
--- Findings ---
|
| 48 |
+
Domain: nutri.com
|
| 49 |
+
Domain age: 1672 days
|
| 50 |
+
SSL: Valid SSL certificate
|
| 51 |
+
Registrar: Cloudflare Registrar
|
| 52 |
+
Redirect chain: No redirects (direct)
|
| 53 |
+
Similarity to known scam templates: 5%
|
| 54 |
+
Key claims on page:
|
| 55 |
+
- Licensed and insured
|
| 56 |
+
- Established since 2010
|
| 57 |
+
- BBB accredited
|
| 58 |
+
Suspicious elements:
|
| 59 |
+
None detected
|
| 60 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
## Step 1
|
| 64 |
+
|
| 65 |
+
### Observation (sent to LLM)
|
| 66 |
+
```
|
| 67 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 68 |
+
Total ads: 5 | Reviewed: 0 | Pending: 5 | Budget remaining: 24 actions | Step: 1
|
| 69 |
+
|
| 70 |
+
Current Ad: === Ad in Focus: ad_001 ===
|
| 71 |
+
Category: local_service
|
| 72 |
+
Ad copy: "Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria."
|
| 73 |
+
Targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 74 |
+
Initial risk signals: None
|
| 75 |
+
Investigations completed: landing_page
|
| 76 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 77 |
+
|
| 78 |
+
Feedback: Investigation complete: landing_page for ad_001.
|
| 79 |
+
--- Findings ---
|
| 80 |
+
Domain: nutri.com
|
| 81 |
+
Domain age: 1672 days
|
| 82 |
+
SSL: Valid SSL certificate
|
| 83 |
+
Registrar: Cloudflare Registrar
|
| 84 |
+
Redirect chain: No redirects (direct)
|
| 85 |
+
Similarity to known scam templates: 5%
|
| 86 |
+
Key claims on page:
|
| 87 |
+
- Licensed and insured
|
| 88 |
+
- Established since 2010
|
| 89 |
+
- BBB accredited
|
| 90 |
+
Suspicious elements:
|
| 91 |
+
None detected
|
| 92 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 93 |
+
|
| 94 |
+
Available ads: ad_001, ad_002, ad_003, ad_004, ad_005
|
| 95 |
+
|
| 96 |
+
Findings:
|
| 97 |
+
[ad_001 / landing_page]
|
| 98 |
+
Domain: nutri.com
|
| 99 |
+
Domain age: 1672 days
|
| 100 |
+
SSL: Valid SSL certificate
|
| 101 |
+
Registrar: Cloudflare Registrar
|
| 102 |
+
Redirect chain: No redirects (direct)
|
| 103 |
+
Similarity to known scam templates: 5%
|
| 104 |
+
Key claims on page:
|
| 105 |
+
- Licensed and insured
|
| 106 |
+
- Established since 2010
|
| 107 |
+
- BBB accredited
|
| 108 |
+
Suspicious elements:
|
| 109 |
+
None detected
|
| 110 |
+
Content summary: E-commerce store with detailed product descrip
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### LLM Response
|
| 114 |
+
```json
|
| 115 |
+
{
|
| 116 |
+
"action_type": "verdict",
|
| 117 |
+
"ad_id": "ad_001",
|
| 118 |
+
"verdict": "approve",
|
| 119 |
+
"confidence": 0.9
|
| 120 |
+
}
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### Parsed Action
|
| 124 |
+
```json
|
| 125 |
+
{
|
| 126 |
+
"action_type": "verdict",
|
| 127 |
+
"ad_id": "ad_001",
|
| 128 |
+
"verdict": "approve",
|
| 129 |
+
"confidence": 0.9
|
| 130 |
+
}
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### Environment Response
|
| 134 |
+
- **Reward:** `+0.00`
|
| 135 |
+
- **Done:** `False`
|
| 136 |
+
|
| 137 |
+
- **Feedback:** Verdict recorded for ad_001: approve (confidence: 0.90). Correct decision. Reward: +0.10
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
## Step 2
|
| 141 |
+
|
| 142 |
+
### Observation (sent to LLM)
|
| 143 |
+
```
|
| 144 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 145 |
+
Total ads: 5 | Reviewed: 1 | Pending: 4 | Budget remaining: 24 actions | Step: 2
|
| 146 |
+
|
| 147 |
+
Current Ad: === Ad in Focus: ad_002 ===
|
| 148 |
+
Category: counterfeit_goods
|
| 149 |
+
Ad copy: "Rolex watches starting at $49.99! 100% genuine! Free worldwide shipping! LuxWatchDeals.net"
|
| 150 |
+
Targeting: Men 25-55, interests: watches, luxury
|
| 151 |
+
Initial risk signals: impossibly low prices for luxury goods, unfamiliar domain, claims of authenticity with extreme discounts
|
| 152 |
+
Investigations completed: None yet
|
| 153 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 154 |
+
|
| 155 |
+
Feedback: Verdict recorded for ad_001: approve (confidence: 0.90). Correct decision. Reward: +0.10
|
| 156 |
+
|
| 157 |
+
Available ads: ad_002, ad_003, ad_004, ad_005
|
| 158 |
+
|
| 159 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 160 |
+
|
| 161 |
+
Findings:
|
| 162 |
+
[ad_001 / landing_page]
|
| 163 |
+
Domain: nutri.com
|
| 164 |
+
Domain age: 1672 days
|
| 165 |
+
SSL: Valid SSL certificate
|
| 166 |
+
Registrar: Cloudflare Registrar
|
| 167 |
+
Redirect chain: No redirects (direct)
|
| 168 |
+
Similarity to known scam templates: 5%
|
| 169 |
+
Key claims on page:
|
| 170 |
+
- Licensed and insured
|
| 171 |
+
- Established since 2010
|
| 172 |
+
- BBB accredited
|
| 173 |
+
Suspicious elements:
|
| 174 |
+
None detected
|
| 175 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
### LLM Response
|
| 179 |
+
```json
|
| 180 |
+
{
|
| 181 |
+
"action_type": "investigate",
|
| 182 |
+
"ad_id": "ad_002",
|
| 183 |
+
"investigation_target": "landing_page"
|
| 184 |
+
}
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
### Parsed Action
|
| 188 |
+
```json
|
| 189 |
+
{
|
| 190 |
+
"action_type": "investigate",
|
| 191 |
+
"ad_id": "ad_002",
|
| 192 |
+
"investigation_target": "landing_page"
|
| 193 |
+
}
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
### Environment Response
|
| 197 |
+
- **Reward:** `+0.00`
|
| 198 |
+
- **Done:** `False`
|
| 199 |
+
|
| 200 |
+
- **Feedback:** Investigation complete: landing_page for ad_002.
|
| 201 |
+
--- Findings ---
|
| 202 |
+
Domain: store4751.site
|
| 203 |
+
Domain age: 6 days
|
| 204 |
+
SSL: NO SSL / expired certificate
|
| 205 |
+
Registrar: NameSilo
|
| 206 |
+
Redirect chain: store4751.site -> redirect651..top -> redirect167..top -> redirect364..click -> redirect687..top -> final-719..site
|
| 207 |
+
Similarity to known scam templates: 67%
|
| 208 |
+
Key claims on page:
|
| 209 |
+
- Authentic products
|
| 210 |
+
- Direct from manufacturer
|
| 211 |
+
- 90% below retail price
|
| 212 |
+
Suspicious elements:
|
| 213 |
+
- Privacy policy copied from template
|
| 214 |
+
- Terms of service link returns 404
|
| 215 |
+
- No physical address listed
|
| 216 |
+
- Price comparison shows 90%+ discount vs retail
|
| 217 |
+
- No contact phone number
|
| 218 |
+
Content summary: Product catalog showing luxury branded items at extreme discounts. Stock photos. Payment accepted via wire transfer and crypto only.
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
## Step 3
|
| 222 |
+
|
| 223 |
+
### Observation (sent to LLM)
|
| 224 |
+
```
|
| 225 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 226 |
+
Total ads: 5 | Reviewed: 1 | Pending: 4 | Budget remaining: 23 actions | Step: 3
|
| 227 |
+
|
| 228 |
+
Current Ad: === Ad in Focus: ad_002 ===
|
| 229 |
+
Category: counterfeit_goods
|
| 230 |
+
Ad copy: "Rolex watches starting at $49.99! 100% genuine! Free worldwide shipping! LuxWatchDeals.net"
|
| 231 |
+
Targeting: Men 25-55, interests: watches, luxury
|
| 232 |
+
Initial risk signals: impossibly low prices for luxury goods, unfamiliar domain, claims of authenticity with extreme discounts
|
| 233 |
+
Investigations completed: landing_page
|
| 234 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 235 |
+
|
| 236 |
+
Feedback: Investigation complete: landing_page for ad_002.
|
| 237 |
+
--- Findings ---
|
| 238 |
+
Domain: store4751.site
|
| 239 |
+
Domain age: 6 days
|
| 240 |
+
SSL: NO SSL / expired certificate
|
| 241 |
+
Registrar: NameSilo
|
| 242 |
+
Redirect chain: store4751.site -> redirect651..top -> redirect167..top -> redirect364..click -> redirect687..top -> final-719..site
|
| 243 |
+
Similarity to known scam templates: 67%
|
| 244 |
+
Key claims on page:
|
| 245 |
+
- Authentic products
|
| 246 |
+
- Direct from manufacturer
|
| 247 |
+
- 90% below retail price
|
| 248 |
+
Suspicious elements:
|
| 249 |
+
- Privacy policy copied from template
|
| 250 |
+
- Terms of service link returns 404
|
| 251 |
+
- No physical address listed
|
| 252 |
+
- Price comparison shows 90%+ disco... [truncated]
|
| 253 |
+
|
| 254 |
+
Available ads: ad_002, ad_003, ad_004, ad_005
|
| 255 |
+
|
| 256 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 257 |
+
|
| 258 |
+
Findings:
|
| 259 |
+
[ad_001 / landing_page]
|
| 260 |
+
Domain: nutri.com
|
| 261 |
+
Domain age: 1672 days
|
| 262 |
+
SSL: Valid SSL certificate
|
| 263 |
+
Registrar: Cloudflare Registrar
|
| 264 |
+
Redirect chain: No
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
### LLM Response
|
| 268 |
+
```json
|
| 269 |
+
{
|
| 270 |
+
"action_type": "verdict",
|
| 271 |
+
"ad_id": "ad_002",
|
| 272 |
+
"verdict": "reject",
|
| 273 |
+
"confidence": 0.95
|
| 274 |
+
}
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
### Parsed Action
|
| 278 |
+
```json
|
| 279 |
+
{
|
| 280 |
+
"action_type": "verdict",
|
| 281 |
+
"ad_id": "ad_002",
|
| 282 |
+
"verdict": "reject",
|
| 283 |
+
"confidence": 0.95
|
| 284 |
+
}
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
### Environment Response
|
| 288 |
+
- **Reward:** `+0.00`
|
| 289 |
+
- **Done:** `False`
|
| 290 |
+
|
| 291 |
+
- **Feedback:** Verdict recorded for ad_002: reject (confidence: 0.95). Correct decision. Reward: +0.38
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
## Step 4
|
| 295 |
+
|
| 296 |
+
### Observation (sent to LLM)
|
| 297 |
+
```
|
| 298 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 299 |
+
Total ads: 5 | Reviewed: 2 | Pending: 3 | Budget remaining: 23 actions | Step: 4
|
| 300 |
+
|
| 301 |
+
Current Ad: === Ad in Focus: ad_003 ===
|
| 302 |
+
Category: fake_giveaway
|
| 303 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 304 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 305 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 306 |
+
Investigations completed: None yet
|
| 307 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 308 |
+
|
| 309 |
+
Feedback: Verdict recorded for ad_002: reject (confidence: 0.95). Correct decision. Reward: +0.38
|
| 310 |
+
|
| 311 |
+
Available ads: ad_003, ad_004, ad_005
|
| 312 |
+
|
| 313 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 314 |
+
ad_002: reject (confidence: 0.95)
|
| 315 |
+
|
| 316 |
+
Findings:
|
| 317 |
+
[ad_001 / landing_page]
|
| 318 |
+
Domain: nutri.com
|
| 319 |
+
Domain age: 1672 days
|
| 320 |
+
SSL: Valid SSL certificate
|
| 321 |
+
Registrar: Cloudflare Registrar
|
| 322 |
+
Redirect chain: No redirects (direct)
|
| 323 |
+
Similarity to known scam templates: 5%
|
| 324 |
+
Key claims on page:
|
| 325 |
+
- Licensed and insured
|
| 326 |
+
- Established since 2010
|
| 327 |
+
- BBB accredited
|
| 328 |
+
Suspicious elements:
|
| 329 |
+
None detected
|
| 330 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 331 |
+
|
| 332 |
+
[ad_002 / landing_page]
|
| 333 |
+
Domain: store4751.site
|
| 334 |
+
Domain age: 6 days
|
| 335 |
+
SSL: NO SSL / expired certificate
|
| 336 |
+
Registrar: NameSilo
|
| 337 |
+
Redirect chain: store4751.... [truncated]
|
| 338 |
+
```
|
| 339 |
+
|
| 340 |
+
### LLM Response
|
| 341 |
+
```json
|
| 342 |
+
{
|
| 343 |
+
"action_type": "investigate",
|
| 344 |
+
"ad_id": "ad_003",
|
| 345 |
+
"investigation_target": "landing_page"
|
| 346 |
+
}
|
| 347 |
+
```
|
| 348 |
+
|
| 349 |
+
### Parsed Action
|
| 350 |
+
```json
|
| 351 |
+
{
|
| 352 |
+
"action_type": "investigate",
|
| 353 |
+
"ad_id": "ad_003",
|
| 354 |
+
"investigation_target": "landing_page"
|
| 355 |
+
}
|
| 356 |
+
```
|
| 357 |
+
|
| 358 |
+
### Environment Response
|
| 359 |
+
- **Reward:** `+0.00`
|
| 360 |
+
- **Done:** `False`
|
| 361 |
+
|
| 362 |
+
- **Feedback:** Investigation complete: landing_page for ad_003.
|
| 363 |
+
--- Findings ---
|
| 364 |
+
Domain: get8189.store
|
| 365 |
+
Domain age: 26 days
|
| 366 |
+
SSL: NO SSL / expired certificate
|
| 367 |
+
Registrar: Tucows (privacy proxy)
|
| 368 |
+
Redirect chain: get8189.store -> redirect253..xyz -> redirect575..xyz -> final-270..store
|
| 369 |
+
Similarity to known scam templates: 56%
|
| 370 |
+
Key claims on page:
|
| 371 |
+
- 100% free, no purchase necessary
|
| 372 |
+
- Guaranteed winner
|
| 373 |
+
- Act now — expires in 2 hours
|
| 374 |
+
Suspicious elements:
|
| 375 |
+
- No contact phone number
|
| 376 |
+
- Requests personal info before any value provided
|
| 377 |
+
- No physical address listed
|
| 378 |
+
- Privacy policy copied from template
|
| 379 |
+
- Terms of service link returns 404
|
| 380 |
+
Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
## Step 5
|
| 384 |
+
|
| 385 |
+
### Observation (sent to LLM)
|
| 386 |
+
```
|
| 387 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 388 |
+
Total ads: 5 | Reviewed: 2 | Pending: 3 | Budget remaining: 22 actions | Step: 5
|
| 389 |
+
|
| 390 |
+
Current Ad: === Ad in Focus: ad_003 ===
|
| 391 |
+
Category: fake_giveaway
|
| 392 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 393 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 394 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 395 |
+
Investigations completed: landing_page
|
| 396 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 397 |
+
|
| 398 |
+
Feedback: Investigation complete: landing_page for ad_003.
|
| 399 |
+
--- Findings ---
|
| 400 |
+
Domain: get8189.store
|
| 401 |
+
Domain age: 26 days
|
| 402 |
+
SSL: NO SSL / expired certificate
|
| 403 |
+
Registrar: Tucows (privacy proxy)
|
| 404 |
+
Redirect chain: get8189.store -> redirect253..xyz -> redirect575..xyz -> final-270..store
|
| 405 |
+
Similarity to known scam templates: 56%
|
| 406 |
+
Key claims on page:
|
| 407 |
+
- 100% free, no purchase necessary
|
| 408 |
+
- Guaranteed winner
|
| 409 |
+
- Act now — expires in 2 hours
|
| 410 |
+
Suspicious elements:
|
| 411 |
+
- No contact phone number
|
| 412 |
+
- Requests personal info before any value provided
|
| 413 |
+
- No physical address listed
|
| 414 |
+
- Privacy policy copied from template
|
| 415 |
+
- Terms... [truncated]
|
| 416 |
+
|
| 417 |
+
Available ads: ad_003, ad_004, ad_005
|
| 418 |
+
|
| 419 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 420 |
+
ad_002: reject (confidence: 0.95)
|
| 421 |
+
|
| 422 |
+
Findings:
|
| 423 |
+
[ad_001 / landing_page]
|
| 424 |
+
Domain: nutri.com
|
| 425 |
+
Domain age: 1672 days
|
| 426 |
+
SSL: Valid SSL certificate
|
| 427 |
+
Registrar: Clou
|
| 428 |
+
```
|
| 429 |
+
|
| 430 |
+
### LLM Response
|
| 431 |
+
```json
|
| 432 |
+
{
|
| 433 |
+
"action_type": "verdict",
|
| 434 |
+
"ad_id": "ad_003",
|
| 435 |
+
"verdict": "reject",
|
| 436 |
+
"confidence": 0.98
|
| 437 |
+
}
|
| 438 |
+
```
|
| 439 |
+
|
| 440 |
+
### Parsed Action
|
| 441 |
+
```json
|
| 442 |
+
{
|
| 443 |
+
"action_type": "verdict",
|
| 444 |
+
"ad_id": "ad_003",
|
| 445 |
+
"verdict": "reject",
|
| 446 |
+
"confidence": 0.98
|
| 447 |
+
}
|
| 448 |
+
```
|
| 449 |
+
|
| 450 |
+
### Environment Response
|
| 451 |
+
- **Reward:** `+0.00`
|
| 452 |
+
- **Done:** `False`
|
| 453 |
+
|
| 454 |
+
- **Feedback:** Verdict recorded for ad_003: reject (confidence: 0.98). Correct decision. Reward: +0.39
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
## Step 6
|
| 458 |
+
|
| 459 |
+
### Observation (sent to LLM)
|
| 460 |
+
```
|
| 461 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 462 |
+
Total ads: 5 | Reviewed: 3 | Pending: 2 | Budget remaining: 22 actions | Step: 6
|
| 463 |
+
|
| 464 |
+
Current Ad: === Ad in Focus: ad_004 ===
|
| 465 |
+
Category: ecommerce
|
| 466 |
+
Ad copy: "Spring Sale — Up to 30% off home essentials. Free shipping on orders over $50. Shop now at HomeNest."
|
| 467 |
+
Targeting: Women 25-54, interests: home decor, shopping
|
| 468 |
+
Initial risk signals: None
|
| 469 |
+
Investigations completed: None yet
|
| 470 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 471 |
+
|
| 472 |
+
Feedback: Verdict recorded for ad_003: reject (confidence: 0.98). Correct decision. Reward: +0.39
|
| 473 |
+
|
| 474 |
+
Available ads: ad_004, ad_005
|
| 475 |
+
|
| 476 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 477 |
+
ad_002: reject (confidence: 0.95)
|
| 478 |
+
ad_003: reject (confidence: 0.98)
|
| 479 |
+
|
| 480 |
+
Findings:
|
| 481 |
+
[ad_001 / landing_page]
|
| 482 |
+
Domain: nutri.com
|
| 483 |
+
Domain age: 1672 days
|
| 484 |
+
SSL: Valid SSL certificate
|
| 485 |
+
Registrar: Cloudflare Registrar
|
| 486 |
+
Redirect chain: No redirects (direct)
|
| 487 |
+
Similarity to known scam templates: 5%
|
| 488 |
+
Key claims on page:
|
| 489 |
+
- Licensed and insured
|
| 490 |
+
- Established since 2010
|
| 491 |
+
- BBB accredited
|
| 492 |
+
Suspicious elements:
|
| 493 |
+
None detected
|
| 494 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 495 |
+
|
| 496 |
+
[ad_002 / landing_page]
|
| 497 |
+
Domain: store4751.site
|
| 498 |
+
Domain age: 6 days
|
| 499 |
+
SSL: NO SSL / expired certificate
|
| 500 |
+
Registrar: NameSilo
|
| 501 |
+
Redirect chain: store4751.... [truncated]
|
| 502 |
+
```
|
| 503 |
+
|
| 504 |
+
### LLM Response
|
| 505 |
+
```json
|
| 506 |
+
{
|
| 507 |
+
"action_type": "investigate",
|
| 508 |
+
"ad_id": "ad_004",
|
| 509 |
+
"investigation_target": "landing_page"
|
| 510 |
+
}
|
| 511 |
+
```
|
| 512 |
+
|
| 513 |
+
### Parsed Action
|
| 514 |
+
```json
|
| 515 |
+
{
|
| 516 |
+
"action_type": "investigate",
|
| 517 |
+
"ad_id": "ad_004",
|
| 518 |
+
"investigation_target": "landing_page"
|
| 519 |
+
}
|
| 520 |
+
```
|
| 521 |
+
|
| 522 |
+
### Environment Response
|
| 523 |
+
- **Reward:** `+0.00`
|
| 524 |
+
- **Done:** `False`
|
| 525 |
+
|
| 526 |
+
- **Feedback:** Investigation complete: landing_page for ad_004.
|
| 527 |
+
--- Findings ---
|
| 528 |
+
Domain: sparkle.io
|
| 529 |
+
Domain age: 2424 days
|
| 530 |
+
SSL: Valid SSL certificate
|
| 531 |
+
Registrar: AWS Route 53
|
| 532 |
+
Redirect chain: No redirects (direct)
|
| 533 |
+
Similarity to known scam templates: 13%
|
| 534 |
+
Key claims on page:
|
| 535 |
+
- 4.5-star average customer rating
|
| 536 |
+
- 14-day free trial
|
| 537 |
+
Suspicious elements:
|
| 538 |
+
None detected
|
| 539 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
## Step 7
|
| 543 |
+
|
| 544 |
+
### Observation (sent to LLM)
|
| 545 |
+
```
|
| 546 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 547 |
+
Total ads: 5 | Reviewed: 3 | Pending: 2 | Budget remaining: 21 actions | Step: 7
|
| 548 |
+
|
| 549 |
+
Current Ad: === Ad in Focus: ad_004 ===
|
| 550 |
+
Category: ecommerce
|
| 551 |
+
Ad copy: "Spring Sale — Up to 30% off home essentials. Free shipping on orders over $50. Shop now at HomeNest."
|
| 552 |
+
Targeting: Women 25-54, interests: home decor, shopping
|
| 553 |
+
Initial risk signals: None
|
| 554 |
+
Investigations completed: landing_page
|
| 555 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 556 |
+
|
| 557 |
+
Feedback: Investigation complete: landing_page for ad_004.
|
| 558 |
+
--- Findings ---
|
| 559 |
+
Domain: sparkle.io
|
| 560 |
+
Domain age: 2424 days
|
| 561 |
+
SSL: Valid SSL certificate
|
| 562 |
+
Registrar: AWS Route 53
|
| 563 |
+
Redirect chain: No redirects (direct)
|
| 564 |
+
Similarity to known scam templates: 13%
|
| 565 |
+
Key claims on page:
|
| 566 |
+
- 4.5-star average customer rating
|
| 567 |
+
- 14-day free trial
|
| 568 |
+
Suspicious elements:
|
| 569 |
+
None detected
|
| 570 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 571 |
+
|
| 572 |
+
Available ads: ad_004, ad_005
|
| 573 |
+
|
| 574 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 575 |
+
ad_002: reject (confidence: 0.95)
|
| 576 |
+
ad_003: reject (confidence: 0.98)
|
| 577 |
+
|
| 578 |
+
Findings:
|
| 579 |
+
[ad_001 / landing_page]
|
| 580 |
+
Domain: nutri.com
|
| 581 |
+
Domain age: 1672 days
|
| 582 |
+
SSL: Valid SSL certificate
|
| 583 |
+
Registrar: Cloudflare Registrar
|
| 584 |
+
Redirect chain: No redirects (direct)
|
| 585 |
+
Similarity to known scam templates: 5%
|
| 586 |
+
Key claims on page:
|
| 587 |
+
- Licensed and insured
|
| 588 |
+
- Established since 2010
|
| 589 |
+
- BBB accredited
|
| 590 |
+
Suspicious elements:
|
| 591 |
+
No
|
| 592 |
+
```
|
| 593 |
+
|
| 594 |
+
### LLM Response
|
| 595 |
+
```json
|
| 596 |
+
{
|
| 597 |
+
"action_type": "verdict",
|
| 598 |
+
"ad_id": "ad_004",
|
| 599 |
+
"verdict": "approve",
|
| 600 |
+
"confidence": 0.92
|
| 601 |
+
}
|
| 602 |
+
```
|
| 603 |
+
|
| 604 |
+
### Parsed Action
|
| 605 |
+
```json
|
| 606 |
+
{
|
| 607 |
+
"action_type": "verdict",
|
| 608 |
+
"ad_id": "ad_004",
|
| 609 |
+
"verdict": "approve",
|
| 610 |
+
"confidence": 0.92
|
| 611 |
+
}
|
| 612 |
+
```
|
| 613 |
+
|
| 614 |
+
### Environment Response
|
| 615 |
+
- **Reward:** `+0.00`
|
| 616 |
+
- **Done:** `False`
|
| 617 |
+
|
| 618 |
+
- **Feedback:** Verdict recorded for ad_004: approve (confidence: 0.92). Correct decision. Reward: +0.10
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
## Step 8
|
| 622 |
+
|
| 623 |
+
### Observation (sent to LLM)
|
| 624 |
+
```
|
| 625 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 626 |
+
Total ads: 5 | Reviewed: 4 | Pending: 1 | Budget remaining: 21 actions | Step: 8
|
| 627 |
+
|
| 628 |
+
Current Ad: === Ad in Focus: ad_005 ===
|
| 629 |
+
Category: fake_giveaway
|
| 630 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 631 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 632 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 633 |
+
Investigations completed: None yet
|
| 634 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 635 |
+
|
| 636 |
+
Feedback: Verdict recorded for ad_004: approve (confidence: 0.92). Correct decision. Reward: +0.10
|
| 637 |
+
|
| 638 |
+
Available ads: ad_005
|
| 639 |
+
|
| 640 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 641 |
+
ad_002: reject (confidence: 0.95)
|
| 642 |
+
ad_003: reject (confidence: 0.98)
|
| 643 |
+
ad_004: approve (confidence: 0.92)
|
| 644 |
+
|
| 645 |
+
Findings:
|
| 646 |
+
[ad_001 / landing_page]
|
| 647 |
+
Domain: nutri.com
|
| 648 |
+
Domain age: 1672 days
|
| 649 |
+
SSL: Valid SSL certificate
|
| 650 |
+
Registrar: Cloudflare Registrar
|
| 651 |
+
Redirect chain: No redirects (direct)
|
| 652 |
+
Similarity to known scam templates: 5%
|
| 653 |
+
Key claims on page:
|
| 654 |
+
- Licensed and insured
|
| 655 |
+
- Established since 2010
|
| 656 |
+
- BBB accredited
|
| 657 |
+
Suspicious elements:
|
| 658 |
+
None detected
|
| 659 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 660 |
+
|
| 661 |
+
[ad_002 / landing_page]
|
| 662 |
+
Domain: store4751.site
|
| 663 |
+
Domain age: 6 days
|
| 664 |
+
SSL: NO SSL / expired certificate
|
| 665 |
+
Registrar: NameSilo
|
| 666 |
+
Redire
|
| 667 |
+
```
|
| 668 |
+
|
| 669 |
+
### LLM Response
|
| 670 |
+
```json
|
| 671 |
+
{
|
| 672 |
+
"action_type": "investigate",
|
| 673 |
+
"ad_id": "ad_005",
|
| 674 |
+
"investigation_target": "landing_page"
|
| 675 |
+
}
|
| 676 |
+
```
|
| 677 |
+
|
| 678 |
+
### Parsed Action
|
| 679 |
+
```json
|
| 680 |
+
{
|
| 681 |
+
"action_type": "investigate",
|
| 682 |
+
"ad_id": "ad_005",
|
| 683 |
+
"investigation_target": "landing_page"
|
| 684 |
+
}
|
| 685 |
+
```
|
| 686 |
+
|
| 687 |
+
### Environment Response
|
| 688 |
+
- **Reward:** `+0.00`
|
| 689 |
+
- **Done:** `False`
|
| 690 |
+
|
| 691 |
+
- **Feedback:** Investigation complete: landing_page for ad_005.
|
| 692 |
+
--- Findings ---
|
| 693 |
+
Domain: pro3268.top
|
| 694 |
+
Domain age: 10 days
|
| 695 |
+
SSL: Valid SSL certificate
|
| 696 |
+
Registrar: Epik
|
| 697 |
+
Redirect chain: pro3268.top -> redirect614..top -> final-501..site
|
| 698 |
+
Similarity to known scam templates: 74%
|
| 699 |
+
Key claims on page:
|
| 700 |
+
- 100% free, no purchase necessary
|
| 701 |
+
- Guaranteed winner
|
| 702 |
+
- Act now — expires in 2 hours
|
| 703 |
+
Suspicious elements:
|
| 704 |
+
- No contact phone number
|
| 705 |
+
- Terms of service link returns 404
|
| 706 |
+
- Requests personal info before any value provided
|
| 707 |
+
Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
## Step 9
|
| 711 |
+
|
| 712 |
+
### Observation (sent to LLM)
|
| 713 |
+
```
|
| 714 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 715 |
+
Total ads: 5 | Reviewed: 4 | Pending: 1 | Budget remaining: 20 actions | Step: 9
|
| 716 |
+
|
| 717 |
+
Current Ad: === Ad in Focus: ad_005 ===
|
| 718 |
+
Category: fake_giveaway
|
| 719 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 720 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 721 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 722 |
+
Investigations completed: landing_page
|
| 723 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 724 |
+
|
| 725 |
+
Feedback: Investigation complete: landing_page for ad_005.
|
| 726 |
+
--- Findings ---
|
| 727 |
+
Domain: pro3268.top
|
| 728 |
+
Domain age: 10 days
|
| 729 |
+
SSL: Valid SSL certificate
|
| 730 |
+
Registrar: Epik
|
| 731 |
+
Redirect chain: pro3268.top -> redirect614..top -> final-501..site
|
| 732 |
+
Similarity to known scam templates: 74%
|
| 733 |
+
Key claims on page:
|
| 734 |
+
- 100% free, no purchase necessary
|
| 735 |
+
- Guaranteed winner
|
| 736 |
+
- Act now — expires in 2 hours
|
| 737 |
+
Suspicious elements:
|
| 738 |
+
- No contact phone number
|
| 739 |
+
- Terms of service link returns 404
|
| 740 |
+
- Requests personal info before any value provided
|
| 741 |
+
Content summary: Landing page features a large countdown timer and a form requesting name, e... [truncated]
|
| 742 |
+
|
| 743 |
+
Available ads: ad_005
|
| 744 |
+
|
| 745 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 746 |
+
ad_002: reject (confidence: 0.95)
|
| 747 |
+
ad_003: reject (confidence: 0.98)
|
| 748 |
+
ad_004: approve (confidence: 0.92)
|
| 749 |
+
|
| 750 |
+
Findings:
|
| 751 |
+
[ad_001 / landing_page]
|
| 752 |
+
Domain: nutri.com
|
| 753 |
+
Domain
|
| 754 |
+
```
|
| 755 |
+
|
| 756 |
+
### LLM Response
|
| 757 |
+
```json
|
| 758 |
+
{
|
| 759 |
+
"action_type": "verdict",
|
| 760 |
+
"ad_id": "ad_005",
|
| 761 |
+
"verdict": "reject",
|
| 762 |
+
"confidence": 0.99
|
| 763 |
+
}
|
| 764 |
+
```
|
| 765 |
+
|
| 766 |
+
### Parsed Action
|
| 767 |
+
```json
|
| 768 |
+
{
|
| 769 |
+
"action_type": "verdict",
|
| 770 |
+
"ad_id": "ad_005",
|
| 771 |
+
"verdict": "reject",
|
| 772 |
+
"confidence": 0.99
|
| 773 |
+
}
|
| 774 |
+
```
|
| 775 |
+
|
| 776 |
+
### Environment Response
|
| 777 |
+
- **Reward:** `+0.00`
|
| 778 |
+
- **Done:** `False`
|
| 779 |
+
|
| 780 |
+
- **Feedback:** Episode complete. Grader score: 0.931/1.000
|
| 781 |
+
Verdicts rendered: 5/5
|
| 782 |
+
Auto-approved (unreviewed): 0 fraud ad(s) missed
|
| 783 |
+
Correct decisions: 5/5
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
## Step 10
|
| 787 |
+
|
| 788 |
+
### Observation (sent to LLM)
|
| 789 |
+
```
|
| 790 |
+
Queue: Task: Basic Ad Triage (easy)
|
| 791 |
+
Total ads: 5 | Reviewed: 5 | Pending: 0 | Budget remaining: 20 actions | Step: 10
|
| 792 |
+
|
| 793 |
+
Current Ad:
|
| 794 |
+
|
| 795 |
+
Feedback: Episode complete. Grader score: 0.931/1.000
|
| 796 |
+
Verdicts rendered: 5/5
|
| 797 |
+
Auto-approved (unreviewed): 0 fraud ad(s) missed
|
| 798 |
+
Correct decisions: 5/5
|
| 799 |
+
|
| 800 |
+
Available ads:
|
| 801 |
+
|
| 802 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 803 |
+
ad_002: reject (confidence: 0.95)
|
| 804 |
+
ad_003: reject (confidence: 0.98)
|
| 805 |
+
ad_004: approve (confidence: 0.92)
|
| 806 |
+
ad_005: reject (confidence: 0.99)
|
| 807 |
+
|
| 808 |
+
Findings:
|
| 809 |
+
[ad_001 / landing_page]
|
| 810 |
+
Domain: nutri.com
|
| 811 |
+
Domain age: 1672 days
|
| 812 |
+
SSL: Valid SSL certificate
|
| 813 |
+
Registrar: Cloudflare Registrar
|
| 814 |
+
Redirect chain: No redirects (direct)
|
| 815 |
+
Similarity to known scam templates: 5%
|
| 816 |
+
Key claims on page:
|
| 817 |
+
- Licensed and insured
|
| 818 |
+
- Established since 2010
|
| 819 |
+
- BBB accredited
|
| 820 |
+
Suspicious elements:
|
| 821 |
+
None detected
|
| 822 |
+
Content summary: E-commerce store with detailed product descriptions, customer reviews, FAQ section, and accessible support.
|
| 823 |
+
|
| 824 |
+
[ad_002 / landing_page]
|
| 825 |
+
Domain: store4751.site
|
| 826 |
+
Domain age: 6 days
|
| 827 |
+
SSL: NO SSL / expired certificate
|
| 828 |
+
Registrar: NameSilo
|
| 829 |
+
Redirect chain: store4751.... [truncated]
|
| 830 |
+
```
|
| 831 |
+
|
| 832 |
+
### LLM Response
|
| 833 |
+
```json
|
| 834 |
+
{}
|
| 835 |
+
```
|
| 836 |
+
|
| 837 |
+
|
| 838 |
+
---
|
| 839 |
+
## Result
|
| 840 |
+
**Score: 0.931 | Steps: 10 | Verdicts: 5/5**
|
outputs/task_2_conversation.md
ADDED
|
@@ -0,0 +1,1251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Episode Log — task_2
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
## Step 0
|
| 5 |
+
|
| 6 |
+
### Observation (sent to LLM)
|
| 7 |
+
```
|
| 8 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 9 |
+
Total ads: 12 | Reviewed: 0 | Pending: 12 | Budget remaining: 30 actions | Step: 0
|
| 10 |
+
|
| 11 |
+
Current Ad: === Ad in Focus: ad_001 ===
|
| 12 |
+
Category: local_service
|
| 13 |
+
Ad copy: "Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria."
|
| 14 |
+
Targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 15 |
+
Initial risk signals: None
|
| 16 |
+
Investigations completed: None yet
|
| 17 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 18 |
+
|
| 19 |
+
Feedback: Episode started. Review the ad queue and begin your investigation.
|
| 20 |
+
|
| 21 |
+
Available ads: ad_001, ad_002, ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### LLM Response
|
| 25 |
+
```json
|
| 26 |
+
{
|
| 27 |
+
"action_type": "investigate",
|
| 28 |
+
"ad_id": "ad_001",
|
| 29 |
+
"investigation_target": "advertiser_history"
|
| 30 |
+
}
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### Parsed Action
|
| 34 |
+
```json
|
| 35 |
+
{
|
| 36 |
+
"action_type": "investigate",
|
| 37 |
+
"ad_id": "ad_001",
|
| 38 |
+
"investigation_target": "advertiser_history"
|
| 39 |
+
}
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
### Environment Response
|
| 43 |
+
- **Reward:** `+0.00`
|
| 44 |
+
- **Done:** `False`
|
| 45 |
+
|
| 46 |
+
- **Feedback:** Investigation complete: advertiser_history for ad_001.
|
| 47 |
+
--- Findings ---
|
| 48 |
+
Advertiser: FitVisit (Verified Business)
|
| 49 |
+
Account age: 1558 days
|
| 50 |
+
Country: Canada
|
| 51 |
+
Total historical spend: $55,594.09
|
| 52 |
+
Ads submitted in last 30 days: 13
|
| 53 |
+
Historical approval rate: 91%
|
| 54 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
## Step 1
|
| 58 |
+
|
| 59 |
+
### Observation (sent to LLM)
|
| 60 |
+
```
|
| 61 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 62 |
+
Total ads: 12 | Reviewed: 0 | Pending: 12 | Budget remaining: 29 actions | Step: 1
|
| 63 |
+
|
| 64 |
+
Current Ad: === Ad in Focus: ad_001 ===
|
| 65 |
+
Category: local_service
|
| 66 |
+
Ad copy: "Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria."
|
| 67 |
+
Targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 68 |
+
Initial risk signals: None
|
| 69 |
+
Investigations completed: advertiser_history
|
| 70 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 71 |
+
|
| 72 |
+
Feedback: Investigation complete: advertiser_history for ad_001.
|
| 73 |
+
--- Findings ---
|
| 74 |
+
Advertiser: FitVisit (Verified Business)
|
| 75 |
+
Account age: 1558 days
|
| 76 |
+
Country: Canada
|
| 77 |
+
Total historical spend: $55,594.09
|
| 78 |
+
Ads submitted in last 30 days: 13
|
| 79 |
+
Historical approval rate: 91%
|
| 80 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 81 |
+
|
| 82 |
+
Available ads: ad_001, ad_002, ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 83 |
+
|
| 84 |
+
Findings:
|
| 85 |
+
[ad_001 / advertiser_history]
|
| 86 |
+
Advertiser: FitVisit (Verified Business)
|
| 87 |
+
Account age: 1558 days
|
| 88 |
+
Country: Canada
|
| 89 |
+
Total historical spend: $55,594.09
|
| 90 |
+
Ads submitted in last 30 days: 13
|
| 91 |
+
Historical approval rate: 91%
|
| 92 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### LLM Response
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"action_type": "investigate",
|
| 99 |
+
"ad_id": "ad_001",
|
| 100 |
+
"investigation_target": "payment_method"
|
| 101 |
+
}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Parsed Action
|
| 105 |
+
```json
|
| 106 |
+
{
|
| 107 |
+
"action_type": "investigate",
|
| 108 |
+
"ad_id": "ad_001",
|
| 109 |
+
"investigation_target": "payment_method"
|
| 110 |
+
}
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### Environment Response
|
| 114 |
+
- **Reward:** `+0.00`
|
| 115 |
+
- **Done:** `False`
|
| 116 |
+
|
| 117 |
+
- **Feedback:** Investigation complete: payment_method for ad_001.
|
| 118 |
+
--- Findings ---
|
| 119 |
+
Payment Method Analysis for ad_001:
|
| 120 |
+
Method type: credit_card
|
| 121 |
+
Payment ID: pmt_865179
|
| 122 |
+
No other accounts in the current queue share this payment method.
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
## Step 2
|
| 126 |
+
|
| 127 |
+
### Observation (sent to LLM)
|
| 128 |
+
```
|
| 129 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 130 |
+
Total ads: 12 | Reviewed: 0 | Pending: 12 | Budget remaining: 28 actions | Step: 2
|
| 131 |
+
|
| 132 |
+
Current Ad: === Ad in Focus: ad_001 ===
|
| 133 |
+
Category: local_service
|
| 134 |
+
Ad copy: "Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria."
|
| 135 |
+
Targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 136 |
+
Initial risk signals: None
|
| 137 |
+
Investigations completed: advertiser_history, payment_method
|
| 138 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 139 |
+
|
| 140 |
+
Feedback: Investigation complete: payment_method for ad_001.
|
| 141 |
+
--- Findings ---
|
| 142 |
+
Payment Method Analysis for ad_001:
|
| 143 |
+
Method type: credit_card
|
| 144 |
+
Payment ID: pmt_865179
|
| 145 |
+
No other accounts in the current queue share this payment method.
|
| 146 |
+
|
| 147 |
+
Available ads: ad_001, ad_002, ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 148 |
+
|
| 149 |
+
Findings:
|
| 150 |
+
[ad_001 / advertiser_history]
|
| 151 |
+
Advertiser: FitVisit (Verified Business)
|
| 152 |
+
Account age: 1558 days
|
| 153 |
+
Country: Canada
|
| 154 |
+
Total historical spend: $55,594.09
|
| 155 |
+
Ads submitted in last 30 days: 13
|
| 156 |
+
Historical approval rate: 91%
|
| 157 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 158 |
+
|
| 159 |
+
[ad_001 / payment_method]
|
| 160 |
+
Payment Method Analysis for ad_001:
|
| 161 |
+
Method type: credit_card
|
| 162 |
+
Payment ID: pmt_865179
|
| 163 |
+
No other accounts in the current queue share this payment method.
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
### LLM Response
|
| 167 |
+
```json
|
| 168 |
+
{
|
| 169 |
+
"action_type": "verdict",
|
| 170 |
+
"ad_id": "ad_001",
|
| 171 |
+
"verdict": "approve",
|
| 172 |
+
"confidence": 0.9
|
| 173 |
+
}
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
### Parsed Action
|
| 177 |
+
```json
|
| 178 |
+
{
|
| 179 |
+
"action_type": "verdict",
|
| 180 |
+
"ad_id": "ad_001",
|
| 181 |
+
"verdict": "approve",
|
| 182 |
+
"confidence": 0.9
|
| 183 |
+
}
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
### Environment Response
|
| 187 |
+
- **Reward:** `+0.00`
|
| 188 |
+
- **Done:** `False`
|
| 189 |
+
|
| 190 |
+
- **Feedback:** Verdict recorded for ad_001: approve (confidence: 0.90). Correct decision. Reward: +0.10
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
## Step 3
|
| 194 |
+
|
| 195 |
+
### Observation (sent to LLM)
|
| 196 |
+
```
|
| 197 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 198 |
+
Total ads: 12 | Reviewed: 1 | Pending: 11 | Budget remaining: 28 actions | Step: 3
|
| 199 |
+
|
| 200 |
+
Current Ad: === Ad in Focus: ad_002 ===
|
| 201 |
+
Category: fake_giveaway
|
| 202 |
+
Ad copy: "CONGRATULATIONS!! You've been selected to win a FREE iPhone 16 Pro!! Click NOW before offer expires!!!"
|
| 203 |
+
Targeting: All adults 18+, broad targeting, worldwide
|
| 204 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 205 |
+
Investigations completed: None yet
|
| 206 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 207 |
+
|
| 208 |
+
Feedback: Verdict recorded for ad_001: approve (confidence: 0.90). Correct decision. Reward: +0.10
|
| 209 |
+
|
| 210 |
+
Available ads: ad_002, ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 211 |
+
|
| 212 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 213 |
+
|
| 214 |
+
Findings:
|
| 215 |
+
[ad_001 / advertiser_history]
|
| 216 |
+
Advertiser: FitVisit (Verified Business)
|
| 217 |
+
Account age: 1558 days
|
| 218 |
+
Country: Canada
|
| 219 |
+
Total historical spend: $55,594.09
|
| 220 |
+
Ads submitted in last 30 days: 13
|
| 221 |
+
Historical approval rate: 91%
|
| 222 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 223 |
+
|
| 224 |
+
[ad_001 / payment_method]
|
| 225 |
+
Payment Method Analysis for ad_001:
|
| 226 |
+
Method type: credit_card
|
| 227 |
+
Payment ID: pmt_865179
|
| 228 |
+
No other accounts in the current queue share this payment method.
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
### LLM Response
|
| 232 |
+
```json
|
| 233 |
+
{
|
| 234 |
+
"action_type": "investigate",
|
| 235 |
+
"ad_id": "ad_002",
|
| 236 |
+
"investigation_target": "landing_page"
|
| 237 |
+
}
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
### Parsed Action
|
| 241 |
+
```json
|
| 242 |
+
{
|
| 243 |
+
"action_type": "investigate",
|
| 244 |
+
"ad_id": "ad_002",
|
| 245 |
+
"investigation_target": "landing_page"
|
| 246 |
+
}
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
### Environment Response
|
| 250 |
+
- **Reward:** `+0.00`
|
| 251 |
+
- **Done:** `False`
|
| 252 |
+
|
| 253 |
+
- **Feedback:** Investigation complete: landing_page for ad_002.
|
| 254 |
+
--- Findings ---
|
| 255 |
+
Domain: shop4020.top
|
| 256 |
+
Domain age: 11 days
|
| 257 |
+
SSL: NO SSL / expired certificate
|
| 258 |
+
Registrar: Njalla (privacy)
|
| 259 |
+
Redirect chain: shop4020.top -> redirect702..top -> redirect864..click -> final-235..site
|
| 260 |
+
Similarity to known scam templates: 95%
|
| 261 |
+
Key claims on page:
|
| 262 |
+
- 100% free, no purchase necessary
|
| 263 |
+
- Guaranteed winner
|
| 264 |
+
- Act now — expires in 2 hours
|
| 265 |
+
Suspicious elements:
|
| 266 |
+
- No contact phone number
|
| 267 |
+
- Countdown timer with fake urgency
|
| 268 |
+
- No physical address listed
|
| 269 |
+
- Requests personal info before any value provided
|
| 270 |
+
Content summary: Landing page features a large countdown timer and a form requesting name, email, phone, and address. No clear sponsor or rules disclosure.
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
## Step 4
|
| 274 |
+
|
| 275 |
+
### Observation (sent to LLM)
|
| 276 |
+
```
|
| 277 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 278 |
+
Total ads: 12 | Reviewed: 1 | Pending: 11 | Budget remaining: 27 actions | Step: 4
|
| 279 |
+
|
| 280 |
+
Current Ad: === Ad in Focus: ad_002 ===
|
| 281 |
+
Category: fake_giveaway
|
| 282 |
+
Ad copy: "CONGRATULATIONS!! You've been selected to win a FREE iPhone 16 Pro!! Click NOW before offer expires!!!"
|
| 283 |
+
Targeting: All adults 18+, broad targeting, worldwide
|
| 284 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 285 |
+
Investigations completed: landing_page
|
| 286 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 287 |
+
|
| 288 |
+
Feedback: Investigation complete: landing_page for ad_002.
|
| 289 |
+
--- Findings ---
|
| 290 |
+
Domain: shop4020.top
|
| 291 |
+
Domain age: 11 days
|
| 292 |
+
SSL: NO SSL / expired certificate
|
| 293 |
+
Registrar: Njalla (privacy)
|
| 294 |
+
Redirect chain: shop4020.top -> redirect702..top -> redirect864..click -> final-235..site
|
| 295 |
+
Similarity to known scam templates: 95%
|
| 296 |
+
Key claims on page:
|
| 297 |
+
- 100% free, no purchase necessary
|
| 298 |
+
- Guaranteed winner
|
| 299 |
+
- Act now — expires in 2 hours
|
| 300 |
+
Suspicious elements:
|
| 301 |
+
- No contact phone number
|
| 302 |
+
- Countdown timer with fake urgency
|
| 303 |
+
- No physical address listed
|
| 304 |
+
- Requests personal info before any value provided
|
| 305 |
+
Content summary: L... [truncated]
|
| 306 |
+
|
| 307 |
+
Available ads: ad_002, ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 308 |
+
|
| 309 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 310 |
+
|
| 311 |
+
Findings:
|
| 312 |
+
[ad_001 / advertiser_history]
|
| 313 |
+
Advert
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
### LLM Response
|
| 317 |
+
```json
|
| 318 |
+
{
|
| 319 |
+
"action_type": "verdict",
|
| 320 |
+
"ad_id": "ad_002",
|
| 321 |
+
"verdict": "reject",
|
| 322 |
+
"confidence": 0.95
|
| 323 |
+
}
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
### Parsed Action
|
| 327 |
+
```json
|
| 328 |
+
{
|
| 329 |
+
"action_type": "verdict",
|
| 330 |
+
"ad_id": "ad_002",
|
| 331 |
+
"verdict": "reject",
|
| 332 |
+
"confidence": 0.95
|
| 333 |
+
}
|
| 334 |
+
```
|
| 335 |
+
|
| 336 |
+
### Environment Response
|
| 337 |
+
- **Reward:** `+0.00`
|
| 338 |
+
- **Done:** `False`
|
| 339 |
+
|
| 340 |
+
- **Feedback:** Verdict recorded for ad_002: reject (confidence: 0.95). Correct decision. Reward: +0.39
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
## Step 5
|
| 344 |
+
|
| 345 |
+
### Observation (sent to LLM)
|
| 346 |
+
```
|
| 347 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 348 |
+
Total ads: 12 | Reviewed: 2 | Pending: 10 | Budget remaining: 27 actions | Step: 5
|
| 349 |
+
|
| 350 |
+
Current Ad: === Ad in Focus: ad_003 ===
|
| 351 |
+
Category: fake_crypto
|
| 352 |
+
Ad copy: "Web3 real estate tokenization. Own fractions of premium properties. Starting at $100. Licensed broker-dealer. PropChain.io"
|
| 353 |
+
Targeting: Adults 30-55, interests: real estate, investing, crypto
|
| 354 |
+
Initial risk signals: high guaranteed returns, complex financial product
|
| 355 |
+
Investigations completed: None yet
|
| 356 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 357 |
+
|
| 358 |
+
Feedback: Verdict recorded for ad_002: reject (confidence: 0.95). Correct decision. Reward: +0.39
|
| 359 |
+
|
| 360 |
+
Available ads: ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 361 |
+
|
| 362 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 363 |
+
ad_002: reject (confidence: 0.95)
|
| 364 |
+
|
| 365 |
+
Findings:
|
| 366 |
+
[ad_001 / advertiser_history]
|
| 367 |
+
Advertiser: FitVisit (Verified Business)
|
| 368 |
+
Account age: 1558 days
|
| 369 |
+
Country: Canada
|
| 370 |
+
Total historical spend: $55,594.09
|
| 371 |
+
Ads submitted in last 30 days: 13
|
| 372 |
+
Historical approval rate: 91%
|
| 373 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 374 |
+
|
| 375 |
+
[ad_001 / payment_method]
|
| 376 |
+
Payment Method Analysis for ad_001:
|
| 377 |
+
Method type: credit_card
|
| 378 |
+
Payment ID: pmt_865179
|
| 379 |
+
No other accounts in the current queue share this payment method.
|
| 380 |
+
|
| 381 |
+
[ad_002 / landing_page]
|
| 382 |
+
Domain: shop4020.top
|
| 383 |
+
Domain age: 11 days
|
| 384 |
+
SSL: NO SSL / expired certificate
|
| 385 |
+
Registrar: Njalla (privacy)
|
| 386 |
+
Redir
|
| 387 |
+
```
|
| 388 |
+
|
| 389 |
+
### LLM Response
|
| 390 |
+
```json
|
| 391 |
+
{
|
| 392 |
+
"action_type": "investigate",
|
| 393 |
+
"ad_id": "ad_003",
|
| 394 |
+
"investigation_target": "landing_page"
|
| 395 |
+
}
|
| 396 |
+
```
|
| 397 |
+
|
| 398 |
+
### Parsed Action
|
| 399 |
+
```json
|
| 400 |
+
{
|
| 401 |
+
"action_type": "investigate",
|
| 402 |
+
"ad_id": "ad_003",
|
| 403 |
+
"investigation_target": "landing_page"
|
| 404 |
+
}
|
| 405 |
+
```
|
| 406 |
+
|
| 407 |
+
### Environment Response
|
| 408 |
+
- **Reward:** `+0.00`
|
| 409 |
+
- **Done:** `False`
|
| 410 |
+
|
| 411 |
+
- **Feedback:** Investigation complete: landing_page for ad_003.
|
| 412 |
+
--- Findings ---
|
| 413 |
+
Domain: best3603.xyz
|
| 414 |
+
Domain age: 9 days
|
| 415 |
+
SSL: NO SSL / expired certificate
|
| 416 |
+
Registrar: NameSilo
|
| 417 |
+
Redirect chain: best3603.xyz -> redirect157..xyz -> final-164..store
|
| 418 |
+
Similarity to known scam templates: 84%
|
| 419 |
+
Key claims on page:
|
| 420 |
+
- 12-15% monthly returns guaranteed
|
| 421 |
+
- Audited by top security firms
|
| 422 |
+
- Risk-free investment
|
| 423 |
+
Suspicious elements:
|
| 424 |
+
- Team photos are stock images
|
| 425 |
+
- Privacy policy copied from template
|
| 426 |
+
- No contact phone number
|
| 427 |
+
- Terms of service link returns 404
|
| 428 |
+
Content summary: Professional-looking platform with dashboard screenshots. Whitepaper link leads to a generic PDF. Team bios use stock photos.
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
## Step 6
|
| 432 |
+
|
| 433 |
+
### Observation (sent to LLM)
|
| 434 |
+
```
|
| 435 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 436 |
+
Total ads: 12 | Reviewed: 2 | Pending: 10 | Budget remaining: 26 actions | Step: 6
|
| 437 |
+
|
| 438 |
+
Current Ad: === Ad in Focus: ad_003 ===
|
| 439 |
+
Category: fake_crypto
|
| 440 |
+
Ad copy: "Web3 real estate tokenization. Own fractions of premium properties. Starting at $100. Licensed broker-dealer. PropChain.io"
|
| 441 |
+
Targeting: Adults 30-55, interests: real estate, investing, crypto
|
| 442 |
+
Initial risk signals: high guaranteed returns, complex financial product
|
| 443 |
+
Investigations completed: landing_page
|
| 444 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 445 |
+
|
| 446 |
+
Feedback: Investigation complete: landing_page for ad_003.
|
| 447 |
+
--- Findings ---
|
| 448 |
+
Domain: best3603.xyz
|
| 449 |
+
Domain age: 9 days
|
| 450 |
+
SSL: NO SSL / expired certificate
|
| 451 |
+
Registrar: NameSilo
|
| 452 |
+
Redirect chain: best3603.xyz -> redirect157..xyz -> final-164..store
|
| 453 |
+
Similarity to known scam templates: 84%
|
| 454 |
+
Key claims on page:
|
| 455 |
+
- 12-15% monthly returns guaranteed
|
| 456 |
+
- Audited by top security firms
|
| 457 |
+
- Risk-free investment
|
| 458 |
+
Suspicious elements:
|
| 459 |
+
- Team photos are stock images
|
| 460 |
+
- Privacy policy copied from template
|
| 461 |
+
- No contact phone number
|
| 462 |
+
- Terms of service link returns 404
|
| 463 |
+
Content summary: Professional-looking platform with da... [truncated]
|
| 464 |
+
|
| 465 |
+
Available ads: ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 466 |
+
|
| 467 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 468 |
+
ad_002: reject (confidence: 0.95)
|
| 469 |
+
|
| 470 |
+
Findings:
|
| 471 |
+
[ad_001 / advertiser_history]
|
| 472 |
+
Advertis
|
| 473 |
+
```
|
| 474 |
+
|
| 475 |
+
### LLM Response
|
| 476 |
+
```json
|
| 477 |
+
{
|
| 478 |
+
"action_type": "investigate",
|
| 479 |
+
"ad_id": "ad_003",
|
| 480 |
+
"investigation_target": "payment_method"
|
| 481 |
+
}
|
| 482 |
+
```
|
| 483 |
+
|
| 484 |
+
### Parsed Action
|
| 485 |
+
```json
|
| 486 |
+
{
|
| 487 |
+
"action_type": "investigate",
|
| 488 |
+
"ad_id": "ad_003",
|
| 489 |
+
"investigation_target": "payment_method"
|
| 490 |
+
}
|
| 491 |
+
```
|
| 492 |
+
|
| 493 |
+
### Environment Response
|
| 494 |
+
- **Reward:** `+0.00`
|
| 495 |
+
- **Done:** `False`
|
| 496 |
+
|
| 497 |
+
- **Feedback:** Investigation complete: payment_method for ad_003.
|
| 498 |
+
--- Findings ---
|
| 499 |
+
Payment Method Analysis for ad_003:
|
| 500 |
+
Method type: wire_transfer
|
| 501 |
+
Payment ID: pmt_711878
|
| 502 |
+
No other accounts in the current queue share this payment method.
|
| 503 |
+
Payment history: 1 chargebacks/disputes on record.
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
## Step 7
|
| 507 |
+
|
| 508 |
+
### Observation (sent to LLM)
|
| 509 |
+
```
|
| 510 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 511 |
+
Total ads: 12 | Reviewed: 2 | Pending: 10 | Budget remaining: 25 actions | Step: 7
|
| 512 |
+
|
| 513 |
+
Current Ad: === Ad in Focus: ad_003 ===
|
| 514 |
+
Category: fake_crypto
|
| 515 |
+
Ad copy: "Web3 real estate tokenization. Own fractions of premium properties. Starting at $100. Licensed broker-dealer. PropChain.io"
|
| 516 |
+
Targeting: Adults 30-55, interests: real estate, investing, crypto
|
| 517 |
+
Initial risk signals: high guaranteed returns, complex financial product
|
| 518 |
+
Investigations completed: landing_page, payment_method
|
| 519 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 520 |
+
|
| 521 |
+
Feedback: Investigation complete: payment_method for ad_003.
|
| 522 |
+
--- Findings ---
|
| 523 |
+
Payment Method Analysis for ad_003:
|
| 524 |
+
Method type: wire_transfer
|
| 525 |
+
Payment ID: pmt_711878
|
| 526 |
+
No other accounts in the current queue share this payment method.
|
| 527 |
+
Payment history: 1 chargebacks/disputes on record.
|
| 528 |
+
|
| 529 |
+
Available ads: ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 530 |
+
|
| 531 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 532 |
+
ad_002: reject (confidence: 0.95)
|
| 533 |
+
|
| 534 |
+
Findings:
|
| 535 |
+
[ad_001 / advertiser_history]
|
| 536 |
+
Advertiser: FitVisit (Verified Business)
|
| 537 |
+
Account age: 1558 days
|
| 538 |
+
Country: Canada
|
| 539 |
+
Total historical spend: $55,594.09
|
| 540 |
+
Ads submitted in last 30 days: 13
|
| 541 |
+
Historical approval rate: 91%
|
| 542 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 543 |
+
|
| 544 |
+
[ad_001 / payment_method]
|
| 545 |
+
Payment Method Analysis for ad_001:
|
| 546 |
+
Method type: credit_card
|
| 547 |
+
Payment ID: p
|
| 548 |
+
```
|
| 549 |
+
|
| 550 |
+
### LLM Response
|
| 551 |
+
```json
|
| 552 |
+
{}
|
| 553 |
+
```
|
| 554 |
+
|
| 555 |
+
### Parsed Action [FALLBACK]
|
| 556 |
+
```json
|
| 557 |
+
{
|
| 558 |
+
"action_type": "verdict",
|
| 559 |
+
"ad_id": "ad_003",
|
| 560 |
+
"verdict": "escalate",
|
| 561 |
+
"confidence": 0.3
|
| 562 |
+
}
|
| 563 |
+
```
|
| 564 |
+
|
| 565 |
+
### Environment Response
|
| 566 |
+
- **Reward:** `+0.00`
|
| 567 |
+
- **Done:** `False`
|
| 568 |
+
|
| 569 |
+
- **Feedback:** Verdict recorded for ad_003: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
## Step 8
|
| 573 |
+
|
| 574 |
+
### Observation (sent to LLM)
|
| 575 |
+
```
|
| 576 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 577 |
+
Total ads: 12 | Reviewed: 3 | Pending: 9 | Budget remaining: 25 actions | Step: 8
|
| 578 |
+
|
| 579 |
+
Current Ad: === Ad in Focus: ad_004 ===
|
| 580 |
+
Category: gray_area_supplements
|
| 581 |
+
Ad copy: "SleepDeep Ultra — melatonin-free natural sleep aid. Non-habit forming. Money-back guarantee. PharmGrade Labs."
|
| 582 |
+
Targeting: Adults 30-65, interests: sleep, insomnia, wellness
|
| 583 |
+
Initial risk signals: supplement with strong efficacy claims, medical-sounding language
|
| 584 |
+
Investigations completed: None yet
|
| 585 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 586 |
+
|
| 587 |
+
Feedback: Verdict recorded for ad_003: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 588 |
+
|
| 589 |
+
Available ads: ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 590 |
+
|
| 591 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 592 |
+
ad_002: reject (confidence: 0.95)
|
| 593 |
+
ad_003: escalate (confidence: 0.30)
|
| 594 |
+
|
| 595 |
+
Findings:
|
| 596 |
+
[ad_001 / advertiser_history]
|
| 597 |
+
Advertiser: FitVisit (Verified Business)
|
| 598 |
+
Account age: 1558 days
|
| 599 |
+
Country: Canada
|
| 600 |
+
Total historical spend: $55,594.09
|
| 601 |
+
Ads submitted in last 30 days: 13
|
| 602 |
+
Historical approval rate: 91%
|
| 603 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 604 |
+
|
| 605 |
+
[ad_001 / payment_method]
|
| 606 |
+
Payment Method Analysis for ad_001:
|
| 607 |
+
Method type: credit_card
|
| 608 |
+
Payment ID: pmt_865179
|
| 609 |
+
No other accounts in the current queue share this payment method.
|
| 610 |
+
|
| 611 |
+
[ad_002 / landing_page]
|
| 612 |
+
Domain: shop4020.top
|
| 613 |
+
Domain age: 11 days
|
| 614 |
+
SSL: NO SSL / expired certi
|
| 615 |
+
```
|
| 616 |
+
|
| 617 |
+
### LLM Response
|
| 618 |
+
```json
|
| 619 |
+
{}
|
| 620 |
+
```
|
| 621 |
+
|
| 622 |
+
### Parsed Action [FALLBACK]
|
| 623 |
+
```json
|
| 624 |
+
{
|
| 625 |
+
"action_type": "verdict",
|
| 626 |
+
"ad_id": "ad_004",
|
| 627 |
+
"verdict": "escalate",
|
| 628 |
+
"confidence": 0.3
|
| 629 |
+
}
|
| 630 |
+
```
|
| 631 |
+
|
| 632 |
+
### Environment Response
|
| 633 |
+
- **Reward:** `+0.00`
|
| 634 |
+
- **Done:** `False`
|
| 635 |
+
|
| 636 |
+
- **Feedback:** Verdict recorded for ad_004: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
## Step 9
|
| 640 |
+
|
| 641 |
+
### Observation (sent to LLM)
|
| 642 |
+
```
|
| 643 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 644 |
+
Total ads: 12 | Reviewed: 4 | Pending: 8 | Budget remaining: 25 actions | Step: 9
|
| 645 |
+
|
| 646 |
+
Current Ad: === Ad in Focus: ad_005 ===
|
| 647 |
+
Category: gray_area_supplements
|
| 648 |
+
Ad copy: "TestoMax Elite — natural testosterone support for men over 40. Doctor-formulated. Published clinical trial results."
|
| 649 |
+
Targeting: Men 40-65, interests: men's health, testosterone, fitness
|
| 650 |
+
Initial risk signals: supplement with strong efficacy claims, medical-sounding language
|
| 651 |
+
Investigations completed: None yet
|
| 652 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 653 |
+
|
| 654 |
+
Feedback: Verdict recorded for ad_004: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 655 |
+
|
| 656 |
+
Available ads: ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 657 |
+
|
| 658 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 659 |
+
ad_002: reject (confidence: 0.95)
|
| 660 |
+
ad_003: escalate (confidence: 0.30)
|
| 661 |
+
ad_004: escalate (confidence: 0.30)
|
| 662 |
+
|
| 663 |
+
Findings:
|
| 664 |
+
[ad_001 / advertiser_history]
|
| 665 |
+
Advertiser: FitVisit (Verified Business)
|
| 666 |
+
Account age: 1558 days
|
| 667 |
+
Country: Canada
|
| 668 |
+
Total historical spend: $55,594.09
|
| 669 |
+
Ads submitted in last 30 days: 13
|
| 670 |
+
Historical approval rate: 91%
|
| 671 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 672 |
+
|
| 673 |
+
[ad_001 / payment_method]
|
| 674 |
+
Payment Method Analysis for ad_001:
|
| 675 |
+
Method type: credit_card
|
| 676 |
+
Payment ID: pmt_865179
|
| 677 |
+
No other accounts in the current queue share this payment method.
|
| 678 |
+
|
| 679 |
+
[ad_002 / landing_page]
|
| 680 |
+
Domain: shop4020.top
|
| 681 |
+
Domain
|
| 682 |
+
```
|
| 683 |
+
|
| 684 |
+
### LLM Response
|
| 685 |
+
```json
|
| 686 |
+
{}
|
| 687 |
+
```
|
| 688 |
+
|
| 689 |
+
### Parsed Action [FALLBACK]
|
| 690 |
+
```json
|
| 691 |
+
{
|
| 692 |
+
"action_type": "verdict",
|
| 693 |
+
"ad_id": "ad_005",
|
| 694 |
+
"verdict": "escalate",
|
| 695 |
+
"confidence": 0.3
|
| 696 |
+
}
|
| 697 |
+
```
|
| 698 |
+
|
| 699 |
+
### Environment Response
|
| 700 |
+
- **Reward:** `+0.00`
|
| 701 |
+
- **Done:** `False`
|
| 702 |
+
|
| 703 |
+
- **Feedback:** Verdict recorded for ad_005: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
## Step 10
|
| 707 |
+
|
| 708 |
+
### Observation (sent to LLM)
|
| 709 |
+
```
|
| 710 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 711 |
+
Total ads: 12 | Reviewed: 5 | Pending: 7 | Budget remaining: 25 actions | Step: 10
|
| 712 |
+
|
| 713 |
+
Current Ad: === Ad in Focus: ad_006 ===
|
| 714 |
+
Category: ecommerce
|
| 715 |
+
Ad copy: "Handcrafted leather bags — designed to last. Each piece unique. ArtisanHide.com"
|
| 716 |
+
Targeting: Adults 25-55, interests: fashion, luxury goods
|
| 717 |
+
Initial risk signals: None
|
| 718 |
+
Investigations completed: None yet
|
| 719 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 720 |
+
|
| 721 |
+
Feedback: Verdict recorded for ad_005: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 722 |
+
|
| 723 |
+
Available ads: ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 724 |
+
|
| 725 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 726 |
+
ad_002: reject (confidence: 0.95)
|
| 727 |
+
ad_003: escalate (confidence: 0.30)
|
| 728 |
+
ad_004: escalate (confidence: 0.30)
|
| 729 |
+
ad_005: escalate (confidence: 0.30)
|
| 730 |
+
|
| 731 |
+
Findings:
|
| 732 |
+
[ad_001 / advertiser_history]
|
| 733 |
+
Advertiser: FitVisit (Verified Business)
|
| 734 |
+
Account age: 1558 days
|
| 735 |
+
Country: Canada
|
| 736 |
+
Total historical spend: $55,594.09
|
| 737 |
+
Ads submitted in last 30 days: 13
|
| 738 |
+
Historical approval rate: 91%
|
| 739 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 740 |
+
|
| 741 |
+
[ad_001 / payment_method]
|
| 742 |
+
Payment Method Analysis for ad_001:
|
| 743 |
+
Method type: credit_card
|
| 744 |
+
Payment ID: pmt_865179
|
| 745 |
+
No other accounts in the current queue share this payment method.
|
| 746 |
+
|
| 747 |
+
[ad_002 / landing_page]
|
| 748 |
+
Domain: shop4020.top
|
| 749 |
+
Domain age: 11 days
|
| 750 |
+
SSL: NO SSL / expired certificate
|
| 751 |
+
Registrar: Njalla (privacy)
|
| 752 |
+
Redirect chai
|
| 753 |
+
```
|
| 754 |
+
|
| 755 |
+
### LLM Response
|
| 756 |
+
```json
|
| 757 |
+
{}
|
| 758 |
+
```
|
| 759 |
+
|
| 760 |
+
### Parsed Action [FALLBACK]
|
| 761 |
+
```json
|
| 762 |
+
{
|
| 763 |
+
"action_type": "verdict",
|
| 764 |
+
"ad_id": "ad_006",
|
| 765 |
+
"verdict": "escalate",
|
| 766 |
+
"confidence": 0.3
|
| 767 |
+
}
|
| 768 |
+
```
|
| 769 |
+
|
| 770 |
+
### Environment Response
|
| 771 |
+
- **Reward:** `+0.00`
|
| 772 |
+
- **Done:** `False`
|
| 773 |
+
|
| 774 |
+
- **Feedback:** Verdict recorded for ad_006: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
## Step 11
|
| 778 |
+
|
| 779 |
+
### Observation (sent to LLM)
|
| 780 |
+
```
|
| 781 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 782 |
+
Total ads: 12 | Reviewed: 6 | Pending: 6 | Budget remaining: 25 actions | Step: 11
|
| 783 |
+
|
| 784 |
+
Current Ad: === Ad in Focus: ad_007 ===
|
| 785 |
+
Category: advance_fee
|
| 786 |
+
Ad copy: "URGENT: I am Prince Adebayo of Nigeria. I need your help to transfer $4.5 million. You keep 30%."
|
| 787 |
+
Targeting: All adults 18+, broad targeting
|
| 788 |
+
Initial risk signals: advance fee request, unsolicited money offer, urgency pressure, request for personal information
|
| 789 |
+
Investigations completed: None yet
|
| 790 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 791 |
+
|
| 792 |
+
Feedback: Verdict recorded for ad_006: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 793 |
+
|
| 794 |
+
Available ads: ad_007, ad_008, ad_009, ad_010, ad_011, ad_012
|
| 795 |
+
|
| 796 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 797 |
+
ad_002: reject (confidence: 0.95)
|
| 798 |
+
ad_003: escalate (confidence: 0.30)
|
| 799 |
+
ad_004: escalate (confidence: 0.30)
|
| 800 |
+
ad_005: escalate (confidence: 0.30)
|
| 801 |
+
ad_006: escalate (confidence: 0.30)
|
| 802 |
+
|
| 803 |
+
Findings:
|
| 804 |
+
[ad_001 / advertiser_history]
|
| 805 |
+
Advertiser: FitVisit (Verified Business)
|
| 806 |
+
Account age: 1558 days
|
| 807 |
+
Country: Canada
|
| 808 |
+
Total historical spend: $55,594.09
|
| 809 |
+
Ads submitted in last 30 days: 13
|
| 810 |
+
Historical approval rate: 91%
|
| 811 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 812 |
+
|
| 813 |
+
[ad_001 / payment_method]
|
| 814 |
+
Payment Method Analysis for ad_001:
|
| 815 |
+
Method type: credit_card
|
| 816 |
+
Payment ID: pmt_865179
|
| 817 |
+
No other accounts in the current queue share this payment method.
|
| 818 |
+
|
| 819 |
+
[ad_002 / la
|
| 820 |
+
```
|
| 821 |
+
|
| 822 |
+
### LLM Response
|
| 823 |
+
```json
|
| 824 |
+
{}
|
| 825 |
+
```
|
| 826 |
+
|
| 827 |
+
### Parsed Action [FALLBACK]
|
| 828 |
+
```json
|
| 829 |
+
{
|
| 830 |
+
"action_type": "verdict",
|
| 831 |
+
"ad_id": "ad_007",
|
| 832 |
+
"verdict": "escalate",
|
| 833 |
+
"confidence": 0.3
|
| 834 |
+
}
|
| 835 |
+
```
|
| 836 |
+
|
| 837 |
+
### Environment Response
|
| 838 |
+
- **Reward:** `+0.00`
|
| 839 |
+
- **Done:** `False`
|
| 840 |
+
|
| 841 |
+
- **Feedback:** Verdict recorded for ad_007: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 842 |
+
|
| 843 |
+
|
| 844 |
+
## Step 12
|
| 845 |
+
|
| 846 |
+
### Observation (sent to LLM)
|
| 847 |
+
```
|
| 848 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 849 |
+
Total ads: 12 | Reviewed: 7 | Pending: 5 | Budget remaining: 25 actions | Step: 12
|
| 850 |
+
|
| 851 |
+
Current Ad: === Ad in Focus: ad_008 ===
|
| 852 |
+
Category: saas
|
| 853 |
+
Ad copy: "Automate your invoicing in minutes. Trusted by 50,000+ small businesses. InvoiceSimple."
|
| 854 |
+
Targeting: Small business owners, interests: finance, accounting
|
| 855 |
+
Initial risk signals: None
|
| 856 |
+
Investigations completed: None yet
|
| 857 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 858 |
+
|
| 859 |
+
Feedback: Verdict recorded for ad_007: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 860 |
+
|
| 861 |
+
Available ads: ad_008, ad_009, ad_010, ad_011, ad_012
|
| 862 |
+
|
| 863 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 864 |
+
ad_002: reject (confidence: 0.95)
|
| 865 |
+
ad_003: escalate (confidence: 0.30)
|
| 866 |
+
ad_004: escalate (confidence: 0.30)
|
| 867 |
+
ad_005: escalate (confidence: 0.30)
|
| 868 |
+
ad_006: escalate (confidence: 0.30)
|
| 869 |
+
ad_007: escalate (confidence: 0.30)
|
| 870 |
+
|
| 871 |
+
Findings:
|
| 872 |
+
[ad_001 / advertiser_history]
|
| 873 |
+
Advertiser: FitVisit (Verified Business)
|
| 874 |
+
Account age: 1558 days
|
| 875 |
+
Country: Canada
|
| 876 |
+
Total historical spend: $55,594.09
|
| 877 |
+
Ads submitted in last 30 days: 13
|
| 878 |
+
Historical approval rate: 91%
|
| 879 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 880 |
+
|
| 881 |
+
[ad_001 / payment_method]
|
| 882 |
+
Payment Method Analysis for ad_001:
|
| 883 |
+
Method type: credit_card
|
| 884 |
+
Payment ID: pmt_865179
|
| 885 |
+
No other accounts in the current queue share this payment method.
|
| 886 |
+
|
| 887 |
+
[ad_002 / landing_page]
|
| 888 |
+
Domain: shop4020.top
|
| 889 |
+
Domain age: 11 days
|
| 890 |
+
SSL
|
| 891 |
+
```
|
| 892 |
+
|
| 893 |
+
### LLM Response
|
| 894 |
+
```json
|
| 895 |
+
{}
|
| 896 |
+
```
|
| 897 |
+
|
| 898 |
+
### Parsed Action [FALLBACK]
|
| 899 |
+
```json
|
| 900 |
+
{
|
| 901 |
+
"action_type": "verdict",
|
| 902 |
+
"ad_id": "ad_008",
|
| 903 |
+
"verdict": "escalate",
|
| 904 |
+
"confidence": 0.3
|
| 905 |
+
}
|
| 906 |
+
```
|
| 907 |
+
|
| 908 |
+
### Environment Response
|
| 909 |
+
- **Reward:** `+0.00`
|
| 910 |
+
- **Done:** `False`
|
| 911 |
+
|
| 912 |
+
- **Feedback:** Verdict recorded for ad_008: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 913 |
+
|
| 914 |
+
|
| 915 |
+
## Step 13
|
| 916 |
+
|
| 917 |
+
### Observation (sent to LLM)
|
| 918 |
+
```
|
| 919 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 920 |
+
Total ads: 12 | Reviewed: 8 | Pending: 4 | Budget remaining: 25 actions | Step: 13
|
| 921 |
+
|
| 922 |
+
Current Ad: === Ad in Focus: ad_009 ===
|
| 923 |
+
Category: ecommerce
|
| 924 |
+
Ad copy: "Spring Sale — Up to 30% off home essentials. Free shipping on orders over $50. Shop now at HomeNest."
|
| 925 |
+
Targeting: Women 25-54, interests: home decor, shopping
|
| 926 |
+
Initial risk signals: None
|
| 927 |
+
Investigations completed: None yet
|
| 928 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 929 |
+
|
| 930 |
+
Feedback: Verdict recorded for ad_008: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 931 |
+
|
| 932 |
+
Available ads: ad_009, ad_010, ad_011, ad_012
|
| 933 |
+
|
| 934 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 935 |
+
ad_002: reject (confidence: 0.95)
|
| 936 |
+
ad_003: escalate (confidence: 0.30)
|
| 937 |
+
ad_004: escalate (confidence: 0.30)
|
| 938 |
+
ad_005: escalate (confidence: 0.30)
|
| 939 |
+
ad_006: escalate (confidence: 0.30)
|
| 940 |
+
ad_007: escalate (confidence: 0.30)
|
| 941 |
+
ad_008: escalate (confidence: 0.30)
|
| 942 |
+
|
| 943 |
+
Findings:
|
| 944 |
+
[ad_001 / advertiser_history]
|
| 945 |
+
Advertiser: FitVisit (Verified Business)
|
| 946 |
+
Account age: 1558 days
|
| 947 |
+
Country: Canada
|
| 948 |
+
Total historical spend: $55,594.09
|
| 949 |
+
Ads submitted in last 30 days: 13
|
| 950 |
+
Historical approval rate: 91%
|
| 951 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 952 |
+
|
| 953 |
+
[ad_001 / payment_method]
|
| 954 |
+
Payment Method Analysis for ad_001:
|
| 955 |
+
Method type: credit_card
|
| 956 |
+
Payment ID: pmt_865179
|
| 957 |
+
No other accounts in the current queue share this payment method.
|
| 958 |
+
|
| 959 |
+
[ad_002 / landing_page]
|
| 960 |
+
Domai
|
| 961 |
+
```
|
| 962 |
+
|
| 963 |
+
### LLM Response
|
| 964 |
+
```json
|
| 965 |
+
{}
|
| 966 |
+
```
|
| 967 |
+
|
| 968 |
+
### Parsed Action [FALLBACK]
|
| 969 |
+
```json
|
| 970 |
+
{
|
| 971 |
+
"action_type": "verdict",
|
| 972 |
+
"ad_id": "ad_009",
|
| 973 |
+
"verdict": "escalate",
|
| 974 |
+
"confidence": 0.3
|
| 975 |
+
}
|
| 976 |
+
```
|
| 977 |
+
|
| 978 |
+
### Environment Response
|
| 979 |
+
- **Reward:** `+0.00`
|
| 980 |
+
- **Done:** `False`
|
| 981 |
+
|
| 982 |
+
- **Feedback:** Verdict recorded for ad_009: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 983 |
+
|
| 984 |
+
|
| 985 |
+
## Step 14
|
| 986 |
+
|
| 987 |
+
### Observation (sent to LLM)
|
| 988 |
+
```
|
| 989 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 990 |
+
Total ads: 12 | Reviewed: 9 | Pending: 3 | Budget remaining: 25 actions | Step: 14
|
| 991 |
+
|
| 992 |
+
Current Ad: === Ad in Focus: ad_010 ===
|
| 993 |
+
Category: ecommerce
|
| 994 |
+
Ad copy: "Handcrafted leather bags — designed to last. Each piece unique. ArtisanHide.com"
|
| 995 |
+
Targeting: Adults 25-55, interests: fashion, luxury goods
|
| 996 |
+
Initial risk signals: None
|
| 997 |
+
Investigations completed: None yet
|
| 998 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 999 |
+
|
| 1000 |
+
Feedback: Verdict recorded for ad_009: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 1001 |
+
|
| 1002 |
+
Available ads: ad_010, ad_011, ad_012
|
| 1003 |
+
|
| 1004 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 1005 |
+
ad_002: reject (confidence: 0.95)
|
| 1006 |
+
ad_003: escalate (confidence: 0.30)
|
| 1007 |
+
ad_004: escalate (confidence: 0.30)
|
| 1008 |
+
ad_005: escalate (confidence: 0.30)
|
| 1009 |
+
ad_006: escalate (confidence: 0.30)
|
| 1010 |
+
ad_007: escalate (confidence: 0.30)
|
| 1011 |
+
ad_008: escalate (confidence: 0.30)
|
| 1012 |
+
ad_009: escalate (confidence: 0.30)
|
| 1013 |
+
|
| 1014 |
+
Findings:
|
| 1015 |
+
[ad_001 / advertiser_history]
|
| 1016 |
+
Advertiser: FitVisit (Verified Business)
|
| 1017 |
+
Account age: 1558 days
|
| 1018 |
+
Country: Canada
|
| 1019 |
+
Total historical spend: $55,594.09
|
| 1020 |
+
Ads submitted in last 30 days: 13
|
| 1021 |
+
Historical approval rate: 91%
|
| 1022 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 1023 |
+
|
| 1024 |
+
[ad_001 / payment_method]
|
| 1025 |
+
Payment Method Analysis for ad_001:
|
| 1026 |
+
Method type: credit_card
|
| 1027 |
+
Payment ID: pmt_865179
|
| 1028 |
+
No other accounts in the current queue share this payment method.
|
| 1029 |
+
|
| 1030 |
+
[ad_002 / landing_
|
| 1031 |
+
```
|
| 1032 |
+
|
| 1033 |
+
### LLM Response
|
| 1034 |
+
```json
|
| 1035 |
+
{}
|
| 1036 |
+
```
|
| 1037 |
+
|
| 1038 |
+
### Parsed Action [FALLBACK]
|
| 1039 |
+
```json
|
| 1040 |
+
{
|
| 1041 |
+
"action_type": "verdict",
|
| 1042 |
+
"ad_id": "ad_010",
|
| 1043 |
+
"verdict": "escalate",
|
| 1044 |
+
"confidence": 0.3
|
| 1045 |
+
}
|
| 1046 |
+
```
|
| 1047 |
+
|
| 1048 |
+
### Environment Response
|
| 1049 |
+
- **Reward:** `+0.00`
|
| 1050 |
+
- **Done:** `False`
|
| 1051 |
+
|
| 1052 |
+
- **Feedback:** Verdict recorded for ad_010: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 1053 |
+
|
| 1054 |
+
|
| 1055 |
+
## Step 15
|
| 1056 |
+
|
| 1057 |
+
### Observation (sent to LLM)
|
| 1058 |
+
```
|
| 1059 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 1060 |
+
Total ads: 12 | Reviewed: 10 | Pending: 2 | Budget remaining: 25 actions | Step: 15
|
| 1061 |
+
|
| 1062 |
+
Current Ad: === Ad in Focus: ad_011 ===
|
| 1063 |
+
Category: fake_giveaway
|
| 1064 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 1065 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 1066 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 1067 |
+
Investigations completed: None yet
|
| 1068 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 1069 |
+
|
| 1070 |
+
Feedback: Verdict recorded for ad_010: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 1071 |
+
|
| 1072 |
+
Available ads: ad_011, ad_012
|
| 1073 |
+
|
| 1074 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 1075 |
+
ad_002: reject (confidence: 0.95)
|
| 1076 |
+
ad_003: escalate (confidence: 0.30)
|
| 1077 |
+
ad_004: escalate (confidence: 0.30)
|
| 1078 |
+
ad_005: escalate (confidence: 0.30)
|
| 1079 |
+
ad_006: escalate (confidence: 0.30)
|
| 1080 |
+
ad_007: escalate (confidence: 0.30)
|
| 1081 |
+
ad_008: escalate (confidence: 0.30)
|
| 1082 |
+
ad_009: escalate (confidence: 0.30)
|
| 1083 |
+
ad_010: escalate (confidence: 0.30)
|
| 1084 |
+
|
| 1085 |
+
Findings:
|
| 1086 |
+
[ad_001 / advertiser_history]
|
| 1087 |
+
Advertiser: FitVisit (Verified Business)
|
| 1088 |
+
Account age: 1558 days
|
| 1089 |
+
Country: Canada
|
| 1090 |
+
Total historical spend: $55,594.09
|
| 1091 |
+
Ads submitted in last 30 days: 13
|
| 1092 |
+
Historical approval rate: 91%
|
| 1093 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 1094 |
+
|
| 1095 |
+
[ad_001 / payment_method]
|
| 1096 |
+
Payment Method Analysis fo
|
| 1097 |
+
```
|
| 1098 |
+
|
| 1099 |
+
### LLM Response
|
| 1100 |
+
```json
|
| 1101 |
+
{}
|
| 1102 |
+
```
|
| 1103 |
+
|
| 1104 |
+
### Parsed Action [FALLBACK]
|
| 1105 |
+
```json
|
| 1106 |
+
{
|
| 1107 |
+
"action_type": "verdict",
|
| 1108 |
+
"ad_id": "ad_011",
|
| 1109 |
+
"verdict": "escalate",
|
| 1110 |
+
"confidence": 0.3
|
| 1111 |
+
}
|
| 1112 |
+
```
|
| 1113 |
+
|
| 1114 |
+
### Environment Response
|
| 1115 |
+
- **Reward:** `+0.00`
|
| 1116 |
+
- **Done:** `False`
|
| 1117 |
+
|
| 1118 |
+
- **Feedback:** Verdict recorded for ad_011: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 1119 |
+
|
| 1120 |
+
|
| 1121 |
+
## Step 16
|
| 1122 |
+
|
| 1123 |
+
### Observation (sent to LLM)
|
| 1124 |
+
```
|
| 1125 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 1126 |
+
Total ads: 12 | Reviewed: 11 | Pending: 1 | Budget remaining: 25 actions | Step: 16
|
| 1127 |
+
|
| 1128 |
+
Current Ad: === Ad in Focus: ad_012 ===
|
| 1129 |
+
Category: counterfeit_goods
|
| 1130 |
+
Ad copy: "Rolex watches starting at $49.99! 100% genuine! Free worldwide shipping! LuxWatchDeals.net"
|
| 1131 |
+
Targeting: Men 25-55, interests: watches, luxury
|
| 1132 |
+
Initial risk signals: impossibly low prices for luxury goods, unfamiliar domain, claims of authenticity with extreme discounts
|
| 1133 |
+
Investigations completed: None yet
|
| 1134 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 1135 |
+
|
| 1136 |
+
Feedback: Verdict recorded for ad_011: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 1137 |
+
|
| 1138 |
+
Available ads: ad_012
|
| 1139 |
+
|
| 1140 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 1141 |
+
ad_002: reject (confidence: 0.95)
|
| 1142 |
+
ad_003: escalate (confidence: 0.30)
|
| 1143 |
+
ad_004: escalate (confidence: 0.30)
|
| 1144 |
+
ad_005: escalate (confidence: 0.30)
|
| 1145 |
+
ad_006: escalate (confidence: 0.30)
|
| 1146 |
+
ad_007: escalate (confidence: 0.30)
|
| 1147 |
+
ad_008: escalate (confidence: 0.30)
|
| 1148 |
+
ad_009: escalate (confidence: 0.30)
|
| 1149 |
+
ad_010: escalate (confidence: 0.30)
|
| 1150 |
+
ad_011: escalate (con... [truncated]
|
| 1151 |
+
|
| 1152 |
+
Findings:
|
| 1153 |
+
[ad_001 / advertiser_history]
|
| 1154 |
+
Advertiser: FitVisit (Verified Business)
|
| 1155 |
+
Account age: 1558 days
|
| 1156 |
+
Country: Canada
|
| 1157 |
+
Total historical spend: $55,594.09
|
| 1158 |
+
Ads submitted in last 30 days: 13
|
| 1159 |
+
Historical approval rate: 91%
|
| 1160 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 1161 |
+
|
| 1162 |
+
[ad_001 / payment_method]
|
| 1163 |
+
Pay
|
| 1164 |
+
```
|
| 1165 |
+
|
| 1166 |
+
### LLM Response
|
| 1167 |
+
```json
|
| 1168 |
+
{}
|
| 1169 |
+
```
|
| 1170 |
+
|
| 1171 |
+
### Parsed Action [FALLBACK]
|
| 1172 |
+
```json
|
| 1173 |
+
{
|
| 1174 |
+
"action_type": "verdict",
|
| 1175 |
+
"ad_id": "ad_012",
|
| 1176 |
+
"verdict": "escalate",
|
| 1177 |
+
"confidence": 0.3
|
| 1178 |
+
}
|
| 1179 |
+
```
|
| 1180 |
+
|
| 1181 |
+
### Environment Response
|
| 1182 |
+
- **Reward:** `+0.00`
|
| 1183 |
+
- **Done:** `False`
|
| 1184 |
+
|
| 1185 |
+
- **Feedback:** Episode complete. Grader score: 0.684/1.000
|
| 1186 |
+
Verdicts rendered: 12/12
|
| 1187 |
+
Auto-approved (unreviewed): 0 fraud ad(s) missed
|
| 1188 |
+
Correct decisions: 4/12
|
| 1189 |
+
|
| 1190 |
+
|
| 1191 |
+
## Step 17
|
| 1192 |
+
|
| 1193 |
+
### Observation (sent to LLM)
|
| 1194 |
+
```
|
| 1195 |
+
Queue: Task: Sophisticated Fraud Under Budget Pressure (medium)
|
| 1196 |
+
Total ads: 12 | Reviewed: 12 | Pending: 0 | Budget remaining: 25 actions | Step: 17
|
| 1197 |
+
|
| 1198 |
+
Current Ad:
|
| 1199 |
+
|
| 1200 |
+
Feedback: Episode complete. Grader score: 0.684/1.000
|
| 1201 |
+
Verdicts rendered: 12/12
|
| 1202 |
+
Auto-approved (unreviewed): 0 fraud ad(s) missed
|
| 1203 |
+
Correct decisions: 4/12
|
| 1204 |
+
|
| 1205 |
+
Available ads:
|
| 1206 |
+
|
| 1207 |
+
Verdicts: ad_001: approve (confidence: 0.90)
|
| 1208 |
+
ad_002: reject (confidence: 0.95)
|
| 1209 |
+
ad_003: escalate (confidence: 0.30)
|
| 1210 |
+
ad_004: escalate (confidence: 0.30)
|
| 1211 |
+
ad_005: escalate (confidence: 0.30)
|
| 1212 |
+
ad_006: escalate (confidence: 0.30)
|
| 1213 |
+
ad_007: escalate (confidence: 0.30)
|
| 1214 |
+
ad_008: escalate (confidence: 0.30)
|
| 1215 |
+
ad_009: escalate (confidence: 0.30)
|
| 1216 |
+
ad_010: escalate (confidence: 0.30)
|
| 1217 |
+
ad_011: escalate (con... [truncated]
|
| 1218 |
+
|
| 1219 |
+
Findings:
|
| 1220 |
+
[ad_001 / advertiser_history]
|
| 1221 |
+
Advertiser: FitVisit (Verified Business)
|
| 1222 |
+
Account age: 1558 days
|
| 1223 |
+
Country: Canada
|
| 1224 |
+
Total historical spend: $55,594.09
|
| 1225 |
+
Ads submitted in last 30 days: 13
|
| 1226 |
+
Historical approval rate: 91%
|
| 1227 |
+
Payment method: credit_card (ID: pmt_865179)
|
| 1228 |
+
|
| 1229 |
+
[ad_001 / payment_method]
|
| 1230 |
+
Payment Method Analysis for ad_001:
|
| 1231 |
+
Method type: credit_card
|
| 1232 |
+
Payment ID: pmt_865179
|
| 1233 |
+
No other accounts in the current queue share this payment method.
|
| 1234 |
+
|
| 1235 |
+
[ad_002 / landing_page]
|
| 1236 |
+
Domain: shop4020.top
|
| 1237 |
+
Domain age: 11 days
|
| 1238 |
+
SSL: NO SSL / expired certificate
|
| 1239 |
+
Registrar: Njalla (privacy)
|
| 1240 |
+
Redirect chain: shop4020.top -> red... [truncated]
|
| 1241 |
+
```
|
| 1242 |
+
|
| 1243 |
+
### LLM Response
|
| 1244 |
+
```json
|
| 1245 |
+
{}
|
| 1246 |
+
```
|
| 1247 |
+
|
| 1248 |
+
|
| 1249 |
+
---
|
| 1250 |
+
## Result
|
| 1251 |
+
**Score: 0.684 | Steps: 17 | Verdicts: 12/12**
|
outputs/task_3_conversation.md
ADDED
|
@@ -0,0 +1,1057 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Episode Log — task_3
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
## Step 0
|
| 5 |
+
|
| 6 |
+
### Observation (sent to LLM)
|
| 7 |
+
```
|
| 8 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 9 |
+
Total ads: 20 | Reviewed: 0 | Pending: 20 | Budget remaining: 40 actions | Step: 0
|
| 10 |
+
|
| 11 |
+
Current Ad: === Ad in Focus: ad_001 ===
|
| 12 |
+
Category: saas
|
| 13 |
+
Ad copy: "Automate your invoicing in minutes. Trusted by 50,000+ small businesses. InvoiceSimple."
|
| 14 |
+
Targeting: Small business owners, interests: finance, accounting
|
| 15 |
+
Initial risk signals: None
|
| 16 |
+
Investigations completed: None yet
|
| 17 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 18 |
+
|
| 19 |
+
Feedback: Episode started. Review the ad queue and begin your investigation.
|
| 20 |
+
|
| 21 |
+
Available ads: ad_001, ad_002, ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### LLM Response
|
| 25 |
+
```json
|
| 26 |
+
{}
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
### Parsed Action [FALLBACK]
|
| 30 |
+
```json
|
| 31 |
+
{
|
| 32 |
+
"action_type": "verdict",
|
| 33 |
+
"ad_id": "ad_001",
|
| 34 |
+
"verdict": "escalate",
|
| 35 |
+
"confidence": 0.3
|
| 36 |
+
}
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
### Environment Response
|
| 40 |
+
- **Reward:** `+0.00`
|
| 41 |
+
- **Done:** `False`
|
| 42 |
+
|
| 43 |
+
- **Feedback:** Verdict recorded for ad_001: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
## Step 1
|
| 47 |
+
|
| 48 |
+
### Observation (sent to LLM)
|
| 49 |
+
```
|
| 50 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 51 |
+
Total ads: 20 | Reviewed: 1 | Pending: 19 | Budget remaining: 40 actions | Step: 1
|
| 52 |
+
|
| 53 |
+
Current Ad: === Ad in Focus: ad_002 ===
|
| 54 |
+
Category: advance_fee
|
| 55 |
+
Ad copy: "You've won the UK National Lottery! Send $100 for tax processing to receive your $1.5M prize."
|
| 56 |
+
Targeting: Adults 25-70, interests: lottery, gambling, luck
|
| 57 |
+
Initial risk signals: advance fee request, unsolicited money offer, urgency pressure, request for personal information
|
| 58 |
+
Investigations completed: None yet
|
| 59 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 60 |
+
|
| 61 |
+
Feedback: Verdict recorded for ad_001: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 62 |
+
|
| 63 |
+
Available ads: ad_002, ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 64 |
+
|
| 65 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### LLM Response
|
| 69 |
+
```json
|
| 70 |
+
{}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Parsed Action [FALLBACK]
|
| 74 |
+
```json
|
| 75 |
+
{
|
| 76 |
+
"action_type": "verdict",
|
| 77 |
+
"ad_id": "ad_002",
|
| 78 |
+
"verdict": "escalate",
|
| 79 |
+
"confidence": 0.3
|
| 80 |
+
}
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Environment Response
|
| 84 |
+
- **Reward:** `+0.00`
|
| 85 |
+
- **Done:** `False`
|
| 86 |
+
|
| 87 |
+
- **Feedback:** Verdict recorded for ad_002: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
## Step 2
|
| 91 |
+
|
| 92 |
+
### Observation (sent to LLM)
|
| 93 |
+
```
|
| 94 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 95 |
+
Total ads: 20 | Reviewed: 2 | Pending: 18 | Budget remaining: 40 actions | Step: 2
|
| 96 |
+
|
| 97 |
+
Current Ad: === Ad in Focus: ad_003 ===
|
| 98 |
+
Category: advance_fee
|
| 99 |
+
Ad copy: "Congratulations! You've inherited $2.8 million from a distant relative. Pay $200 processing fee to claim."
|
| 100 |
+
Targeting: Adults 30-70, interests: finance, inheritance
|
| 101 |
+
Initial risk signals: advance fee request, unsolicited money offer, urgency pressure, request for personal information
|
| 102 |
+
Investigations completed: None yet
|
| 103 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 104 |
+
|
| 105 |
+
Feedback: Verdict recorded for ad_002: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 106 |
+
|
| 107 |
+
Available ads: ad_003, ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 108 |
+
|
| 109 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 110 |
+
ad_002: escalate (confidence: 0.30)
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### LLM Response
|
| 114 |
+
```json
|
| 115 |
+
{}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### Parsed Action [FALLBACK]
|
| 119 |
+
```json
|
| 120 |
+
{
|
| 121 |
+
"action_type": "verdict",
|
| 122 |
+
"ad_id": "ad_003",
|
| 123 |
+
"verdict": "escalate",
|
| 124 |
+
"confidence": 0.3
|
| 125 |
+
}
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
### Environment Response
|
| 129 |
+
- **Reward:** `+0.00`
|
| 130 |
+
- **Done:** `False`
|
| 131 |
+
|
| 132 |
+
- **Feedback:** Verdict recorded for ad_003: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
## Step 3
|
| 136 |
+
|
| 137 |
+
### Observation (sent to LLM)
|
| 138 |
+
```
|
| 139 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 140 |
+
Total ads: 20 | Reviewed: 3 | Pending: 17 | Budget remaining: 40 actions | Step: 3
|
| 141 |
+
|
| 142 |
+
Current Ad: === Ad in Focus: ad_004 ===
|
| 143 |
+
Category: network_crypto
|
| 144 |
+
Ad copy: "Crypto tax software — auto-import from 50+ exchanges. CPA-approved reports. TaxChain Solutions."
|
| 145 |
+
Targeting: Adults 25-55, interests: crypto, tax, finance
|
| 146 |
+
Initial risk signals: None
|
| 147 |
+
Investigations completed: None yet
|
| 148 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 149 |
+
|
| 150 |
+
Feedback: Verdict recorded for ad_003: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 151 |
+
|
| 152 |
+
Available ads: ad_004, ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 153 |
+
|
| 154 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 155 |
+
ad_002: escalate (confidence: 0.30)
|
| 156 |
+
ad_003: escalate (confidence: 0.30)
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
### LLM Response
|
| 160 |
+
```json
|
| 161 |
+
{}
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
### Parsed Action [FALLBACK]
|
| 165 |
+
```json
|
| 166 |
+
{
|
| 167 |
+
"action_type": "verdict",
|
| 168 |
+
"ad_id": "ad_004",
|
| 169 |
+
"verdict": "escalate",
|
| 170 |
+
"confidence": 0.3
|
| 171 |
+
}
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
### Environment Response
|
| 175 |
+
- **Reward:** `+0.00`
|
| 176 |
+
- **Done:** `False`
|
| 177 |
+
|
| 178 |
+
- **Feedback:** Verdict recorded for ad_004: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
## Step 4
|
| 182 |
+
|
| 183 |
+
### Observation (sent to LLM)
|
| 184 |
+
```
|
| 185 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 186 |
+
Total ads: 20 | Reviewed: 4 | Pending: 16 | Budget remaining: 40 actions | Step: 4
|
| 187 |
+
|
| 188 |
+
Current Ad: === Ad in Focus: ad_005 ===
|
| 189 |
+
Category: fake_crypto
|
| 190 |
+
Ad copy: "Invest in DeFi with confidence. Our AI-powered trading platform delivers 12-15% monthly returns. Audited smart contracts. Join CryptoYield Pro."
|
| 191 |
+
Targeting: Men 25-45, interests: cryptocurrency, DeFi, investing
|
| 192 |
+
Initial risk signals: high guaranteed returns, complex financial product
|
| 193 |
+
Investigations completed: None yet
|
| 194 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 195 |
+
|
| 196 |
+
Feedback: Verdict recorded for ad_004: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 197 |
+
|
| 198 |
+
Available ads: ad_005, ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 199 |
+
|
| 200 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 201 |
+
ad_002: escalate (confidence: 0.30)
|
| 202 |
+
ad_003: escalate (confidence: 0.30)
|
| 203 |
+
ad_004: escalate (confidence: 0.30)
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
### LLM Response
|
| 207 |
+
```json
|
| 208 |
+
{}
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
### Parsed Action [FALLBACK]
|
| 212 |
+
```json
|
| 213 |
+
{
|
| 214 |
+
"action_type": "verdict",
|
| 215 |
+
"ad_id": "ad_005",
|
| 216 |
+
"verdict": "escalate",
|
| 217 |
+
"confidence": 0.3
|
| 218 |
+
}
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
### Environment Response
|
| 222 |
+
- **Reward:** `+0.00`
|
| 223 |
+
- **Done:** `False`
|
| 224 |
+
|
| 225 |
+
- **Feedback:** Verdict recorded for ad_005: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
## Step 5
|
| 229 |
+
|
| 230 |
+
### Observation (sent to LLM)
|
| 231 |
+
```
|
| 232 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 233 |
+
Total ads: 20 | Reviewed: 5 | Pending: 15 | Budget remaining: 40 actions | Step: 5
|
| 234 |
+
|
| 235 |
+
Current Ad: === Ad in Focus: ad_006 ===
|
| 236 |
+
Category: ecommerce
|
| 237 |
+
Ad copy: "Spring Sale — Up to 30% off home essentials. Free shipping on orders over $50. Shop now at HomeNest."
|
| 238 |
+
Targeting: Women 25-54, interests: home decor, shopping
|
| 239 |
+
Initial risk signals: None
|
| 240 |
+
Investigations completed: None yet
|
| 241 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 242 |
+
|
| 243 |
+
Feedback: Verdict recorded for ad_005: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 244 |
+
|
| 245 |
+
Available ads: ad_006, ad_007, ad_008, ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 246 |
+
|
| 247 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 248 |
+
ad_002: escalate (confidence: 0.30)
|
| 249 |
+
ad_003: escalate (confidence: 0.30)
|
| 250 |
+
ad_004: escalate (confidence: 0.30)
|
| 251 |
+
ad_005: escalate (confidence: 0.30)
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
### LLM Response
|
| 255 |
+
```json
|
| 256 |
+
{}
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
### Parsed Action [FALLBACK]
|
| 260 |
+
```json
|
| 261 |
+
{
|
| 262 |
+
"action_type": "verdict",
|
| 263 |
+
"ad_id": "ad_006",
|
| 264 |
+
"verdict": "escalate",
|
| 265 |
+
"confidence": 0.3
|
| 266 |
+
}
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
### Environment Response
|
| 270 |
+
- **Reward:** `+0.00`
|
| 271 |
+
- **Done:** `False`
|
| 272 |
+
|
| 273 |
+
- **Feedback:** Verdict recorded for ad_006: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
## Step 6
|
| 277 |
+
|
| 278 |
+
### Observation (sent to LLM)
|
| 279 |
+
```
|
| 280 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 281 |
+
Total ads: 20 | Reviewed: 6 | Pending: 14 | Budget remaining: 40 actions | Step: 6
|
| 282 |
+
|
| 283 |
+
Current Ad: === Ad in Focus: ad_007 ===
|
| 284 |
+
Category: gray_area_supplements
|
| 285 |
+
Ad copy: "SlimBiome Pro — proprietary probiotic blend for healthy weight management. Featured in Health Magazine. GMP certified."
|
| 286 |
+
Targeting: Adults 30-60, interests: weight loss, probiotics, gut health
|
| 287 |
+
Initial risk signals: supplement with strong efficacy claims, medical-sounding language
|
| 288 |
+
Investigations completed: None yet
|
| 289 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 290 |
+
|
| 291 |
+
Feedback: Verdict recorded for ad_006: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 292 |
+
|
| 293 |
+
Available ads: ad_007, ad_008, ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 294 |
+
|
| 295 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 296 |
+
ad_002: escalate (confidence: 0.30)
|
| 297 |
+
ad_003: escalate (confidence: 0.30)
|
| 298 |
+
ad_004: escalate (confidence: 0.30)
|
| 299 |
+
ad_005: escalate (confidence: 0.30)
|
| 300 |
+
ad_006: escalate (confidence: 0.30)
|
| 301 |
+
```
|
| 302 |
+
|
| 303 |
+
### LLM Response
|
| 304 |
+
```json
|
| 305 |
+
{}
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
### Parsed Action [FALLBACK]
|
| 309 |
+
```json
|
| 310 |
+
{
|
| 311 |
+
"action_type": "verdict",
|
| 312 |
+
"ad_id": "ad_007",
|
| 313 |
+
"verdict": "escalate",
|
| 314 |
+
"confidence": 0.3
|
| 315 |
+
}
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### Environment Response
|
| 319 |
+
- **Reward:** `+0.00`
|
| 320 |
+
- **Done:** `False`
|
| 321 |
+
|
| 322 |
+
- **Feedback:** Verdict recorded for ad_007: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
## Step 7
|
| 326 |
+
|
| 327 |
+
### Observation (sent to LLM)
|
| 328 |
+
```
|
| 329 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 330 |
+
Total ads: 20 | Reviewed: 7 | Pending: 13 | Budget remaining: 40 actions | Step: 7
|
| 331 |
+
|
| 332 |
+
Current Ad: === Ad in Focus: ad_008 ===
|
| 333 |
+
Category: celebrity_endorsement_fraud
|
| 334 |
+
Ad copy: "Oprah's favorite weight loss secret finally revealed. 'I lost 40 pounds without dieting.' Doctor-approved method."
|
| 335 |
+
Targeting: Women 25-60, interests: weight loss, Oprah, wellness
|
| 336 |
+
Initial risk signals: celebrity name used without verification, urgency/exclusivity language
|
| 337 |
+
Investigations completed: None yet
|
| 338 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 339 |
+
|
| 340 |
+
Feedback: Verdict recorded for ad_007: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 341 |
+
|
| 342 |
+
Available ads: ad_008, ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 343 |
+
|
| 344 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 345 |
+
ad_002: escalate (confidence: 0.30)
|
| 346 |
+
ad_003: escalate (confidence: 0.30)
|
| 347 |
+
ad_004: escalate (confidence: 0.30)
|
| 348 |
+
ad_005: escalate (confidence: 0.30)
|
| 349 |
+
ad_006: escalate (confidence: 0.30)
|
| 350 |
+
ad_007: escalate (confidence: 0.30)
|
| 351 |
+
```
|
| 352 |
+
|
| 353 |
+
### LLM Response
|
| 354 |
+
```json
|
| 355 |
+
{}
|
| 356 |
+
```
|
| 357 |
+
|
| 358 |
+
### Parsed Action [FALLBACK]
|
| 359 |
+
```json
|
| 360 |
+
{
|
| 361 |
+
"action_type": "verdict",
|
| 362 |
+
"ad_id": "ad_008",
|
| 363 |
+
"verdict": "escalate",
|
| 364 |
+
"confidence": 0.3
|
| 365 |
+
}
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
### Environment Response
|
| 369 |
+
- **Reward:** `+0.00`
|
| 370 |
+
- **Done:** `False`
|
| 371 |
+
|
| 372 |
+
- **Feedback:** Verdict recorded for ad_008: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
## Step 8
|
| 376 |
+
|
| 377 |
+
### Observation (sent to LLM)
|
| 378 |
+
```
|
| 379 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 380 |
+
Total ads: 20 | Reviewed: 8 | Pending: 12 | Budget remaining: 40 actions | Step: 8
|
| 381 |
+
|
| 382 |
+
Current Ad: === Ad in Focus: ad_009 ===
|
| 383 |
+
Category: education
|
| 384 |
+
Ad copy: "Learn Python programming in 12 weeks. Live instructor, project-based curriculum. CodeAcademy Pro."
|
| 385 |
+
Targeting: Adults 18-35, interests: programming, tech careers
|
| 386 |
+
Initial risk signals: None
|
| 387 |
+
Investigations completed: None yet
|
| 388 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 389 |
+
|
| 390 |
+
Feedback: Verdict recorded for ad_008: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 391 |
+
|
| 392 |
+
Available ads: ad_009, ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 393 |
+
|
| 394 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 395 |
+
ad_002: escalate (confidence: 0.30)
|
| 396 |
+
ad_003: escalate (confidence: 0.30)
|
| 397 |
+
ad_004: escalate (confidence: 0.30)
|
| 398 |
+
ad_005: escalate (confidence: 0.30)
|
| 399 |
+
ad_006: escalate (confidence: 0.30)
|
| 400 |
+
ad_007: escalate (confidence: 0.30)
|
| 401 |
+
ad_008: escalate (confidence: 0.30)
|
| 402 |
+
```
|
| 403 |
+
|
| 404 |
+
### LLM Response
|
| 405 |
+
```json
|
| 406 |
+
{}
|
| 407 |
+
```
|
| 408 |
+
|
| 409 |
+
### Parsed Action [FALLBACK]
|
| 410 |
+
```json
|
| 411 |
+
{
|
| 412 |
+
"action_type": "verdict",
|
| 413 |
+
"ad_id": "ad_009",
|
| 414 |
+
"verdict": "escalate",
|
| 415 |
+
"confidence": 0.3
|
| 416 |
+
}
|
| 417 |
+
```
|
| 418 |
+
|
| 419 |
+
### Environment Response
|
| 420 |
+
- **Reward:** `+0.00`
|
| 421 |
+
- **Done:** `False`
|
| 422 |
+
|
| 423 |
+
- **Feedback:** Verdict recorded for ad_009: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
## Step 9
|
| 427 |
+
|
| 428 |
+
### Observation (sent to LLM)
|
| 429 |
+
```
|
| 430 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 431 |
+
Total ads: 20 | Reviewed: 9 | Pending: 11 | Budget remaining: 40 actions | Step: 9
|
| 432 |
+
|
| 433 |
+
Current Ad: === Ad in Focus: ad_010 ===
|
| 434 |
+
Category: fake_giveaway
|
| 435 |
+
Ad copy: "EXCLUSIVE: Win a Tesla Model 3! Enter your email NOW! Drawing ends tonight!!!"
|
| 436 |
+
Targeting: Adults 18-55, interests: cars, Tesla, electric vehicles
|
| 437 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 438 |
+
Investigations completed: None yet
|
| 439 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 440 |
+
|
| 441 |
+
Feedback: Verdict recorded for ad_009: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 442 |
+
|
| 443 |
+
Available ads: ad_010, ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 444 |
+
|
| 445 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 446 |
+
ad_002: escalate (confidence: 0.30)
|
| 447 |
+
ad_003: escalate (confidence: 0.30)
|
| 448 |
+
ad_004: escalate (confidence: 0.30)
|
| 449 |
+
ad_005: escalate (confidence: 0.30)
|
| 450 |
+
ad_006: escalate (confidence: 0.30)
|
| 451 |
+
ad_007: escalate (confidence: 0.30)
|
| 452 |
+
ad_008: escalate (confidence: 0.30)
|
| 453 |
+
ad_009: escalate (confidence: 0.30)
|
| 454 |
+
```
|
| 455 |
+
|
| 456 |
+
### LLM Response
|
| 457 |
+
```json
|
| 458 |
+
{}
|
| 459 |
+
```
|
| 460 |
+
|
| 461 |
+
### Parsed Action [FALLBACK]
|
| 462 |
+
```json
|
| 463 |
+
{
|
| 464 |
+
"action_type": "verdict",
|
| 465 |
+
"ad_id": "ad_010",
|
| 466 |
+
"verdict": "escalate",
|
| 467 |
+
"confidence": 0.3
|
| 468 |
+
}
|
| 469 |
+
```
|
| 470 |
+
|
| 471 |
+
### Environment Response
|
| 472 |
+
- **Reward:** `+0.00`
|
| 473 |
+
- **Done:** `False`
|
| 474 |
+
|
| 475 |
+
- **Feedback:** Verdict recorded for ad_010: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
## Step 10
|
| 479 |
+
|
| 480 |
+
### Observation (sent to LLM)
|
| 481 |
+
```
|
| 482 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 483 |
+
Total ads: 20 | Reviewed: 10 | Pending: 10 | Budget remaining: 40 actions | Step: 10
|
| 484 |
+
|
| 485 |
+
Current Ad: === Ad in Focus: ad_011 ===
|
| 486 |
+
Category: ecommerce
|
| 487 |
+
Ad copy: "Handcrafted leather bags — designed to last. Each piece unique. ArtisanHide.com"
|
| 488 |
+
Targeting: Adults 25-55, interests: fashion, luxury goods
|
| 489 |
+
Initial risk signals: None
|
| 490 |
+
Investigations completed: None yet
|
| 491 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 492 |
+
|
| 493 |
+
Feedback: Verdict recorded for ad_010: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 494 |
+
|
| 495 |
+
Available ads: ad_011, ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 496 |
+
|
| 497 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 498 |
+
ad_002: escalate (confidence: 0.30)
|
| 499 |
+
ad_003: escalate (confidence: 0.30)
|
| 500 |
+
ad_004: escalate (confidence: 0.30)
|
| 501 |
+
ad_005: escalate (confidence: 0.30)
|
| 502 |
+
ad_006: escalate (confidence: 0.30)
|
| 503 |
+
ad_007: escalate (confidence: 0.30)
|
| 504 |
+
ad_008: escalate (confidence: 0.30)
|
| 505 |
+
ad_009: escalate (confidence: 0.30)
|
| 506 |
+
ad_010: escalate (confidence: 0.30)
|
| 507 |
+
```
|
| 508 |
+
|
| 509 |
+
### LLM Response
|
| 510 |
+
```json
|
| 511 |
+
{}
|
| 512 |
+
```
|
| 513 |
+
|
| 514 |
+
### Parsed Action [FALLBACK]
|
| 515 |
+
```json
|
| 516 |
+
{
|
| 517 |
+
"action_type": "verdict",
|
| 518 |
+
"ad_id": "ad_011",
|
| 519 |
+
"verdict": "escalate",
|
| 520 |
+
"confidence": 0.3
|
| 521 |
+
}
|
| 522 |
+
```
|
| 523 |
+
|
| 524 |
+
### Environment Response
|
| 525 |
+
- **Reward:** `+0.00`
|
| 526 |
+
- **Done:** `False`
|
| 527 |
+
|
| 528 |
+
- **Feedback:** Verdict recorded for ad_011: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
## Step 11
|
| 532 |
+
|
| 533 |
+
### Observation (sent to LLM)
|
| 534 |
+
```
|
| 535 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 536 |
+
Total ads: 20 | Reviewed: 11 | Pending: 9 | Budget remaining: 40 actions | Step: 11
|
| 537 |
+
|
| 538 |
+
Current Ad: === Ad in Focus: ad_012 ===
|
| 539 |
+
Category: gray_area_supplements
|
| 540 |
+
Ad copy: "Joint Restore Plus — advanced collagen peptide formula. Thousands of 5-star reviews. Orthopedist recommended."
|
| 541 |
+
Targeting: Adults 45-75, interests: joint health, supplements, arthritis
|
| 542 |
+
Initial risk signals: supplement with strong efficacy claims, medical-sounding language
|
| 543 |
+
Investigations completed: None yet
|
| 544 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 545 |
+
|
| 546 |
+
Feedback: Verdict recorded for ad_011: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 547 |
+
|
| 548 |
+
Available ads: ad_012, ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 549 |
+
|
| 550 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 551 |
+
ad_002: escalate (confidence: 0.30)
|
| 552 |
+
ad_003: escalate (confidence: 0.30)
|
| 553 |
+
ad_004: escalate (confidence: 0.30)
|
| 554 |
+
ad_005: escalate (confidence: 0.30)
|
| 555 |
+
ad_006: escalate (confidence: 0.30)
|
| 556 |
+
ad_007: escalate (confidence: 0.30)
|
| 557 |
+
ad_008: escalate (confidence: 0.30)
|
| 558 |
+
ad_009: escalate (confidence: 0.30)
|
| 559 |
+
ad_010: escalate (confidence: 0.30)
|
| 560 |
+
ad_011: escalate (... [truncated]
|
| 561 |
+
```
|
| 562 |
+
|
| 563 |
+
### LLM Response
|
| 564 |
+
```json
|
| 565 |
+
{}
|
| 566 |
+
```
|
| 567 |
+
|
| 568 |
+
### Parsed Action [FALLBACK]
|
| 569 |
+
```json
|
| 570 |
+
{
|
| 571 |
+
"action_type": "verdict",
|
| 572 |
+
"ad_id": "ad_012",
|
| 573 |
+
"verdict": "escalate",
|
| 574 |
+
"confidence": 0.3
|
| 575 |
+
}
|
| 576 |
+
```
|
| 577 |
+
|
| 578 |
+
### Environment Response
|
| 579 |
+
- **Reward:** `+0.00`
|
| 580 |
+
- **Done:** `False`
|
| 581 |
+
|
| 582 |
+
- **Feedback:** Verdict recorded for ad_012: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
## Step 12
|
| 586 |
+
|
| 587 |
+
### Observation (sent to LLM)
|
| 588 |
+
```
|
| 589 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 590 |
+
Total ads: 20 | Reviewed: 12 | Pending: 8 | Budget remaining: 40 actions | Step: 12
|
| 591 |
+
|
| 592 |
+
Current Ad: === Ad in Focus: ad_013 ===
|
| 593 |
+
Category: gray_area_supplements
|
| 594 |
+
Ad copy: "NeuroPeak — clinically studied nootropic for enhanced focus and memory. 30-day money-back guarantee. FDA-registered facility."
|
| 595 |
+
Targeting: Adults 25-55, interests: nootropics, productivity, brain health
|
| 596 |
+
Initial risk signals: supplement with strong efficacy claims, medical-sounding language
|
| 597 |
+
Investigations completed: None yet
|
| 598 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 599 |
+
|
| 600 |
+
Feedback: Verdict recorded for ad_012: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 601 |
+
|
| 602 |
+
Available ads: ad_013, ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 603 |
+
|
| 604 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 605 |
+
ad_002: escalate (confidence: 0.30)
|
| 606 |
+
ad_003: escalate (confidence: 0.30)
|
| 607 |
+
ad_004: escalate (confidence: 0.30)
|
| 608 |
+
ad_005: escalate (confidence: 0.30)
|
| 609 |
+
ad_006: escalate (confidence: 0.30)
|
| 610 |
+
ad_007: escalate (confidence: 0.30)
|
| 611 |
+
ad_008: escalate (confidence: 0.30)
|
| 612 |
+
ad_009: escalate (confidence: 0.30)
|
| 613 |
+
ad_010: escalate (confidence: 0.30)
|
| 614 |
+
ad_011: escalate (... [truncated]
|
| 615 |
+
```
|
| 616 |
+
|
| 617 |
+
### LLM Response
|
| 618 |
+
```json
|
| 619 |
+
{}
|
| 620 |
+
```
|
| 621 |
+
|
| 622 |
+
### Parsed Action [FALLBACK]
|
| 623 |
+
```json
|
| 624 |
+
{
|
| 625 |
+
"action_type": "verdict",
|
| 626 |
+
"ad_id": "ad_013",
|
| 627 |
+
"verdict": "escalate",
|
| 628 |
+
"confidence": 0.3
|
| 629 |
+
}
|
| 630 |
+
```
|
| 631 |
+
|
| 632 |
+
### Environment Response
|
| 633 |
+
- **Reward:** `+0.00`
|
| 634 |
+
- **Done:** `False`
|
| 635 |
+
|
| 636 |
+
- **Feedback:** Verdict recorded for ad_013: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
## Step 13
|
| 640 |
+
|
| 641 |
+
### Observation (sent to LLM)
|
| 642 |
+
```
|
| 643 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 644 |
+
Total ads: 20 | Reviewed: 13 | Pending: 7 | Budget remaining: 40 actions | Step: 13
|
| 645 |
+
|
| 646 |
+
Current Ad: === Ad in Focus: ad_014 ===
|
| 647 |
+
Category: fake_giveaway
|
| 648 |
+
Ad copy: "CONGRATULATIONS!! You've been selected to win a FREE iPhone 16 Pro!! Click NOW before offer expires!!!"
|
| 649 |
+
Targeting: All adults 18+, broad targeting, worldwide
|
| 650 |
+
Initial risk signals: excessive urgency language, too-good-to-be-true offer, excessive punctuation/caps, no purchase necessary claims
|
| 651 |
+
Investigations completed: None yet
|
| 652 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 653 |
+
|
| 654 |
+
Feedback: Verdict recorded for ad_013: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 655 |
+
|
| 656 |
+
Available ads: ad_014, ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 657 |
+
|
| 658 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 659 |
+
ad_002: escalate (confidence: 0.30)
|
| 660 |
+
ad_003: escalate (confidence: 0.30)
|
| 661 |
+
ad_004: escalate (confidence: 0.30)
|
| 662 |
+
ad_005: escalate (confidence: 0.30)
|
| 663 |
+
ad_006: escalate (confidence: 0.30)
|
| 664 |
+
ad_007: escalate (confidence: 0.30)
|
| 665 |
+
ad_008: escalate (confidence: 0.30)
|
| 666 |
+
ad_009: escalate (confidence: 0.30)
|
| 667 |
+
ad_010: escalate (confidence: 0.30)
|
| 668 |
+
ad_011: escalate (... [truncated]
|
| 669 |
+
```
|
| 670 |
+
|
| 671 |
+
### LLM Response
|
| 672 |
+
```json
|
| 673 |
+
{}
|
| 674 |
+
```
|
| 675 |
+
|
| 676 |
+
### Parsed Action [FALLBACK]
|
| 677 |
+
```json
|
| 678 |
+
{
|
| 679 |
+
"action_type": "verdict",
|
| 680 |
+
"ad_id": "ad_014",
|
| 681 |
+
"verdict": "escalate",
|
| 682 |
+
"confidence": 0.3
|
| 683 |
+
}
|
| 684 |
+
```
|
| 685 |
+
|
| 686 |
+
### Environment Response
|
| 687 |
+
- **Reward:** `+0.00`
|
| 688 |
+
- **Done:** `False`
|
| 689 |
+
|
| 690 |
+
- **Feedback:** Verdict recorded for ad_014: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 691 |
+
|
| 692 |
+
|
| 693 |
+
## Step 14
|
| 694 |
+
|
| 695 |
+
### Observation (sent to LLM)
|
| 696 |
+
```
|
| 697 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 698 |
+
Total ads: 20 | Reviewed: 14 | Pending: 6 | Budget remaining: 40 actions | Step: 14
|
| 699 |
+
|
| 700 |
+
Current Ad: === Ad in Focus: ad_015 ===
|
| 701 |
+
Category: gray_area_supplements
|
| 702 |
+
Ad copy: "TestoMax Elite — natural testosterone support for men over 40. Doctor-formulated. Published clinical trial results."
|
| 703 |
+
Targeting: Men 40-65, interests: men's health, testosterone, fitness
|
| 704 |
+
Initial risk signals: supplement with strong efficacy claims, medical-sounding language
|
| 705 |
+
Investigations completed: None yet
|
| 706 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 707 |
+
|
| 708 |
+
Feedback: Verdict recorded for ad_014: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 709 |
+
|
| 710 |
+
Available ads: ad_015, ad_016, ad_017, ad_018, ad_019, ad_020
|
| 711 |
+
|
| 712 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 713 |
+
ad_002: escalate (confidence: 0.30)
|
| 714 |
+
ad_003: escalate (confidence: 0.30)
|
| 715 |
+
ad_004: escalate (confidence: 0.30)
|
| 716 |
+
ad_005: escalate (confidence: 0.30)
|
| 717 |
+
ad_006: escalate (confidence: 0.30)
|
| 718 |
+
ad_007: escalate (confidence: 0.30)
|
| 719 |
+
ad_008: escalate (confidence: 0.30)
|
| 720 |
+
ad_009: escalate (confidence: 0.30)
|
| 721 |
+
ad_010: escalate (confidence: 0.30)
|
| 722 |
+
ad_011: escalate (... [truncated]
|
| 723 |
+
```
|
| 724 |
+
|
| 725 |
+
### LLM Response
|
| 726 |
+
```json
|
| 727 |
+
{}
|
| 728 |
+
```
|
| 729 |
+
|
| 730 |
+
### Parsed Action [FALLBACK]
|
| 731 |
+
```json
|
| 732 |
+
{
|
| 733 |
+
"action_type": "verdict",
|
| 734 |
+
"ad_id": "ad_015",
|
| 735 |
+
"verdict": "escalate",
|
| 736 |
+
"confidence": 0.3
|
| 737 |
+
}
|
| 738 |
+
```
|
| 739 |
+
|
| 740 |
+
### Environment Response
|
| 741 |
+
- **Reward:** `+0.00`
|
| 742 |
+
- **Done:** `False`
|
| 743 |
+
|
| 744 |
+
- **Feedback:** Verdict recorded for ad_015: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
## Step 15
|
| 748 |
+
|
| 749 |
+
### Observation (sent to LLM)
|
| 750 |
+
```
|
| 751 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 752 |
+
Total ads: 20 | Reviewed: 15 | Pending: 5 | Budget remaining: 40 actions | Step: 15
|
| 753 |
+
|
| 754 |
+
Current Ad: === Ad in Focus: ad_016 ===
|
| 755 |
+
Category: ecommerce
|
| 756 |
+
Ad copy: "Handcrafted leather bags — designed to last. Each piece unique. ArtisanHide.com"
|
| 757 |
+
Targeting: Adults 25-55, interests: fashion, luxury goods
|
| 758 |
+
Initial risk signals: None
|
| 759 |
+
Investigations completed: None yet
|
| 760 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 761 |
+
|
| 762 |
+
Feedback: Verdict recorded for ad_015: escalate (confidence: 0.30). Correct decision. Reward: +0.15
|
| 763 |
+
|
| 764 |
+
Available ads: ad_016, ad_017, ad_018, ad_019, ad_020
|
| 765 |
+
|
| 766 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 767 |
+
ad_002: escalate (confidence: 0.30)
|
| 768 |
+
ad_003: escalate (confidence: 0.30)
|
| 769 |
+
ad_004: escalate (confidence: 0.30)
|
| 770 |
+
ad_005: escalate (confidence: 0.30)
|
| 771 |
+
ad_006: escalate (confidence: 0.30)
|
| 772 |
+
ad_007: escalate (confidence: 0.30)
|
| 773 |
+
ad_008: escalate (confidence: 0.30)
|
| 774 |
+
ad_009: escalate (confidence: 0.30)
|
| 775 |
+
ad_010: escalate (confidence: 0.30)
|
| 776 |
+
ad_011: escalate (... [truncated]
|
| 777 |
+
```
|
| 778 |
+
|
| 779 |
+
### LLM Response
|
| 780 |
+
```json
|
| 781 |
+
{}
|
| 782 |
+
```
|
| 783 |
+
|
| 784 |
+
### Parsed Action [FALLBACK]
|
| 785 |
+
```json
|
| 786 |
+
{
|
| 787 |
+
"action_type": "verdict",
|
| 788 |
+
"ad_id": "ad_016",
|
| 789 |
+
"verdict": "escalate",
|
| 790 |
+
"confidence": 0.3
|
| 791 |
+
}
|
| 792 |
+
```
|
| 793 |
+
|
| 794 |
+
### Environment Response
|
| 795 |
+
- **Reward:** `+0.00`
|
| 796 |
+
- **Done:** `False`
|
| 797 |
+
|
| 798 |
+
- **Feedback:** Verdict recorded for ad_016: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
## Step 16
|
| 802 |
+
|
| 803 |
+
### Observation (sent to LLM)
|
| 804 |
+
```
|
| 805 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 806 |
+
Total ads: 20 | Reviewed: 16 | Pending: 4 | Budget remaining: 40 actions | Step: 16
|
| 807 |
+
|
| 808 |
+
Current Ad: === Ad in Focus: ad_017 ===
|
| 809 |
+
Category: miracle_cure
|
| 810 |
+
Ad copy: "Reverse aging in 14 days! Scientists discover fountain of youth pill! Limited supply!"
|
| 811 |
+
Targeting: Adults 35-65, interests: anti-aging, beauty, health
|
| 812 |
+
Initial risk signals: unsubstantiated medical claims, conspiracy language ('they don't want you to know'), impossible health promises, urgency/scarcity tactics
|
| 813 |
+
Investigations completed: None yet
|
| 814 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 815 |
+
|
| 816 |
+
Feedback: Verdict recorded for ad_016: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 817 |
+
|
| 818 |
+
Available ads: ad_017, ad_018, ad_019, ad_020
|
| 819 |
+
|
| 820 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 821 |
+
ad_002: escalate (confidence: 0.30)
|
| 822 |
+
ad_003: escalate (confidence: 0.30)
|
| 823 |
+
ad_004: escalate (confidence: 0.30)
|
| 824 |
+
ad_005: escalate (confidence: 0.30)
|
| 825 |
+
ad_006: escalate (confidence: 0.30)
|
| 826 |
+
ad_007: escalate (confidence: 0.30)
|
| 827 |
+
ad_008: escalate (confidence: 0.30)
|
| 828 |
+
ad_009: escalate (confidence: 0.30)
|
| 829 |
+
ad_010: escalate (confidence: 0.30)
|
| 830 |
+
ad_011: escalate (... [truncated]
|
| 831 |
+
```
|
| 832 |
+
|
| 833 |
+
### LLM Response
|
| 834 |
+
```json
|
| 835 |
+
{}
|
| 836 |
+
```
|
| 837 |
+
|
| 838 |
+
### Parsed Action [FALLBACK]
|
| 839 |
+
```json
|
| 840 |
+
{
|
| 841 |
+
"action_type": "verdict",
|
| 842 |
+
"ad_id": "ad_017",
|
| 843 |
+
"verdict": "escalate",
|
| 844 |
+
"confidence": 0.3
|
| 845 |
+
}
|
| 846 |
+
```
|
| 847 |
+
|
| 848 |
+
### Environment Response
|
| 849 |
+
- **Reward:** `+0.00`
|
| 850 |
+
- **Done:** `False`
|
| 851 |
+
|
| 852 |
+
- **Feedback:** Verdict recorded for ad_017: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
## Step 17
|
| 856 |
+
|
| 857 |
+
### Observation (sent to LLM)
|
| 858 |
+
```
|
| 859 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 860 |
+
Total ads: 20 | Reviewed: 17 | Pending: 3 | Budget remaining: 40 actions | Step: 17
|
| 861 |
+
|
| 862 |
+
Current Ad: === Ad in Focus: ad_018 ===
|
| 863 |
+
Category: local_service
|
| 864 |
+
Ad copy: "Award-winning pizza delivery in Brooklyn. Family recipes since 1985. Order from Sal's Pizzeria."
|
| 865 |
+
Targeting: Adults 18-55, Brooklyn, interests: food delivery
|
| 866 |
+
Initial risk signals: None
|
| 867 |
+
Investigations completed: None yet
|
| 868 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 869 |
+
|
| 870 |
+
Feedback: Verdict recorded for ad_017: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 871 |
+
|
| 872 |
+
Available ads: ad_018, ad_019, ad_020
|
| 873 |
+
|
| 874 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 875 |
+
ad_002: escalate (confidence: 0.30)
|
| 876 |
+
ad_003: escalate (confidence: 0.30)
|
| 877 |
+
ad_004: escalate (confidence: 0.30)
|
| 878 |
+
ad_005: escalate (confidence: 0.30)
|
| 879 |
+
ad_006: escalate (confidence: 0.30)
|
| 880 |
+
ad_007: escalate (confidence: 0.30)
|
| 881 |
+
ad_008: escalate (confidence: 0.30)
|
| 882 |
+
ad_009: escalate (confidence: 0.30)
|
| 883 |
+
ad_010: escalate (confidence: 0.30)
|
| 884 |
+
ad_011: escalate (... [truncated]
|
| 885 |
+
```
|
| 886 |
+
|
| 887 |
+
### LLM Response
|
| 888 |
+
```json
|
| 889 |
+
{}
|
| 890 |
+
```
|
| 891 |
+
|
| 892 |
+
### Parsed Action [FALLBACK]
|
| 893 |
+
```json
|
| 894 |
+
{
|
| 895 |
+
"action_type": "verdict",
|
| 896 |
+
"ad_id": "ad_018",
|
| 897 |
+
"verdict": "escalate",
|
| 898 |
+
"confidence": 0.3
|
| 899 |
+
}
|
| 900 |
+
```
|
| 901 |
+
|
| 902 |
+
### Environment Response
|
| 903 |
+
- **Reward:** `+0.00`
|
| 904 |
+
- **Done:** `False`
|
| 905 |
+
|
| 906 |
+
- **Feedback:** Verdict recorded for ad_018: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
## Step 18
|
| 910 |
+
|
| 911 |
+
### Observation (sent to LLM)
|
| 912 |
+
```
|
| 913 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 914 |
+
Total ads: 20 | Reviewed: 18 | Pending: 2 | Budget remaining: 40 actions | Step: 18
|
| 915 |
+
|
| 916 |
+
Current Ad: === Ad in Focus: ad_019 ===
|
| 917 |
+
Category: network_ecommerce
|
| 918 |
+
Ad copy: "Robot vacuum with LiDAR mapping. Self-emptying dock. Smart home compatible. CleanBot Pro."
|
| 919 |
+
Targeting: Adults 25-55, interests: smart home, cleaning
|
| 920 |
+
Initial risk signals: None
|
| 921 |
+
Investigations completed: None yet
|
| 922 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 923 |
+
|
| 924 |
+
Feedback: Verdict recorded for ad_018: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 925 |
+
|
| 926 |
+
Available ads: ad_019, ad_020
|
| 927 |
+
|
| 928 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 929 |
+
ad_002: escalate (confidence: 0.30)
|
| 930 |
+
ad_003: escalate (confidence: 0.30)
|
| 931 |
+
ad_004: escalate (confidence: 0.30)
|
| 932 |
+
ad_005: escalate (confidence: 0.30)
|
| 933 |
+
ad_006: escalate (confidence: 0.30)
|
| 934 |
+
ad_007: escalate (confidence: 0.30)
|
| 935 |
+
ad_008: escalate (confidence: 0.30)
|
| 936 |
+
ad_009: escalate (confidence: 0.30)
|
| 937 |
+
ad_010: escalate (confidence: 0.30)
|
| 938 |
+
ad_011: escalate (... [truncated]
|
| 939 |
+
```
|
| 940 |
+
|
| 941 |
+
### LLM Response
|
| 942 |
+
```json
|
| 943 |
+
{}
|
| 944 |
+
```
|
| 945 |
+
|
| 946 |
+
### Parsed Action [FALLBACK]
|
| 947 |
+
```json
|
| 948 |
+
{
|
| 949 |
+
"action_type": "verdict",
|
| 950 |
+
"ad_id": "ad_019",
|
| 951 |
+
"verdict": "escalate",
|
| 952 |
+
"confidence": 0.3
|
| 953 |
+
}
|
| 954 |
+
```
|
| 955 |
+
|
| 956 |
+
### Environment Response
|
| 957 |
+
- **Reward:** `+0.00`
|
| 958 |
+
- **Done:** `False`
|
| 959 |
+
|
| 960 |
+
- **Feedback:** Verdict recorded for ad_019: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 961 |
+
|
| 962 |
+
|
| 963 |
+
## Step 19
|
| 964 |
+
|
| 965 |
+
### Observation (sent to LLM)
|
| 966 |
+
```
|
| 967 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 968 |
+
Total ads: 20 | Reviewed: 19 | Pending: 1 | Budget remaining: 40 actions | Step: 19
|
| 969 |
+
|
| 970 |
+
Current Ad: === Ad in Focus: ad_020 ===
|
| 971 |
+
Category: clone_brand
|
| 972 |
+
Ad copy: "PayPaI Security Alert: Your account needs verification. Update your information to avoid suspension. Secure portal."
|
| 973 |
+
Targeting: Adults 18-65, interests: online payments, PayPal
|
| 974 |
+
Initial risk signals: brand name misspelling, unofficial domain
|
| 975 |
+
Investigations completed: None yet
|
| 976 |
+
Available investigation targets: advertiser_history, landing_page, payment_method, targeting_overlap, creative_similarity
|
| 977 |
+
|
| 978 |
+
Feedback: Verdict recorded for ad_019: escalate (confidence: 0.30). Incorrect decision. Reward: -0.05
|
| 979 |
+
|
| 980 |
+
Available ads: ad_020
|
| 981 |
+
|
| 982 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 983 |
+
ad_002: escalate (confidence: 0.30)
|
| 984 |
+
ad_003: escalate (confidence: 0.30)
|
| 985 |
+
ad_004: escalate (confidence: 0.30)
|
| 986 |
+
ad_005: escalate (confidence: 0.30)
|
| 987 |
+
ad_006: escalate (confidence: 0.30)
|
| 988 |
+
ad_007: escalate (confidence: 0.30)
|
| 989 |
+
ad_008: escalate (confidence: 0.30)
|
| 990 |
+
ad_009: escalate (confidence: 0.30)
|
| 991 |
+
ad_010: escalate (confidence: 0.30)
|
| 992 |
+
ad_011: escalate (... [truncated]
|
| 993 |
+
```
|
| 994 |
+
|
| 995 |
+
### LLM Response
|
| 996 |
+
```json
|
| 997 |
+
{}
|
| 998 |
+
```
|
| 999 |
+
|
| 1000 |
+
### Parsed Action [FALLBACK]
|
| 1001 |
+
```json
|
| 1002 |
+
{
|
| 1003 |
+
"action_type": "verdict",
|
| 1004 |
+
"ad_id": "ad_020",
|
| 1005 |
+
"verdict": "escalate",
|
| 1006 |
+
"confidence": 0.3
|
| 1007 |
+
}
|
| 1008 |
+
```
|
| 1009 |
+
|
| 1010 |
+
### Environment Response
|
| 1011 |
+
- **Reward:** `+0.00`
|
| 1012 |
+
- **Done:** `False`
|
| 1013 |
+
|
| 1014 |
+
- **Feedback:** Episode complete. Grader score: 0.579/1.000
|
| 1015 |
+
Verdicts rendered: 20/20
|
| 1016 |
+
Auto-approved (unreviewed): 0 fraud ad(s) missed
|
| 1017 |
+
Correct decisions: 4/20
|
| 1018 |
+
|
| 1019 |
+
|
| 1020 |
+
## Step 20
|
| 1021 |
+
|
| 1022 |
+
### Observation (sent to LLM)
|
| 1023 |
+
```
|
| 1024 |
+
Queue: Task: Coordinated Fraud Network Detection (hard)
|
| 1025 |
+
Total ads: 20 | Reviewed: 20 | Pending: 0 | Budget remaining: 40 actions | Step: 20
|
| 1026 |
+
|
| 1027 |
+
Current Ad:
|
| 1028 |
+
|
| 1029 |
+
Feedback: Episode complete. Grader score: 0.579/1.000
|
| 1030 |
+
Verdicts rendered: 20/20
|
| 1031 |
+
Auto-approved (unreviewed): 0 fraud ad(s) missed
|
| 1032 |
+
Correct decisions: 4/20
|
| 1033 |
+
|
| 1034 |
+
Available ads:
|
| 1035 |
+
|
| 1036 |
+
Verdicts: ad_001: escalate (confidence: 0.30)
|
| 1037 |
+
ad_002: escalate (confidence: 0.30)
|
| 1038 |
+
ad_003: escalate (confidence: 0.30)
|
| 1039 |
+
ad_004: escalate (confidence: 0.30)
|
| 1040 |
+
ad_005: escalate (confidence: 0.30)
|
| 1041 |
+
ad_006: escalate (confidence: 0.30)
|
| 1042 |
+
ad_007: escalate (confidence: 0.30)
|
| 1043 |
+
ad_008: escalate (confidence: 0.30)
|
| 1044 |
+
ad_009: escalate (confidence: 0.30)
|
| 1045 |
+
ad_010: escalate (confidence: 0.30)
|
| 1046 |
+
ad_011: escalate (... [truncated]
|
| 1047 |
+
```
|
| 1048 |
+
|
| 1049 |
+
### LLM Response
|
| 1050 |
+
```json
|
| 1051 |
+
{}
|
| 1052 |
+
```
|
| 1053 |
+
|
| 1054 |
+
|
| 1055 |
+
---
|
| 1056 |
+
## Result
|
| 1057 |
+
**Score: 0.579 | Steps: 20 | Verdicts: 20/20**
|
pyproject.toml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=45", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "openenv-ad-fraud-env"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Ad Fraud Investigation Environment for OpenEnv — real-world ad fraud detection and investigation simulation"
|
| 9 |
+
requires-python = ">=3.11"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"openenv-core[core]>=0.2.2",
|
| 12 |
+
"fastapi>=0.115.0",
|
| 13 |
+
"pydantic>=2.0.0",
|
| 14 |
+
"uvicorn[standard]>=0.24.0",
|
| 15 |
+
"requests>=2.31.0",
|
| 16 |
+
"faker==33.1.0",
|
| 17 |
+
"networkx>=3.2",
|
| 18 |
+
"openai>=1.0.0",
|
| 19 |
+
"python-dotenv>=1.0.0",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
[project.optional-dependencies]
|
| 23 |
+
dev = [
|
| 24 |
+
"pytest>=8.0.0",
|
| 25 |
+
"pytest-cov>=4.0.0",
|
| 26 |
+
"pytest-asyncio>=0.23.0",
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
[project.scripts]
|
| 30 |
+
server = "ad_fraud_env.server.app:main"
|
| 31 |
+
|
| 32 |
+
[tool.setuptools]
|
| 33 |
+
packages = [
|
| 34 |
+
"ad_fraud_env",
|
| 35 |
+
"ad_fraud_env.server",
|
| 36 |
+
"ad_fraud_env.data",
|
| 37 |
+
"ad_fraud_env.graders",
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
[tool.setuptools.package-dir]
|
| 41 |
+
"ad_fraud_env" = "."
|
| 42 |
+
"ad_fraud_env.server" = "server"
|
| 43 |
+
"ad_fraud_env.data" = "data"
|
| 44 |
+
"ad_fraud_env.graders" = "graders"
|
server/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Server package for the Ad Fraud Investigation Environment."""
|
server/app.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI application for the Ad Fraud Investigation Environment.
|
| 3 |
+
|
| 4 |
+
Creates the OpenEnv server via create_app() and registers custom HTTP
|
| 5 |
+
endpoints required by the hackathon: /tasks, /baseline, /grader.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import logging
|
| 12 |
+
import os
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Any, Dict
|
| 15 |
+
|
| 16 |
+
from openenv.core.env_server import create_app
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
from ..models import AdReviewAction, AdReviewObservation
|
| 20 |
+
except ImportError:
|
| 21 |
+
from models import AdReviewAction, AdReviewObservation
|
| 22 |
+
|
| 23 |
+
from .environment import AdFraudEnvironment, get_last_grader_result
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
app = create_app(
|
| 28 |
+
AdFraudEnvironment,
|
| 29 |
+
AdReviewAction,
|
| 30 |
+
AdReviewObservation,
|
| 31 |
+
env_name="ad_fraud_env",
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ------------------------------------------------------------------
|
| 36 |
+
# Custom endpoints required by the competition
|
| 37 |
+
# ------------------------------------------------------------------
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@app.get("/tasks", tags=["Competition"])
|
| 41 |
+
async def tasks() -> Dict[str, Any]:
|
| 42 |
+
"""Return the list of tasks and the action schema."""
|
| 43 |
+
try:
|
| 44 |
+
from ..data.ad_generator import TASK_CONFIGS
|
| 45 |
+
except ImportError:
|
| 46 |
+
from data.ad_generator import TASK_CONFIGS
|
| 47 |
+
|
| 48 |
+
task_list = []
|
| 49 |
+
for tid, cfg in TASK_CONFIGS.items():
|
| 50 |
+
task_list.append({
|
| 51 |
+
"id": cfg.task_id,
|
| 52 |
+
"name": cfg.name,
|
| 53 |
+
"difficulty": cfg.difficulty,
|
| 54 |
+
"queue_size": cfg.queue_size,
|
| 55 |
+
"action_budget": cfg.action_budget,
|
| 56 |
+
"description": cfg.description,
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
return {
|
| 60 |
+
"tasks": task_list,
|
| 61 |
+
"action_schema": AdReviewAction.model_json_schema(),
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@app.get("/baseline", tags=["Competition"])
|
| 66 |
+
async def baseline() -> Dict[str, Any]:
|
| 67 |
+
"""Return baseline scores, running live inference if credentials are available."""
|
| 68 |
+
baseline_path = Path(__file__).resolve().parent.parent / "baseline_scores.json"
|
| 69 |
+
|
| 70 |
+
has_creds = all(os.getenv(v) for v in ("API_BASE_URL", "MODEL_NAME", "HF_TOKEN"))
|
| 71 |
+
if has_creds:
|
| 72 |
+
try:
|
| 73 |
+
try:
|
| 74 |
+
from ..inference import run_baseline
|
| 75 |
+
except ImportError:
|
| 76 |
+
from inference import run_baseline
|
| 77 |
+
scores = run_baseline()
|
| 78 |
+
with open(baseline_path, "w") as f:
|
| 79 |
+
json.dump(scores, f, indent=2)
|
| 80 |
+
return scores
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.warning("Live baseline failed, falling back to cached: %s", e)
|
| 83 |
+
|
| 84 |
+
if baseline_path.exists():
|
| 85 |
+
with open(baseline_path) as f:
|
| 86 |
+
return json.load(f)
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"error": "No baseline scores available. Set API_BASE_URL, MODEL_NAME, and HF_TOKEN to run live inference.",
|
| 90 |
+
"tasks": {},
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@app.get("/grader", tags=["Competition"])
|
| 95 |
+
async def grader() -> Dict[str, Any]:
|
| 96 |
+
"""Return grader score from the most recently completed episode."""
|
| 97 |
+
result = get_last_grader_result()
|
| 98 |
+
if not result:
|
| 99 |
+
return {
|
| 100 |
+
"error": "No completed episode. Run an episode via WebSocket first.",
|
| 101 |
+
"grader_score": None,
|
| 102 |
+
}
|
| 103 |
+
return result
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def main() -> None:
|
| 107 |
+
"""Entry point for direct execution."""
|
| 108 |
+
import uvicorn
|
| 109 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if __name__ == "__main__":
|
| 113 |
+
main()
|
server/environment.py
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Core Ad Fraud Investigation Environment.
|
| 3 |
+
|
| 4 |
+
Implements the OpenEnv Environment interface. The agent reviews a queue of ads,
|
| 5 |
+
investigates them, and renders verdicts under a budget constraint.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import logging
|
| 11 |
+
from typing import Any, Dict, List, Optional, Set
|
| 12 |
+
from uuid import uuid4
|
| 13 |
+
|
| 14 |
+
from openenv.core.env_server.interfaces import Environment
|
| 15 |
+
from openenv.core.env_server.types import State
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
from ..data.ad_generator import (
|
| 19 |
+
TASK_CONFIGS,
|
| 20 |
+
Ad,
|
| 21 |
+
GeneratedEpisode,
|
| 22 |
+
generate_episode,
|
| 23 |
+
)
|
| 24 |
+
from ..models import AdFraudState, AdReviewAction, AdReviewObservation
|
| 25 |
+
from ..graders.base_grader import EpisodeRecord, LinkResult, VerdictResult, grade_episode
|
| 26 |
+
except ImportError:
|
| 27 |
+
from data.ad_generator import (
|
| 28 |
+
TASK_CONFIGS,
|
| 29 |
+
Ad,
|
| 30 |
+
GeneratedEpisode,
|
| 31 |
+
generate_episode,
|
| 32 |
+
)
|
| 33 |
+
from models import AdFraudState, AdReviewAction, AdReviewObservation
|
| 34 |
+
from graders.base_grader import EpisodeRecord, LinkResult, VerdictResult, grade_episode
|
| 35 |
+
|
| 36 |
+
logger = logging.getLogger(__name__)
|
| 37 |
+
|
| 38 |
+
# Module-level store so the /grader endpoint can read the last score.
|
| 39 |
+
_last_grader_result: Dict[str, Any] = {}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_last_grader_result() -> Dict[str, Any]:
|
| 43 |
+
return dict(_last_grader_result)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class AdFraudEnvironment(
|
| 47 |
+
Environment[AdReviewAction, AdReviewObservation, AdFraudState]
|
| 48 |
+
):
|
| 49 |
+
"""
|
| 50 |
+
Ad fraud investigation environment.
|
| 51 |
+
|
| 52 |
+
Each episode is a review session: the agent processes a queue of N ads
|
| 53 |
+
within a limited action budget, choosing what to investigate and when
|
| 54 |
+
to render verdicts. Unreviewed ads auto-approve at episode end.
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
SUPPORTS_CONCURRENT_SESSIONS = True
|
| 58 |
+
|
| 59 |
+
def __init__(self) -> None:
|
| 60 |
+
super().__init__()
|
| 61 |
+
self._state = AdFraudState(episode_id=str(uuid4()), step_count=0)
|
| 62 |
+
self._episode: Optional[GeneratedEpisode] = None
|
| 63 |
+
self._verdicts: Dict[str, Dict[str, Any]] = {}
|
| 64 |
+
self._links: List[Dict[str, Any]] = []
|
| 65 |
+
self._investigations: Dict[str, List[str]] = {}
|
| 66 |
+
self._cumulative_reward: float = 0.0
|
| 67 |
+
self._done = False
|
| 68 |
+
self._last_feedback = ""
|
| 69 |
+
self._focused_ad_id: Optional[str] = None
|
| 70 |
+
|
| 71 |
+
# ------------------------------------------------------------------
|
| 72 |
+
# OpenEnv interface
|
| 73 |
+
# ------------------------------------------------------------------
|
| 74 |
+
|
| 75 |
+
def reset(
|
| 76 |
+
self,
|
| 77 |
+
seed: int | None = None,
|
| 78 |
+
episode_id: str | None = None,
|
| 79 |
+
**kwargs: Any,
|
| 80 |
+
) -> AdReviewObservation:
|
| 81 |
+
task_id = kwargs.get("task_id", "task_1")
|
| 82 |
+
if task_id not in TASK_CONFIGS:
|
| 83 |
+
task_id = "task_1"
|
| 84 |
+
|
| 85 |
+
effective_seed = seed if seed is not None else hash(uuid4()) & 0xFFFFFFFF
|
| 86 |
+
self._episode = generate_episode(effective_seed, task_id)
|
| 87 |
+
|
| 88 |
+
config = self._episode.task_config
|
| 89 |
+
self._state = AdFraudState(
|
| 90 |
+
episode_id=episode_id or str(uuid4()),
|
| 91 |
+
step_count=0,
|
| 92 |
+
task_id=task_id,
|
| 93 |
+
total_ads=config.queue_size,
|
| 94 |
+
reviewed_count=0,
|
| 95 |
+
remaining_budget=config.action_budget,
|
| 96 |
+
verdicts={},
|
| 97 |
+
grader_score=None,
|
| 98 |
+
)
|
| 99 |
+
self._verdicts = {}
|
| 100 |
+
self._links = []
|
| 101 |
+
self._investigations = {}
|
| 102 |
+
self._cumulative_reward = 0.0
|
| 103 |
+
self._done = False
|
| 104 |
+
self._last_feedback = "Episode started. Review the ad queue and begin your investigation."
|
| 105 |
+
self._focused_ad_id = self._episode.ads[0].ad_id if self._episode.ads else None
|
| 106 |
+
|
| 107 |
+
return self._build_observation(reward=0.0, done=False)
|
| 108 |
+
|
| 109 |
+
def step(
|
| 110 |
+
self,
|
| 111 |
+
action: AdReviewAction,
|
| 112 |
+
timeout_s: float | None = None,
|
| 113 |
+
**kwargs: Any,
|
| 114 |
+
) -> AdReviewObservation:
|
| 115 |
+
if self._done:
|
| 116 |
+
return self._build_observation(
|
| 117 |
+
reward=0.0, done=True,
|
| 118 |
+
feedback_override="Episode is already complete. Call reset() to start a new episode.",
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
if self._episode is None:
|
| 122 |
+
return self._build_observation(
|
| 123 |
+
reward=0.0, done=False,
|
| 124 |
+
feedback_override="Environment not initialized. Call reset() first.",
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
self._state.step_count += 1
|
| 128 |
+
|
| 129 |
+
ad_ids = {a.ad_id for a in self._episode.ads}
|
| 130 |
+
if action.ad_id not in ad_ids:
|
| 131 |
+
self._last_feedback = f"Invalid ad_id '{action.ad_id}'. Valid IDs: {', '.join(sorted(ad_ids))}"
|
| 132 |
+
return self._build_observation(reward=-0.05, done=False)
|
| 133 |
+
|
| 134 |
+
if action.action_type == "investigate":
|
| 135 |
+
reward = self._handle_investigate(action)
|
| 136 |
+
elif action.action_type == "verdict":
|
| 137 |
+
reward = self._handle_verdict(action)
|
| 138 |
+
elif action.action_type == "link_accounts":
|
| 139 |
+
reward = self._handle_link(action)
|
| 140 |
+
else:
|
| 141 |
+
self._last_feedback = f"Unknown action_type '{action.action_type}'."
|
| 142 |
+
reward = -0.05
|
| 143 |
+
|
| 144 |
+
self._cumulative_reward += reward
|
| 145 |
+
|
| 146 |
+
done = self._check_done()
|
| 147 |
+
if done and not self._done:
|
| 148 |
+
end_reward = self._handle_episode_end()
|
| 149 |
+
reward += end_reward
|
| 150 |
+
self._cumulative_reward += end_reward
|
| 151 |
+
self._done = True
|
| 152 |
+
|
| 153 |
+
self._state.remaining_budget = max(0, self._state.remaining_budget)
|
| 154 |
+
self._state.reviewed_count = len(self._verdicts)
|
| 155 |
+
self._state.verdicts = {
|
| 156 |
+
ad_id: v.get("verdict", "") for ad_id, v in self._verdicts.items()
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
return self._build_observation(reward=reward, done=self._done)
|
| 160 |
+
|
| 161 |
+
@property
|
| 162 |
+
def state(self) -> AdFraudState:
|
| 163 |
+
return self._state
|
| 164 |
+
|
| 165 |
+
# ------------------------------------------------------------------
|
| 166 |
+
# Action handlers
|
| 167 |
+
# ------------------------------------------------------------------
|
| 168 |
+
|
| 169 |
+
def _handle_investigate(self, action: AdReviewAction) -> float:
|
| 170 |
+
if self._state.remaining_budget <= 0:
|
| 171 |
+
self._last_feedback = "No budget remaining. You must render verdicts on remaining ads or end the episode."
|
| 172 |
+
return -0.02
|
| 173 |
+
|
| 174 |
+
if action.investigation_target is None:
|
| 175 |
+
self._last_feedback = "investigation_target is required for action_type='investigate'."
|
| 176 |
+
return -0.05
|
| 177 |
+
|
| 178 |
+
ad_id = action.ad_id
|
| 179 |
+
target = action.investigation_target
|
| 180 |
+
|
| 181 |
+
prev = self._investigations.setdefault(ad_id, [])
|
| 182 |
+
if target in prev:
|
| 183 |
+
self._last_feedback = (
|
| 184 |
+
f"You already investigated '{target}' for {ad_id}. "
|
| 185 |
+
"Choose a different investigation target or render a verdict."
|
| 186 |
+
)
|
| 187 |
+
return -0.02
|
| 188 |
+
|
| 189 |
+
if ad_id in self._verdicts:
|
| 190 |
+
self._last_feedback = f"You already rendered a verdict on {ad_id}. Choose a different ad."
|
| 191 |
+
return -0.02
|
| 192 |
+
|
| 193 |
+
self._state.remaining_budget -= 1
|
| 194 |
+
prev.append(target)
|
| 195 |
+
self._focused_ad_id = ad_id
|
| 196 |
+
|
| 197 |
+
findings = self._episode.investigation_data.get(ad_id, {}).get(
|
| 198 |
+
target, "No data available for this investigation type."
|
| 199 |
+
)
|
| 200 |
+
self._last_feedback = (
|
| 201 |
+
f"Investigation complete: {target} for {ad_id}.\n"
|
| 202 |
+
f"--- Findings ---\n{findings}"
|
| 203 |
+
)
|
| 204 |
+
return -0.02
|
| 205 |
+
|
| 206 |
+
def _handle_verdict(self, action: AdReviewAction) -> float:
|
| 207 |
+
ad_id = action.ad_id
|
| 208 |
+
|
| 209 |
+
if ad_id in self._verdicts:
|
| 210 |
+
self._last_feedback = f"You already rendered a verdict on {ad_id}."
|
| 211 |
+
return -0.02
|
| 212 |
+
|
| 213 |
+
if action.verdict is None:
|
| 214 |
+
self._last_feedback = "verdict field is required for action_type='verdict'."
|
| 215 |
+
return -0.05
|
| 216 |
+
|
| 217 |
+
confidence = action.confidence if action.confidence is not None else 0.5
|
| 218 |
+
ad = self._get_ad(ad_id)
|
| 219 |
+
ground_truth = ad.ground_truth_label if ad else "legit"
|
| 220 |
+
severity = ad.severity if ad else 0.0
|
| 221 |
+
|
| 222 |
+
self._verdicts[ad_id] = {
|
| 223 |
+
"verdict": action.verdict,
|
| 224 |
+
"confidence": confidence,
|
| 225 |
+
"ground_truth": ground_truth,
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
reward = self._compute_verdict_reward(action.verdict, ground_truth, severity, confidence)
|
| 229 |
+
|
| 230 |
+
pending = [a.ad_id for a in self._episode.ads if a.ad_id not in self._verdicts]
|
| 231 |
+
self._last_feedback = (
|
| 232 |
+
f"Verdict recorded for {ad_id}: {action.verdict} "
|
| 233 |
+
f"(confidence: {confidence:.2f}). "
|
| 234 |
+
f"{len(pending)} ad(s) remaining in queue."
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
if pending:
|
| 238 |
+
self._focused_ad_id = pending[0]
|
| 239 |
+
|
| 240 |
+
return reward
|
| 241 |
+
|
| 242 |
+
def _handle_link(self, action: AdReviewAction) -> float:
|
| 243 |
+
if action.linked_ad_id is None:
|
| 244 |
+
self._last_feedback = "linked_ad_id is required for action_type='link_accounts'."
|
| 245 |
+
return -0.05
|
| 246 |
+
|
| 247 |
+
ad_ids = {a.ad_id for a in self._episode.ads}
|
| 248 |
+
if action.linked_ad_id not in ad_ids:
|
| 249 |
+
self._last_feedback = f"Invalid linked_ad_id '{action.linked_ad_id}'."
|
| 250 |
+
return -0.05
|
| 251 |
+
|
| 252 |
+
if action.ad_id == action.linked_ad_id:
|
| 253 |
+
self._last_feedback = "Cannot link an ad to itself."
|
| 254 |
+
return -0.05
|
| 255 |
+
|
| 256 |
+
link_key = tuple(sorted([action.ad_id, action.linked_ad_id]))
|
| 257 |
+
existing = {tuple(sorted([l["ad_id_1"], l["ad_id_2"]])) for l in self._links}
|
| 258 |
+
if link_key in existing:
|
| 259 |
+
self._last_feedback = f"Link between {action.ad_id} and {action.linked_ad_id} already recorded."
|
| 260 |
+
return -0.02
|
| 261 |
+
|
| 262 |
+
is_correct = self._check_link_correct(action.ad_id, action.linked_ad_id)
|
| 263 |
+
|
| 264 |
+
self._links.append({
|
| 265 |
+
"ad_id_1": action.ad_id,
|
| 266 |
+
"ad_id_2": action.linked_ad_id,
|
| 267 |
+
"reason": action.link_reason or "",
|
| 268 |
+
"correct": is_correct,
|
| 269 |
+
})
|
| 270 |
+
|
| 271 |
+
self._last_feedback = (
|
| 272 |
+
f"Network link recorded: {action.ad_id} <-> {action.linked_ad_id}. "
|
| 273 |
+
f"Reason: {action.link_reason or 'not specified'}."
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
return 0.4 if is_correct else -0.25
|
| 277 |
+
|
| 278 |
+
# ------------------------------------------------------------------
|
| 279 |
+
# Reward computation
|
| 280 |
+
# ------------------------------------------------------------------
|
| 281 |
+
|
| 282 |
+
def _compute_verdict_reward(
|
| 283 |
+
self, verdict: str, ground_truth: str, severity: float, confidence: float
|
| 284 |
+
) -> float:
|
| 285 |
+
if verdict == "reject" and ground_truth == "fraud":
|
| 286 |
+
return 0.3 + 0.1 * severity
|
| 287 |
+
elif verdict == "approve" and ground_truth == "legit":
|
| 288 |
+
return 0.1
|
| 289 |
+
elif verdict == "escalate" and ground_truth == "escalate":
|
| 290 |
+
return 0.15
|
| 291 |
+
elif verdict == "reject" and ground_truth == "legit":
|
| 292 |
+
return -0.35
|
| 293 |
+
elif verdict == "approve" and ground_truth == "fraud":
|
| 294 |
+
return -0.5
|
| 295 |
+
elif verdict == "escalate":
|
| 296 |
+
return -0.05
|
| 297 |
+
elif verdict == "approve" and ground_truth == "escalate":
|
| 298 |
+
return -0.15
|
| 299 |
+
elif verdict == "reject" and ground_truth == "escalate":
|
| 300 |
+
return -0.1
|
| 301 |
+
else:
|
| 302 |
+
return -0.05
|
| 303 |
+
|
| 304 |
+
def _handle_episode_end(self) -> float:
|
| 305 |
+
"""Apply end-of-episode adjustments for unreviewed ads, then delegate to graders."""
|
| 306 |
+
unreviewed_fraud = 0
|
| 307 |
+
for ad in self._episode.ads:
|
| 308 |
+
if ad.ad_id not in self._verdicts:
|
| 309 |
+
self._verdicts[ad.ad_id] = {
|
| 310 |
+
"verdict": "approve",
|
| 311 |
+
"confidence": 0.0,
|
| 312 |
+
"ground_truth": ad.ground_truth_label,
|
| 313 |
+
"auto_approved": True,
|
| 314 |
+
}
|
| 315 |
+
if ad.ground_truth_label == "fraud":
|
| 316 |
+
unreviewed_fraud += 1
|
| 317 |
+
|
| 318 |
+
record = self._build_episode_record()
|
| 319 |
+
grader_score = grade_episode(record)
|
| 320 |
+
self._state.grader_score = grader_score
|
| 321 |
+
|
| 322 |
+
reviewed_count = len([v for v in self._verdicts.values() if not v.get("auto_approved")])
|
| 323 |
+
total_ads = len(self._episode.ads)
|
| 324 |
+
total_correct = sum(
|
| 325 |
+
1 for v in self._verdicts.values()
|
| 326 |
+
if not v.get("auto_approved")
|
| 327 |
+
and (
|
| 328 |
+
(v["verdict"] == "reject" and v["ground_truth"] == "fraud")
|
| 329 |
+
or (v["verdict"] == "approve" and v["ground_truth"] == "legit")
|
| 330 |
+
or (v["verdict"] == "escalate" and v["ground_truth"] == "escalate")
|
| 331 |
+
)
|
| 332 |
+
)
|
| 333 |
+
false_positives = sum(
|
| 334 |
+
1 for v in self._verdicts.values()
|
| 335 |
+
if not v.get("auto_approved")
|
| 336 |
+
and v["verdict"] == "reject" and v["ground_truth"] == "legit"
|
| 337 |
+
)
|
| 338 |
+
false_negatives = sum(
|
| 339 |
+
1 for v in self._verdicts.values()
|
| 340 |
+
if not v.get("auto_approved")
|
| 341 |
+
and v["verdict"] == "approve" and v["ground_truth"] == "fraud"
|
| 342 |
+
)
|
| 343 |
+
correct_links = sum(1 for l in self._links if l.get("correct"))
|
| 344 |
+
incorrect_links = sum(1 for l in self._links if not l.get("correct"))
|
| 345 |
+
|
| 346 |
+
global _last_grader_result
|
| 347 |
+
_last_grader_result = {
|
| 348 |
+
"task_id": self._state.task_id,
|
| 349 |
+
"grader_score": grader_score,
|
| 350 |
+
"episode_id": self._state.episode_id,
|
| 351 |
+
"total_steps": self._state.step_count,
|
| 352 |
+
"verdicts_rendered": reviewed_count,
|
| 353 |
+
"correct_decisions": total_correct,
|
| 354 |
+
"false_positives": false_positives,
|
| 355 |
+
"false_negatives": false_negatives,
|
| 356 |
+
"auto_approved": total_ads - reviewed_count,
|
| 357 |
+
"unreviewed_fraud": unreviewed_fraud,
|
| 358 |
+
"network_links_correct": correct_links,
|
| 359 |
+
"network_links_incorrect": incorrect_links,
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
feedback_lines = [
|
| 363 |
+
f"Episode complete. Grader score: {grader_score:.3f}/1.000",
|
| 364 |
+
f"Verdicts rendered: {reviewed_count}/{total_ads}",
|
| 365 |
+
f"Correct decisions: {total_correct}/{reviewed_count}",
|
| 366 |
+
f"False positives (legit rejected): {false_positives}",
|
| 367 |
+
f"False negatives (fraud approved): {false_negatives}",
|
| 368 |
+
f"Unreviewed ads auto-approved: {unreviewed_fraud}",
|
| 369 |
+
]
|
| 370 |
+
if self._links:
|
| 371 |
+
feedback_lines.append(
|
| 372 |
+
f"Network links: {correct_links} correct, {incorrect_links} incorrect"
|
| 373 |
+
)
|
| 374 |
+
self._last_feedback = "\n".join(feedback_lines)
|
| 375 |
+
|
| 376 |
+
return 0.0
|
| 377 |
+
|
| 378 |
+
def _build_episode_record(self) -> EpisodeRecord:
|
| 379 |
+
"""Convert internal state into an EpisodeRecord for the grader."""
|
| 380 |
+
verdict_results = []
|
| 381 |
+
for ad in self._episode.ads:
|
| 382 |
+
v = self._verdicts.get(ad.ad_id)
|
| 383 |
+
if v:
|
| 384 |
+
verdict_results.append(VerdictResult(
|
| 385 |
+
ad_id=ad.ad_id,
|
| 386 |
+
verdict=v["verdict"],
|
| 387 |
+
confidence=v.get("confidence", 0.5),
|
| 388 |
+
ground_truth=v["ground_truth"],
|
| 389 |
+
auto_approved=v.get("auto_approved", False),
|
| 390 |
+
))
|
| 391 |
+
|
| 392 |
+
link_results = [
|
| 393 |
+
LinkResult(ad_id_1=l["ad_id_1"], ad_id_2=l["ad_id_2"], correct=l["correct"])
|
| 394 |
+
for l in self._links
|
| 395 |
+
]
|
| 396 |
+
|
| 397 |
+
ads_metadata = [
|
| 398 |
+
{"ad_id": ad.ad_id, "ground_truth": ad.ground_truth_label, "severity": ad.severity}
|
| 399 |
+
for ad in self._episode.ads
|
| 400 |
+
]
|
| 401 |
+
|
| 402 |
+
return EpisodeRecord(
|
| 403 |
+
task_id=self._state.task_id,
|
| 404 |
+
total_steps=self._state.step_count,
|
| 405 |
+
action_budget=self._episode.task_config.action_budget,
|
| 406 |
+
verdicts=verdict_results,
|
| 407 |
+
links=link_results,
|
| 408 |
+
ads_metadata=ads_metadata,
|
| 409 |
+
n_fraud_rings=len(self._episode.fraud_rings),
|
| 410 |
+
ring_sizes=[len(r.member_ad_ids) for r in self._episode.fraud_rings],
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
# ------------------------------------------------------------------
|
| 414 |
+
# Helpers
|
| 415 |
+
# ------------------------------------------------------------------
|
| 416 |
+
|
| 417 |
+
def _check_done(self) -> bool:
|
| 418 |
+
if self._episode is None:
|
| 419 |
+
return True
|
| 420 |
+
all_reviewed = all(
|
| 421 |
+
ad.ad_id in self._verdicts for ad in self._episode.ads
|
| 422 |
+
)
|
| 423 |
+
steps_exhausted = self._state.step_count >= self._episode.task_config.action_budget
|
| 424 |
+
return all_reviewed or steps_exhausted
|
| 425 |
+
|
| 426 |
+
def _check_link_correct(self, ad_id_1: str, ad_id_2: str) -> bool:
|
| 427 |
+
"""Check if two ads share a fraud ring."""
|
| 428 |
+
for ring in self._episode.fraud_rings:
|
| 429 |
+
if ad_id_1 in ring.member_ad_ids and ad_id_2 in ring.member_ad_ids:
|
| 430 |
+
return True
|
| 431 |
+
return False
|
| 432 |
+
|
| 433 |
+
def _get_ad(self, ad_id: str) -> Optional[Ad]:
|
| 434 |
+
if self._episode is None:
|
| 435 |
+
return None
|
| 436 |
+
for ad in self._episode.ads:
|
| 437 |
+
if ad.ad_id == ad_id:
|
| 438 |
+
return ad
|
| 439 |
+
return None
|
| 440 |
+
|
| 441 |
+
def _build_observation(
|
| 442 |
+
self,
|
| 443 |
+
reward: float,
|
| 444 |
+
done: bool,
|
| 445 |
+
feedback_override: str | None = None,
|
| 446 |
+
) -> AdReviewObservation:
|
| 447 |
+
feedback = feedback_override or self._last_feedback
|
| 448 |
+
|
| 449 |
+
if self._episode is None:
|
| 450 |
+
return AdReviewObservation(
|
| 451 |
+
done=done,
|
| 452 |
+
reward=reward,
|
| 453 |
+
queue_summary="No episode loaded.",
|
| 454 |
+
current_ad_info="",
|
| 455 |
+
investigation_findings="",
|
| 456 |
+
verdict_history_summary="",
|
| 457 |
+
feedback=feedback,
|
| 458 |
+
available_ads=[],
|
| 459 |
+
queue_status={},
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
config = self._episode.task_config
|
| 463 |
+
pending = [a for a in self._episode.ads if a.ad_id not in self._verdicts]
|
| 464 |
+
reviewed = [a for a in self._episode.ads if a.ad_id in self._verdicts]
|
| 465 |
+
|
| 466 |
+
steps_remaining = max(0, config.action_budget - self._state.step_count)
|
| 467 |
+
queue_summary = (
|
| 468 |
+
f"Task: {config.name} ({config.difficulty})\n"
|
| 469 |
+
f"Total ads: {config.queue_size} | "
|
| 470 |
+
f"Reviewed: {len(reviewed)} | "
|
| 471 |
+
f"Pending: {len(pending)} | "
|
| 472 |
+
f"Steps remaining: {steps_remaining}/{config.action_budget} | "
|
| 473 |
+
f"Investigation budget: {self._state.remaining_budget} | "
|
| 474 |
+
f"Step: {self._state.step_count}"
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
current_ad_info = ""
|
| 478 |
+
if self._focused_ad_id and not done:
|
| 479 |
+
ad = self._get_ad(self._focused_ad_id)
|
| 480 |
+
if ad and ad.ad_id not in self._verdicts:
|
| 481 |
+
signals = ", ".join(ad.initial_risk_signals) if ad.initial_risk_signals else "None"
|
| 482 |
+
investigated = self._investigations.get(ad.ad_id, [])
|
| 483 |
+
inv_status = ", ".join(investigated) if investigated else "None yet"
|
| 484 |
+
|
| 485 |
+
# Contextual metadata visible before investigation
|
| 486 |
+
profile = self._episode.advertiser_profiles.get(ad.ad_id)
|
| 487 |
+
meta_lines = []
|
| 488 |
+
if profile:
|
| 489 |
+
meta_lines.append(f"Advertiser country: {profile.country}")
|
| 490 |
+
meta_lines.append(f"Account age: {profile.account_age_days} days")
|
| 491 |
+
if profile.account_age_days < 30:
|
| 492 |
+
meta_lines.append("Flag: New account (< 30 days)")
|
| 493 |
+
context_meta = "\n".join(meta_lines)
|
| 494 |
+
|
| 495 |
+
current_ad_info = (
|
| 496 |
+
f"=== Ad in Focus: {ad.ad_id} ===\n"
|
| 497 |
+
f"Category: {ad.category}\n"
|
| 498 |
+
f"Ad copy: \"{ad.ad_copy}\"\n"
|
| 499 |
+
f"Targeting: {ad.targeting_summary}\n"
|
| 500 |
+
f"Initial risk signals: {signals}\n"
|
| 501 |
+
f"{context_meta}\n"
|
| 502 |
+
f"Investigations completed: {inv_status}\n"
|
| 503 |
+
f"Available investigation targets: advertiser_history, landing_page, "
|
| 504 |
+
f"payment_method, targeting_overlap, creative_similarity, campaign_structure"
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
investigation_findings = ""
|
| 508 |
+
for ad_id, targets in self._investigations.items():
|
| 509 |
+
for target in targets:
|
| 510 |
+
finding = self._episode.investigation_data.get(ad_id, {}).get(target, "")
|
| 511 |
+
if finding:
|
| 512 |
+
investigation_findings += f"\n[{ad_id} / {target}]\n{finding}\n"
|
| 513 |
+
|
| 514 |
+
manual_verdicts = {
|
| 515 |
+
ad_id: v for ad_id, v in self._verdicts.items()
|
| 516 |
+
if not v.get("auto_approved")
|
| 517 |
+
}
|
| 518 |
+
if manual_verdicts:
|
| 519 |
+
counts = {"approve": 0, "reject": 0, "escalate": 0}
|
| 520 |
+
by_decision = {"approve": [], "reject": [], "escalate": []}
|
| 521 |
+
for ad_id, v in manual_verdicts.items():
|
| 522 |
+
counts[v["verdict"]] = counts.get(v["verdict"], 0) + 1
|
| 523 |
+
by_decision[v["verdict"]].append(ad_id)
|
| 524 |
+
summary_parts = [f"{c} {k}" for k, c in counts.items() if c > 0]
|
| 525 |
+
verdict_lines = [
|
| 526 |
+
f"Reviewed {len(manual_verdicts)} ad(s): {', '.join(summary_parts)}."
|
| 527 |
+
]
|
| 528 |
+
for decision in ("reject", "approve", "escalate"):
|
| 529 |
+
if by_decision[decision]:
|
| 530 |
+
verdict_lines.append(
|
| 531 |
+
f" {decision}: {', '.join(by_decision[decision])}"
|
| 532 |
+
)
|
| 533 |
+
verdict_history_summary = "\n".join(verdict_lines)
|
| 534 |
+
else:
|
| 535 |
+
verdict_history_summary = "No verdicts yet."
|
| 536 |
+
|
| 537 |
+
available_ads = [a.ad_id for a in pending]
|
| 538 |
+
|
| 539 |
+
queue_status = {
|
| 540 |
+
"total_ads": config.queue_size,
|
| 541 |
+
"reviewed": len(reviewed),
|
| 542 |
+
"pending": len(pending),
|
| 543 |
+
"investigation_budget": self._state.remaining_budget,
|
| 544 |
+
"steps_remaining": steps_remaining,
|
| 545 |
+
"step": self._state.step_count,
|
| 546 |
+
"task_id": config.task_id,
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
return AdReviewObservation(
|
| 550 |
+
done=done,
|
| 551 |
+
reward=reward,
|
| 552 |
+
queue_summary=queue_summary,
|
| 553 |
+
current_ad_info=current_ad_info,
|
| 554 |
+
investigation_findings=investigation_findings.strip(),
|
| 555 |
+
verdict_history_summary=verdict_history_summary,
|
| 556 |
+
feedback=feedback,
|
| 557 |
+
available_ads=available_ads,
|
| 558 |
+
queue_status=queue_status,
|
| 559 |
+
)
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core[core]>=0.2.2
|
| 2 |
+
fastapi>=0.115.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
uvicorn[standard]>=0.24.0
|
| 5 |
+
requests>=2.31.0
|
| 6 |
+
faker==33.1.0
|
| 7 |
+
networkx>=3.2
|
| 8 |
+
openai>=1.0.0
|
| 9 |
+
python-dotenv>=1.0.0
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_data_generation.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for deterministic data generation."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from ad_fraud_env.data.ad_generator import generate_episode
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class TestDeterminism:
|
| 9 |
+
def test_same_seed_produces_identical_output(self):
|
| 10 |
+
"""Generate with seed=42 twice — output must be byte-identical."""
|
| 11 |
+
ep1 = generate_episode(seed=42, task_id="task_1")
|
| 12 |
+
ep2 = generate_episode(seed=42, task_id="task_1")
|
| 13 |
+
|
| 14 |
+
assert len(ep1.ads) == len(ep2.ads)
|
| 15 |
+
for a1, a2 in zip(ep1.ads, ep2.ads):
|
| 16 |
+
assert a1.ad_id == a2.ad_id
|
| 17 |
+
assert a1.ad_copy == a2.ad_copy
|
| 18 |
+
assert a1.ground_truth_label == a2.ground_truth_label
|
| 19 |
+
|
| 20 |
+
for ad_id in ep1.investigation_data:
|
| 21 |
+
for target in ep1.investigation_data[ad_id]:
|
| 22 |
+
assert (
|
| 23 |
+
ep1.investigation_data[ad_id][target]
|
| 24 |
+
== ep2.investigation_data[ad_id][target]
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def test_different_seeds_produce_different_output(self):
|
| 28 |
+
ep1 = generate_episode(seed=42, task_id="task_1")
|
| 29 |
+
ep2 = generate_episode(seed=99, task_id="task_1")
|
| 30 |
+
|
| 31 |
+
copies_1 = {a.ad_copy for a in ep1.ads}
|
| 32 |
+
copies_2 = {a.ad_copy for a in ep2.ads}
|
| 33 |
+
assert copies_1 != copies_2
|
| 34 |
+
|
| 35 |
+
def test_task_configs_produce_correct_queue_sizes(self):
|
| 36 |
+
for task_id, expected_size in [("task_1", 5), ("task_2", 12), ("task_3", 20)]:
|
| 37 |
+
ep = generate_episode(seed=42, task_id=task_id)
|
| 38 |
+
assert len(ep.ads) == expected_size, f"{task_id}: expected {expected_size}, got {len(ep.ads)}"
|
| 39 |
+
|
| 40 |
+
def test_task3_has_fraud_rings(self):
|
| 41 |
+
ep = generate_episode(seed=42, task_id="task_3")
|
| 42 |
+
assert len(ep.fraud_rings) > 0, "Task 3 should have fraud rings"
|
| 43 |
+
for ring in ep.fraud_rings:
|
| 44 |
+
assert len(ring.member_ad_ids) >= 3
|
| 45 |
+
assert len(ring.shared_signals) >= 2
|
| 46 |
+
assert ring.topology in ("clique", "chain", "hub_spoke")
|
| 47 |
+
|
| 48 |
+
def test_investigation_data_exists_for_all_ads(self):
|
| 49 |
+
ep = generate_episode(seed=42, task_id="task_2")
|
| 50 |
+
expected_targets = [
|
| 51 |
+
"advertiser_history", "landing_page", "payment_method",
|
| 52 |
+
"targeting_overlap", "creative_similarity", "campaign_structure",
|
| 53 |
+
]
|
| 54 |
+
for ad in ep.ads:
|
| 55 |
+
assert ad.ad_id in ep.investigation_data
|
| 56 |
+
for target in expected_targets:
|
| 57 |
+
assert target in ep.investigation_data[ad.ad_id], (
|
| 58 |
+
f"Missing {target} for {ad.ad_id}"
|
| 59 |
+
)
|
| 60 |
+
assert len(ep.investigation_data[ad.ad_id][target]) > 0
|
| 61 |
+
|
| 62 |
+
def test_ground_truth_distribution(self):
|
| 63 |
+
ep = generate_episode(seed=42, task_id="task_2")
|
| 64 |
+
labels = [a.ground_truth_label for a in ep.ads]
|
| 65 |
+
assert "fraud" in labels
|
| 66 |
+
assert "legit" in labels
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class TestNoExplicitCrossAdReferences:
|
| 70 |
+
"""Investigation text must not explicitly name other ad IDs."""
|
| 71 |
+
|
| 72 |
+
def test_payment_investigation_no_cross_refs(self):
|
| 73 |
+
ep = generate_episode(seed=42, task_id="task_3")
|
| 74 |
+
for ad_id, inv in ep.investigation_data.items():
|
| 75 |
+
text = inv["payment_method"]
|
| 76 |
+
for other_ad in ep.investigation_data:
|
| 77 |
+
if other_ad == ad_id:
|
| 78 |
+
continue
|
| 79 |
+
assert other_ad not in text, (
|
| 80 |
+
f"Payment investigation for {ad_id} references {other_ad}"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def test_targeting_investigation_no_cross_refs(self):
|
| 84 |
+
ep = generate_episode(seed=42, task_id="task_3")
|
| 85 |
+
for ad_id, inv in ep.investigation_data.items():
|
| 86 |
+
text = inv["targeting_overlap"]
|
| 87 |
+
assert "HIGH OVERLAP detected with:" not in text
|
| 88 |
+
|
| 89 |
+
def test_creative_investigation_no_cross_refs(self):
|
| 90 |
+
ep = generate_episode(seed=42, task_id="task_3")
|
| 91 |
+
for ad_id, inv in ep.investigation_data.items():
|
| 92 |
+
text = inv["creative_similarity"]
|
| 93 |
+
assert "STRONG SIMILARITY detected with:" not in text
|
| 94 |
+
|
| 95 |
+
def test_campaign_investigation_no_cross_refs(self):
|
| 96 |
+
ep = generate_episode(seed=42, task_id="task_3")
|
| 97 |
+
for ad_id, inv in ep.investigation_data.items():
|
| 98 |
+
text = inv["campaign_structure"]
|
| 99 |
+
assert "MATCH:" not in text
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class TestDecoysAndRealism:
|
| 103 |
+
def test_advertiser_profiles_have_temporal_signals(self):
|
| 104 |
+
ep = generate_episode(seed=42, task_id="task_2")
|
| 105 |
+
for ad_id, profile in ep.advertiser_profiles.items():
|
| 106 |
+
assert profile.account_created_date, f"Missing created date for {ad_id}"
|
| 107 |
+
assert profile.spend_velocity, f"Missing spend velocity for {ad_id}"
|
| 108 |
+
assert profile.ad_submission_pattern, f"Missing submission pattern for {ad_id}"
|
| 109 |
+
|
| 110 |
+
def test_temporal_signals_appear_in_investigation(self):
|
| 111 |
+
ep = generate_episode(seed=42, task_id="task_2")
|
| 112 |
+
for ad_id, inv in ep.investigation_data.items():
|
| 113 |
+
text = inv["advertiser_history"]
|
| 114 |
+
assert "Account created:" in text or "Account age:" in text
|
| 115 |
+
assert "Spend velocity:" in text or "spend" in text.lower()
|
| 116 |
+
|
| 117 |
+
def test_ring_members_share_creation_week(self):
|
| 118 |
+
"""Ring members should have account creation dates within 7 days of each other."""
|
| 119 |
+
from datetime import date
|
| 120 |
+
ep = generate_episode(seed=42, task_id="task_3")
|
| 121 |
+
for ring in ep.fraud_rings:
|
| 122 |
+
dates = []
|
| 123 |
+
for ad_id in ring.member_ad_ids:
|
| 124 |
+
profile = ep.advertiser_profiles[ad_id]
|
| 125 |
+
d = date.fromisoformat(profile.account_created_date)
|
| 126 |
+
dates.append(d)
|
| 127 |
+
if len(dates) >= 2:
|
| 128 |
+
spread = (max(dates) - min(dates)).days
|
| 129 |
+
assert spread <= 7, (
|
| 130 |
+
f"Ring {ring.ring_id} creation dates spread: {spread} days"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
def test_investigation_has_whois_privacy_info(self):
|
| 134 |
+
ep = generate_episode(seed=42, task_id="task_2")
|
| 135 |
+
found_whois = False
|
| 136 |
+
for ad_id, inv in ep.investigation_data.items():
|
| 137 |
+
text = inv["landing_page"]
|
| 138 |
+
if "WHOIS privacy:" in text:
|
| 139 |
+
found_whois = True
|
| 140 |
+
break
|
| 141 |
+
assert found_whois, "At least one landing page should mention WHOIS privacy"
|
tests/test_environment.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the core AdFraudEnvironment."""
|
| 2 |
+
|
| 3 |
+
from ad_fraud_env.models import AdReviewAction, AdReviewObservation, AdFraudState
|
| 4 |
+
from ad_fraud_env.server.environment import AdFraudEnvironment
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class TestReset:
|
| 8 |
+
def test_reset_returns_observation(self):
|
| 9 |
+
env = AdFraudEnvironment()
|
| 10 |
+
obs = env.reset(seed=42, task_id="task_1")
|
| 11 |
+
assert isinstance(obs, AdReviewObservation)
|
| 12 |
+
assert obs.done is False
|
| 13 |
+
assert obs.reward == 0.0
|
| 14 |
+
assert len(obs.available_ads) == 5
|
| 15 |
+
|
| 16 |
+
def test_reset_clears_state(self):
|
| 17 |
+
env = AdFraudEnvironment()
|
| 18 |
+
env.reset(seed=42, task_id="task_1")
|
| 19 |
+
env.step(AdReviewAction(
|
| 20 |
+
action_type="verdict", ad_id="ad_001",
|
| 21 |
+
verdict="approve", confidence=0.9,
|
| 22 |
+
))
|
| 23 |
+
obs = env.reset(seed=42, task_id="task_1")
|
| 24 |
+
state = env.state
|
| 25 |
+
assert state.step_count == 0
|
| 26 |
+
assert state.reviewed_count == 0
|
| 27 |
+
assert len(obs.available_ads) == 5
|
| 28 |
+
|
| 29 |
+
def test_reset_different_tasks(self):
|
| 30 |
+
env = AdFraudEnvironment()
|
| 31 |
+
for task_id, expected in [("task_1", 5), ("task_2", 12), ("task_3", 20)]:
|
| 32 |
+
obs = env.reset(seed=42, task_id=task_id)
|
| 33 |
+
assert len(obs.available_ads) == expected
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class TestStep:
|
| 37 |
+
def test_investigate_returns_findings(self):
|
| 38 |
+
env = AdFraudEnvironment()
|
| 39 |
+
env.reset(seed=42, task_id="task_1")
|
| 40 |
+
obs = env.step(AdReviewAction(
|
| 41 |
+
action_type="investigate",
|
| 42 |
+
ad_id="ad_001",
|
| 43 |
+
investigation_target="advertiser_history",
|
| 44 |
+
))
|
| 45 |
+
assert obs.done is False
|
| 46 |
+
assert obs.reward == -0.02
|
| 47 |
+
assert "Advertiser" in obs.feedback or "Investigation complete" in obs.feedback
|
| 48 |
+
|
| 49 |
+
def test_verdict_correct_rejection(self):
|
| 50 |
+
env = AdFraudEnvironment()
|
| 51 |
+
env.reset(seed=42, task_id="task_1")
|
| 52 |
+
fraud_ads = [
|
| 53 |
+
a for a in env._episode.ads if a.ground_truth_label == "fraud"
|
| 54 |
+
]
|
| 55 |
+
assert len(fraud_ads) > 0
|
| 56 |
+
ad = fraud_ads[0]
|
| 57 |
+
obs = env.step(AdReviewAction(
|
| 58 |
+
action_type="verdict", ad_id=ad.ad_id,
|
| 59 |
+
verdict="reject", confidence=0.9,
|
| 60 |
+
))
|
| 61 |
+
assert obs.reward > 0
|
| 62 |
+
|
| 63 |
+
def test_verdict_false_negative_penalty(self):
|
| 64 |
+
env = AdFraudEnvironment()
|
| 65 |
+
env.reset(seed=42, task_id="task_1")
|
| 66 |
+
fraud_ads = [
|
| 67 |
+
a for a in env._episode.ads if a.ground_truth_label == "fraud"
|
| 68 |
+
]
|
| 69 |
+
ad = fraud_ads[0]
|
| 70 |
+
obs = env.step(AdReviewAction(
|
| 71 |
+
action_type="verdict", ad_id=ad.ad_id,
|
| 72 |
+
verdict="approve", confidence=0.9,
|
| 73 |
+
))
|
| 74 |
+
assert obs.reward < 0
|
| 75 |
+
|
| 76 |
+
def test_duplicate_verdict_rejected(self):
|
| 77 |
+
env = AdFraudEnvironment()
|
| 78 |
+
env.reset(seed=42, task_id="task_1")
|
| 79 |
+
env.step(AdReviewAction(
|
| 80 |
+
action_type="verdict", ad_id="ad_001",
|
| 81 |
+
verdict="approve", confidence=0.5,
|
| 82 |
+
))
|
| 83 |
+
obs = env.step(AdReviewAction(
|
| 84 |
+
action_type="verdict", ad_id="ad_001",
|
| 85 |
+
verdict="reject", confidence=0.9,
|
| 86 |
+
))
|
| 87 |
+
assert obs.reward == -0.02
|
| 88 |
+
|
| 89 |
+
def test_invalid_ad_id(self):
|
| 90 |
+
env = AdFraudEnvironment()
|
| 91 |
+
env.reset(seed=42, task_id="task_1")
|
| 92 |
+
obs = env.step(AdReviewAction(
|
| 93 |
+
action_type="investigate", ad_id="ad_999",
|
| 94 |
+
investigation_target="landing_page",
|
| 95 |
+
))
|
| 96 |
+
assert obs.reward == -0.05
|
| 97 |
+
assert "Invalid" in obs.feedback
|
| 98 |
+
|
| 99 |
+
def test_episode_ends_when_all_reviewed(self):
|
| 100 |
+
env = AdFraudEnvironment()
|
| 101 |
+
obs = env.reset(seed=42, task_id="task_1")
|
| 102 |
+
for ad_id in list(obs.available_ads):
|
| 103 |
+
obs = env.step(AdReviewAction(
|
| 104 |
+
action_type="verdict", ad_id=ad_id,
|
| 105 |
+
verdict="reject", confidence=0.5,
|
| 106 |
+
))
|
| 107 |
+
assert obs.done is True
|
| 108 |
+
|
| 109 |
+
def test_step_after_done_returns_done(self):
|
| 110 |
+
env = AdFraudEnvironment()
|
| 111 |
+
obs = env.reset(seed=42, task_id="task_1")
|
| 112 |
+
for ad_id in list(obs.available_ads):
|
| 113 |
+
obs = env.step(AdReviewAction(
|
| 114 |
+
action_type="verdict", ad_id=ad_id,
|
| 115 |
+
verdict="reject", confidence=0.5,
|
| 116 |
+
))
|
| 117 |
+
obs = env.step(AdReviewAction(
|
| 118 |
+
action_type="investigate", ad_id="ad_001",
|
| 119 |
+
investigation_target="landing_page",
|
| 120 |
+
))
|
| 121 |
+
assert obs.done is True
|
| 122 |
+
assert "already complete" in obs.feedback.lower()
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class TestState:
|
| 126 |
+
def test_state_tracks_progress(self):
|
| 127 |
+
env = AdFraudEnvironment()
|
| 128 |
+
env.reset(seed=42, task_id="task_1")
|
| 129 |
+
state = env.state
|
| 130 |
+
assert state.task_id == "task_1"
|
| 131 |
+
assert state.total_ads == 5
|
| 132 |
+
assert state.remaining_budget == 25
|
| 133 |
+
assert state.step_count == 0
|
| 134 |
+
|
| 135 |
+
env.step(AdReviewAction(
|
| 136 |
+
action_type="investigate", ad_id="ad_001",
|
| 137 |
+
investigation_target="landing_page",
|
| 138 |
+
))
|
| 139 |
+
state = env.state
|
| 140 |
+
assert state.step_count == 1
|
| 141 |
+
assert state.remaining_budget == 24
|
| 142 |
+
|
| 143 |
+
def test_grader_score_set_on_completion(self):
|
| 144 |
+
env = AdFraudEnvironment()
|
| 145 |
+
obs = env.reset(seed=42, task_id="task_1")
|
| 146 |
+
for ad_id in list(obs.available_ads):
|
| 147 |
+
env.step(AdReviewAction(
|
| 148 |
+
action_type="verdict", ad_id=ad_id,
|
| 149 |
+
verdict="reject", confidence=0.5,
|
| 150 |
+
))
|
| 151 |
+
state = env.state
|
| 152 |
+
assert state.grader_score is not None
|
| 153 |
+
assert 0.0 <= state.grader_score <= 1.0
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class TestAntiExploit:
|
| 157 |
+
def test_always_reject_scores_poorly(self):
|
| 158 |
+
"""Always-reject on task_2 (5 legit / 5 fraud / 2 escalate) should be punished."""
|
| 159 |
+
env = AdFraudEnvironment()
|
| 160 |
+
obs = env.reset(seed=42, task_id="task_2")
|
| 161 |
+
for ad_id in list(obs.available_ads):
|
| 162 |
+
env.step(AdReviewAction(
|
| 163 |
+
action_type="verdict", ad_id=ad_id,
|
| 164 |
+
verdict="reject", confidence=0.9,
|
| 165 |
+
))
|
| 166 |
+
score = env.state.grader_score
|
| 167 |
+
assert score < 0.7, f"Always-reject should score poorly, got {score}"
|
| 168 |
+
|
| 169 |
+
def test_always_escalate_scores_poorly(self):
|
| 170 |
+
env = AdFraudEnvironment()
|
| 171 |
+
obs = env.reset(seed=42, task_id="task_1")
|
| 172 |
+
for ad_id in list(obs.available_ads):
|
| 173 |
+
env.step(AdReviewAction(
|
| 174 |
+
action_type="verdict", ad_id=ad_id,
|
| 175 |
+
verdict="escalate", confidence=0.5,
|
| 176 |
+
))
|
| 177 |
+
score = env.state.grader_score
|
| 178 |
+
assert score < 0.7, f"Always-escalate should score poorly, got {score}"
|
tests/test_graders.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the grading system."""
|
| 2 |
+
|
| 3 |
+
from ad_fraud_env.graders.base_grader import (
|
| 4 |
+
BaseGrader,
|
| 5 |
+
EpisodeRecord,
|
| 6 |
+
LinkResult,
|
| 7 |
+
VerdictResult,
|
| 8 |
+
grade_episode,
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _make_record(
|
| 13 |
+
task_id: str = "task_1",
|
| 14 |
+
verdicts: list | None = None,
|
| 15 |
+
links: list | None = None,
|
| 16 |
+
total_steps: int = 5,
|
| 17 |
+
action_budget: int = 25,
|
| 18 |
+
ads_metadata: list | None = None,
|
| 19 |
+
n_fraud_rings: int = 0,
|
| 20 |
+
ring_sizes: list | None = None,
|
| 21 |
+
) -> EpisodeRecord:
|
| 22 |
+
if verdicts is None:
|
| 23 |
+
verdicts = []
|
| 24 |
+
if links is None:
|
| 25 |
+
links = []
|
| 26 |
+
if ads_metadata is None:
|
| 27 |
+
ads_metadata = [
|
| 28 |
+
{"ad_id": v.ad_id, "severity": 0.8, "ground_truth": v.ground_truth}
|
| 29 |
+
for v in verdicts
|
| 30 |
+
]
|
| 31 |
+
return EpisodeRecord(
|
| 32 |
+
task_id=task_id,
|
| 33 |
+
total_steps=total_steps,
|
| 34 |
+
action_budget=action_budget,
|
| 35 |
+
verdicts=verdicts,
|
| 36 |
+
links=links,
|
| 37 |
+
ads_metadata=ads_metadata,
|
| 38 |
+
n_fraud_rings=n_fraud_rings,
|
| 39 |
+
ring_sizes=ring_sizes,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class TestGraderScoreRange:
|
| 44 |
+
def test_scores_in_valid_range(self):
|
| 45 |
+
verdicts = [
|
| 46 |
+
VerdictResult("ad_001", "reject", 0.9, "fraud"),
|
| 47 |
+
VerdictResult("ad_002", "approve", 0.9, "legit"),
|
| 48 |
+
VerdictResult("ad_003", "reject", 0.8, "fraud"),
|
| 49 |
+
]
|
| 50 |
+
record = _make_record(verdicts=verdicts, total_steps=3)
|
| 51 |
+
score = grade_episode(record)
|
| 52 |
+
assert 0.0 <= score <= 1.0
|
| 53 |
+
|
| 54 |
+
def test_perfect_score_is_high(self):
|
| 55 |
+
verdicts = [
|
| 56 |
+
VerdictResult("ad_001", "reject", 0.95, "fraud"),
|
| 57 |
+
VerdictResult("ad_002", "approve", 0.95, "legit"),
|
| 58 |
+
VerdictResult("ad_003", "reject", 0.95, "fraud"),
|
| 59 |
+
VerdictResult("ad_004", "approve", 0.95, "legit"),
|
| 60 |
+
VerdictResult("ad_005", "reject", 0.95, "fraud"),
|
| 61 |
+
]
|
| 62 |
+
record = _make_record(verdicts=verdicts, total_steps=5)
|
| 63 |
+
score = grade_episode(record)
|
| 64 |
+
assert score > 0.7, f"Perfect verdicts should score high, got {score}"
|
| 65 |
+
|
| 66 |
+
def test_all_wrong_scores_low(self):
|
| 67 |
+
verdicts = [
|
| 68 |
+
VerdictResult("ad_001", "approve", 0.9, "fraud"),
|
| 69 |
+
VerdictResult("ad_002", "reject", 0.9, "legit"),
|
| 70 |
+
VerdictResult("ad_003", "approve", 0.9, "fraud"),
|
| 71 |
+
]
|
| 72 |
+
record = _make_record(verdicts=verdicts, total_steps=3)
|
| 73 |
+
score = grade_episode(record)
|
| 74 |
+
assert score < 0.3, f"All wrong verdicts should score low, got {score}"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class TestTask2Grader:
|
| 78 |
+
def test_calibration_bonus(self):
|
| 79 |
+
well_calibrated = [
|
| 80 |
+
VerdictResult("ad_001", "reject", 0.9, "fraud"),
|
| 81 |
+
VerdictResult("ad_002", "approve", 0.9, "legit"),
|
| 82 |
+
VerdictResult("ad_003", "reject", 0.8, "fraud"),
|
| 83 |
+
VerdictResult("ad_004", "approve", 0.85, "legit"),
|
| 84 |
+
]
|
| 85 |
+
poorly_calibrated = [
|
| 86 |
+
VerdictResult("ad_001", "reject", 0.2, "fraud"),
|
| 87 |
+
VerdictResult("ad_002", "approve", 0.2, "legit"),
|
| 88 |
+
VerdictResult("ad_003", "reject", 0.2, "fraud"),
|
| 89 |
+
VerdictResult("ad_004", "approve", 0.2, "legit"),
|
| 90 |
+
]
|
| 91 |
+
r1 = _make_record(task_id="task_2", verdicts=well_calibrated, total_steps=4, action_budget=30)
|
| 92 |
+
r2 = _make_record(task_id="task_2", verdicts=poorly_calibrated, total_steps=4, action_budget=30)
|
| 93 |
+
s1 = grade_episode(r1)
|
| 94 |
+
s2 = grade_episode(r2)
|
| 95 |
+
assert s1 >= s2, f"Well calibrated ({s1}) should score >= poorly calibrated ({s2})"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class TestTask3Grader:
|
| 99 |
+
def test_network_link_bonus(self):
|
| 100 |
+
verdicts = [
|
| 101 |
+
VerdictResult("ad_001", "reject", 0.9, "fraud"),
|
| 102 |
+
VerdictResult("ad_002", "reject", 0.9, "fraud"),
|
| 103 |
+
VerdictResult("ad_003", "reject", 0.9, "fraud"),
|
| 104 |
+
]
|
| 105 |
+
links_correct = [
|
| 106 |
+
LinkResult("ad_001", "ad_002", True),
|
| 107 |
+
LinkResult("ad_002", "ad_003", True),
|
| 108 |
+
]
|
| 109 |
+
links_wrong = [
|
| 110 |
+
LinkResult("ad_001", "ad_002", False),
|
| 111 |
+
]
|
| 112 |
+
|
| 113 |
+
r1 = _make_record(
|
| 114 |
+
task_id="task_3", verdicts=verdicts, links=links_correct,
|
| 115 |
+
total_steps=5, action_budget=35, n_fraud_rings=1,
|
| 116 |
+
ring_sizes=[3],
|
| 117 |
+
)
|
| 118 |
+
r2 = _make_record(
|
| 119 |
+
task_id="task_3", verdicts=verdicts, links=links_wrong,
|
| 120 |
+
total_steps=4, action_budget=35, n_fraud_rings=1,
|
| 121 |
+
ring_sizes=[3],
|
| 122 |
+
)
|
| 123 |
+
s1 = grade_episode(r1)
|
| 124 |
+
s2 = grade_episode(r2)
|
| 125 |
+
assert s1 > s2, f"Correct links ({s1}) should score > wrong links ({s2})"
|
| 126 |
+
|
| 127 |
+
def test_graph_based_scoring(self):
|
| 128 |
+
"""Task 3 grader should use edge coverage from ground truth graph."""
|
| 129 |
+
verdicts = [
|
| 130 |
+
VerdictResult("ad_001", "reject", 0.9, "fraud"),
|
| 131 |
+
VerdictResult("ad_002", "reject", 0.9, "fraud"),
|
| 132 |
+
VerdictResult("ad_003", "reject", 0.9, "fraud"),
|
| 133 |
+
VerdictResult("ad_004", "reject", 0.9, "fraud"),
|
| 134 |
+
]
|
| 135 |
+
# 4 ads in a ring of 4 -> 6 ground truth edges
|
| 136 |
+
# Discover 3 of them
|
| 137 |
+
links = [
|
| 138 |
+
LinkResult("ad_001", "ad_002", True),
|
| 139 |
+
LinkResult("ad_002", "ad_003", True),
|
| 140 |
+
LinkResult("ad_003", "ad_004", True),
|
| 141 |
+
]
|
| 142 |
+
r = _make_record(
|
| 143 |
+
task_id="task_3", verdicts=verdicts, links=links,
|
| 144 |
+
total_steps=7, action_budget=35, n_fraud_rings=1,
|
| 145 |
+
ring_sizes=[4],
|
| 146 |
+
)
|
| 147 |
+
score = grade_episode(r)
|
| 148 |
+
assert 0.0 <= score <= 1.0
|
| 149 |
+
|
| 150 |
+
def test_false_links_penalized(self):
|
| 151 |
+
"""False link_accounts should reduce score."""
|
| 152 |
+
verdicts = [
|
| 153 |
+
VerdictResult("ad_001", "reject", 0.9, "fraud"),
|
| 154 |
+
VerdictResult("ad_002", "approve", 0.9, "legit"),
|
| 155 |
+
]
|
| 156 |
+
no_links = _make_record(
|
| 157 |
+
task_id="task_3", verdicts=verdicts, links=[],
|
| 158 |
+
total_steps=2, action_budget=35, n_fraud_rings=1,
|
| 159 |
+
ring_sizes=[3],
|
| 160 |
+
)
|
| 161 |
+
false_links = _make_record(
|
| 162 |
+
task_id="task_3", verdicts=verdicts,
|
| 163 |
+
links=[LinkResult("ad_001", "ad_002", False)],
|
| 164 |
+
total_steps=3, action_budget=35, n_fraud_rings=1,
|
| 165 |
+
ring_sizes=[3],
|
| 166 |
+
)
|
| 167 |
+
s_none = grade_episode(no_links)
|
| 168 |
+
s_false = grade_episode(false_links)
|
| 169 |
+
assert s_none >= s_false, (
|
| 170 |
+
f"No links ({s_none}) should score >= false links ({s_false})"
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
def test_coverage_bonus(self):
|
| 174 |
+
"""Agents that review more ads should get a coverage bonus."""
|
| 175 |
+
few_verdicts = [
|
| 176 |
+
VerdictResult("ad_001", "reject", 0.9, "fraud"),
|
| 177 |
+
]
|
| 178 |
+
many_verdicts = [
|
| 179 |
+
VerdictResult("ad_001", "reject", 0.9, "fraud"),
|
| 180 |
+
VerdictResult("ad_002", "approve", 0.9, "legit"),
|
| 181 |
+
VerdictResult("ad_003", "reject", 0.9, "fraud"),
|
| 182 |
+
VerdictResult("ad_004", "approve", 0.9, "legit"),
|
| 183 |
+
]
|
| 184 |
+
ads_meta = [
|
| 185 |
+
{"ad_id": "ad_001", "severity": 0.8, "ground_truth": "fraud"},
|
| 186 |
+
{"ad_id": "ad_002", "severity": 0.5, "ground_truth": "legit"},
|
| 187 |
+
{"ad_id": "ad_003", "severity": 0.8, "ground_truth": "fraud"},
|
| 188 |
+
{"ad_id": "ad_004", "severity": 0.5, "ground_truth": "legit"},
|
| 189 |
+
{"ad_id": "ad_005", "severity": 0.5, "ground_truth": "legit"},
|
| 190 |
+
]
|
| 191 |
+
r_few = _make_record(
|
| 192 |
+
task_id="task_3", verdicts=few_verdicts, total_steps=1,
|
| 193 |
+
action_budget=35, ads_metadata=ads_meta, ring_sizes=[3],
|
| 194 |
+
)
|
| 195 |
+
r_many = _make_record(
|
| 196 |
+
task_id="task_3", verdicts=many_verdicts, total_steps=4,
|
| 197 |
+
action_budget=35, ads_metadata=ads_meta, ring_sizes=[3],
|
| 198 |
+
)
|
| 199 |
+
s_few = grade_episode(r_few)
|
| 200 |
+
s_many = grade_episode(r_many)
|
| 201 |
+
assert s_many > s_few, (
|
| 202 |
+
f"More coverage ({s_many}) should score > less coverage ({s_few})"
|
| 203 |
+
)
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|