Cleaned up the mess
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""No-op FILTER-2 worker for exercising redmine_contacts.py loop-run.
|
||||
|
||||
Reads the loop bundle on stdin and returns false so the runner applies the
|
||||
profile default tag. This proves the conveyor belt without classifying or
|
||||
mutating Redmine directly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
json.load(sys.stdin)
|
||||
json.dump(
|
||||
{
|
||||
"result": False,
|
||||
"summary": "noop FILTER-2 did not classify this contact",
|
||||
"mutations": [],
|
||||
},
|
||||
sys.stdout,
|
||||
)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FILTER-2 worker: spam / promo scorer for the contact triage loop.
|
||||
|
||||
Reads the loop bundle on stdin and returns tag recommendations
|
||||
based on simple evidence scoring for promotional and spam senders.
|
||||
|
||||
Contract:
|
||||
- Input: JSON {profile, contact, issues, runtime}
|
||||
- Output: JSON {result: bool, summary: str, mutations?: list[str]}
|
||||
|
||||
Scoring signals (from contact_triage_loop_raw_notes):
|
||||
- Sender hints (no-reply, noreply, deals, promotions) → +1
|
||||
- Message mentions discount/coupon/deal language → +1
|
||||
Threshold: score >= 2 → tag as spam + promo (or just spam for v0)
|
||||
Otherwise: result=false (runner will add default_tag)
|
||||
|
||||
This worker never mutates Redmine directly; it only recommends.
|
||||
The runner owns tag application and ai-checked fallback.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
SPAM_HINTS = re.compile(
|
||||
r"\b(no-?reply|noreply|deals?|promotions?|marketing|newsletter|offers?)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
DEAL_LANGUAGE = re.compile(
|
||||
r"\b(discount|coupon|deal|promo|sale|offer|free|save|% off|limited time)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def score_contact(bundle: dict[str, Any]) -> tuple[bool, str, list[str]]:
|
||||
contact = bundle.get("contact", {})
|
||||
issues = bundle.get("issues", [])
|
||||
profile = bundle.get("profile", {})
|
||||
runtime = bundle.get("runtime", {})
|
||||
dry_run = runtime.get("dry_run", True)
|
||||
|
||||
contact_id = contact.get("id")
|
||||
sender_hints = 0
|
||||
deal_hits = 0
|
||||
|
||||
# Quick contact-level signals (if any metadata is present)
|
||||
# For now we rely on helpdesk messages; extend later if contact fields improve.
|
||||
|
||||
for issue in issues:
|
||||
summary = issue.get("issue_summary", {})
|
||||
subject = (summary.get("subject") or "").lower()
|
||||
# Full messages are in the bundle if fetch_full_issues was true; here we use what was provided.
|
||||
# The runner already sanitizes; we still run our patterns.
|
||||
if SPAM_HINTS.search(subject):
|
||||
sender_hints += 1
|
||||
if DEAL_LANGUAGE.search(subject):
|
||||
deal_hits += 1
|
||||
|
||||
# Also scan a small amount of message text if present in the payload
|
||||
# (the runner passes compact issue data; we keep it simple)
|
||||
for msg in issue.get("messages", [])[:3]: # light scan
|
||||
text = (msg.get("body") or "")[:500].lower()
|
||||
if SPAM_HINTS.search(text):
|
||||
sender_hints += 1
|
||||
if DEAL_LANGUAGE.search(text):
|
||||
deal_hits += 1
|
||||
|
||||
score = sender_hints + deal_hits
|
||||
tags_to_add: list[str] = []
|
||||
|
||||
if score >= 2:
|
||||
tags_to_add = ["spam", "promo"]
|
||||
summary = (
|
||||
f"Score {score} (sender hints={sender_hints}, deal language={deal_hits}). "
|
||||
f"Recommending spam + promo tags for contact {contact_id}."
|
||||
)
|
||||
result = True
|
||||
else:
|
||||
summary = (
|
||||
f"Score {score} (sender hints={sender_hints}, deal language={deal_hits}). "
|
||||
f"Insufficient evidence for spam/promo on contact {contact_id}."
|
||||
)
|
||||
result = False
|
||||
|
||||
# mutations are informational only in v0; runner applies the tags
|
||||
mutations = [f"contact:{contact_id} add {', '.join(tags_to_add)}"] if tags_to_add else []
|
||||
|
||||
# In strict dry-run mode we still return the recommendation so the operator can review.
|
||||
return result, summary, mutations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
bundle = json.load(sys.stdin)
|
||||
except Exception as exc:
|
||||
json.dump(
|
||||
{
|
||||
"result": False,
|
||||
"summary": f"FILTER-2 failed to parse input: {exc}",
|
||||
"mutations": [],
|
||||
},
|
||||
sys.stdout,
|
||||
)
|
||||
sys.stdout.write("\n")
|
||||
return 1
|
||||
|
||||
result, summary, mutations = score_contact(bundle)
|
||||
|
||||
json.dump(
|
||||
{
|
||||
"result": result,
|
||||
"summary": summary,
|
||||
"mutations": mutations,
|
||||
},
|
||||
sys.stdout,
|
||||
indent=2,
|
||||
)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user