Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(platform): Implement Auto-Top-Up credits capability #9278

Merged
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
96f850d
Add `useCredits` hook
kcze Dec 11, 2024
d915b80
Top-up section in user profile
kcze Dec 12, 2024
0f917f7
Request top-up endpoint
kcze Dec 12, 2024
2481d3d
Merge branch 'dev' into kpczerwinski/secrt-1012-mvp-implement-top-up-…
kcze Dec 12, 2024
1596671
Top-Up intent logic
kcze Dec 13, 2024
43d0752
Merge branch 'dev' into kpczerwinski/secrt-1012-mvp-implement-top-up-…
kcze Dec 18, 2024
afdb6e2
Install `stripe-js`
kcze Dec 19, 2024
e8fa2a1
Use stripe checkout
kcze Dec 19, 2024
869c31c
Update top up endpoint
kcze Dec 25, 2024
9cddffb
Fulfill checkout function
kcze Dec 27, 2024
76b5259
Handle Stripe webhook events
kcze Dec 28, 2024
b3d7804
Update `credit.py`
kcze Dec 31, 2024
7063751
Fix top-up credit amounts
kcze Dec 31, 2024
27019e4
Move top-up UI to `/store/credits`
kcze Jan 1, 2025
b3de888
Hide Credits tab conditionally
kcze Jan 1, 2025
bb69c5a
Success and cancel topup message
kcze Jan 1, 2025
2caf498
Address feedback
kcze Jan 1, 2025
deddcdd
Client-side `/credits` PATCH
kcze Jan 3, 2025
c6eaa58
Merge branch 'dev' into kpczerwinski/secrt-1012-mvp-implement-top-up-…
kcze Jan 3, 2025
7958143
Cleanup
kcze Jan 3, 2025
4cd2f60
Update `poetry.lock`
kcze Jan 5, 2025
1583fda
Merge branch 'dev' into kpczerwinski/secrt-1012-mvp-implement-top-up-…
majdyz Jan 6, 2025
38b662b
Merge branch 'dev' into kpczerwinski/secrt-1012-mvp-implement-top-up-…
kcze Jan 7, 2025
0c579fd
Add missing line to `poetry.lock`
kcze Jan 7, 2025
5ec40c2
Fix refill value
kcze Jan 7, 2025
f28b298
Address feedback
kcze Jan 7, 2025
c7e994c
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into kp…
majdyz Jan 11, 2025
9c2bc4b
Set default value for `PLATFORM_BASE_URL`
majdyz Jan 11, 2025
427da2f
Set default value for `PLATFORM_BASE_URL`
majdyz Jan 11, 2025
86406cd
Revert unused change
majdyz Jan 11, 2025
5af0648
feat(backend): Add running balance and introduce atomicity on transac…
majdyz Jan 13, 2025
46eb6c8
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into kp…
majdyz Jan 13, 2025
d1dce4b
Merge branch 'kpczerwinski/secrt-1012-mvp-implement-top-up-flow' of g…
majdyz Jan 13, 2025
a5b6393
Refresh migraiton time
majdyz Jan 13, 2025
9e27039
Merge branch 'dev' into kpczerwinski/secrt-1012-mvp-implement-top-up-…
majdyz Jan 13, 2025
732a3a0
feat(platform): Add billing portal entry point
majdyz Jan 13, 2025
43905f5
Merge branch 'kpczerwinski/secrt-1012-mvp-implement-top-up-flow' into…
majdyz Jan 13, 2025
11a62b7
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into za…
majdyz Jan 14, 2025
66fbefb
Reformat
majdyz Jan 14, 2025
601b139
Merge remote-tracking branch 'origin/zamilmajdy/secrt-1018-phase-2-cr…
majdyz Jan 14, 2025
c136013
feat(platform): Implement Auto-Top-Up credits capability
majdyz Jan 15, 2025
bd7d251
feat(platform): Implement Auto-Top-Up credits capability
majdyz Jan 15, 2025
7688224
lint
majdyz Jan 15, 2025
206d1b8
refresh migration date
majdyz Jan 15, 2025
af97f44
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into za…
majdyz Jan 16, 2025
4b1d83f
refresh migration date
majdyz Jan 16, 2025
9349101
Merge branch 'dev' into zamilmajdy/secrt-1018-phase-2-create-payment-…
majdyz Jan 16, 2025
8fbc1f2
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into za…
majdyz Jan 16, 2025
ef3a4ec
Merge branch 'zamilmajdy/secrt-1018-phase-2-create-payment-method-cru…
majdyz Jan 16, 2025
f83c31f
fix
majdyz Jan 16, 2025
d0bce61
fix
majdyz Jan 16, 2025
96c4fea
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into za…
majdyz Jan 16, 2025
495d44e
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
kcze Jan 19, 2025
8dcb52e
Address comments
majdyz Jan 21, 2025
ee3c013
Merge remote-tracking branch 'origin/zamilmajdy/secrt-1017-phase-2-cr…
majdyz Jan 21, 2025
f6f4364
Address comments
majdyz Jan 21, 2025
e26e1d8
Fix test
majdyz Jan 21, 2025
4bb865c
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 22, 2025
b4d7937
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 24, 2025
11f029b
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 24, 2025
23cd852
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 24, 2025
6ec4414
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 25, 2025
0b7d02a
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 26, 2025
0b101c5
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 27, 2025
fc9fd80
Fix failing test
majdyz Jan 27, 2025
6fdb283
Remove new line
majdyz Jan 27, 2025
b2e3d0e
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 27, 2025
1b1d068
Merge branch 'dev' into zamilmajdy/secrt-1017-phase-2-create-auto-top…
majdyz Jan 28, 2025
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
97 changes: 88 additions & 9 deletions autogpt_platform/backend/backend/data/credit.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from abc import ABC, abstractmethod
from datetime import datetime, timezone

