kingabzpro Codex commited on
Commit
818c308
·
1 Parent(s): e4f211a

Simplify image trace classification

Browse files

Derive privacy-safe image categories and tactics from the existing assessment explanation and red flags instead of requesting separate model metadata.

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

Files changed (4) hide show
  1. app.py +1 -48
  2. tests/test_tracing.py +1 -1
  3. traces/dataset_card.md +4 -5
  4. traces/runtime.py +0 -35
app.py CHANGED
@@ -37,33 +37,6 @@ REQUIRED_FIELDS = {
37
  "safe_next_steps",
38
  "reply_draft",
39
  }
40
- TRACE_CATEGORIES = (
41
- "fbr",
42
- "bank",
43
- "wallet",
44
- "utility",
45
- "traffic_challan",
46
- "courier",
47
- "customs",
48
- "university",
49
- "job",
50
- "marketplace",
51
- "unknown",
52
- )
53
- TRACE_TACTICS = (
54
- "otp",
55
- "cnic",
56
- "credentials",
57
- "link",
58
- "urgency",
59
- "payment",
60
- "refund_or_prize",
61
- "courier",
62
- "challan",
63
- "account_threat",
64
- "off_platform_contact",
65
- "impersonation",
66
- )
67
  EXAMPLE_CACHE_PATH = ROOT / "data" / "example_assessments.json"
68
 
69
  SYSTEM_PROMPT = """Assess Pakistani notices and messages for scam risk.
@@ -94,11 +67,6 @@ Evidence rules:
94
  relevant notice; do not call it irrelevant merely because it looks harmless.
95
 
96
  Output rules:
97
- - trace_category: choose exactly one privacy-safe category from the schema based
98
- on the visible content. Use unknown only when no listed category applies.
99
- - trace_tactics: choose every visible tactic from the schema. Use an empty
100
- array when none applies. These values are metadata and must not contain raw
101
- text, names, numbers, URLs, or explanations.
102
  - explanation: 1-3 short sentences naming the decisive visible evidence.
103
  - red_flags: 1-4 concise evidence-based items. For a normal relevant notice,
104
  use one item such as "No clear scam indicators in the supplied message."
@@ -141,14 +109,8 @@ OUTPUT_SCHEMA: dict[str, Any] = {
141
  "red_flags": {"type": "array", "items": {"type": "string"}},
142
  "safe_next_steps": {"type": "array", "items": {"type": "string"}},
143
  "reply_draft": {"type": "string"},
144
- "trace_category": {"type": "string", "enum": list(TRACE_CATEGORIES)},
145
- "trace_tactics": {
146
- "type": "array",
147
- "items": {"type": "string", "enum": list(TRACE_TACTICS)},
148
- "uniqueItems": True,
149
- },
150
  },
151
- "required": sorted(REQUIRED_FIELDS | {"trace_category", "trace_tactics"}),
152
  "additionalProperties": False,
153
  }
154
 
@@ -211,15 +173,6 @@ def normalize_assessment(value: Any) -> dict[str, Any]:
211
  else ""
212
  ),
213
  }
214
- trace_category = value.get("trace_category")
215
- trace_tactics = value.get("trace_tactics")
216
- if trace_category in TRACE_CATEGORIES:
217
- result["trace_category"] = trace_category
218
- if isinstance(trace_tactics, list):
219
- normalized_tactics = list(dict.fromkeys(
220
- str(item) for item in trace_tactics if str(item) in TRACE_TACTICS
221
- ))
222
- result["trace_tactics"] = normalized_tactics
223
  for field in ("simple_explanation",):
224
  if not result[field]:
225
  raise ValueError(f"{field} must not be empty.")
 
37
  "safe_next_steps",
38
  "reply_draft",
39
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  EXAMPLE_CACHE_PATH = ROOT / "data" / "example_assessments.json"
41
 
42
  SYSTEM_PROMPT = """Assess Pakistani notices and messages for scam risk.
 
67
  relevant notice; do not call it irrelevant merely because it looks harmless.
68
 
69
  Output rules:
 
 
 
 
 
70
  - explanation: 1-3 short sentences naming the decisive visible evidence.
71
  - red_flags: 1-4 concise evidence-based items. For a normal relevant notice,
72
  use one item such as "No clear scam indicators in the supplied message."
 
109
  "red_flags": {"type": "array", "items": {"type": "string"}},
110
  "safe_next_steps": {"type": "array", "items": {"type": "string"}},
111
  "reply_draft": {"type": "string"},
 
 
 
 
 
 
112
  },
113
+ "required": sorted(REQUIRED_FIELDS),
114
  "additionalProperties": False,
