Skip to content
Closed
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
182927a
feat(platform): add nightly copilot automation flow
Swiftyos Mar 13, 2026
97d83aa
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 13, 2026
c773fac
refactor(platform): clean up copilot typing and session flows
Swiftyos Mar 13, 2026
5f574a5
PR Comments
Swiftyos Mar 16, 2026
72856b0
fix(backend): load autopilot prompts from langfuse
Swiftyos Mar 16, 2026
050dcd0
feat(platform): add admin copilot manual triggers
Swiftyos Mar 16, 2026
43d7110
fix tests
Swiftyos Mar 16, 2026
5fe35fd
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 16, 2026
2456882
fix(backend): address PR review comments for nightly copilot
Swiftyos Mar 16, 2026
2246799
fix(backend): fix email_test broken by postmark_sender_email guard
Swiftyos Mar 16, 2026
f13e4f6
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 16, 2026
dfda583
added admin email sending. also minor fixes
Swiftyos Mar 16, 2026
2f6f029
updating email templates
Swiftyos Mar 16, 2026
e0e7b12
Merge branch 'swiftyos/nightly-autopilot' of github.com:Significant-G…
Swiftyos Mar 16, 2026
9b6903b
fmt
Swiftyos Mar 16, 2026
f17e970
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
478e805
fmt
Swiftyos Mar 17, 2026
de62131
fmt
Swiftyos Mar 17, 2026
d5d56dc
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
99cd6aa
fix(backend): TOCTOU race, XSS, type safety, and query correctness in…
Swiftyos Mar 17, 2026
0353d79
update poetry lock
Swiftyos Mar 17, 2026
34d8dec
fmt
Swiftyos Mar 17, 2026
20ed8e1
Squash merge claude/confident-shockley into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
0b66ec1
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
bf0eec2
refactor(platform): revert non-essential refactoring from nightly cop…
Swiftyos Mar 17, 2026
a6fba9a
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
e1cd29f
Merge branch 'claude/festive-gates' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
eeade34
refactor(platform): finish nightly copilot cleanup
Swiftyos Mar 17, 2026
4de43fd
refactor(platform): trim unused email helper
Swiftyos Mar 17, 2026
2fbe65c
fix(backend): restore chat workspace file lookup
Swiftyos Mar 17, 2026
7340c10
dx(platform): sync staged schema and test updates
Swiftyos Mar 17, 2026
3771d8c
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
54ae458
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
a3ea52d
fmt
Swiftyos Mar 17, 2026
a870060
refactor(backend): tighten autopilot internals
Swiftyos Mar 17, 2026
7c54c51
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
7a40cff
fix(backend): stop retrying failed autopilot emails
Swiftyos Mar 17, 2026
3a23cd3
refactor(backend): address autopilot review nits
Swiftyos Mar 17, 2026
9115541
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
c6f5090
Merge branch 'dev' into swiftyos/nightly-autopilot
Swiftyos Mar 17, 2026
9f83b85
fix test
Swiftyos Mar 17, 2026
7eee04f
fix linting issues due to upstream async change
Swiftyos Mar 17, 2026
8b43087
fix(backend): resolve forward references in dynamically created Pydan…
Swiftyos Mar 17, 2026
a57c2c7
fix serialisation
Swiftyos Mar 17, 2026
bb0c25d
fixed ipc issue due to sential types
Swiftyos Mar 17, 2026
6868a54
fix(backend): update notification test mocks for async send_templated…
Swiftyos Mar 18, 2026
3554583
adding log messages
Swiftyos Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions autogpt_platform/backend/backend/api/features/admin/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import prisma.enums
from pydantic import BaseModel, EmailStr

from backend.copilot.session_types import ChatSessionStartType
from backend.data.model import UserTransaction
from backend.util.models import Pagination

if TYPE_CHECKING:
from backend.data.invited_user import BulkInvitedUsersResult, InvitedUserRecord
from backend.data.model import User


class UserHistoryResponse(BaseModel):
Expand Down Expand Up @@ -90,3 +92,51 @@ def from_result(cls, result: BulkInvitedUsersResult) -> BulkInvitedUsersResponse
for row in result.results
],
)


class AdminCopilotUserSummary(BaseModel):
id: str
email: str
name: Optional[str] = None
timezone: str
created_at: datetime
updated_at: datetime

