Abid Ali Awan Codex commited on
Commit ·
11407b6
1
Parent(s): 1663316
Improve image trace classification
Browse filesCo-authored-by: Codex <codex@openai.com>
- tests/test_tracing.py +55 -0
- traces/dataset_card.md +8 -5
- traces/runtime.py +104 -26
tests/test_tracing.py
CHANGED
|
@@ -127,6 +127,61 @@ class TraceTests(unittest.TestCase):
|
|
| 127 |
))
|
| 128 |
self.assertEqual(next(iter(text_record)), "trace_id")
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
def test_opt_out_does_not_queue_trace(self) -> None:
|
| 131 |
with patch("app.queue_trace") as queue_mock:
|
| 132 |
result = app.analyze_notice("", "", save_trace=False)
|
|
|
|
| 127 |
))
|
| 128 |
self.assertEqual(next(iter(text_record)), "trace_id")
|
| 129 |
|
| 130 |
+
def test_image_trace_uses_model_assessment_for_fixed_description(self) -> None:
|
| 131 |
+
assessment = {
|
| 132 |
+
"risk_label": "Suspicious",
|
| 133 |
+
"simple_explanation": (
|
| 134 |
+
"The screenshot claims a Pakistan Post parcel delivery failed "
|
| 135 |
+
"and demands an immediate address update through a short link."
|
| 136 |
+
),
|
| 137 |
+
"red_flags": [
|
| 138 |
+
"Unknown sender with no official courier branding.",
|
| 139 |
+
"Urgent delivery action through a suspicious URL.",
|
| 140 |
+
],
|
| 141 |
+
"safe_next_steps": ["Contact the courier through its official app."],
|
| 142 |
+
"reply_draft": "",
|
| 143 |
+
}
|
| 144 |
+
record = trace_runtime.build_trace_record(
|
| 145 |
+
text="",
|
| 146 |
+
image_data_url="data:image/png;base64,PRIVATE_IMAGE_BYTES",
|
| 147 |
+
example_id="",
|
| 148 |
+
assessment=assessment,
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
self.assertEqual(record["input_category"], "courier")
|
| 152 |
+
self.assertTrue(record["urgency"])
|
| 153 |
+
self.assertEqual(
|
| 154 |
+
record["input"],
|
| 155 |
+
"image: Courier-style content with link, urgency, courier signals",
|
| 156 |
+
)
|
| 157 |
+
self.assertIn("Known courier pattern", record["result_summary"])
|
| 158 |
+
serialized = json.dumps(record)
|
| 159 |
+
self.assertNotIn(assessment["simple_explanation"], serialized)
|
| 160 |
+
self.assertNotIn(assessment["red_flags"][0], serialized)
|
| 161 |
+
self.assertNotIn("PRIVATE_IMAGE_BYTES", serialized)
|
| 162 |
+
|
| 163 |
+
def test_urdu_image_assessment_maps_to_traffic_challan(self) -> None:
|
| 164 |
+
record = trace_runtime.build_trace_record(
|
| 165 |
+
text="",
|
| 166 |
+
image_data_url="data:image/jpeg;base64,PRIVATE_IMAGE_BYTES",
|
| 167 |
+
example_id="",
|
| 168 |
+
assessment={
|
| 169 |
+
"risk_label": "Likely scam",
|
| 170 |
+
"simple_explanation": (
|
| 171 |
+
"یہ ٹریفک چالان کی فوری ادائیگی کے لیے مشکوک لنک استعمال کرتا ہے۔"
|
| 172 |
+
),
|
| 173 |
+
"red_flags": ["آج جرمانہ ادا کرنے کا دباؤ"],
|
| 174 |
+
"safe_next_steps": ["سرکاری ذریعے سے تصدیق کریں۔"],
|
| 175 |
+
"reply_draft": "",
|
| 176 |
+
},
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
self.assertEqual(record["input_category"], "traffic_challan")
|
| 180 |
+
self.assertTrue(record["urgency"])
|
| 181 |
+
self.assertIn("Traffic-challan-style", record["input"])
|
| 182 |
+
self.assertIn("link", record["scam_tactics"])
|
| 183 |
+
self.assertIn("payment", record["scam_tactics"])
|
| 184 |
+
|
| 185 |
def test_opt_out_does_not_queue_trace(self) -> None:
|
| 186 |
with patch("app.queue_trace") as queue_mock:
|
| 187 |
result = app.analyze_notice("", "", save_trace=False)
|
traces/dataset_card.md
CHANGED
|
@@ -28,8 +28,11 @@ Helper scam-check requests. It does not contain hidden model reasoning or
|
|
| 28 |
autonomous-agent trajectories.
|
| 29 |
|
| 30 |
The application uses a Modal-hosted Qwen model for normal assessments. Creating
|
| 31 |
-
a trace never
|
| 32 |
-
and convert it into allow-listed categories, booleans,
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
## Fields
|
| 35 |
|
|
@@ -70,9 +73,9 @@ credentials, addresses, tracking IDs, long numbers, and title-case names or
|
|
| 70 |
entities. Regex redaction cannot guarantee removal of every possible
|
| 71 |
identifier, so users should opt out when submitting sensitive content.
|
| 72 |
|
| 73 |
-
Images store only fixed descriptions
|
| 74 |
-
Users see a checked trace disclosure in the
|
| 75 |
-
request.
|
| 76 |
|
| 77 |
## Provenance
|
| 78 |
|
|
|
|
| 28 |
autonomous-agent trajectories.
|
| 29 |
|
| 30 |
The application uses a Modal-hosted Qwen model for normal assessments. Creating
|
| 31 |
+
a trace never makes an additional AI model call. Traces only observe the
|
| 32 |
+
existing request path and convert it into allow-listed categories, booleans,
|
| 33 |
+
and fixed descriptions. For image submissions, the existing assessment's
|
| 34 |
+
explanation and red flags may be inspected transiently for this mapping, but
|
| 35 |
+
their text is not stored.
|
| 36 |
|
| 37 |
## Fields
|
| 38 |
|
|
|
|
| 73 |
entities. Regex redaction cannot guarantee removal of every possible
|
| 74 |
identifier, so users should opt out when submitting sensitive content.
|
| 75 |
|
| 76 |
+
Images store only fixed descriptions. Screenshots, OCR text, model explanations,
|
| 77 |
+
and model red flags are not stored. Users see a checked trace disclosure in the
|
| 78 |
+
app and may opt out before each request.
|
| 79 |
|
| 80 |
## Provenance
|
| 81 |
|
traces/runtime.py
CHANGED
|
@@ -33,15 +33,36 @@ RISK_LABELS = {
|
|
| 33 |
"none",
|
| 34 |
}
|
| 35 |
SIGNAL_PATTERNS = {
|
| 36 |
-
"otp": r"\b(?:otp|one[- ]time (?:pin|password)|verification code)\b",
|
| 37 |
-
"cnic": r"\bcnic\b",
|
| 38 |
-
"credentials":
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
"
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
"account_threat": (
|
| 46 |
r"\b(?:account|sim|service|electricity)\b.{0,50}"
|
| 47 |
r"\b(?:block|blocked|suspend|closed|disconnect)\b"
|
|
@@ -111,24 +132,50 @@ def detect_category(text: str, signals: dict[str, bool], example_id: str = "") -
|
|
| 111 |
profile = EXAMPLE_PROFILES.get(example_id)
|
| 112 |
if profile:
|
| 113 |
return profile[1]
|
|
|
|
|
|
|
| 114 |
lowered = (text or "").lower()
|
| 115 |
categories = (
|
| 116 |
-
("fbr", ("fbr", "taxpayer", "tax refund")),
|
| 117 |
-
("bank", ("bank", "hbl", "ubl", "meezan", "alfalah")),
|
| 118 |
-
("wallet", ("easypaisa", "jazzcash", "wallet")),
|
| 119 |
-
(
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
(
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
)
|
| 127 |
for category, terms in categories:
|
| 128 |
if any(term in lowered for term in terms):
|
| 129 |
return category
|
| 130 |
-
if signals["challan"]:
|
| 131 |
-
return "traffic_challan"
|
| 132 |
if signals["courier"]:
|
| 133 |
return "courier"
|
| 134 |
return "unknown"
|
|
@@ -210,7 +257,23 @@ def result_summary(
|
|
| 210 |
return f"{risk_label}: {pattern}. {novelty} {RESULT_GUIDANCE[risk_label]}"
|
| 211 |
|
| 212 |
|
| 213 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
profile = EXAMPLE_PROFILES.get(example_id)
|
| 215 |
if profile:
|
| 216 |
input_type = profile[0]
|
|
@@ -218,8 +281,13 @@ def build_input_profile(text: str, image_data_url: str, example_id: str = "") ->
|
|
| 218 |
input_type = "image"
|
| 219 |
else:
|
| 220 |
input_type = "text"
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
tactics = [name for name, enabled in signals.items() if enabled]
|
| 224 |
return {
|
| 225 |
"input": (
|
|
@@ -244,8 +312,18 @@ def build_trace_record(
|
|
| 244 |
risk_label = str((assessment or {}).get("risk_label", "none"))
|
| 245 |
if risk_label not in RISK_LABELS:
|
| 246 |
risk_label = "none"
|
| 247 |
-
input_profile = build_input_profile(
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
category = input_profile["input_category"]
|
| 250 |
assessment = assessment or {}
|
| 251 |
return {
|
|
|
|
| 33 |
"none",
|
| 34 |
}
|
| 35 |
SIGNAL_PATTERNS = {
|
| 36 |
+
"otp": r"\b(?:otp|one[- ]time (?:pin|password)|verification code)\b|او ٹی پی",
|
| 37 |
+
"cnic": r"\bcnic\b|شناختی کارڈ",
|
| 38 |
+
"credentials": (
|
| 39 |
+
r"\b(?:pin|password|cvv|card details?|bank details?)\b|"
|
| 40 |
+
r"پاس ورڈ|کارڈ کی تفصیلات"
|
| 41 |
+
),
|
| 42 |
+
"link": (
|
| 43 |
+
r"(?:https?://|www\.|bit\.ly|tinyurl\.|cutt\.ly|\.xyz\b|\.top\b)|"
|
| 44 |
+
r"\b(?:link|url|domain)\b|لنک"
|
| 45 |
+
),
|
| 46 |
+
"urgency": (
|
| 47 |
+
r"\b(?:urgent|immediately|today|now|within \d+|last warning)\b|"
|
| 48 |
+
r"فوری|فوراً|آج|آخری وارننگ"
|
| 49 |
+
),
|
| 50 |
+
"payment": (
|
| 51 |
+
r"\b(?:pay|payment|fee|fine|transfer|send money|rs\.?|pkr)\b|"
|
| 52 |
+
r"ادائیگی|جرمانہ|رقم"
|
| 53 |
+
),
|
| 54 |
+
"refund_or_prize": (
|
| 55 |
+
r"\b(?:refund|prize|winner|lottery|cashback|reward)\b|"
|
| 56 |
+
r"انعام|ریفنڈ|قرعہ اندازی"
|
| 57 |
+
),
|
| 58 |
+
"courier": (
|
| 59 |
+
r"\b(?:parcel|courier|delivery|pakistan post|leopards|tcs|customs)\b|"
|
| 60 |
+
r"پارسل|کوریئر|ڈیلیوری|پاکستان پوسٹ"
|
| 61 |
+
),
|
| 62 |
+
"challan": (
|
| 63 |
+
r"\b(?:challan|traffic fine|traffic violation|e-challan)\b|"
|
| 64 |
+
r"چالان|ٹریفک جرمانہ"
|
| 65 |
+
),
|
| 66 |
"account_threat": (
|
| 67 |
r"\b(?:account|sim|service|electricity)\b.{0,50}"
|
| 68 |
r"\b(?:block|blocked|suspend|closed|disconnect)\b"
|
|
|
|
| 132 |
profile = EXAMPLE_PROFILES.get(example_id)
|
| 133 |
if profile:
|
| 134 |
return profile[1]
|
| 135 |
+
if signals["challan"]:
|
| 136 |
+
return "traffic_challan"
|
| 137 |
lowered = (text or "").lower()
|
| 138 |
categories = (
|
| 139 |
+
("fbr", ("fbr", "taxpayer", "tax refund", "ایف بی آر", "ٹیکس")),
|
| 140 |
+
("bank", ("bank", "hbl", "ubl", "meezan", "alfalah", "بینک")),
|
| 141 |
+
("wallet", ("easypaisa", "jazzcash", "wallet", "ایزی پیسہ", "جاز کیش")),
|
| 142 |
+
(
|
| 143 |
+
"utility",
|
| 144 |
+
("electricity", "gas bill", "utility", "lesco", "k-electric", "بجلی", "گیس بل"),
|
| 145 |
+
),
|
| 146 |
+
(
|
| 147 |
+
"traffic_challan",
|
| 148 |
+
("challan", "traffic fine", "traffic violation", "چالان", "ٹریفک جرمانہ"),
|
| 149 |
+
),
|
| 150 |
+
(
|
| 151 |
+
"courier",
|
| 152 |
+
(
|
| 153 |
+
"parcel",
|
| 154 |
+
"courier",
|
| 155 |
+
"delivery",
|
| 156 |
+
"pakistan post",
|
| 157 |
+
"leopards",
|
| 158 |
+
"tcs",
|
| 159 |
+
"پارسل",
|
| 160 |
+
"کوریئر",
|
| 161 |
+
"ڈیلیوری",
|
| 162 |
+
"پاکستان پوسٹ",
|
| 163 |
+
),
|
| 164 |
+
),
|
| 165 |
+
("customs", ("customs", "duty", "کسٹمز")),
|
| 166 |
+
(
|
| 167 |
+
"university",
|
| 168 |
+
("university", "admission", "scholarship", "hec", "یونیورسٹی", "داخلہ"),
|
| 169 |
+
),
|
| 170 |
+
("job", ("job", "salary", "recruiter", "employment", "نوکری", "تنخواہ")),
|
| 171 |
+
(
|
| 172 |
+
"marketplace",
|
| 173 |
+
("buyer", "seller", "marketplace", "whatsapp", "خریدار", "فروخت", "واٹس ایپ"),
|
| 174 |
+
),
|
| 175 |
)
|
| 176 |
for category, terms in categories:
|
| 177 |
if any(term in lowered for term in terms):
|
| 178 |
return category
|
|
|
|
|
|
|
| 179 |
if signals["courier"]:
|
| 180 |
return "courier"
|
| 181 |
return "unknown"
|
|
|
|
| 257 |
return f"{risk_label}: {pattern}. {novelty} {RESULT_GUIDANCE[risk_label]}"
|
| 258 |
|
| 259 |
|
| 260 |
+
def assessment_evidence(assessment: dict[str, Any] | None) -> str:
|
| 261 |
+
"""Return transient model evidence used only for allow-list classification."""
|
| 262 |
+
if not isinstance(assessment, dict):
|
| 263 |
+
return ""
|
| 264 |
+
values: list[str] = [str(assessment.get("simple_explanation", ""))]
|
| 265 |
+
red_flags = assessment.get("red_flags", [])
|
| 266 |
+
if isinstance(red_flags, list):
|
| 267 |
+
values.extend(str(item) for item in red_flags)
|
| 268 |
+
return " ".join(values)[:4000]
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def build_input_profile(
|
| 272 |
+
text: str,
|
| 273 |
+
image_data_url: str,
|
| 274 |
+
example_id: str = "",
|
| 275 |
+
assessment: dict[str, Any] | None = None,
|
| 276 |
+
) -> dict[str, Any]:
|
| 277 |
profile = EXAMPLE_PROFILES.get(example_id)
|
| 278 |
if profile:
|
| 279 |
input_type = profile[0]
|
|
|
|
| 281 |
input_type = "image"
|
| 282 |
else:
|
| 283 |
input_type = "text"
|
| 284 |
+
classification_text = text
|
| 285 |
+
if input_type == "image" and not example_id:
|
| 286 |
+
classification_text = " ".join(
|
| 287 |
+
part for part in (text, assessment_evidence(assessment)) if part
|
| 288 |
+
)
|
| 289 |
+
signals = detect_signals(classification_text, example_id)
|
| 290 |
+
category = detect_category(classification_text, signals, example_id)
|
| 291 |
tactics = [name for name, enabled in signals.items() if enabled]
|
| 292 |
return {
|
| 293 |
"input": (
|
|
|
|
| 312 |
risk_label = str((assessment or {}).get("risk_label", "none"))
|
| 313 |
if risk_label not in RISK_LABELS:
|
| 314 |
risk_label = "none"
|
| 315 |
+
input_profile = build_input_profile(
|
| 316 |
+
text,
|
| 317 |
+
image_data_url,
|
| 318 |
+
example_id,
|
| 319 |
+
assessment,
|
| 320 |
+
)
|
| 321 |
+
classification_text = text
|
| 322 |
+
if image_data_url and not example_id:
|
| 323 |
+
classification_text = " ".join(
|
| 324 |
+
part for part in (text, assessment_evidence(assessment)) if part
|
| 325 |
+
)
|
| 326 |
+
signals = detect_signals(classification_text, example_id)
|
| 327 |
category = input_profile["input_category"]
|
| 328 |
assessment = assessment or {}
|
| 329 |
return {
|