diff --git a/CLAUDE.md b/CLAUDE.md index c992f6e..c6a753a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,10 +31,10 @@ Create `.env` file with: - `MAILGUN_API_KEY`: Mailgun API key (if not set, notifications are silently disabled) - `MAILGUN_DOMAIN`: Mailgun sending domain (e.g. your-domain.mailgun.org) - `MAILGUN_FROM_EMAIL`: Sender address (default: `AgentHub `) -- `TOKEN_USAGE_THRESHOLD`: Token count that triggers an alert (default: 100000) +- `TOKEN_USAGE_THRESHOLD`: Weekly token count that triggers an alert (default: 100000) - `NOTIFICATION_COOLDOWN_HOURS`: Hours between repeat alerts for the same agent (default: 24) - `CLIENT_AGENT_NOTIFY_EMAILS`: Comma-separated list of emails for client agent notifications -- `DAILY_DIGEST_HOUR`: Hour (24h format) to send daily digest (default: 7) +- `WEEKLY_DIGEST_HOUR`: Hour (24h format) to send weekly digest on Mondays (default: 7) ### Default Login Credentials - Admin: `admin@agenthub.com` / `admin123` @@ -84,11 +84,11 @@ Create `.env` file with: - `is_mailgun_configured()`: Returns False if env vars not set (gracefully disabled) - `send_mailgun_email()`: POST to Mailgun HTTP API with 10s timeout - `build_threshold_email()`: HTML email template for threshold alerts -- `check_and_notify_threshold()`: Checks token usage against threshold, enforces cooldown via `token_notifications` collection, sends to admin users +- `check_and_notify_threshold()`: Checks 7-day token usage against threshold, enforces cooldown via `token_notifications` collection, sends to admin users - `send_client_agent_notification()`: Sends email when client-facing agent is created (to `CLIENT_AGENT_NOTIFY_EMAILS`) - `build_client_agent_email()`: HTML email template for client agent notifications -- `send_daily_agent_digest()`: Queries agents created in last 24 hours, sends summary to all admin users -- `build_daily_digest_email()`: HTML email template for daily digest +- `send_weekly_agent_digest()`: Queries agents created in last 7 days, sends summary to all admin users +- `build_weekly_digest_email()`: HTML email template for weekly digest **auth.py**: JWT authentication with: - bcrypt password hashing @@ -154,12 +154,12 @@ Located in `templates/` directory: - Body includes: Agent Name, Description, Purpose, Client Name, Studio Name, Tool, Created By - Non-blocking: failure does not break agent creation -### Daily Agent Digest Email -- Scheduled via APScheduler at configured hour (default 7:00 AM, `DAILY_DIGEST_HOUR` env var) -- Queries agents created in last 24 hours +### Weekly Agent Digest Email +- Scheduled via APScheduler to run every Monday morning (default 7:00 AM, `WEEKLY_DIGEST_HOUR` env var) +- Queries agents created in last 7 days - Sends to all active admin users - Body includes: Agent Name, Purpose, Description, Created By (email) -- Subject: "Agents Created Last 24 Hours" +- Subject: "Agents Created in Last Week" - Skips sending if no agents created - Can be manually triggered via `POST /api/admin/digest/send` (admin-only) @@ -194,7 +194,8 @@ Located in `templates/` directory: ### High Usage Email Notifications - Entirely optional — silently disabled when Mailgun env vars are not set -- Triggered from the Agent Collector endpoint (POST `/agents`) when `total_tokens` exceeds threshold +- Triggered from the Agent Collector endpoint (POST `/agents`); checks 7-day rolling token usage from `usage_timeline` +- Alerts when weekly token usage exceeds threshold (default 100,000, configurable via `TOKEN_USAGE_THRESHOLD`) - Non-blocking — notification failure never breaks the collector API - Cooldown tracking in MongoDB `token_notifications` collection (default 24h, configurable) - Sends to all active admin users' email addresses @@ -260,7 +261,7 @@ Key dependencies from requirements.txt: - **jinja2**: Template engine - **python-multipart**: Form handling - **requests**: HTTP client (used for Mailgun API calls) -- **apscheduler**: Task scheduling (daily digest email) +- **apscheduler**: Task scheduling (weekly digest email) ## API Endpoints (New) @@ -268,5 +269,5 @@ Key dependencies from requirements.txt: - `GET /api/admin/agents/pending-verification` — List agents with verification status (admin + readonly_admin) - `PUT /api/admin/agents/{agent_id}/verify` — Approve/verify an agent (admin only) -### Daily Digest -- `POST /api/admin/digest/send` — Manually trigger the daily agent digest email (admin only) \ No newline at end of file +### Weekly Digest +- `POST /api/admin/digest/send` — Manually trigger the weekly agent digest email (admin only) \ No newline at end of file diff --git a/main.py b/main.py index 5a36cfc..d898d41 100644 --- a/main.py +++ b/main.py @@ -343,22 +343,23 @@ async def startup_event(): if count > 0: print(f"Pencil Agents migration: updated {count} agent(s)") - # Start daily digest scheduler + # Start weekly digest scheduler (runs Monday mornings) try: from apscheduler.schedulers.asyncio import AsyncIOScheduler - digest_hour = int(os.getenv("DAILY_DIGEST_HOUR", "7")) + digest_hour = int(os.getenv("WEEKLY_DIGEST_HOUR", "7")) scheduler = AsyncIOScheduler() scheduler.add_job( - notifications.send_daily_agent_digest, + notifications.send_weekly_agent_digest, 'cron', + day_of_week='mon', hour=digest_hour, minute=0, - id='daily_agent_digest', + id='weekly_agent_digest', ) scheduler.start() - print(f"Daily digest scheduler started (runs at {digest_hour}:00)") + print(f"Weekly digest scheduler started (runs Monday at {digest_hour}:00)") except Exception as e: - print(f"Warning: Failed to start daily digest scheduler: {e}") + print(f"Warning: Failed to start weekly digest scheduler: {e}") # HTML Routes @app.get("/") @@ -1252,11 +1253,11 @@ async def verify_agent(agent_id: str, current_user: dict = Depends(require_admin raise HTTPException(status_code=500, detail="Failed to verify agent") @app.post("/api/admin/digest/send") -async def trigger_daily_digest(current_user: dict = Depends(require_admin)): - """Manually trigger the daily agent digest email""" +async def trigger_weekly_digest(current_user: dict = Depends(require_admin)): + """Manually trigger the weekly agent digest email""" try: - await notifications.send_daily_agent_digest() - return {"message": "Daily digest sent successfully"} + await notifications.send_weekly_agent_digest() + return {"message": "Weekly digest sent successfully"} except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to send digest: {str(e)}") @@ -1639,12 +1640,11 @@ async def create_agent_collector( ) print(f"🔗 URL DEBUG: Agent '{agent.name}' - Update executed: matched={result.matched_count}, modified={result.modified_count}") - # Check token threshold and send notification if needed (non-blocking) - if internal_data.get("total_tokens"): - try: - await notifications.check_and_notify_threshold(agent.name, internal_data["total_tokens"]) - except Exception as notify_err: - print(f"Notification check failed for '{agent.name}': {notify_err}") + # Check weekly token threshold and send notification if needed (non-blocking) + try: + await notifications.check_and_notify_threshold(agent.name) + except Exception as notify_err: + print(f"Notification check failed for '{agent.name}': {notify_err}") return models.AgentUsageTrackingResponse( status="usage_logged", @@ -1664,12 +1664,11 @@ async def create_agent_collector( # Create agent using collector-specific function created_agent = await crud.create_agent_from_collector(internal_data) - # Check token threshold and send notification if needed (non-blocking) - if internal_data.get("total_tokens"): - try: - await notifications.check_and_notify_threshold(agent.name, internal_data["total_tokens"]) - except Exception as notify_err: - print(f"Notification check failed for '{agent.name}': {notify_err}") + # Check weekly token threshold and send notification if needed (non-blocking) + try: + await notifications.check_and_notify_threshold(agent.name) + except Exception as notify_err: + print(f"Notification check failed for '{agent.name}': {notify_err}") return models.AgentCollectorResponse( status="success", diff --git a/notifications.py b/notifications.py index 75ca028..bd3e9e7 100644 --- a/notifications.py +++ b/notifications.py @@ -29,32 +29,32 @@ def send_mailgun_email(to_emails: list[str], subject: str, html_body: str) -> bo return response.status_code == 200 -def build_threshold_email(agent_name: str, total_tokens: int, threshold: int) -> str: - """Build HTML email body for a token threshold notification.""" +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"""
-