115
  }
116
 
 
173
  else ""
174
  ),
175
  }
 
 
 
 
 
 
 
 
 
176
  for field in ("simple_explanation",):
177
  if not result[field]:
178
  raise ValueError(f"{field} must not be empty.")
tests/test_tracing.py CHANGED
@@ -195,7 +195,7 @@ class TraceTests(unittest.TestCase):
195
  self.assertNotIn(assessment["red_flags"][0], serialized)
196
  self.assertNotIn("PRIVATE_IMAGE_BYTES", serialized)
197
 
198
- def test_image_trace_rejects_unsupported_structured_model_metadata(self) -> None:
199
  record = trace_runtime.build_trace_record(
200
  text="",
201
  image_data_url="data:image/png;base64,PRIVATE_IMAGE_BYTES",
 
195
  self.assertNotIn(assessment["red_flags"][0], serialized)
196
  self.assertNotIn("PRIVATE_IMAGE_BYTES", serialized)
197
 
198
+ def test_image_trace_uses_result_summary_not_extra_metadata(self) -> None:
199
  record = trace_runtime.build_trace_record(
200
  text="",
201
  image_data_url="data:image/png;base64,PRIVATE_IMAGE_BYTES",
traces/dataset_card.md CHANGED
@@ -31,11 +31,10 @@ 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. New model responses also include enum-only category
36
- and tactic hints. These hints are treated as untrusted: the trace mapper keeps
37
- only values supported by the explanation or red flags and falls back to its
38
- deterministic English/Urdu evidence rules when they disagree.
39
 
40
  ## Fields
41
 
 
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 are inspected transiently for this mapping, but their
35
+ text is not stored. The trace mapper predicts the privacy-safe image category
36
+ and tactics directly from that result summary using deterministic English/Urdu
37
+ evidence rules.
 
38
 
39
  ## Fields
40
 
traces/runtime.py CHANGED
@@ -377,23 +377,6 @@ def assessment_evidence(assessment: dict[str, Any] | None) -> str:
377
  return " ".join(values)[:4000]
378
 
379
 
380
- def structured_assessment_profile(
381
- assessment: dict[str, Any] | None,
382
- ) -> tuple[str, dict[str, bool]] | None:
383
- if not isinstance(assessment, dict):
384
- return None
385
- category = assessment.get("trace_category")
386
- tactics = assessment.get("trace_tactics")
387
- if category not in CATEGORY_DISPLAY_NAMES or not isinstance(tactics, list):
388
- return None
389
- if any(tactic not in SIGNAL_PATTERNS for tactic in tactics):
390
- return None
391
- return category, {
392
- name: name in tactics
393
- for name in SIGNAL_PATTERNS
394
- }
395
-
396
-
397
  def build_input_profile(
398
  text: str,
399
  image_data_url: str,
@@ -407,11 +390,6 @@ def build_input_profile(
407
  input_type = "image"
408
  else:
409
  input_type = "text"
410
- structured_profile = (
411
- structured_assessment_profile(assessment)
412
- if input_type == "image" and not example_id
413
- else None
414
- )
415
  classification_text = text
416
  if input_type == "image" and not example_id:
417
  classification_text = " ".join(
@@ -419,19 +397,6 @@ def build_input_profile(
419
  )
420
  signals = detect_signals(classification_text, example_id)
421
  category = detect_category(classification_text, signals, example_id)
422
- if structured_profile:
423
- structured_category, structured_signals = structured_profile
424
- confirmed_tactics = {
425
- name
426
- for name, enabled in structured_signals.items()
427
- if enabled and signals[name]
428
- }
429
- if (
430
- category == "unknown"
431
- and structured_category != "unknown"
432
- and confirmed_tactics
433
- ):
434
- category = structured_category
435
  tactics = [name for name, enabled in signals.items() if enabled]
436
  if input_type == "image" and not assessment and not example_id:
437
  input_description = "image: Assessment unavailable"
 
377
  return " ".join(values)[:4000]
378
 
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  def build_input_profile(
381
  text: str,
382
  image_data_url: str,
 
390
  input_type = "image"
391
  else:
392
  input_type = "text"
 
 
 
 
 
393
  classification_text = text
394
  if input_type == "image" and not example_id:
395
  classification_text = " ".join(
 
397
  )
398
  signals = detect_signals(classification_text, example_id)
399
  category = detect_category(classification_text, signals, example_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  tactics = [name for name, enabled in signals.items() if enabled]
401
  if input_type == "image" and not assessment and not example_id:
402
  input_description = "image: Assessment unavailable"