feat(phase2): fact/inference labeling, change-driven alerts, admin cleanup
- Add label: cited_fact | inference to LLM brief schema (all 4 providers) - Inferred badge in AIBriefCard for inference-labeled points - backfill_brief_labels Celery task: classifies existing cited points in-place - POST /api/admin/backfill-labels + unlabeled_briefs stat counter - Expand milestone keywords: markup, conference - Add is_referral_action() for committee referrals (referred to) - Two-tier milestone notifications: progress tier (all follow modes) and referral tier (pocket_veto/boost only, neutral suppressed) - Topic followers now receive bill_updated milestone notifications via latest brief topic_tags lookup in _update_bill_if_changed() - Admin Manual Controls: collapsible Maintenance section for backfill tasks - Update ARCHITECTURE.md and roadmap for Phase 2 completion Co-Authored-By: Jack Levy
This commit is contained in:
@@ -10,8 +10,15 @@ _MILESTONE_KEYWORDS = [
|
||||
"presented to the president",
|
||||
"ordered to be reported", "ordered reported",
|
||||
"reported by", "discharged",
|
||||
"placed on", # placed on calendar
|
||||
"placed on", # placed on calendar
|
||||
"cloture", "roll call",
|
||||
"markup", # markup session scheduled/completed
|
||||
"conference", # conference committee activity
|
||||
]
|
||||
|
||||
# Committee referral — meaningful for pocket_veto/boost but noisy for neutral
|
||||
_REFERRAL_KEYWORDS = [
|
||||
"referred to",
|
||||
]
|
||||
|
||||
# Events created within this window for the same (user, bill, event_type) are suppressed
|
||||
@@ -23,7 +30,12 @@ def is_milestone_action(action_text: str) -> bool:
|
||||
return any(kw in t for kw in _MILESTONE_KEYWORDS)
|
||||
|
||||
|
||||
def _build_payload(bill, action_summary: str) -> dict:
|
||||
def is_referral_action(action_text: str) -> bool:
|
||||
t = (action_text or "").lower()
|
||||
return any(kw in t for kw in _REFERRAL_KEYWORDS)
|
||||
|
||||
|
||||
def _build_payload(bill, action_summary: str, milestone_tier: str = "progress") -> dict:
|
||||
from app.config import settings
|
||||
base_url = (settings.PUBLIC_URL or settings.LOCAL_URL).rstrip("/")
|
||||
return {
|
||||
@@ -31,6 +43,7 @@ def _build_payload(bill, action_summary: str) -> dict:
|
||||
"bill_label": f"{bill.bill_type.upper()} {bill.bill_number}",
|
||||
"brief_summary": (action_summary or "")[:300],
|
||||
"bill_url": f"{base_url}/bills/{bill.bill_id}",
|
||||
"milestone_tier": milestone_tier,
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +58,9 @@ def _is_duplicate(db, user_id: int, bill_id: str, event_type: str) -> bool:
|
||||
).filter(NotificationEvent.created_at > cutoff).first() is not None
|
||||
|
||||
|
||||
def emit_bill_notification(db, bill, event_type: str, action_summary: str) -> int:
|
||||
def emit_bill_notification(
|
||||
db, bill, event_type: str, action_summary: str, milestone_tier: str = "progress"
|
||||
) -> int:
|
||||
"""Create NotificationEvent rows for every user following this bill. Returns count."""
|
||||
from app.models.follow import Follow
|
||||
from app.models.notification import NotificationEvent
|
||||
@@ -54,7 +69,7 @@ def emit_bill_notification(db, bill, event_type: str, action_summary: str) -> in
|
||||
if not followers:
|
||||
return 0
|
||||
|
||||
payload = _build_payload(bill, action_summary)
|
||||
payload = _build_payload(bill, action_summary, milestone_tier)
|
||||
count = 0
|
||||
for follow in followers:
|
||||
if _is_duplicate(db, follow.user_id, bill.bill_id, event_type):
|
||||
@@ -71,7 +86,9 @@ def emit_bill_notification(db, bill, event_type: str, action_summary: str) -> in
|
||||
return count
|
||||
|
||||
|
||||
def emit_member_follow_notifications(db, bill, event_type: str, action_summary: str) -> int:
|
||||
def emit_member_follow_notifications(
|
||||
db, bill, event_type: str, action_summary: str, milestone_tier: str = "progress"
|
||||
) -> int:
|
||||
"""Notify users following the bill's sponsor (dedup prevents double-alerts for bill+member followers)."""
|
||||
if not bill.sponsor_id:
|
||||
return 0
|
||||
@@ -83,7 +100,7 @@ def emit_member_follow_notifications(db, bill, event_type: str, action_summary:
|
||||
if not followers:
|
||||
return 0
|
||||
|
||||
payload = _build_payload(bill, action_summary)
|
||||
payload = _build_payload(bill, action_summary, milestone_tier)
|
||||
count = 0
|
||||
for follow in followers:
|
||||
if _is_duplicate(db, follow.user_id, bill.bill_id, event_type):
|
||||
@@ -100,7 +117,10 @@ def emit_member_follow_notifications(db, bill, event_type: str, action_summary:
|
||||
return count
|
||||
|
||||
|
||||
def emit_topic_follow_notifications(db, bill, event_type: str, action_summary: str, topic_tags: list) -> int:
|
||||
def emit_topic_follow_notifications(
|
||||
db, bill, event_type: str, action_summary: str, topic_tags: list,
|
||||
milestone_tier: str = "progress",
|
||||
) -> int:
|
||||
"""Notify users following any of the bill's topic tags."""
|
||||
if not topic_tags:
|
||||
return 0
|
||||
@@ -120,7 +140,7 @@ def emit_topic_follow_notifications(db, bill, event_type: str, action_summary: s
|
||||
if not followers:
|
||||
return 0
|
||||
|
||||
payload = _build_payload(bill, action_summary)
|
||||
payload = _build_payload(bill, action_summary, milestone_tier)
|
||||
count = 0
|
||||
for follow in followers:
|
||||
if _is_duplicate(db, follow.user_id, bill.bill_id, event_type):
|
||||
|
||||
Reference in New Issue
Block a user