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())
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "noop-ai-checked",
|
||||
"default_tag": "ai-checked",
|
||||
"remove_tags": [],
|
||||
"filter_1": {
|
||||
"type": "builtin",
|
||||
"name": "fixed-ticket-limit",
|
||||
"limit": 5
|
||||
},
|
||||
"ticket_fetch": {
|
||||
"fetch_full_issues": false,
|
||||
"message_limit": 25,
|
||||
"sanitize_text": true
|
||||
},
|
||||
"filter_2": {
|
||||
"type": "command",
|
||||
"argv": ["python3", "filters/noop_filter2.py"],
|
||||
"timeout_seconds": 30,
|
||||
"allow_mutation": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "spam-scorer",
|
||||
"default_tag": "ai-checked",
|
||||
"remove_tags": [],
|
||||
"filter_1": {
|
||||
"type": "builtin",
|
||||
"name": "fixed-ticket-limit",
|
||||
"limit": 5
|
||||
},
|
||||
"ticket_fetch": {
|
||||
"fetch_full_issues": false,
|
||||
"message_limit": 25,
|
||||
"sanitize_text": true
|
||||
},
|
||||
"filter_2": {
|
||||
"type": "command",
|
||||
"argv": ["python3", "filters/spam_promo_scorer.py"],
|
||||
"timeout_seconds": 30,
|
||||
"allow_mutation": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"version": 1,
|
||||
"project": "customer-service",
|
||||
"description": "Known spam / clutter profiles seeded from verified production broom passes and legacy Rebot KnownSpam rules. Customer and maybe-customer contact safety gates must be applied by the tool before any close.",
|
||||
"universal_safety_gates": {
|
||||
"skip_contact_tags": [
|
||||
"Customer",
|
||||
"maybe-customer"
|
||||
],
|
||||
"require_open_ticket": true,
|
||||
"require_project": "customer-service",
|
||||
"write_impersonate_user": "rebot"
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"name": "globalindustrial-promos",
|
||||
"category": "spam-promotional",
|
||||
"confidence": "high",
|
||||
"source": "verified-production-and-rebot",
|
||||
"sender_regexes": [
|
||||
"^reply@globalindustrial\\.com$"
|
||||
],
|
||||
"subject_regexes": [
|
||||
"(?i)\\b(clearance|sale|save|% off|bestsellers|fans|blowers|cooling|storage|power tools|kee safety|roadway safety|advanced ac)\\b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vistaprint-promos",
|
||||
"category": "spam-promotional",
|
||||
"confidence": "high",
|
||||
"source": "verified-production",
|
||||
"sender_regexes": [
|
||||
"^noreply@mail\\.vistaprint\\.com$"
|
||||
],
|
||||
"subject_regexes": [
|
||||
"(?i)\\b(sale|save|% off|email exclusive|ends|qualifying orders|favorite sale|customer faves|buy more)\\b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "qnap-newsletters",
|
||||
"category": "spam-promotional",
|
||||
"confidence": "high",
|
||||
"source": "verified-production",
|
||||
"sender_regexes": [
|
||||
"^enews-noreply@qnap\\.com$"
|
||||
],
|
||||
"subject_regexes": [
|
||||
"(?i)\\b(qnap|photodirector|nas|security advisory|product of the year|jc-star|file backup|heic)\\b|^—$|^61|New$"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "business-journals-promos",
|
||||
"category": "spam-promotional",
|
||||
"confidence": "high",
|
||||
"source": "verified-production",
|
||||
"sender_regexes": [
|
||||
"^replycincinnati@engaged\\.bizjournals\\.com$",
|
||||
"^replycincinnati@partner\\.bizjournals\\.com$",
|
||||
"^acollett@bizjournals\\.com$"
|
||||
],
|
||||
"subject_regexes": [
|
||||
"(?i)\\b(events|nominations|business summit|brand next to key business lists|chambercomp|workers.? comp)\\b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "legacy-rebot-promotional-senders",
|
||||
"category": "spam-promotional",
|
||||
"confidence": "medium",
|
||||
"source": "legacy-rebot-knownspam",
|
||||
"sender_regexes": [
|
||||
"^.*@.*roku\\.com$",
|
||||
"^no\\.reply@masterclass\\.com$",
|
||||
"^donotreply@walmart\\.com$",
|
||||
"^.*@uber\\.com$",
|
||||
"^no-reply@inhome\\.walmart\\.com$",
|
||||
"^.*wingsandrings\\.com$",
|
||||
"^.*@tryfinally\\.co$",
|
||||
"^.*crackerbarrel\\.com$",
|
||||
"^.*shop\\.tiktok\\.com$",
|
||||
"^noreply_samsung@sea\\.samsung\\.com$",
|
||||
"^noreply@email\\.crunchbase\\.com$",
|
||||
"^.*webstaurantstore\\.com$",
|
||||
"^support@helium10\\.com$",
|
||||
"^noreply@popmenu\\.com$",
|
||||
"^info@go\\.cbts\\.com$",
|
||||
"^noreply@rush-delivery\\.com$"
|
||||
],
|
||||
"subject_regexes": []
|
||||
},
|
||||
{
|
||||
"name": "legacy-rebot-operational-clutter",
|
||||
"category": "automated-operational",
|
||||
"confidence": "medium",
|
||||
"source": "legacy-rebot-knownspam",
|
||||
"sender_regexes": [
|
||||
"^.*@indeedemail\\.com$",
|
||||
"^no-reply@indeed\\.com$",
|
||||
"mailnull@amazon\\.com",
|
||||
"^info@emailbiz\\.dukeenergy\\.com$"
|
||||
],
|
||||
"subject_regexes": [
|
||||
"(?i)Your Amazon\\.com order.*has shipped",
|
||||
"(?i)^Your Duke energy statement is ready",
|
||||
"(?i)^Your Amazon\\.com order.*",
|
||||
"(?i)^UPS Billing: Payment Received.*",
|
||||
"(?i)^UPS Billing:.*new invoice.*",
|
||||
"(?i)^.applied on indeed.*",
|
||||
"(?i)^See results for the assessments you sent to.*",
|
||||
"(?i)^Reminder: Leave feedback on your interview with.*",
|
||||
"(?i)^Pekin AutoPay Payment.*",
|
||||
"(?i)^Notification of Active Listings Report availability"
|
||||
],
|
||||
"default_action": "report-only-until-approved"
|
||||
},
|
||||
{
|
||||
"name": "legacy-rebot-receipts-routing-reference",
|
||||
"category": "routing-reference",
|
||||
"confidence": "medium",
|
||||
"source": "legacy-rebot-receipts-classifier",
|
||||
"sender_regexes": [
|
||||
"culliganbillingoh@culliganwaterco\\.com",
|
||||
"accounting@amshippingsupplies\\.com",
|
||||
"bookkeeperasl@gmail\\.com",
|
||||
"bounces-noreply@mandrillapp\\.com"
|
||||
],
|
||||
"subject_regexes": [
|
||||
"(?i)You have a new bill from altafiber"
|
||||
],
|
||||
"default_action": "do-not-close-as-spam-route-later"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user