mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 07:08:09 -05:00
feat(backend,frontend): Send applicant email on review response (#10718)
### Changes 🏗️ This PR implements email notifications for agent creators when their agent submissions are approved or rejected by an admin in the marketplace. Specifically, the changes include: - Added `AGENT_APPROVED` and `AGENT_REJECTED` notification types to `schema.prisma`. - Created `AgentApprovalData` and `AgentRejectionData` Pydantic models for notification data. - Configured the notification system to use immediate queues and new Jinja2 templates for these types. - Designed two new email templates: `agent_approved.html.jinja2` and `agent_rejected.html.jinja2`, with dynamic content for agent details, reviewer feedback, and relevant action links. - Modified the `review_store_submission` function to: - Include `User` and `Reviewer` data in the database query. - Construct and queue the appropriate email notification based on the approval/rejection status. - Ensure email sending failures do not block the agent review process. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Approve an agent via the admin dashboard. - [x] Verify the agent creator receives an "Agent Approved" email with correct details and a link to the store. - [x] Reject an agent via the admin dashboard (providing a reason). - [x] Verify the agent creator receives an "Agent Rejected" email with correct details, the rejection reason, and a link to resubmit. - [x] Verify that if email sending fails (e.g., misconfigured SMTP), the agent approval/rejection process still completes successfully without error. <img width="664" height="975" alt="image" src="https://github.com/user-attachments/assets/d397f2dc-56eb-45ab-877e-b17f1fc234d1" /> <img width="664" height="975" alt="image" src="https://github.com/user-attachments/assets/25597752-f68c-46fe-8888-6c32f5dada01" /> --- Linear Issue: [SECRT-1168](https://linear.app/autogpt/issue/SECRT-1168) <a href="https://cursor.com/background-agent?bcId=bc-7394906c-0341-4bd0-8842-6d9d6f83c56c"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg"> <source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg"> <img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg"> </picture> </a> <a href="https://cursor.com/agents?id=bc-7394906c-0341-4bd0-8842-6d9d6f83c56c"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg"> <source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg"> <img alt="Open in Web" src="https://cursor.com/open-in-web.svg"> </picture> </a> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -181,6 +181,42 @@ class RefundRequestData(BaseNotificationData):
|
||||
balance: int
|
||||
|
||||
|
||||
class AgentApprovalData(BaseNotificationData):
|
||||
agent_name: str
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
reviewer_name: str
|
||||
reviewer_email: str
|
||||
comments: str
|
||||
reviewed_at: datetime
|
||||
store_url: str
|
||||
|
||||
@field_validator("reviewed_at")
|
||||
@classmethod
|
||||
def validate_timezone(cls, value: datetime):
|
||||
if value.tzinfo is None:
|
||||
raise ValueError("datetime must have timezone information")
|
||||
return value
|
||||
|
||||
|
||||
class AgentRejectionData(BaseNotificationData):
|
||||
agent_name: str
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
reviewer_name: str
|
||||
reviewer_email: str
|
||||
comments: str
|
||||
reviewed_at: datetime
|
||||
resubmit_url: str
|
||||
|
||||
@field_validator("reviewed_at")
|
||||
@classmethod
|
||||
def validate_timezone(cls, value: datetime):
|
||||
if value.tzinfo is None:
|
||||
raise ValueError("datetime must have timezone information")
|
||||
return value
|
||||
|
||||
|
||||
NotificationData = Annotated[
|
||||
Union[
|
||||
AgentRunData,
|
||||
@@ -240,6 +276,8 @@ def get_notif_data_type(
|
||||
NotificationType.MONTHLY_SUMMARY: MonthlySummaryData,
|
||||
NotificationType.REFUND_REQUEST: RefundRequestData,
|
||||
NotificationType.REFUND_PROCESSED: RefundRequestData,
|
||||
NotificationType.AGENT_APPROVED: AgentApprovalData,
|
||||
NotificationType.AGENT_REJECTED: AgentRejectionData,
|
||||
}[notification_type]
|
||||
|
||||
|
||||
@@ -283,6 +321,8 @@ class NotificationTypeOverride:
|
||||
NotificationType.MONTHLY_SUMMARY: QueueType.SUMMARY,
|
||||
NotificationType.REFUND_REQUEST: QueueType.ADMIN,
|
||||
NotificationType.REFUND_PROCESSED: QueueType.ADMIN,
|
||||
NotificationType.AGENT_APPROVED: QueueType.IMMEDIATE,
|
||||
NotificationType.AGENT_REJECTED: QueueType.IMMEDIATE,
|
||||
}
|
||||
return BATCHING_RULES.get(self.notification_type, QueueType.IMMEDIATE)
|
||||
|
||||
@@ -300,6 +340,8 @@ class NotificationTypeOverride:
|
||||
NotificationType.MONTHLY_SUMMARY: "monthly_summary.html",
|
||||
NotificationType.REFUND_REQUEST: "refund_request.html",
|
||||
NotificationType.REFUND_PROCESSED: "refund_processed.html",
|
||||
NotificationType.AGENT_APPROVED: "agent_approved.html",
|
||||
NotificationType.AGENT_REJECTED: "agent_rejected.html",
|
||||
}[self.notification_type]
|
||||
|
||||
@property
|
||||
@@ -315,6 +357,8 @@ class NotificationTypeOverride:
|
||||
NotificationType.MONTHLY_SUMMARY: "We did a lot this month!",
|
||||
NotificationType.REFUND_REQUEST: "[ACTION REQUIRED] You got a ${{data.amount / 100}} refund request from {{data.user_name}}",
|
||||
NotificationType.REFUND_PROCESSED: "Refund for ${{data.amount / 100}} to {{data.user_name}} has been processed",
|
||||
NotificationType.AGENT_APPROVED: "🎉 Your agent '{{data.agent_name}}' has been approved!",
|
||||
NotificationType.AGENT_REJECTED: "Your agent '{{data.agent_name}}' needs some updates",
|
||||
}[self.notification_type]
|
||||
|
||||
|
||||
|
||||
151
autogpt_platform/backend/backend/data/notifications_test.py
Normal file
151
autogpt_platform/backend/backend/data/notifications_test.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Tests for notification data models."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from backend.data.notifications import AgentApprovalData, AgentRejectionData
|
||||
|
||||
|
||||
class TestAgentApprovalData:
|
||||
"""Test cases for AgentApprovalData model."""
|
||||
|
||||
def test_valid_agent_approval_data(self):
|
||||
"""Test creating valid AgentApprovalData."""
|
||||
data = AgentApprovalData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
reviewer_name="John Doe",
|
||||
reviewer_email="john@example.com",
|
||||
comments="Great agent, approved!",
|
||||
reviewed_at=datetime.now(timezone.utc),
|
||||
store_url="https://app.autogpt.com/store/test-agent-123",
|
||||
)
|
||||
|
||||
assert data.agent_name == "Test Agent"
|
||||
assert data.agent_id == "test-agent-123"
|
||||
assert data.agent_version == 1
|
||||
assert data.reviewer_name == "John Doe"
|
||||
assert data.reviewer_email == "john@example.com"
|
||||
assert data.comments == "Great agent, approved!"
|
||||
assert data.store_url == "https://app.autogpt.com/store/test-agent-123"
|
||||
assert data.reviewed_at.tzinfo is not None
|
||||
|
||||
def test_agent_approval_data_without_timezone_raises_error(self):
|
||||
"""Test that AgentApprovalData raises error without timezone."""
|
||||
with pytest.raises(
|
||||
ValidationError, match="datetime must have timezone information"
|
||||
):
|
||||
AgentApprovalData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
reviewer_name="John Doe",
|
||||
reviewer_email="john@example.com",
|
||||
comments="Great agent, approved!",
|
||||
reviewed_at=datetime.now(), # No timezone
|
||||
store_url="https://app.autogpt.com/store/test-agent-123",
|
||||
)
|
||||
|
||||
def test_agent_approval_data_with_empty_comments(self):
|
||||
"""Test AgentApprovalData with empty comments."""
|
||||
data = AgentApprovalData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
reviewer_name="John Doe",
|
||||
reviewer_email="john@example.com",
|
||||
comments="", # Empty comments
|
||||
reviewed_at=datetime.now(timezone.utc),
|
||||
store_url="https://app.autogpt.com/store/test-agent-123",
|
||||
)
|
||||
|
||||
assert data.comments == ""
|
||||
|
||||
|
||||
class TestAgentRejectionData:
|
||||
"""Test cases for AgentRejectionData model."""
|
||||
|
||||
def test_valid_agent_rejection_data(self):
|
||||
"""Test creating valid AgentRejectionData."""
|
||||
data = AgentRejectionData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
reviewer_name="Jane Doe",
|
||||
reviewer_email="jane@example.com",
|
||||
comments="Please fix the security issues before resubmitting.",
|
||||
reviewed_at=datetime.now(timezone.utc),
|
||||
resubmit_url="https://app.autogpt.com/build/test-agent-123",
|
||||
)
|
||||
|
||||
assert data.agent_name == "Test Agent"
|
||||
assert data.agent_id == "test-agent-123"
|
||||
assert data.agent_version == 1
|
||||
assert data.reviewer_name == "Jane Doe"
|
||||
assert data.reviewer_email == "jane@example.com"
|
||||
assert data.comments == "Please fix the security issues before resubmitting."
|
||||
assert data.resubmit_url == "https://app.autogpt.com/build/test-agent-123"
|
||||
assert data.reviewed_at.tzinfo is not None
|
||||
|
||||
def test_agent_rejection_data_without_timezone_raises_error(self):
|
||||
"""Test that AgentRejectionData raises error without timezone."""
|
||||
with pytest.raises(
|
||||
ValidationError, match="datetime must have timezone information"
|
||||
):
|
||||
AgentRejectionData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
reviewer_name="Jane Doe",
|
||||
reviewer_email="jane@example.com",
|
||||
comments="Please fix the security issues.",
|
||||
reviewed_at=datetime.now(), # No timezone
|
||||
resubmit_url="https://app.autogpt.com/build/test-agent-123",
|
||||
)
|
||||
|
||||
def test_agent_rejection_data_with_long_comments(self):
|
||||
"""Test AgentRejectionData with long comments."""
|
||||
long_comment = "A" * 1000 # Very long comment
|
||||
data = AgentRejectionData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
reviewer_name="Jane Doe",
|
||||
reviewer_email="jane@example.com",
|
||||
comments=long_comment,
|
||||
reviewed_at=datetime.now(timezone.utc),
|
||||
resubmit_url="https://app.autogpt.com/build/test-agent-123",
|
||||
)
|
||||
|
||||
assert data.comments == long_comment
|
||||
|
||||
def test_model_serialization(self):
|
||||
"""Test that models can be serialized and deserialized."""
|
||||
original_data = AgentRejectionData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
reviewer_name="Jane Doe",
|
||||
reviewer_email="jane@example.com",
|
||||
comments="Please fix the issues.",
|
||||
reviewed_at=datetime.now(timezone.utc),
|
||||
resubmit_url="https://app.autogpt.com/build/test-agent-123",
|
||||
)
|
||||
|
||||
# Serialize to dict
|
||||
data_dict = original_data.model_dump()
|
||||
|
||||
# Deserialize back
|
||||
restored_data = AgentRejectionData.model_validate(data_dict)
|
||||
|
||||
assert restored_data.agent_name == original_data.agent_name
|
||||
assert restored_data.agent_id == original_data.agent_id
|
||||
assert restored_data.agent_version == original_data.agent_version
|
||||
assert restored_data.reviewer_name == original_data.reviewer_name
|
||||
assert restored_data.reviewer_email == original_data.reviewer_email
|
||||
assert restored_data.comments == original_data.comments
|
||||
assert restored_data.reviewed_at == original_data.reviewed_at
|
||||
assert restored_data.resubmit_url == original_data.resubmit_url
|
||||
@@ -208,6 +208,8 @@ async def get_user_notification_preference(user_id: str) -> NotificationPreferen
|
||||
NotificationType.DAILY_SUMMARY: user.notifyOnDailySummary or False,
|
||||
NotificationType.WEEKLY_SUMMARY: user.notifyOnWeeklySummary or False,
|
||||
NotificationType.MONTHLY_SUMMARY: user.notifyOnMonthlySummary or False,
|
||||
NotificationType.AGENT_APPROVED: user.notifyOnAgentApproved or False,
|
||||
NotificationType.AGENT_REJECTED: user.notifyOnAgentRejected or False,
|
||||
}
|
||||
daily_limit = user.maxEmailsPerDay or 3
|
||||
notification_preference = NotificationPreference(
|
||||
@@ -266,6 +268,14 @@ async def update_user_notification_preference(
|
||||
update_data["notifyOnMonthlySummary"] = data.preferences[
|
||||
NotificationType.MONTHLY_SUMMARY
|
||||
]
|
||||
if NotificationType.AGENT_APPROVED in data.preferences:
|
||||
update_data["notifyOnAgentApproved"] = data.preferences[
|
||||
NotificationType.AGENT_APPROVED
|
||||
]
|
||||
if NotificationType.AGENT_REJECTED in data.preferences:
|
||||
update_data["notifyOnAgentRejected"] = data.preferences[
|
||||
NotificationType.AGENT_REJECTED
|
||||
]
|
||||
if data.daily_limit:
|
||||
update_data["maxEmailsPerDay"] = data.daily_limit
|
||||
|
||||
@@ -286,6 +296,8 @@ async def update_user_notification_preference(
|
||||
NotificationType.DAILY_SUMMARY: user.notifyOnDailySummary or True,
|
||||
NotificationType.WEEKLY_SUMMARY: user.notifyOnWeeklySummary or True,
|
||||
NotificationType.MONTHLY_SUMMARY: user.notifyOnMonthlySummary or True,
|
||||
NotificationType.AGENT_APPROVED: user.notifyOnAgentApproved or True,
|
||||
NotificationType.AGENT_REJECTED: user.notifyOnAgentRejected or True,
|
||||
}
|
||||
notification_preference = NotificationPreference(
|
||||
user_id=user.id,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
{# Agent Approved Notification Email Template #}
|
||||
{#
|
||||
Template variables:
|
||||
data.agent_name: the name of the approved agent
|
||||
data.agent_id: the ID of the agent
|
||||
data.agent_version: the version of the agent
|
||||
data.reviewer_name: the name of the reviewer who approved it
|
||||
data.reviewer_email: the email of the reviewer
|
||||
data.comments: comments from the reviewer
|
||||
data.reviewed_at: when the agent was reviewed
|
||||
data.store_url: URL to view the agent in the store
|
||||
|
||||
Subject: 🎉 Your agent '{{ data.agent_name }}' has been approved!
|
||||
#}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="color: #28a745; font-size: 32px; font-weight: 700; margin: 0 0 24px 0; text-align: center;">
|
||||
🎉 Congratulations!
|
||||
</h1>
|
||||
|
||||
<p style="color: #586069; font-size: 18px; text-align: center; margin: 0 0 24px 0;">
|
||||
Your agent <strong>'{{ data.agent_name }}'</strong> has been approved and is now live in the store!
|
||||
</p>
|
||||
|
||||
<div style="height: 32px; background: transparent;"></div>
|
||||
|
||||
{% if data.comments %}
|
||||
<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 20px; margin: 0;">
|
||||
<h3 style="color: #155724; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">
|
||||
💬 Creator feedback area
|
||||
</h3>
|
||||
<p style="color: #155724; margin: 0; font-size: 16px; line-height: 1.5;">
|
||||
{{ data.comments }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="height: 40px; background: transparent;"></div>
|
||||
{% endif %}
|
||||
|
||||
<div style="background: #d1ecf1; border: 1px solid #bee5eb; border-radius: 8px; padding: 20px; margin: 0;">
|
||||
<h3 style="color: #0c5460; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">
|
||||
What's Next?
|
||||
</h3>
|
||||
<ul style="color: #0c5460; margin: 0; padding-left: 18px; font-size: 16px; line-height: 1.6;">
|
||||
<li>Your agent is now live and discoverable in the AutoGPT Store</li>
|
||||
<li>Users can find, install, and run your agent</li>
|
||||
<li>You can update your agent anytime by submitting a new version</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="height: 32px; background: transparent;"></div>
|
||||
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
<a href="{{ data.store_url }}" style="display: inline-block; background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); color: black; text-decoration: none; padding: 14px 28px; border-radius: 6px; font-weight: 600; font-size: 16px;">
|
||||
View Your Agent in Store
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="height: 32px; background: transparent;"></div>
|
||||
|
||||
<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; padding: 16px; margin: 24px 0; text-align: center;">
|
||||
<p style="margin: 0; color: #856404; font-size: 14px;">
|
||||
<strong>💡 Pro Tip:</strong> Share your agent with the community! Post about it on social media, forums, or your blog to help more users discover and benefit from your creation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="height: 32px; background: transparent;"></div>
|
||||
|
||||
<p style="color: #6a737d; font-size: 14px; text-align: center; margin: 24px 0;">
|
||||
Thank you for contributing to the AutoGPT ecosystem! 🚀
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
{# Agent Rejected Notification Email Template #}
|
||||
{#
|
||||
Template variables:
|
||||
data.agent_name: the name of the rejected agent
|
||||
data.agent_id: the ID of the agent
|
||||
data.agent_version: the version of the agent
|
||||
data.reviewer_name: the name of the reviewer who rejected it
|
||||
data.reviewer_email: the email of the reviewer
|
||||
data.comments: comments from the reviewer explaining the rejection
|
||||
data.reviewed_at: when the agent was reviewed
|
||||
data.resubmit_url: URL to resubmit the agent
|
||||
|
||||
Subject: Your agent '{{ data.agent_name }}' needs some updates
|
||||
#}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h1 style="color: #d73a49; font-size: 32px; font-weight: 700; margin: 0 0 24px 0; text-align: center;">
|
||||
📝 Review Complete
|
||||
</h1>
|
||||
|
||||
<p style="color: #586069; font-size: 18px; text-align: center; margin: 0 0 24px 0;">
|
||||
Your agent <strong>'{{ data.agent_name }}'</strong> needs some updates before approval.
|
||||
</p>
|
||||
|
||||
<div style="height: 32px; background: transparent;"></div>
|
||||
|
||||
<div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 8px; padding: 20px; margin: 0 0 24px 0;">
|
||||
<h3 style="color: #721c24; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">
|
||||
💬 Creator feedback area
|
||||
</h3>
|
||||
<p style="color: #721c24; margin: 0; font-size: 16px; line-height: 1.5;">
|
||||
{{ data.comments }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="height: 40px; background: transparent;"></div>
|
||||
|
||||
<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 20px; margin: 0 0 24px 0;">
|
||||
<h3 style="color: #155724; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">
|
||||
☑ Steps to Resubmit:
|
||||
</h3>
|
||||
<ul style="color: #155724; margin: 0; padding-left: 18px; font-size: 16px; line-height: 1.6;">
|
||||
<li>Review the feedback provided above carefully</li>
|
||||
<li>Make the necessary updates to your agent</li>
|
||||
<li>Test your agent thoroughly to ensure it works as expected</li>
|
||||
<li>Submit your updated agent for review</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="height: 32px; background: transparent;"></div>
|
||||
|
||||
<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; padding: 12px; margin: 0 0 24px 0; text-align: center;">
|
||||
<p style="margin: 0; color: #856404; font-size: 14px;">
|
||||
<strong>💡 Tip:</strong> Address all the points mentioned in the feedback to increase your chances of approval in the next review.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 32px 0;">
|
||||
<a href="{{ data.resubmit_url }}" style="display: inline-block; background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); color: black; text-decoration: none; padding: 14px 28px; border-radius: 6px; font-weight: 600; font-size: 16px;">
|
||||
Update & Resubmit Agent
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="background: #d1ecf1; border: 1px solid #bee5eb; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
||||
<p style="margin: 0; color: #0c5460; font-size: 14px; text-align: center;">
|
||||
<strong>🌟 Don't Give Up!</strong> Many successful agents go through multiple iterations before approval. Our review team is here to help you succeed!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="height: 32px; background: transparent;"></div>
|
||||
|
||||
<p style="color: #6a737d; font-size: 14px; text-align: center; margin: 32px 0 24px 0;">
|
||||
We're excited to see your improved agent submission! 🚀
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -17,8 +17,21 @@ from backend.data.graph import (
|
||||
get_sub_graphs,
|
||||
)
|
||||
from backend.data.includes import AGENT_GRAPH_INCLUDE
|
||||
from backend.data.notifications import (
|
||||
AgentApprovalData,
|
||||
AgentRejectionData,
|
||||
NotificationEventModel,
|
||||
)
|
||||
from backend.notifications.notifications import queue_notification_async
|
||||
from backend.util.settings import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = Settings()
|
||||
|
||||
|
||||
# Constants for default admin values
|
||||
DEFAULT_ADMIN_NAME = "AutoGPT Admin"
|
||||
DEFAULT_ADMIN_EMAIL = "admin@autogpt.co"
|
||||
|
||||
|
||||
def sanitize_query(query: str | None) -> str | None:
|
||||
@@ -1241,7 +1254,8 @@ async def review_store_submission(
|
||||
where={"id": store_listing_version_id},
|
||||
include={
|
||||
"StoreListing": True,
|
||||
"AgentGraph": {"include": AGENT_GRAPH_INCLUDE},
|
||||
"AgentGraph": {"include": {**AGENT_GRAPH_INCLUDE, "User": True}},
|
||||
"Reviewer": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -1348,6 +1362,89 @@ async def review_store_submission(
|
||||
f"Failed to update store listing version {store_listing_version_id}"
|
||||
)
|
||||
|
||||
# Send email notification to the agent creator
|
||||
if store_listing_version.AgentGraph and store_listing_version.AgentGraph.User:
|
||||
agent_creator = store_listing_version.AgentGraph.User
|
||||
reviewer = (
|
||||
store_listing_version.Reviewer
|
||||
if store_listing_version.Reviewer
|
||||
else None
|
||||
)
|
||||
|
||||
try:
|
||||
base_url = (
|
||||
settings.config.frontend_base_url
|
||||
or settings.config.platform_base_url
|
||||
)
|
||||
|
||||
if is_approved:
|
||||
store_agent = (
|
||||
await prisma.models.StoreAgent.prisma().find_first_or_raise(
|
||||
where={"storeListingVersionId": submission.id}
|
||||
)
|
||||
)
|
||||
|
||||
# Send approval notification
|
||||
notification_data = AgentApprovalData(
|
||||
agent_name=submission.name,
|
||||
agent_id=submission.agentGraphId,
|
||||
agent_version=submission.agentGraphVersion,
|
||||
reviewer_name=(
|
||||
reviewer.name
|
||||
if reviewer and reviewer.name
|
||||
else DEFAULT_ADMIN_NAME
|
||||
),
|
||||
reviewer_email=(
|
||||
reviewer.email if reviewer else DEFAULT_ADMIN_EMAIL
|
||||
),
|
||||
comments=external_comments,
|
||||
reviewed_at=submission.reviewedAt
|
||||
or datetime.now(tz=timezone.utc),
|
||||
store_url=f"{base_url}/marketplace/agent/{store_agent.creator_username}/{store_agent.slug}",
|
||||
)
|
||||
|
||||
notification_event = NotificationEventModel[AgentApprovalData](
|
||||
user_id=agent_creator.id,
|
||||
type=prisma.enums.NotificationType.AGENT_APPROVED,
|
||||
data=notification_data,
|
||||
)
|
||||
else:
|
||||
# Send rejection notification
|
||||
notification_data = AgentRejectionData(
|
||||
agent_name=submission.name,
|
||||
agent_id=submission.agentGraphId,
|
||||
agent_version=submission.agentGraphVersion,
|
||||
reviewer_name=(
|
||||
reviewer.name
|
||||
if reviewer and reviewer.name
|
||||
else DEFAULT_ADMIN_NAME
|
||||
),
|
||||
reviewer_email=(
|
||||
reviewer.email if reviewer else DEFAULT_ADMIN_EMAIL
|
||||
),
|
||||
comments=external_comments,
|
||||
reviewed_at=submission.reviewedAt
|
||||
or datetime.now(tz=timezone.utc),
|
||||
resubmit_url=f"{base_url}/build?flowID={submission.agentGraphId}",
|
||||
)
|
||||
|
||||
notification_event = NotificationEventModel[AgentRejectionData](
|
||||
user_id=agent_creator.id,
|
||||
type=prisma.enums.NotificationType.AGENT_REJECTED,
|
||||
data=notification_data,
|
||||
)
|
||||
|
||||
# Queue the notification for immediate sending
|
||||
await queue_notification_async(notification_event)
|
||||
logger.info(
|
||||
f"Queued {'approval' if is_approved else 'rejection'} notification for user {agent_creator.id} and agent {submission.name}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email notification for agent review: {e}")
|
||||
# Don't fail the review process if email sending fails
|
||||
pass
|
||||
|
||||
# Convert to Pydantic model for consistency
|
||||
return backend.server.v2.store.model.StoreSubmission(
|
||||
agent_id=submission.agentGraphId,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "NotificationType" ADD VALUE 'AGENT_APPROVED';
|
||||
ALTER TYPE "NotificationType" ADD VALUE 'AGENT_REJECTED';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "notifyOnAgentApproved" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "notifyOnAgentRejected" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -33,6 +33,8 @@ model User {
|
||||
notifyOnDailySummary Boolean @default(true)
|
||||
notifyOnWeeklySummary Boolean @default(true)
|
||||
notifyOnMonthlySummary Boolean @default(true)
|
||||
notifyOnAgentApproved Boolean @default(true)
|
||||
notifyOnAgentRejected Boolean @default(true)
|
||||
|
||||
// Relations
|
||||
|
||||
@@ -187,6 +189,8 @@ enum NotificationType {
|
||||
MONTHLY_SUMMARY
|
||||
REFUND_REQUEST
|
||||
REFUND_PROCESSED
|
||||
AGENT_APPROVED
|
||||
AGENT_REJECTED
|
||||
}
|
||||
|
||||
model NotificationEvent {
|
||||
|
||||
@@ -106,6 +106,60 @@ export function NotificationForm({ preferences, user }: NotificationFormProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Store Notifications */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<Text variant="h4" size="body-medium" className="text-slate-400">
|
||||
Store Notifications
|
||||
</Text>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notifyOnAgentApproved"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Text variant="h4" size="body-medium">
|
||||
Agent Approved
|
||||
</Text>
|
||||
<Text variant="body">
|
||||
Get notified when your submitted agent is approved for the
|
||||
store
|
||||
</Text>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notifyOnAgentRejected"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Text variant="h4" size="body-medium">
|
||||
Agent Rejected
|
||||
</Text>
|
||||
<Text variant="body">
|
||||
Receive notifications when your agent submission needs
|
||||
updates
|
||||
</Text>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Balance Notifications */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Text variant="h4" size="body-medium" className="text-slate-400">
|
||||
|
||||
@@ -18,6 +18,8 @@ const notificationFormSchema = z.object({
|
||||
notifyOnDailySummary: z.boolean(),
|
||||
notifyOnWeeklySummary: z.boolean(),
|
||||
notifyOnMonthlySummary: z.boolean(),
|
||||
notifyOnAgentApproved: z.boolean(),
|
||||
notifyOnAgentRejected: z.boolean(),
|
||||
});
|
||||
|
||||
function createNotificationDefaultValues(preferences: {
|
||||
@@ -34,6 +36,8 @@ function createNotificationDefaultValues(preferences: {
|
||||
notifyOnDailySummary: preferences.preferences?.DAILY_SUMMARY,
|
||||
notifyOnWeeklySummary: preferences.preferences?.WEEKLY_SUMMARY,
|
||||
notifyOnMonthlySummary: preferences.preferences?.MONTHLY_SUMMARY,
|
||||
notifyOnAgentApproved: preferences.preferences?.AGENT_APPROVED,
|
||||
notifyOnAgentRejected: preferences.preferences?.AGENT_REJECTED,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,6 +84,8 @@ export function useNotificationForm({
|
||||
DAILY_SUMMARY: values.notifyOnDailySummary,
|
||||
WEEKLY_SUMMARY: values.notifyOnWeeklySummary,
|
||||
MONTHLY_SUMMARY: values.notifyOnMonthlySummary,
|
||||
AGENT_APPROVED: values.notifyOnAgentApproved,
|
||||
AGENT_REJECTED: values.notifyOnAgentRejected,
|
||||
},
|
||||
daily_limit: 0,
|
||||
};
|
||||
|
||||
@@ -5496,7 +5496,9 @@
|
||||
"WEEKLY_SUMMARY",
|
||||
"MONTHLY_SUMMARY",
|
||||
"REFUND_REQUEST",
|
||||
"REFUND_PROCESSED"
|
||||
"REFUND_PROCESSED",
|
||||
"AGENT_APPROVED",
|
||||
"AGENT_REJECTED"
|
||||
],
|
||||
"title": "NotificationType"
|
||||
},
|
||||
|
||||
@@ -571,7 +571,9 @@ export type NotificationType =
|
||||
| "CONTINUOUS_AGENT_ERROR"
|
||||
| "DAILY_SUMMARY"
|
||||
| "WEEKLY_SUMMARY"
|
||||
| "MONTHLY_SUMMARY";
|
||||
| "MONTHLY_SUMMARY"
|
||||
| "AGENT_APPROVED"
|
||||
| "AGENT_REJECTED";
|
||||
|
||||
// Mirror of backend/backend/data/notifications.py:NotificationPreference
|
||||
export type NotificationPreferenceDTO = {
|
||||
|
||||
Reference in New Issue
Block a user