Abid Ali Awan Codex commited on
Commit
11407b6
·
1 Parent(s): 1663316

Improve image trace classification

Browse files

Co-authored-by: Codex <codex@openai.com>

Files changed (3) hide show
  1. tests/test_tracing.py +55 -0
  2. traces/dataset_card.md +8 -5
  3. 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 calls an AI model. Traces only observe the existing request path
32
- and convert it into allow-listed categories, booleans, buckets, and counts.
 
 
 
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; screenshots and OCR text are not stored.
74
- Users see a checked trace disclosure in the app and may opt out before each
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": r"\b(?:pin|password|cvv|card details?|bank details?)\b",
39
- "link": r"(?:https?://|www\.|bit\.ly|tinyurl\.|cutt\.ly|\.xyz\b|\.top\b)",
40
- "urgency": r"\b(?:urgent|immediately|today|now|within \d+|last warning)\b",
41
- "payment": r"\b(?:pay|payment|fee|fine|transfer|send money|rs\.?|pkr)\b",
42
- "refund_or_prize": r"\b(?:refund|prize|winner|lottery|cashback|reward)\b",
43
- "courier": r"\b(?:parcel|courier|delivery|pakistan post|leopards|tcs|customs)\b",
44
- "challan": r"\b(?:challan|traffic fine|traffic violation|e-challan)\b",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ("utility", ("electricity", "gas bill", "utility", "lesco", "k-electric")),
120
- ("traffic_challan", ("challan", "traffic fine", "traffic violation")),
121
- ("courier", ("parcel", "courier", "delivery", "pakistan post", "leopards", "tcs")),
122
- ("customs", ("customs", "duty")),
123
- ("university", ("university", "admission", "scholarship", "hec")),
124
- ("job", ("job", "salary", "recruiter", "employment")),
125
- ("marketplace", ("buyer", "seller", "marketplace", "whatsapp")),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 build_input_profile(text: str, image_data_url: str, example_id: str = "") -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- signals = detect_signals(text, example_id)
222
- category = detect_category(text, signals, example_id)
 
 
 
 
 
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(text, image_data_url, example_id)
248
- signals = detect_signals(text, example_id)
 
 
 
 
 
 
 
 
 
 
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 {