@classmethod
def from_user(cls, user: "User") -> "AdminCopilotUserSummary":
return cls(
id=user.id,
email=user.email,
name=user.name,
timezone=user.timezone,
created_at=user.created_at,
updated_at=user.updated_at,
)


class AdminCopilotUsersResponse(BaseModel):
users: list[AdminCopilotUserSummary]


class TriggerCopilotSessionRequest(BaseModel):
user_id: str
start_type: ChatSessionStartType


class TriggerCopilotSessionResponse(BaseModel):
session_id: str
start_type: ChatSessionStartType


class SendCopilotEmailsRequest(BaseModel):
user_id: str


class SendCopilotEmailsResponse(BaseModel):
candidate_count: int
processed_count: int
sent_count: int
skipped_count: int
repair_queued_count: int
running_count: int
failed_count: int
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
import math

from autogpt_libs.auth import get_user_id, requires_admin_user
from fastapi import APIRouter, File, Query, Security, UploadFile
from fastapi import APIRouter, File, HTTPException, Query, Security, UploadFile

from backend.copilot.autopilot import (
send_pending_copilot_emails_for_user,
trigger_autopilot_session_for_user,
)
from backend.data.invited_user import (
bulk_create_invited_users_from_file,
create_invited_user,
Expand All @@ -12,13 +16,20 @@
revoke_invited_user,
)
from backend.data.tally import mask_email
from backend.data.user import search_users
from backend.util.models import Pagination

from .model import (
AdminCopilotUsersResponse,
AdminCopilotUserSummary,
BulkInvitedUsersResponse,
CreateInvitedUserRequest,
InvitedUserResponse,
InvitedUsersResponse,
SendCopilotEmailsRequest,
SendCopilotEmailsResponse,
TriggerCopilotSessionRequest,
TriggerCopilotSessionResponse,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -135,3 +146,95 @@ async def retry_invited_user_tally_route(
invited_user_id,
)
return InvitedUserResponse.from_record(invited_user)


@router.get(
"/copilot/users",
response_model=AdminCopilotUsersResponse,
summary="Search Copilot Users",
operation_id="getV2SearchCopilotUsers",
)
async def search_copilot_users_route(
search: str = Query("", description="Search by email, name, or user ID"),
limit: int = Query(20, ge=1, le=50),
admin_user_id: str = Security(get_user_id),
) -> AdminCopilotUsersResponse:
logger.info(
"Admin user %s searched Copilot users (query_length=%s, limit=%s)",
admin_user_id,
len(search.strip()),
limit,
)
users = await search_users(search, limit=limit)
return AdminCopilotUsersResponse(
users=[AdminCopilotUserSummary.from_user(user) for user in users]
)


@router.post(
"/copilot/trigger",
response_model=TriggerCopilotSessionResponse,
summary="Trigger Copilot Session",
operation_id="postV2TriggerCopilotSession",
)
async def trigger_copilot_session_route(
request: TriggerCopilotSessionRequest,
admin_user_id: str = Security(get_user_id),
) -> TriggerCopilotSessionResponse:
logger.info(
"Admin user %s manually triggered %s for user %s",
admin_user_id,
request.start_type,
request.user_id,
)
try:
session = await trigger_autopilot_session_for_user(
request.user_id,
start_type=request.start_type,
)
except LookupError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc

logger.info(
"Admin user %s created manual Copilot session %s for user %s",
admin_user_id,
session.session_id,
request.user_id,
)
return TriggerCopilotSessionResponse(
session_id=session.session_id,
start_type=request.start_type,
)


@router.post(
"/copilot/send-emails",
response_model=SendCopilotEmailsResponse,
summary="Send Pending Copilot Emails",
operation_id="postV2SendPendingCopilotEmails",
)
async def send_pending_copilot_emails_route(
request: SendCopilotEmailsRequest,
admin_user_id: str = Security(get_user_id),
) -> SendCopilotEmailsResponse:
logger.info(
"Admin user %s manually triggered pending Copilot emails for user %s",
admin_user_id,
request.user_id,
)
result = await send_pending_copilot_emails_for_user(request.user_id)
logger.info(
"Admin user %s completed pending Copilot email sweep for user %s "
"(candidates=%s, sent=%s, skipped=%s, repairs=%s, running=%s, failed=%s)",
admin_user_id,
request.user_id,
result.candidate_count,
result.sent_count,
result.skipped_count,
result.repair_queued_count,
result.running_count,
result.failed_count,
)
return SendCopilotEmailsResponse(**result.model_dump())
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
import pytest_mock
from autogpt_libs.auth.jwt_utils import get_jwt_payload

from backend.copilot.autopilot_email import PendingCopilotEmailSweepResult
from backend.copilot.model import ChatSession
from backend.copilot.session_types import ChatSessionStartType
from backend.data.invited_user import (
BulkInvitedUserRowResult,
BulkInvitedUsersResult,
InvitedUserRecord,
)
from backend.data.model import User

from .user_admin_routes import router as user_admin_router

Expand Down Expand Up @@ -72,6 +76,20 @@ def _sample_bulk_invited_users_result() -> BulkInvitedUsersResult:
)


