Abid Ali Awan Codex commited on
Commit ·
773ad7d
1
Parent(s): 09e22a1
Trim trace dataset columns
Browse filesCo-Authored-By: Codex <codex@openai.com>
- README.md +2 -1
- tests/test_tracing.py +18 -2
- traces/data/trace_samples.jsonl +6 -6
- traces/dataset_card.md +3 -6
- traces/runtime.py +20 -51
- traces/scripts/export_pending_traces.py +1 -1
- traces/scripts/seed_trace_dataset.py +1 -1
README.md
CHANGED
|
@@ -132,7 +132,8 @@ images use a fixed `image: ...` description without OCR or image storage. The
|
|
| 132 |
trace also records category, urgency, fixed signals, result counts, and a
|
| 133 |
deterministic `result_summary` explaining the scam pattern and risk label.
|
| 134 |
All trace columns are flat scalar values; no dataset cell contains a nested
|
| 135 |
-
dictionary.
|
|
|
|
| 136 |
It never stores raw messages, screenshots, links, detected identifiers, model
|
| 137 |
explanations, reply text, exceptions, or credentials.
|
| 138 |
|
|
|
|
| 132 |
trace also records category, urgency, fixed signals, result counts, and a
|
| 133 |
deterministic `result_summary` explaining the scam pattern and risk label.
|
| 134 |
All trace columns are flat scalar values; no dataset cell contains a nested
|
| 135 |
+
dictionary. Detected signals are combined into the readable `scam_tactics`
|
| 136 |
+
column.
|
| 137 |
It never stores raw messages, screenshots, links, detected identifiers, model
|
| 138 |
explanations, reply text, exceptions, or credentials.
|
| 139 |
|
tests/test_tracing.py
CHANGED
|
@@ -72,7 +72,6 @@ class TraceTests(unittest.TestCase):
|
|
| 72 |
image_record["result_summary"],
|
| 73 |
)
|
| 74 |
self.assertIn("Strong scam indicators", image_record["result_summary"])
|
| 75 |
-
self.assertEqual(image_record["input_storage"], "image_description_only")
|
| 76 |
for removed in (
|
| 77 |
"pipeline_steps",
|
| 78 |
"cache",
|
|
@@ -85,6 +84,23 @@ class TraceTests(unittest.TestCase):
|
|
| 85 |
"image_size_bucket",
|
| 86 |
"language_hint",
|
| 87 |
"modal",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
):
|
| 89 |
self.assertNotIn(removed, image_record)
|
| 90 |
|
|
@@ -104,11 +120,11 @@ class TraceTests(unittest.TestCase):
|
|
| 104 |
"No completed assessment result was available",
|
| 105 |
text_record["result_summary"],
|
| 106 |
)
|
| 107 |
-
self.assertEqual(text_record["input_storage"], "redacted_text")
|
| 108 |
self.assertFalse(any(
|
| 109 |
isinstance(value, (dict, list))
|
| 110 |
for value in text_record.values()
|
| 111 |
))
|
|
|
|
| 112 |
|
| 113 |
def test_opt_out_does_not_queue_trace(self) -> None:
|
| 114 |
with patch("app.queue_trace") as queue_mock:
|
|
|
|
| 72 |
image_record["result_summary"],
|
| 73 |
)
|
| 74 |
self.assertIn("Strong scam indicators", image_record["result_summary"])
|
|
|
|
| 75 |
for removed in (
|
| 76 |
"pipeline_steps",
|
| 77 |
"cache",
|
|
|
|
| 84 |
"image_size_bucket",
|
| 85 |
"language_hint",
|
| 86 |
"modal",
|
| 87 |
+
"exception_text_stored",
|
| 88 |
+
"identifiers_stored",
|
| 89 |
+
"input_storage",
|
| 90 |
+
"raw_image_stored",
|
| 91 |
+
"raw_input_stored",
|
| 92 |
+
"raw_model_output_stored",
|
| 93 |
+
"red_flag_count",
|
| 94 |
+
"reply_draft_returned",
|
| 95 |
+
"signal_account_threat",
|
| 96 |
+
"signal_challan",
|
| 97 |
+
"signal_cnic",
|
| 98 |
+
"signal_courier",
|
| 99 |
+
"signal_credentials",
|
| 100 |
+
"signal_link",
|
| 101 |
+
"signal_otp",
|
| 102 |
+
"signal_payment",
|
| 103 |
+
"signal_refund_or_prize",
|
| 104 |
):
|
| 105 |
self.assertNotIn(removed, image_record)
|
| 106 |
|
|
|
|
| 120 |
"No completed assessment result was available",
|
| 121 |
text_record["result_summary"],
|
| 122 |
)
|
|
|
|
| 123 |
self.assertFalse(any(
|
| 124 |
isinstance(value, (dict, list))
|
| 125 |
for value in text_record.values()
|
| 126 |
))
|
| 127 |
+
self.assertEqual(next(iter(text_record)), "trace_id")
|
| 128 |
|
| 129 |
def test_opt_out_does_not_queue_trace(self) -> None:
|
| 130 |
with patch("app.queue_trace") as queue_mock:
|
traces/data/trace_samples.jsonl
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
{"
|
| 2 |
-
{"
|
| 3 |
-
{"
|
| 4 |
-
{"
|
| 5 |
-
{"
|
| 6 |
-
{"
|
|
|
|
| 1 |
+
{"trace_id": "44123b4e-1e27-42db-a25e-c152d40db44f", "timestamp": "2026-06-07T06:57:14.262543+00:00", "input": "text: PAKISTAN POST: [NAME_OR_ENTITY] parcel [ADDRESS]. [NAME_OR_ENTITY] Rs. 85 today at [LINK] or the parcel will be destroyed.", "input_category": "courier", "urgency": true, "scam_tactics": "link, urgency, payment, courier", "result_summary": "Likely scam: Courier-style content with link, urgency, payment, courier signals. Known courier pattern. Strong scam indicators were found; avoid payments, links, and sharing credentials.", "risk_label": "Likely scam", "safe_next_step_count": 4, "reply_draft_policy": "suppressed"}
|
| 2 |
+
{"trace_id": "166594ae-e201-49cb-bbff-07482bcdd779", "timestamp": "2026-06-07T06:57:14.262745+00:00", "input": "text: FBR REFUND: [NAME_OR_ENTITY] are eligible for Rs 42,[NUMBER]. [NAME_OR_ENTITY] your CNIC and bank card [REDACTED] at the link today to receive payment.", "input_category": "fbr", "urgency": true, "scam_tactics": "cnic, credentials, urgency, payment, refund_or_prize", "result_summary": "Likely scam: FBR-style content with CNIC, credential, urgency, payment signals. Known FBR pattern. Strong scam indicators were found; avoid payments, links, and sharing credentials.", "risk_label": "Likely scam", "safe_next_step_count": 4, "reply_draft_policy": "suppressed"}
|
| 3 |
+
{"trace_id": "a8c5849b-19af-4ecd-9e31-482f594055ac", "timestamp": "2026-06-07T06:57:14.262936+00:00", "input": "text: HBL [NAME_OR_ENTITY]: [NAME_OR_ENTITY] account [REDACTED] be suspended. [NAME_OR_ENTITY] the OTP [REDACTED] to your phone with our support team immediately.", "input_category": "bank", "urgency": true, "scam_tactics": "otp, urgency, account_threat", "result_summary": "Likely scam: Bank-style content with OTP, urgency, account-threat signals. Known bank pattern. Strong scam indicators were found; avoid payments, links, and sharing credentials.", "risk_label": "Likely scam", "safe_next_step_count": 5, "reply_draft_policy": "suppressed"}
|
| 4 |
+
{"trace_id": "1f832008-9ab6-497a-9a15-1beaa0bf6410", "timestamp": "2026-06-07T06:57:14.263070+00:00", "input": "image: Courier-style content with link, urgency, courier signals", "input_category": "courier", "urgency": true, "scam_tactics": "link, urgency, courier", "result_summary": "Likely scam: Courier-style content with link, urgency, courier signals. Known courier pattern. Strong scam indicators were found; avoid payments, links, and sharing credentials.", "risk_label": "Likely scam", "safe_next_step_count": 4, "reply_draft_policy": "suppressed"}
|
| 5 |
+
{"trace_id": "4478636b-37d7-4646-b58c-7f8cb4d0be90", "timestamp": "2026-06-07T06:57:14.263462+00:00", "input": "image: Marketplace-style content with credential signals", "input_category": "marketplace", "urgency": false, "scam_tactics": "credentials", "result_summary": "Likely scam: Marketplace-style content with credential signals. Known marketplace pattern. Strong scam indicators were found; avoid payments, links, and sharing credentials.", "risk_label": "Likely scam", "safe_next_step_count": 4, "reply_draft_policy": "suppressed"}
|
| 6 |
+
{"trace_id": "c439562d-cafd-4c07-a44e-03002728d7ba", "timestamp": "2026-06-07T06:57:14.263640+00:00", "input": "image: Traffic-challan-style content with link, urgency, payment, challan signals", "input_category": "traffic_challan", "urgency": true, "scam_tactics": "link, urgency, payment, challan", "result_summary": "Likely scam: Traffic-challan-style content with link, urgency, payment, challan signals. Known traffic-challan pattern. Strong scam indicators were found; avoid payments, links, and sharing credentials.", "risk_label": "Likely scam", "safe_next_step_count": 4, "reply_draft_policy": "suppressed"}
|
traces/dataset_card.md
CHANGED
|
@@ -41,16 +41,13 @@ and convert it into allow-listed categories, booleans, buckets, and counts.
|
|
| 41 |
- `result_summary`: deterministic summary of the mapped pattern, whether it is
|
| 42 |
known or unclassified, and why the risk label matters
|
| 43 |
- `scam_tactics`: readable comma-separated tactics
|
| 44 |
-
- Flat
|
| 45 |
-
`signal_credentials`
|
| 46 |
-
- Flat result columns such as `risk_label`, `red_flag_count`, and
|
| 47 |
`reply_draft_policy`
|
| 48 |
-
- `input_storage`: `redacted_text` or `image_description_only`
|
| 49 |
-
- Flat privacy flags confirming that raw input, images, identifiers, model
|
| 50 |
-
output, and exception text are not stored
|
| 51 |
|
| 52 |
Every dataset cell is a scalar string, number, or boolean. No column contains a
|
| 53 |
dictionary or nested object, which keeps the Hugging Face table easy to read.
|
|
|
|
|
|
|
| 54 |
|
| 55 |
Records do not contain pipeline steps, cache fields, app commits, failure
|
| 56 |
details, request source, schema versions, size buckets, language hints, or
|
|
|
|
| 41 |
- `result_summary`: deterministic summary of the mapped pattern, whether it is
|
| 42 |
known or unclassified, and why the risk label matters
|
| 43 |
- `scam_tactics`: readable comma-separated tactics
|
| 44 |
+
- Flat result columns such as `risk_label`, `safe_next_step_count`, and
|
|
|
|
|
|
|
| 45 |
`reply_draft_policy`
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
Every dataset cell is a scalar string, number, or boolean. No column contains a
|
| 48 |
dictionary or nested object, which keeps the Hugging Face table easy to read.
|
| 49 |
+
`trace_id` is the first serialized column, and all detected boolean signals are
|
| 50 |
+
combined into the single `scam_tactics` category column.
|
| 51 |
|
| 52 |
Records do not contain pipeline steps, cache fields, app commits, failure
|
| 53 |
details, request source, schema versions, size buckets, language hints, or
|
traces/runtime.py
CHANGED
|
@@ -230,11 +230,6 @@ def build_input_profile(text: str, image_data_url: str, example_id: str = "") ->
|
|
| 230 |
"input_category": category,
|
| 231 |
"urgency": signals["urgency"],
|
| 232 |
"scam_tactics": ", ".join(tactics) if tactics else "none",
|
| 233 |
-
**{
|
| 234 |
-
f"signal_{name}": enabled
|
| 235 |
-
for name, enabled in signals.items()
|
| 236 |
-
if name != "urgency"
|
| 237 |
-
},
|
| 238 |
}
|
| 239 |
|
| 240 |
|
|
@@ -259,12 +254,10 @@ def build_trace_record(
|
|
| 259 |
**input_profile,
|
| 260 |
"result_summary": result_summary(risk_label, category, signals),
|
| 261 |
"risk_label": risk_label,
|
| 262 |
-
"red_flag_count": min(len(assessment.get("red_flags", [])), 50),
|
| 263 |
"safe_next_step_count": min(
|
| 264 |
len(assessment.get("safe_next_steps", [])),
|
| 265 |
50,
|
| 266 |
),
|
| 267 |
-
"reply_draft_returned": bool(assessment.get("reply_draft")),
|
| 268 |
"reply_draft_policy": (
|
| 269 |
"allowed"
|
| 270 |
if risk_label in {"Verify first", "Suspicious"}
|
|
@@ -272,16 +265,6 @@ def build_trace_record(
|
|
| 272 |
if risk_label != "none"
|
| 273 |
else "not_applicable"
|
| 274 |
),
|
| 275 |
-
"input_storage": (
|
| 276 |
-
"redacted_text"
|
| 277 |
-
if input_profile["input"].startswith("text: ")
|
| 278 |
-
else "image_description_only"
|
| 279 |
-
),
|
| 280 |
-
"raw_input_stored": False,
|
| 281 |
-
"raw_image_stored": False,
|
| 282 |
-
"raw_model_output_stored": False,
|
| 283 |
-
"exception_text_stored": False,
|
| 284 |
-
"identifiers_stored": False,
|
| 285 |
}
|
| 286 |
|
| 287 |
|
|
@@ -296,31 +279,16 @@ def validate_trace(record: Any) -> list[str]:
|
|
| 296 |
"input_category",
|
| 297 |
"urgency",
|
| 298 |
"scam_tactics",
|
| 299 |
-
"signal_otp",
|
| 300 |
-
"signal_cnic",
|
| 301 |
-
"signal_credentials",
|
| 302 |
-
"signal_link",
|
| 303 |
-
"signal_payment",
|
| 304 |
-
"signal_refund_or_prize",
|
| 305 |
-
"signal_courier",
|
| 306 |
-
"signal_challan",
|
| 307 |
-
"signal_account_threat",
|
| 308 |
"result_summary",
|
| 309 |
"risk_label",
|
| 310 |
-
"red_flag_count",
|
| 311 |
"safe_next_step_count",
|
| 312 |
-
"reply_draft_returned",
|
| 313 |
"reply_draft_policy",
|
| 314 |
-
"input_storage",
|
| 315 |
-
"raw_input_stored",
|
| 316 |
-
"raw_image_stored",
|
| 317 |
-
"raw_model_output_stored",
|
| 318 |
-
"exception_text_stored",
|
| 319 |
-
"identifiers_stored",
|
| 320 |
}
|
| 321 |
missing = required - record.keys()
|
| 322 |
if missing:
|
| 323 |
errors.append("Missing fields: " + ", ".join(sorted(missing)))
|
|
|
|
|
|
|
| 324 |
input_value = record.get("input")
|
| 325 |
if not (
|
| 326 |
isinstance(input_value, str)
|
|
@@ -338,22 +306,6 @@ def validate_trace(record: Any) -> list[str]:
|
|
| 338 |
errors.append("Result summary must be a string.")
|
| 339 |
if record.get("risk_label") not in RISK_LABELS:
|
| 340 |
errors.append("Invalid risk label.")
|
| 341 |
-
private_storage_flags = (
|
| 342 |
-
"raw_input_stored",
|
| 343 |
-
"raw_image_stored",
|
| 344 |
-
"raw_model_output_stored",
|
| 345 |
-
"exception_text_stored",
|
| 346 |
-
"identifiers_stored",
|
| 347 |
-
)
|
| 348 |
-
if any(
|
| 349 |
-
record.get(key) is not False for key in private_storage_flags
|
| 350 |
-
):
|
| 351 |
-
errors.append("Raw/private storage flags must all be false.")
|
| 352 |
-
if record.get("input_storage") not in {
|
| 353 |
-
"redacted_text",
|
| 354 |
-
"image_description_only",
|
| 355 |
-
}:
|
| 356 |
-
errors.append("Invalid input storage category.")
|
| 357 |
if any(isinstance(value, (dict, list)) for value in record.values()):
|
| 358 |
errors.append("Trace columns must contain scalar values only.")
|
| 359 |
forbidden_keys = {
|
|
@@ -368,6 +320,23 @@ def validate_trace(record: Any) -> list[str]:
|
|
| 368 |
"image_size_bucket",
|
| 369 |
"language_hint",
|
| 370 |
"modal",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
"raw_input",
|
| 372 |
"raw_text",
|
| 373 |
"image_data_url",
|
|
@@ -493,7 +462,7 @@ class TracePublisher:
|
|
| 493 |
final_path = PENDING_DIR / filename
|
| 494 |
temporary_path = final_path.with_suffix(".tmp")
|
| 495 |
content = "".join(
|
| 496 |
-
json.dumps(record,
|
| 497 |
for record in accepted
|
| 498 |
)
|
| 499 |
temporary_path.write_text(content, encoding="utf-8")
|
|
|
|
| 230 |
"input_category": category,
|
| 231 |
"urgency": signals["urgency"],
|
| 232 |
"scam_tactics": ", ".join(tactics) if tactics else "none",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
}
|
| 234 |
|
| 235 |
|
|
|
|
| 254 |
**input_profile,
|
| 255 |
"result_summary": result_summary(risk_label, category, signals),
|
| 256 |
"risk_label": risk_label,
|
|
|
|
| 257 |
"safe_next_step_count": min(
|
| 258 |
len(assessment.get("safe_next_steps", [])),
|
| 259 |
50,
|
| 260 |
),
|
|
|
|
| 261 |
"reply_draft_policy": (
|
| 262 |
"allowed"
|
| 263 |
if risk_label in {"Verify first", "Suspicious"}
|
|
|
|
| 265 |
if risk_label != "none"
|
| 266 |
else "not_applicable"
|
| 267 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
}
|
| 269 |
|
| 270 |
|
|
|
|
| 279 |
"input_category",
|
| 280 |
"urgency",
|
| 281 |
"scam_tactics",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
"result_summary",
|
| 283 |
"risk_label",
|
|
|
|
| 284 |
"safe_next_step_count",
|
|
|
|
| 285 |
"reply_draft_policy",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
missing = required - record.keys()
|
| 288 |
if missing:
|
| 289 |
errors.append("Missing fields: " + ", ".join(sorted(missing)))
|
| 290 |
+
if record and next(iter(record)) != "trace_id":
|
| 291 |
+
errors.append("trace_id must be the first column.")
|
| 292 |
input_value = record.get("input")
|
| 293 |
if not (
|
| 294 |
isinstance(input_value, str)
|
|
|
|
| 306 |
errors.append("Result summary must be a string.")
|
| 307 |
if record.get("risk_label") not in RISK_LABELS:
|
| 308 |
errors.append("Invalid risk label.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
if any(isinstance(value, (dict, list)) for value in record.values()):
|
| 310 |
errors.append("Trace columns must contain scalar values only.")
|
| 311 |
forbidden_keys = {
|
|
|
|
| 320 |
"image_size_bucket",
|
| 321 |
"language_hint",
|
| 322 |
"modal",
|
| 323 |
+
"exception_text_stored",
|
| 324 |
+
"identifiers_stored",
|
| 325 |
+
"input_storage",
|
| 326 |
+
"raw_image_stored",
|
| 327 |
+
"raw_input_stored",
|
| 328 |
+
"raw_model_output_stored",
|
| 329 |
+
"red_flag_count",
|
| 330 |
+
"reply_draft_returned",
|
| 331 |
+
"signal_account_threat",
|
| 332 |
+
"signal_challan",
|
| 333 |
+
"signal_cnic",
|
| 334 |
+
"signal_courier",
|
| 335 |
+
"signal_credentials",
|
| 336 |
+
"signal_link",
|
| 337 |
+
"signal_otp",
|
| 338 |
+
"signal_payment",
|
| 339 |
+
"signal_refund_or_prize",
|
| 340 |
"raw_input",
|
| 341 |
"raw_text",
|
| 342 |
"image_data_url",
|
|
|
|
| 462 |
final_path = PENDING_DIR / filename
|
| 463 |
temporary_path = final_path.with_suffix(".tmp")
|
| 464 |
content = "".join(
|
| 465 |
+
json.dumps(record, ensure_ascii=True) + "\n"
|
| 466 |
for record in accepted
|
| 467 |
)
|
| 468 |
temporary_path.write_text(content, encoding="utf-8")
|
traces/scripts/export_pending_traces.py
CHANGED
|
@@ -33,7 +33,7 @@ def main() -> int:
|
|
| 33 |
raise RuntimeError(f"{path}: {'; '.join(errors)}")
|
| 34 |
records.append(record)
|
| 35 |
content = "".join(
|
| 36 |
-
json.dumps(record,
|
| 37 |
for record in records
|
| 38 |
)
|
| 39 |
if args.dry_run:
|
|
|
|
| 33 |
raise RuntimeError(f"{path}: {'; '.join(errors)}")
|
| 34 |
records.append(record)
|
| 35 |
content = "".join(
|
| 36 |
+
json.dumps(record, ensure_ascii=True) + "\n"
|
| 37 |
for record in records
|
| 38 |
)
|
| 39 |
if args.dry_run:
|
traces/scripts/seed_trace_dataset.py
CHANGED
|
@@ -69,7 +69,7 @@ def main() -> int:
|
|
| 69 |
args = parser.parse_args()
|
| 70 |
records = build_seed_records()
|
| 71 |
content = "".join(
|
| 72 |
-
json.dumps(record,
|
| 73 |
for record in records
|
| 74 |
)
|
| 75 |
if args.dry_run:
|
|
|
|
| 69 |
args = parser.parse_args()
|
| 70 |
records = build_seed_records()
|
| 71 |
content = "".join(
|
| 72 |
+
json.dumps(record, ensure_ascii=True) + "\n"
|
| 73 |
for record in records
|
| 74 |
)
|
| 75 |
if args.dry_run:
|