AgentHub - High Token Usage Alert

+

AgentHub - High Weekly Token Usage Alert

-

The following agent has exceeded the token usage threshold:

+

The following agent has exceeded the weekly token usage threshold:

- - + + - +
Agent Name {agent_name}
Total Tokens{total_tokens:,}Tokens (Last 7 Days){weekly_tokens:,}
ThresholdWeekly Threshold {threshold:,}

- This is an automated notification from AgentHub. Please review the agent's token consumption. + This is an automated notification from AgentHub. Please review the agent's recent token consumption.

@@ -62,13 +62,29 @@ def build_threshold_email(agent_name: str, total_tokens: int, threshold: int) -> """ -async def check_and_notify_threshold(agent_name: str, total_tokens: int): - """Check token threshold and send notification if exceeded. Non-blocking, safe to call.""" +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")) - if total_tokens < threshold: + + # 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")) @@ -92,14 +108,14 @@ async def check_and_notify_threshold(agent_name: str, total_tokens: int): return # Send email - subject = f"[AgentHub] High Token Usage: {agent_name} ({total_tokens:,} tokens)" - html_body = build_threshold_email(agent_name, total_tokens, threshold) + 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, - "total_tokens": total_tokens, + "weekly_tokens": weekly_tokens, "threshold": threshold, "recipients": admin_emails, "success": success, @@ -181,8 +197,8 @@ def send_client_agent_notification(agent_data: dict): print(f"Failed to send client agent notification: {e}") -def build_daily_digest_email(agents: list) -> str: - """Build HTML email body for the daily agent digest.""" +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""" @@ -200,10 +216,10 @@ def build_daily_digest_email(agents: list) -> str:
-

