The default admin@agenthub.com account is not a real email address, causing delivery failures when notifications are sent. Filter it out from both threshold alerts and weekly digest recipient queries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
284 lines
12 KiB
Python
284 lines
12 KiB
Python
import os
|
|
import requests
|
|
from datetime import datetime, timedelta
|
|
from database import notifications_collection, users_collection, agents_collection
|
|
|
|
|
|
def is_mailgun_configured() -> bool:
|
|
"""Check if Mailgun environment variables are set."""
|
|
return bool(os.getenv("MAILGUN_API_KEY")) and bool(os.getenv("MAILGUN_DOMAIN"))
|
|
|
|
|
|
def send_mailgun_email(to_emails: list[str], subject: str, html_body: str) -> bool:
|
|
"""Send email via Mailgun HTTP API. Returns True on success."""
|
|
api_key = os.getenv("MAILGUN_API_KEY")
|
|
domain = os.getenv("MAILGUN_DOMAIN")
|
|
from_email = os.getenv("MAILGUN_FROM_EMAIL", f"AgentHub <noreply@{domain}>")
|
|
|
|
response = requests.post(
|
|
f"https://api.mailgun.net/v3/{domain}/messages",
|
|
auth=("api", api_key),
|
|
data={
|
|
"from": from_email,
|
|
"to": to_emails,
|
|
"subject": subject,
|
|
"html": html_body,
|
|
},
|
|
timeout=10,
|
|
)
|
|
return response.status_code == 200
|
|
|
|
|
|
def build_threshold_email(agent_name: str, weekly_tokens: int, threshold: int) -> str:
|
|
"""Build HTML email body for a weekly token threshold notification."""
|
|
return f"""
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<div style="background: linear-gradient(135deg, #f3ae3e, #e09520); padding: 20px; border-radius: 8px 8px 0 0;">
|
|
<h2 style="color: white; margin: 0;">AgentHub - High Weekly Token Usage Alert</h2>
|
|
</div>
|
|
<div style="padding: 20px; border: 1px solid #e2e8f0; border-top: none; border-radius: 0 0 8px 8px;">
|
|
<p>The following agent has exceeded the weekly token usage threshold:</p>
|
|
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Agent Name</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{agent_name}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Tokens (Last 7 Days)</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{weekly_tokens:,}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Weekly Threshold</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{threshold:,}</td>
|
|
</tr>
|
|
</table>
|
|
<p style="color: #666; font-size: 0.9em;">
|
|
This is an automated notification from AgentHub. Please review the agent's recent token consumption.
|
|
</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
async def check_and_notify_threshold(agent_name: str):
|
|
"""Check 7-day token usage against threshold and send notification if exceeded. Non-blocking, safe to call."""
|
|
if not is_mailgun_configured():
|
|
return
|
|
|
|
threshold = int(os.getenv("TOKEN_USAGE_THRESHOLD", "100000"))
|
|
|
|
# Calculate token usage over the last 7 days from usage_timeline
|
|
agent = await agents_collection.find_one(
|
|
{"agent_name": agent_name},
|
|
{"usage_timeline": 1}
|
|
)
|
|
if not agent or not agent.get("usage_timeline"):
|
|
return
|
|
|
|
cutoff_date = (datetime.utcnow() - timedelta(days=7)).strftime("%Y-%m-%d")
|
|
weekly_tokens = sum(
|
|
entry.get("token_count", 0)
|
|
for entry in agent["usage_timeline"]
|
|
if entry.get("date", "") >= cutoff_date
|
|
)
|
|
|
|
if weekly_tokens < threshold:
|
|
return
|
|
|
|
cooldown_hours = int(os.getenv("NOTIFICATION_COOLDOWN_HOURS", "24"))
|
|
cooldown_cutoff = datetime.utcnow() - timedelta(hours=cooldown_hours)
|
|
|
|
# Check cooldown - skip if we already notified within the cooldown period
|
|
recent = await notifications_collection.find_one({
|
|
"agent_name": agent_name,
|
|
"sent_at": {"$gte": cooldown_cutoff},
|
|
})
|
|
if recent:
|
|
return
|
|
|
|
# Get admin user emails (exclude placeholder accounts with no real domain)
|
|
admin_cursor = users_collection.find(
|
|
{"is_admin": True, "is_active": True, "email": {"$not": {"$regex": r"@agenthub\.com$"}}},
|
|
{"email": 1},
|
|
)
|
|
admin_emails = [doc["email"] async for doc in admin_cursor]
|
|
if not admin_emails:
|
|
return
|
|
|
|
# Send email
|
|
subject = f"[AgentHub] High Weekly Token Usage: {agent_name} ({weekly_tokens:,} tokens in 7 days)"
|
|
html_body = build_threshold_email(agent_name, weekly_tokens, threshold)
|
|
success = send_mailgun_email(admin_emails, subject, html_body)
|
|
|
|
# Record the notification attempt
|
|
await notifications_collection.insert_one({
|
|
"agent_name": agent_name,
|
|
"weekly_tokens": weekly_tokens,
|
|
"threshold": threshold,
|
|
"recipients": admin_emails,
|
|
"success": success,
|
|
"sent_at": datetime.utcnow(),
|
|
})
|
|
|
|
|
|
def build_client_agent_email(agent_data: dict) -> str:
|
|
"""Build HTML email body for a client agent creation notification."""
|
|
return f"""
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<div style="background: linear-gradient(135deg, #6f42c1, #5a32a3); padding: 20px; border-radius: 8px 8px 0 0;">
|
|
<h2 style="color: white; margin: 0;">Client Agent Created</h2>
|
|
</div>
|
|
<div style="padding: 20px; border: 1px solid #e2e8f0; border-top: none; border-radius: 0 0 8px 8px;">
|
|
<p>A new client-facing agent has been created and requires verification.</p>
|
|
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Agent Name</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{agent_data.get('agent_name', 'N/A')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Description</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{agent_data.get('agent_description', 'N/A')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Purpose</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{agent_data.get('agent_purpose', 'N/A')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Client</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">Yes</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Client Name</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{agent_data.get('client_name', 'N/A')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Studio Name</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{agent_data.get('studio_name', 'N/A')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Tool</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{agent_data.get('agent_tool', 'N/A')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; font-weight: bold; border-bottom: 1px solid #eee;">Created By</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #eee;">{agent_data.get('created_by_email', 'N/A')}</td>
|
|
</tr>
|
|
</table>
|
|
<p style="color: #666; font-size: 0.9em;">
|
|
Please review this agent in AgentHub.
|
|
</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def send_client_agent_notification(agent_data: dict):
|
|
"""Send email notification when a client-facing agent is created. Non-blocking."""
|
|
if not is_mailgun_configured():
|
|
return
|
|
|
|
notify_emails_str = os.getenv("CLIENT_AGENT_NOTIFY_EMAILS", "")
|
|
if not notify_emails_str:
|
|
return
|
|
|
|
to_emails = [e.strip() for e in notify_emails_str.split(",") if e.strip()]
|
|
if not to_emails:
|
|
return
|
|
|
|
try:
|
|
subject = "Client Agent Created"
|
|
html_body = build_client_agent_email(agent_data)
|
|
send_mailgun_email(to_emails, subject, html_body)
|
|
except Exception as e:
|
|
print(f"Failed to send client agent notification: {e}")
|
|
|
|
|
|
def build_weekly_digest_email(agents: list) -> str:
|
|
"""Build HTML email body for the weekly agent digest."""
|
|
rows = ""
|
|
for i, agent in enumerate(agents, 1):
|
|
rows += f"""
|
|
<tr style="border-bottom: 1px solid #eee;">
|
|
<td style="padding: 10px; vertical-align: top; font-weight: bold; color: #333;">{i}.</td>
|
|
<td style="padding: 10px;">
|
|
<strong>{agent.get('agent_name', 'N/A')}</strong><br>
|
|
<span style="color: #555;">Purpose: {agent.get('agent_purpose', 'N/A')}</span><br>
|
|
<span style="color: #555;">Description: {agent.get('agent_description', 'N/A')}</span><br>
|
|
<span style="color: #888; font-size: 0.9em;">Created by: {agent.get('created_by_email', 'N/A')}</span>
|
|
</td>
|
|
</tr>"""
|
|
|
|
return f"""
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<div style="background: linear-gradient(135deg, #2d6a4f, #40916c); padding: 20px; border-radius: 8px 8px 0 0;">
|
|
<h2 style="color: white; margin: 0;">Agents Created in Last Week</h2>
|
|
</div>
|
|
<div style="padding: 20px; border: 1px solid #e2e8f0; border-top: none; border-radius: 0 0 8px 8px;">
|
|
<p>{len(agents)} agent(s) created in the last week:</p>
|
|
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
|
{rows}
|
|
</table>
|
|
<p style="color: #666; font-size: 0.9em;">
|
|
View full details in the AgentHub admin dashboard.
|
|
</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
async def send_weekly_agent_digest():
|
|
"""Send weekly digest of agents created in the last 7 days to all admins."""
|
|
if not is_mailgun_configured():
|
|
print("Weekly digest: Mailgun not configured, skipping.")
|
|
return
|
|
|
|
cutoff = datetime.utcnow() - timedelta(days=7)
|
|
|
|
# Find agents created in last 7 days
|
|
cursor = agents_collection.find(
|
|
{"created_at": {"$gte": cutoff}},
|
|
{"agent_name": 1, "agent_purpose": 1, "agent_description": 1, "created_by": 1}
|
|
).sort("created_at", -1)
|
|
agents = await cursor.to_list(length=None)
|
|
|
|
if not agents:
|
|
print("Weekly digest: No agents created in last 7 days, skipping.")
|
|
return
|
|
|
|
# Resolve creator emails
|
|
for agent in agents:
|
|
created_by = agent.get("created_by", "")
|
|
if created_by == "agent_collector_api":
|
|
agent["created_by_email"] = "Agent Collector API"
|
|
else:
|
|
try:
|
|
from bson import ObjectId
|
|
user = await users_collection.find_one({"_id": ObjectId(created_by)}, {"email": 1})
|
|
agent["created_by_email"] = user["email"] if user else created_by
|
|
except Exception:
|
|
agent["created_by_email"] = created_by
|
|
|
|
# Get admin emails (exclude placeholder accounts with no real domain)
|
|
admin_cursor = users_collection.find(
|
|
{"is_admin": True, "is_active": True, "email": {"$not": {"$regex": r"@agenthub\.com$"}}},
|
|
{"email": 1},
|
|
)
|
|
admin_emails = [doc["email"] async for doc in admin_cursor]
|
|
if not admin_emails:
|
|
print("Weekly digest: No admin emails found, skipping.")
|
|
return
|
|
|
|
subject = "Agents Created in Last Week"
|
|
html_body = build_weekly_digest_email(agents)
|
|
|
|
try:
|
|
success = send_mailgun_email(admin_emails, subject, html_body)
|
|
print(f"Weekly digest: Sent to {len(admin_emails)} admins, success={success}")
|
|
except Exception as e:
|
|
print(f"Weekly digest: Failed to send: {e}")
|