Cleaned up the mess

This commit is contained in:
Jason Thistlethwaite
2026-10-18 10:46:24 -04:00
parent 7ad3723525
commit 57314dea52
6 changed files with 333 additions and 2 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
/.cache/ /.cache/
/cache/* /cache/*
/cache-dev/* /cache-dev/*
./run-stats.json
manifest.json
/.venv/ /.venv/
/__pycache__/ /__pycache__/
+30
View File
@@ -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
View File
@@ -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
}
}
+21
View File
@@ -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
}
}
+132
View File
@@ -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"
}
]
}