Agents Created Last 24 Hours

+

Agents Created in Last Week

-

{len(agents)} agent(s) created in the last 24 hours:

+

{len(agents)} agent(s) created in the last week:

{rows}
@@ -216,15 +232,15 @@ def build_daily_digest_email(agents: list) -> str: """ -async def send_daily_agent_digest(): - """Send daily digest of agents created in the last 24 hours to all admins.""" +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("Daily digest: Mailgun not configured, skipping.") + print("Weekly digest: Mailgun not configured, skipping.") return - cutoff = datetime.utcnow() - timedelta(hours=24) + cutoff = datetime.utcnow() - timedelta(days=7) - # Find agents created in last 24 hours + # 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} @@ -232,7 +248,7 @@ async def send_daily_agent_digest(): agents = await cursor.to_list(length=None) if not agents: - print("Daily digest: No agents created in last 24 hours, skipping.") + print("Weekly digest: No agents created in last 7 days, skipping.") return # Resolve creator emails @@ -255,14 +271,14 @@ async def send_daily_agent_digest(): ) admin_emails = [doc["email"] async for doc in admin_cursor] if not admin_emails: - print("Daily digest: No admin emails found, skipping.") + print("Weekly digest: No admin emails found, skipping.") return - subject = "Agents Created Last 24 Hours" - html_body = build_daily_digest_email(agents) + 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"Daily digest: Sent to {len(admin_emails)} admins, success={success}") + print(f"Weekly digest: Sent to {len(admin_emails)} admins, success={success}") except Exception as e: - print(f"Daily digest: Failed to send: {e}") + print(f"Weekly digest: Failed to send: {e}")