Expand All @@ -13,11 +14,13 @@
from backend.data.block_cost_config import BLOCK_COSTS
from backend.data.cost import BlockCost, BlockCostType
from backend.data.execution import NodeExecutionEntry
from backend.data.model import AutoTopUpConfig
from backend.data.user import get_user_by_id
from backend.util.settings import Settings

settings = Settings()
stripe.api_key = settings.secrets.stripe_api_key
logger = logging.getLogger(__name__)


class UserCreditBase(ABC):
Expand Down Expand Up @@ -177,10 +180,11 @@ async def _add_transaction(
is_active: bool = True,
transaction_key: str | None = None,
metadata: Json = Json({}),
):
) -> int:
async with db.locked_transaction(f"usr_trx_{user_id}"):
# Get latest balance snapshot
user_balance, _ = await self._get_credits(user_id)
user_balance = await self.get_credits(user_id)

if amount < 0 and user_balance < abs(amount):
raise ValueError(
f"Insufficient balance for user {user_id}, balance: {user_balance}, amount: {amount}"
Expand Down Expand Up @@ -271,7 +275,7 @@ async def spend_credits(
if cost == 0:
return 0

await self._add_transaction(
balance = await self._add_transaction(
user_id=entry.user_id,
amount=-cost,
transaction_type=CreditTransactionType.USAGE,
Expand All @@ -287,17 +291,73 @@ async def spend_credits(
}
),
)
user_id = entry.user_id

# Auto top-up if balance just went below threshold due to this transaction.
auto_top_up = await get_auto_top_up(user_id)
if balance < auto_top_up.threshold <= balance - cost:
try:
await self.top_up_credits(user_id=user_id, amount=auto_top_up.amount)
except Exception as e:
# Failed top-up is not critical, we can move on.
logger.error(
f"Auto top-up failed for user {user_id}, balance: {balance}, amount: {auto_top_up.amount}, error: {e}"
)

return cost

async def top_up_credits(self, user_id: str, amount: int):
if amount < 0:
raise ValueError(f"Top up amount must not be negative: {amount}")

await self._add_transaction(
user_id=user_id,
amount=amount,
transaction_type=CreditTransactionType.TOP_UP,
customer_id = await get_stripe_customer_id(user_id)

payment_methods = stripe.PaymentMethod.list(customer=customer_id, type="card")
if not payment_methods:
raise ValueError("No payment method found, please add it on the platform.")

for payment_method in payment_methods:
if amount == 0:
setup_intent = stripe.SetupIntent.create(
customer=customer_id,
usage="off_session",
confirm=True,
payment_method=payment_method.id,
automatic_payment_methods={
"enabled": True,
"allow_redirects": "never",
},
)
if setup_intent.status == "succeeded":
return

else:
payment_intent = stripe.PaymentIntent.create(
amount=amount,
currency="usd",
description="AutoGPT Platform Credits",
customer=customer_id,
off_session=True,
confirm=True,
payment_method=payment_method.id,
automatic_payment_methods={
"enabled": True,
"allow_redirects": "never",
},
)
if payment_intent.status == "succeeded":
await self._add_transaction(
user_id=user_id,
amount=amount,
transaction_type=CreditTransactionType.TOP_UP,
transaction_key=payment_intent.id,
metadata=Json({"payment_intent": payment_intent}),
is_active=True,
)
return

raise ValueError(
f"Out of {len(payment_methods)} payment methods tried, none is supported"
)

async def top_up_intent(self, user_id: str, amount: int) -> str:
Expand All @@ -320,13 +380,14 @@ async def top_up_intent(self, user_id: str, amount: int) -> str:
}
],
mode="payment",
payment_intent_data={"setup_future_usage": "off_session"},
saved_payment_method_options={"payment_method_save": "enabled"},
success_url=settings.config.platform_base_url
+ "/marketplace/credits?topup=success",
cancel_url=settings.config.platform_base_url
+ "/marketplace/credits?topup=cancel",
)