def _sample_user() -> User:
now = datetime.now(timezone.utc)
return User(
id="user-1",
email="copilot@example.com",
name="Copilot User",
timezone="Europe/Madrid",
created_at=now,
updated_at=now,
stripe_customer_id=None,
top_up_config=None,
)


def test_get_invited_users(
mocker: pytest_mock.MockerFixture,
) -> None:
Expand Down Expand Up @@ -166,3 +184,107 @@ def test_retry_invited_user_tally(

assert response.status_code == 200
assert response.json()["tally_status"] == "RUNNING"


def test_search_copilot_users(
mocker: pytest_mock.MockerFixture,
) -> None:
mocker.patch(
"backend.api.features.admin.user_admin_routes.search_users",
AsyncMock(return_value=[_sample_user()]),
)

response = client.get("/admin/copilot/users", params={"search": "copilot"})

assert response.status_code == 200
data = response.json()
assert len(data["users"]) == 1
assert data["users"][0]["email"] == "copilot@example.com"
assert data["users"][0]["timezone"] == "Europe/Madrid"


def test_trigger_copilot_session(
mocker: pytest_mock.MockerFixture,
) -> None:
session = ChatSession.new(
"user-1",
start_type=ChatSessionStartType.AUTOPILOT_CALLBACK,
)
trigger = mocker.patch(
"backend.api.features.admin.user_admin_routes.trigger_autopilot_session_for_user",
AsyncMock(return_value=session),
)

response = client.post(
"/admin/copilot/trigger",
json={
"user_id": "user-1",
"start_type": ChatSessionStartType.AUTOPILOT_CALLBACK.value,
},
)

assert response.status_code == 200
assert response.json()["session_id"] == session.session_id
assert response.json()["start_type"] == "AUTOPILOT_CALLBACK"
assert trigger.await_args is not None
assert trigger.await_args.args[0] == "user-1"
assert (
trigger.await_args.kwargs["start_type"]
== ChatSessionStartType.AUTOPILOT_CALLBACK
)


def test_trigger_copilot_session_returns_not_found(
mocker: pytest_mock.MockerFixture,
) -> None:
mocker.patch(
"backend.api.features.admin.user_admin_routes.trigger_autopilot_session_for_user",
AsyncMock(side_effect=LookupError("User not found with ID: missing-user")),
)

response = client.post(
"/admin/copilot/trigger",
json={
"user_id": "missing-user",
"start_type": ChatSessionStartType.AUTOPILOT_NIGHTLY.value,
},
)

assert response.status_code == 404
assert response.json()["detail"] == "User not found with ID: missing-user"


def test_send_pending_copilot_emails(
mocker: pytest_mock.MockerFixture,
) -> None:
send_emails = mocker.patch(
"backend.api.features.admin.user_admin_routes.send_pending_copilot_emails_for_user",
AsyncMock(
return_value=PendingCopilotEmailSweepResult(
candidate_count=1,
processed_count=1,
sent_count=1,
skipped_count=0,
repair_queued_count=0,
running_count=0,
failed_count=0,
)
),
)

response = client.post(
"/admin/copilot/send-emails",
json={"user_id": "user-1"},
)

assert response.status_code == 200
assert response.json() == {
"candidate_count": 1,
"processed_count": 1,
"sent_count": 1,
"skipped_count": 0,
"repair_queued_count": 0,
"running_count": 0,
"failed_count": 0,
}
send_emails.assert_awaited_once_with("user-1")
Loading
Loading