# Create pending transaction
await self._add_transaction(
user_id=user_id,
amount=amount,
Expand Down Expand Up @@ -356,7 +417,7 @@ async def fulfill_checkout(
find_filter["userId"] = user_id

# Find the most recent inactive top-up transaction
credit_transaction = await CreditTransaction.prisma().find_first_or_raise(
credit_transaction = await CreditTransaction.prisma().find_first(
where=find_filter,
order={"createdAt": "desc"},
)
Expand Down Expand Up @@ -455,3 +516,21 @@ async def get_stripe_customer_id(user_id: str) -> str:
where={"id": user_id}, data={"stripeCustomerId": customer.id}
)
return customer.id


async def set_auto_top_up(user_id: str, threshold: int, amount: int):
await User.prisma().update(
where={"id": user_id},
data={"topUpConfig": Json({"threshold": threshold, "amount": amount})},
)


async def get_auto_top_up(user_id: str) -> AutoTopUpConfig:
user = await get_user_by_id(user_id)
if not user:
raise ValueError("Invalid user ID")

if not user.topUpConfig:
return AutoTopUpConfig(threshold=0, amount=0)

return AutoTopUpConfig.model_validate(user.topUpConfig)
7 changes: 7 additions & 0 deletions autogpt_platform/backend/backend/data/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,10 @@ def CredentialsField(

class ContributorDetails(BaseModel):
name: str = Field(title="Name", description="The name of the contributor.")


class AutoTopUpConfig(BaseModel):
amount: int
"""Amount of credits to top up."""
threshold: int
"""Threshold to trigger auto top up."""
39 changes: 38 additions & 1 deletion autogpt_platform/backend/backend/server/routers/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@
)
from backend.data.block import BlockInput, CompletedBlockOutput
from backend.data.credit import (
AutoTopUpConfig,
get_auto_top_up,
get_block_costs,
get_stripe_customer_id,
get_user_credit_model,
set_auto_top_up,
)
from backend.data.user import get_or_create_user
from backend.executor import ExecutionManager, ExecutionScheduler, scheduler
Expand Down Expand Up @@ -71,7 +74,6 @@ def execution_scheduler_client() -> ExecutionScheduler:
logger = logging.getLogger(__name__)
integration_creds_manager = IntegrationCredentialsManager()


_user_credit_model = get_user_credit_model()

# Define the API routes
Expand Down Expand Up @@ -161,6 +163,41 @@ async def fulfill_checkout(user_id: Annotated[str, Depends(get_user_id)]):
return Response(status_code=200)


@v1_router.post(
path="/credits/auto-top-up",
tags=["credits"],
dependencies=[Depends(auth_middleware)],
)
async def configure_user_auto_top_up(
request: AutoTopUpConfig, user_id: Annotated[str, Depends(get_user_id)]
) -> str:
if request.threshold < 0:
raise ValueError("Threshold must be greater than 0")
if request.amount < request.threshold:
raise ValueError("Amount must be greater than or equal to threshold")
majdyz marked this conversation as resolved.
Show resolved Hide resolved

current_balance = await _user_credit_model.get_credits(user_id)

if current_balance < request.threshold:
await _user_credit_model.top_up_credits(user_id, request.amount)
else:
await _user_credit_model.top_up_credits(user_id, 0)

await set_auto_top_up(user_id, threshold=request.threshold, amount=request.amount)
return "Auto top-up settings updated"


@v1_router.get(
path="/credits/auto-top-up",
tags=["credits"],
dependencies=[Depends(auth_middleware)],
)
async def get_user_auto_top_up(
user_id: Annotated[str, Depends(get_user_id)]
) -> AutoTopUpConfig:
return await get_auto_top_up(user_id)


@v1_router.post(path="/credits/stripe_webhook", tags=["credits"])
async def stripe_webhook(request: Request):
# Get the raw request body
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "topUpConfig" JSONB;
1 change: 1 addition & 0 deletions autogpt_platform/backend/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ model User {
metadata Json @default("{}")
integrations String @default("")
stripeCustomerId String?
topUpConfig Json?
majdyz marked this conversation as resolved.
Show resolved Hide resolved

// Relations
AgentGraphs AgentGraph[]
Expand Down
15 changes: 12 additions & 3 deletions autogpt_platform/backend/test/data/test_credit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime, timezone

import pytest
from prisma.enums import CreditTransactionType
from prisma.models import CreditTransaction

from backend.blocks.llm import AITextGeneratorBlock
Expand All @@ -18,10 +19,18 @@ async def disable_test_user_transactions():
await CreditTransaction.prisma().delete_many(where={"userId": DEFAULT_USER_ID})


async def top_up(amount: int):
await user_credit._add_transaction(
DEFAULT_USER_ID,
amount,
CreditTransactionType.TOP_UP,
)


@pytest.mark.asyncio(scope="session")
async def test_block_credit_usage(server: SpinTestServer):
await disable_test_user_transactions()
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
await top_up(100)
current_credit = await user_credit.get_credits(DEFAULT_USER_ID)

spending_amount_1 = await user_credit.spend_credits(
Expand Down Expand Up @@ -70,7 +79,7 @@ async def test_block_credit_top_up(server: SpinTestServer):
await disable_test_user_transactions()
current_credit = await user_credit.get_credits(DEFAULT_USER_ID)

await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
await top_up(100)

new_credit = await user_credit.get_credits(DEFAULT_USER_ID)
assert new_credit == current_credit + 100
Expand All @@ -89,7 +98,7 @@ async def test_block_credit_reset(server: SpinTestServer):
# Month 1 result should only affect month 1
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(month=month1)
month1credit = await user_credit.get_credits(DEFAULT_USER_ID)
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
await top_up(100)
assert await user_credit.get_credits(DEFAULT_USER_ID) == month1credit + 100

# Month 2 balance is unaffected
Expand Down
Loading
Loading