feat(platform): setup and configure orval (#10209)

This pull request sets up and configures Orval for API client
generation. It automates the process of creating TypeScript clients from
the backend's OpenAPI specification, improving development efficiency
and reducing manual code maintenance.

### Changes 🏗️

- Configures Orval with a new configuration file (`orval.config.ts`).
- Adds scripts to `package.json` for fetching the OpenAPI spec and
generating the API client.
- Implements a custom mutator for handling authentication.
- Adds API client generation as a step in the CI workflow.
- Adds `.gitignore` entry for generated API client files.
- Adds a security middleware to prevent caching of sensitive data.

### 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] Verified that the API client is generated correctly.
- [x] Confirmed that the custom mutator is functioning as expected for
authentication.
- [x] Ensured that the new CI workflow step for API client generation is
successful.
  - [x] Tested generated API calls

#### For configuration changes:
- [x] `.env.example` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [x] I have included a list of my configuration changes in the PR
description (under **Changes**)
This commit is contained in:
Abhimanyu Yadav
2025-06-24 19:30:19 +05:30
committed by GitHub
parent e701f41e66
commit 94aed94113
21 changed files with 7704 additions and 35 deletions

View File

@@ -55,6 +55,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Generate API client
run: pnpm generate:api-client
- name: Run tsc check - name: Run tsc check
run: pnpm type-check run: pnpm type-check

5
.gitignore vendored
View File

@@ -165,7 +165,7 @@ package-lock.json
# Allow for locally private items # Allow for locally private items
# private # private
pri* pri*
# ignore # ignore
ig* ig*
.github_access_token .github_access_token
@@ -177,3 +177,6 @@ autogpt_platform/backend/settings.py
*.ign.* *.ign.*
.test-contents .test-contents
.claude/settings.local.json .claude/settings.local.json
# Auto generated client
autogpt_platform/frontend/src/api/__generated__

View File

@@ -62,6 +62,12 @@ To run the AutoGPT Platform, follow these steps:
pnpm i pnpm i
``` ```
Generate the API client (this step is required before running the frontend):
```
pnpm generate:api-client
```
Then start the frontend application in development mode: Then start the frontend application in development mode:
``` ```
@@ -164,3 +170,27 @@ To persist data for PostgreSQL and Redis, you can modify the `docker-compose.yml
3. Save the file and run `docker compose up -d` to apply the changes. 3. Save the file and run `docker compose up -d` to apply the changes.
This configuration will create named volumes for PostgreSQL and Redis, ensuring that your data persists across container restarts. This configuration will create named volumes for PostgreSQL and Redis, ensuring that your data persists across container restarts.
### API Client Generation
The platform includes scripts for generating and managing the API client:
- `pnpm fetch:openapi`: Fetches the OpenAPI specification from the backend service (requires backend to be running on port 8006)
- `pnpm generate:api-client`: Generates the TypeScript API client from the OpenAPI specification using Orval
- `pnpm generate:api-all`: Runs both fetch and generate commands in sequence
#### Manual API Client Updates
If you need to update the API client after making changes to the backend API:
1. Ensure the backend services are running:
```
docker compose up -d
```
2. Generate the updated API client:
```
pnpm generate:api-all
```
This will fetch the latest OpenAPI specification and regenerate the TypeScript client code.

View File

@@ -191,10 +191,12 @@ app.include_router(
backend.server.v2.library.routes.router, tags=["v2"], prefix="/api/library" backend.server.v2.library.routes.router, tags=["v2"], prefix="/api/library"
) )
app.include_router( app.include_router(
backend.server.v2.otto.routes.router, tags=["v2"], prefix="/api/otto" backend.server.v2.otto.routes.router, tags=["v2", "otto"], prefix="/api/otto"
) )
app.include_router( app.include_router(
backend.server.v2.turnstile.routes.router, tags=["v2"], prefix="/api/turnstile" backend.server.v2.turnstile.routes.router,
tags=["v2", "turnstile"],
prefix="/api/turnstile",
) )
app.include_router( app.include_router(

View File

@@ -34,7 +34,7 @@ router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@router.post("/unsubscribe") @router.post("/unsubscribe", summary="One Click Email Unsubscribe")
async def unsubscribe_via_one_click(token: Annotated[str, Query()]): async def unsubscribe_via_one_click(token: Annotated[str, Query()]):
logger.info("Received unsubscribe request from One Click Unsubscribe") logger.info("Received unsubscribe request from One Click Unsubscribe")
try: try:
@@ -48,7 +48,11 @@ async def unsubscribe_via_one_click(token: Annotated[str, Query()]):
return JSONResponse(status_code=200, content={"status": "ok"}) return JSONResponse(status_code=200, content={"status": "ok"})
@router.post("/", dependencies=[Depends(postmark_validator.get_dependency())]) @router.post(
"/",
dependencies=[Depends(postmark_validator.get_dependency())],
summary="Handle Postmark Email Webhooks",
)
async def postmark_webhook_handler( async def postmark_webhook_handler(
webhook: Annotated[ webhook: Annotated[
PostmarkWebhook, PostmarkWebhook,

View File

@@ -113,14 +113,22 @@ v1_router.include_router(
######################################################## ########################################################
@v1_router.post("/auth/user", tags=["auth"], dependencies=[Depends(auth_middleware)]) @v1_router.post(
"/auth/user",
summary="Get or create user",
tags=["auth"],
dependencies=[Depends(auth_middleware)],
)
async def get_or_create_user_route(user_data: dict = Depends(auth_middleware)): async def get_or_create_user_route(user_data: dict = Depends(auth_middleware)):
user = await get_or_create_user(user_data) user = await get_or_create_user(user_data)
return user.model_dump() return user.model_dump()
@v1_router.post( @v1_router.post(
"/auth/user/email", tags=["auth"], dependencies=[Depends(auth_middleware)] "/auth/user/email",
summary="Update user email",
tags=["auth"],
dependencies=[Depends(auth_middleware)],
) )
async def update_user_email_route( async def update_user_email_route(
user_id: Annotated[str, Depends(get_user_id)], email: str = Body(...) user_id: Annotated[str, Depends(get_user_id)], email: str = Body(...)
@@ -132,6 +140,7 @@ async def update_user_email_route(
@v1_router.get( @v1_router.get(
"/auth/user/preferences", "/auth/user/preferences",
summary="Get notification preferences",
tags=["auth"], tags=["auth"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -144,6 +153,7 @@ async def get_preferences(
@v1_router.post( @v1_router.post(
"/auth/user/preferences", "/auth/user/preferences",
summary="Update notification preferences",
tags=["auth"], tags=["auth"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -161,14 +171,20 @@ async def update_preferences(
@v1_router.get( @v1_router.get(
"/onboarding", tags=["onboarding"], dependencies=[Depends(auth_middleware)] "/onboarding",
summary="Get onboarding status",
tags=["onboarding"],
dependencies=[Depends(auth_middleware)],
) )
async def get_onboarding(user_id: Annotated[str, Depends(get_user_id)]): async def get_onboarding(user_id: Annotated[str, Depends(get_user_id)]):
return await get_user_onboarding(user_id) return await get_user_onboarding(user_id)
@v1_router.patch( @v1_router.patch(
"/onboarding", tags=["onboarding"], dependencies=[Depends(auth_middleware)] "/onboarding",
summary="Update onboarding progress",
tags=["onboarding"],
dependencies=[Depends(auth_middleware)],
) )
async def update_onboarding( async def update_onboarding(
user_id: Annotated[str, Depends(get_user_id)], data: UserOnboardingUpdate user_id: Annotated[str, Depends(get_user_id)], data: UserOnboardingUpdate
@@ -178,6 +194,7 @@ async def update_onboarding(
@v1_router.get( @v1_router.get(
"/onboarding/agents", "/onboarding/agents",
summary="Get recommended agents",
tags=["onboarding"], tags=["onboarding"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -189,6 +206,7 @@ async def get_onboarding_agents(
@v1_router.get( @v1_router.get(
"/onboarding/enabled", "/onboarding/enabled",
summary="Check onboarding enabled",
tags=["onboarding", "public"], tags=["onboarding", "public"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -201,7 +219,12 @@ async def is_onboarding_enabled():
######################################################## ########################################################
@v1_router.get(path="/blocks", tags=["blocks"], dependencies=[Depends(auth_middleware)]) @v1_router.get(
path="/blocks",
summary="List available blocks",
tags=["blocks"],
dependencies=[Depends(auth_middleware)],
)
def get_graph_blocks() -> Sequence[dict[Any, Any]]: def get_graph_blocks() -> Sequence[dict[Any, Any]]:
blocks = [block() for block in get_blocks().values()] blocks = [block() for block in get_blocks().values()]
costs = get_block_costs() costs = get_block_costs()
@@ -212,6 +235,7 @@ def get_graph_blocks() -> Sequence[dict[Any, Any]]:
@v1_router.post( @v1_router.post(
path="/blocks/{block_id}/execute", path="/blocks/{block_id}/execute",
summary="Execute graph block",
tags=["blocks"], tags=["blocks"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -231,7 +255,12 @@ async def execute_graph_block(block_id: str, data: BlockInput) -> CompletedBlock
######################################################## ########################################################
@v1_router.get(path="/credits", dependencies=[Depends(auth_middleware)]) @v1_router.get(
path="/credits",
tags=["credits"],
summary="Get user credits",
dependencies=[Depends(auth_middleware)],
)
async def get_user_credits( async def get_user_credits(
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
) -> dict[str, int]: ) -> dict[str, int]:
@@ -239,7 +268,10 @@ async def get_user_credits(
@v1_router.post( @v1_router.post(
path="/credits", tags=["credits"], dependencies=[Depends(auth_middleware)] path="/credits",
summary="Request credit top up",
tags=["credits"],
dependencies=[Depends(auth_middleware)],
) )
async def request_top_up( async def request_top_up(
request: RequestTopUp, user_id: Annotated[str, Depends(get_user_id)] request: RequestTopUp, user_id: Annotated[str, Depends(get_user_id)]
@@ -252,6 +284,7 @@ async def request_top_up(
@v1_router.post( @v1_router.post(
path="/credits/{transaction_key}/refund", path="/credits/{transaction_key}/refund",
summary="Refund credit transaction",
tags=["credits"], tags=["credits"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -264,7 +297,10 @@ async def refund_top_up(
@v1_router.patch( @v1_router.patch(
path="/credits", tags=["credits"], dependencies=[Depends(auth_middleware)] path="/credits",
summary="Fulfill checkout session",
tags=["credits"],
dependencies=[Depends(auth_middleware)],
) )
async def fulfill_checkout(user_id: Annotated[str, Depends(get_user_id)]): async def fulfill_checkout(user_id: Annotated[str, Depends(get_user_id)]):
await _user_credit_model.fulfill_checkout(user_id=user_id) await _user_credit_model.fulfill_checkout(user_id=user_id)
@@ -273,6 +309,7 @@ async def fulfill_checkout(user_id: Annotated[str, Depends(get_user_id)]):
@v1_router.post( @v1_router.post(
path="/credits/auto-top-up", path="/credits/auto-top-up",
summary="Configure auto top up",
tags=["credits"], tags=["credits"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -301,6 +338,7 @@ async def configure_user_auto_top_up(
@v1_router.get( @v1_router.get(
path="/credits/auto-top-up", path="/credits/auto-top-up",
summary="Get auto top up",
tags=["credits"], tags=["credits"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -310,7 +348,9 @@ async def get_user_auto_top_up(
return await get_auto_top_up(user_id) return await get_auto_top_up(user_id)
@v1_router.post(path="/credits/stripe_webhook", tags=["credits"]) @v1_router.post(
path="/credits/stripe_webhook", summary="Handle Stripe webhooks", tags=["credits"]
)
async def stripe_webhook(request: Request): async def stripe_webhook(request: Request):
# Get the raw request body # Get the raw request body
payload = await request.body() payload = await request.body()
@@ -345,14 +385,24 @@ async def stripe_webhook(request: Request):
return Response(status_code=200) return Response(status_code=200)
@v1_router.get(path="/credits/manage", dependencies=[Depends(auth_middleware)]) @v1_router.get(
path="/credits/manage",
tags=["credits"],
summary="Manage payment methods",
dependencies=[Depends(auth_middleware)],
)
async def manage_payment_method( async def manage_payment_method(
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
) -> dict[str, str]: ) -> dict[str, str]:
return {"url": await _user_credit_model.create_billing_portal_session(user_id)} return {"url": await _user_credit_model.create_billing_portal_session(user_id)}
@v1_router.get(path="/credits/transactions", dependencies=[Depends(auth_middleware)]) @v1_router.get(
path="/credits/transactions",
tags=["credits"],
summary="Get credit history",
dependencies=[Depends(auth_middleware)],
)
async def get_credit_history( async def get_credit_history(
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
transaction_time: datetime | None = None, transaction_time: datetime | None = None,
@@ -370,7 +420,12 @@ async def get_credit_history(
) )
@v1_router.get(path="/credits/refunds", dependencies=[Depends(auth_middleware)]) @v1_router.get(
path="/credits/refunds",
tags=["credits"],
summary="Get refund requests",
dependencies=[Depends(auth_middleware)],
)
async def get_refund_requests( async def get_refund_requests(
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
) -> list[RefundRequest]: ) -> list[RefundRequest]:
@@ -386,7 +441,12 @@ class DeleteGraphResponse(TypedDict):
version_counts: int version_counts: int
@v1_router.get(path="/graphs", tags=["graphs"], dependencies=[Depends(auth_middleware)]) @v1_router.get(
path="/graphs",
summary="List user graphs",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
)
async def get_graphs( async def get_graphs(
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
) -> Sequence[graph_db.GraphModel]: ) -> Sequence[graph_db.GraphModel]:
@@ -394,10 +454,14 @@ async def get_graphs(
@v1_router.get( @v1_router.get(
path="/graphs/{graph_id}", tags=["graphs"], dependencies=[Depends(auth_middleware)] path="/graphs/{graph_id}",
summary="Get specific graph",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
) )
@v1_router.get( @v1_router.get(
path="/graphs/{graph_id}/versions/{version}", path="/graphs/{graph_id}/versions/{version}",
summary="Get graph version",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -421,6 +485,7 @@ async def get_graph(
@v1_router.get( @v1_router.get(
path="/graphs/{graph_id}/versions", path="/graphs/{graph_id}/versions",
summary="Get all graph versions",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -434,7 +499,10 @@ async def get_graph_all_versions(
@v1_router.post( @v1_router.post(
path="/graphs", tags=["graphs"], dependencies=[Depends(auth_middleware)] path="/graphs",
summary="Create new graph",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
) )
async def create_new_graph( async def create_new_graph(
create_graph: CreateGraph, create_graph: CreateGraph,
@@ -457,7 +525,10 @@ async def create_new_graph(
@v1_router.delete( @v1_router.delete(
path="/graphs/{graph_id}", tags=["graphs"], dependencies=[Depends(auth_middleware)] path="/graphs/{graph_id}",
summary="Delete graph permanently",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
) )
async def delete_graph( async def delete_graph(
graph_id: str, user_id: Annotated[str, Depends(get_user_id)] graph_id: str, user_id: Annotated[str, Depends(get_user_id)]
@@ -469,7 +540,10 @@ async def delete_graph(
@v1_router.put( @v1_router.put(
path="/graphs/{graph_id}", tags=["graphs"], dependencies=[Depends(auth_middleware)] path="/graphs/{graph_id}",
summary="Update graph version",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
) )
async def update_graph( async def update_graph(
graph_id: str, graph_id: str,
@@ -515,6 +589,7 @@ async def update_graph(
@v1_router.put( @v1_router.put(
path="/graphs/{graph_id}/versions/active", path="/graphs/{graph_id}/versions/active",
summary="Set active graph version",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -553,6 +628,7 @@ async def set_graph_active_version(
@v1_router.post( @v1_router.post(
path="/graphs/{graph_id}/execute/{graph_version}", path="/graphs/{graph_id}/execute/{graph_version}",
summary="Execute graph agent",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -586,6 +662,7 @@ async def execute_graph(
@v1_router.post( @v1_router.post(
path="/graphs/{graph_id}/executions/{graph_exec_id}/stop", path="/graphs/{graph_id}/executions/{graph_exec_id}/stop",
summary="Stop graph execution",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -613,6 +690,7 @@ async def stop_graph_run(
@v1_router.get( @v1_router.get(
path="/executions", path="/executions",
summary="Get all executions",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -624,6 +702,7 @@ async def get_graphs_executions(
@v1_router.get( @v1_router.get(
path="/graphs/{graph_id}/executions", path="/graphs/{graph_id}/executions",
summary="Get graph executions",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -636,6 +715,7 @@ async def get_graph_executions(
@v1_router.get( @v1_router.get(
path="/graphs/{graph_id}/executions/{graph_exec_id}", path="/graphs/{graph_id}/executions/{graph_exec_id}",
summary="Get execution details",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -665,6 +745,7 @@ async def get_graph_execution(
@v1_router.delete( @v1_router.delete(
path="/executions/{graph_exec_id}", path="/executions/{graph_exec_id}",
summary="Delete graph execution",
tags=["graphs"], tags=["graphs"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
status_code=HTTP_204_NO_CONTENT, status_code=HTTP_204_NO_CONTENT,
@@ -692,6 +773,7 @@ class ScheduleCreationRequest(pydantic.BaseModel):
@v1_router.post( @v1_router.post(
path="/schedules", path="/schedules",
summary="Create execution schedule",
tags=["schedules"], tags=["schedules"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -719,6 +801,7 @@ async def create_schedule(
@v1_router.delete( @v1_router.delete(
path="/schedules/{schedule_id}", path="/schedules/{schedule_id}",
summary="Delete execution schedule",
tags=["schedules"], tags=["schedules"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -732,6 +815,7 @@ async def delete_schedule(
@v1_router.get( @v1_router.get(
path="/schedules", path="/schedules",
summary="List execution schedules",
tags=["schedules"], tags=["schedules"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
) )
@@ -752,6 +836,7 @@ async def get_execution_schedules(
@v1_router.post( @v1_router.post(
"/api-keys", "/api-keys",
summary="Create new API key",
response_model=CreateAPIKeyResponse, response_model=CreateAPIKeyResponse,
tags=["api-keys"], tags=["api-keys"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
@@ -782,6 +867,7 @@ async def create_api_key(
@v1_router.get( @v1_router.get(
"/api-keys", "/api-keys",
summary="List user API keys",
response_model=list[APIKeyWithoutHash] | dict[str, str], response_model=list[APIKeyWithoutHash] | dict[str, str],
tags=["api-keys"], tags=["api-keys"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
@@ -802,6 +888,7 @@ async def get_api_keys(
@v1_router.get( @v1_router.get(
"/api-keys/{key_id}", "/api-keys/{key_id}",
summary="Get specific API key",
response_model=APIKeyWithoutHash, response_model=APIKeyWithoutHash,
tags=["api-keys"], tags=["api-keys"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
@@ -825,6 +912,7 @@ async def get_api_key(
@v1_router.delete( @v1_router.delete(
"/api-keys/{key_id}", "/api-keys/{key_id}",
summary="Revoke API key",
response_model=APIKeyWithoutHash, response_model=APIKeyWithoutHash,
tags=["api-keys"], tags=["api-keys"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
@@ -853,6 +941,7 @@ async def delete_api_key(
@v1_router.post( @v1_router.post(
"/api-keys/{key_id}/suspend", "/api-keys/{key_id}/suspend",
summary="Suspend API key",
response_model=APIKeyWithoutHash, response_model=APIKeyWithoutHash,
tags=["api-keys"], tags=["api-keys"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],
@@ -878,6 +967,7 @@ async def suspend_key(
@v1_router.put( @v1_router.put(
"/api-keys/{key_id}/permissions", "/api-keys/{key_id}/permissions",
summary="Update key permissions",
response_model=APIKeyWithoutHash, response_model=APIKeyWithoutHash,
tags=["api-keys"], tags=["api-keys"],
dependencies=[Depends(auth_middleware)], dependencies=[Depends(auth_middleware)],

View File

@@ -22,7 +22,9 @@ router = APIRouter(
) )
@router.post("/add_credits", response_model=AddUserCreditsResponse) @router.post(
"/add_credits", response_model=AddUserCreditsResponse, summary="Add Credits to User"
)
async def add_user_credits( async def add_user_credits(
user_id: typing.Annotated[str, Body()], user_id: typing.Annotated[str, Body()],
amount: typing.Annotated[int, Body()], amount: typing.Annotated[int, Body()],
@@ -49,6 +51,7 @@ async def add_user_credits(
@router.get( @router.get(
"/users_history", "/users_history",
response_model=UserHistoryResponse, response_model=UserHistoryResponse,
summary="Get All Users History",
) )
async def admin_get_all_user_history( async def admin_get_all_user_history(
admin_user: typing.Annotated[ admin_user: typing.Annotated[

View File

@@ -19,6 +19,7 @@ router = fastapi.APIRouter(prefix="/admin", tags=["store", "admin"])
@router.get( @router.get(
"/listings", "/listings",
summary="Get Admin Listings History",
response_model=backend.server.v2.store.model.StoreListingsWithVersionsResponse, response_model=backend.server.v2.store.model.StoreListingsWithVersionsResponse,
dependencies=[fastapi.Depends(autogpt_libs.auth.depends.requires_admin_user)], dependencies=[fastapi.Depends(autogpt_libs.auth.depends.requires_admin_user)],
) )
@@ -63,6 +64,7 @@ async def get_admin_listings_with_versions(
@router.post( @router.post(
"/submissions/{store_listing_version_id}/review", "/submissions/{store_listing_version_id}/review",
summary="Review Store Submission",
response_model=backend.server.v2.store.model.StoreSubmission, response_model=backend.server.v2.store.model.StoreSubmission,
dependencies=[fastapi.Depends(autogpt_libs.auth.depends.requires_admin_user)], dependencies=[fastapi.Depends(autogpt_libs.auth.depends.requires_admin_user)],
) )
@@ -104,6 +106,7 @@ async def review_submission(
@router.get( @router.get(
"/submissions/download/{store_listing_version_id}", "/submissions/download/{store_listing_version_id}",
summary="Admin Download Agent File",
tags=["store", "admin"], tags=["store", "admin"],
dependencies=[fastapi.Depends(autogpt_libs.auth.depends.requires_admin_user)], dependencies=[fastapi.Depends(autogpt_libs.auth.depends.requires_admin_user)],
) )

View File

@@ -20,6 +20,7 @@ router = APIRouter(
@router.get( @router.get(
"", "",
summary="List Library Agents",
responses={ responses={
500: {"description": "Server error", "content": {"application/json": {}}}, 500: {"description": "Server error", "content": {"application/json": {}}},
}, },
@@ -77,7 +78,7 @@ async def list_library_agents(
) from e ) from e
@router.get("/{library_agent_id}") @router.get("/{library_agent_id}", summary="Get Library Agent")
async def get_library_agent( async def get_library_agent(
library_agent_id: str, library_agent_id: str,
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id), user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
@@ -87,6 +88,7 @@ async def get_library_agent(
@router.get( @router.get(
"/marketplace/{store_listing_version_id}", "/marketplace/{store_listing_version_id}",
summary="Get Agent By Store ID",
tags=["store, library"], tags=["store, library"],
response_model=library_model.LibraryAgent | None, response_model=library_model.LibraryAgent | None,
) )
@@ -118,6 +120,7 @@ async def get_library_agent_by_store_listing_version_id(
@router.post( @router.post(
"", "",
summary="Add Marketplace Agent",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={ responses={
201: {"description": "Agent added successfully"}, 201: {"description": "Agent added successfully"},
@@ -180,6 +183,7 @@ async def add_marketplace_agent_to_library(
@router.put( @router.put(
"/{library_agent_id}", "/{library_agent_id}",
summary="Update Library Agent",
status_code=status.HTTP_204_NO_CONTENT, status_code=status.HTTP_204_NO_CONTENT,
responses={ responses={
204: {"description": "Agent updated successfully"}, 204: {"description": "Agent updated successfully"},
@@ -232,7 +236,7 @@ async def update_library_agent(
) from e ) from e
@router.post("/{library_agent_id}/fork") @router.post("/{library_agent_id}/fork", summary="Fork Library Agent")
async def fork_library_agent( async def fork_library_agent(
library_agent_id: str, library_agent_id: str,
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id), user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),

View File

@@ -11,7 +11,9 @@ from backend.util.exceptions import NotFoundError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter(
tags=["presets"],
)
@router.get( @router.get(

View File

@@ -14,7 +14,10 @@ router = APIRouter()
@router.post( @router.post(
"/ask", response_model=ApiResponse, dependencies=[Depends(auth_middleware)] "/ask",
response_model=ApiResponse,
dependencies=[Depends(auth_middleware)],
summary="Proxy Otto Chat Request",
) )
async def proxy_otto_request( async def proxy_otto_request(
request: ChatRequest, user_id: str = Depends(get_user_id) request: ChatRequest, user_id: str = Depends(get_user_id)

View File

@@ -29,6 +29,7 @@ router = fastapi.APIRouter()
@router.get( @router.get(
"/profile", "/profile",
summary="Get user profile",
tags=["store", "private"], tags=["store", "private"],
response_model=backend.server.v2.store.model.ProfileDetails, response_model=backend.server.v2.store.model.ProfileDetails,
) )
@@ -61,6 +62,7 @@ async def get_profile(
@router.post( @router.post(
"/profile", "/profile",
summary="Update user profile",
tags=["store", "private"], tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
response_model=backend.server.v2.store.model.CreatorDetails, response_model=backend.server.v2.store.model.CreatorDetails,
@@ -107,6 +109,7 @@ async def update_or_create_profile(
@router.get( @router.get(
"/agents", "/agents",
summary="List store agents",
tags=["store", "public"], tags=["store", "public"],
response_model=backend.server.v2.store.model.StoreAgentsResponse, response_model=backend.server.v2.store.model.StoreAgentsResponse,
) )
@@ -179,6 +182,7 @@ async def get_agents(
@router.get( @router.get(
"/agents/{username}/{agent_name}", "/agents/{username}/{agent_name}",
summary="Get specific agent",
tags=["store", "public"], tags=["store", "public"],
response_model=backend.server.v2.store.model.StoreAgentDetails, response_model=backend.server.v2.store.model.StoreAgentDetails,
) )
@@ -208,6 +212,7 @@ async def get_agent(username: str, agent_name: str):
@router.get( @router.get(
"/graph/{store_listing_version_id}", "/graph/{store_listing_version_id}",
summary="Get agent graph",
tags=["store"], tags=["store"],
) )
async def get_graph_meta_by_store_listing_version_id( async def get_graph_meta_by_store_listing_version_id(
@@ -232,6 +237,7 @@ async def get_graph_meta_by_store_listing_version_id(
@router.get( @router.get(
"/agents/{store_listing_version_id}", "/agents/{store_listing_version_id}",
summary="Get agent by version",
tags=["store"], tags=["store"],
response_model=backend.server.v2.store.model.StoreAgentDetails, response_model=backend.server.v2.store.model.StoreAgentDetails,
) )
@@ -257,6 +263,7 @@ async def get_store_agent(
@router.post( @router.post(
"/agents/{username}/{agent_name}/review", "/agents/{username}/{agent_name}/review",
summary="Create agent review",
tags=["store"], tags=["store"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
response_model=backend.server.v2.store.model.StoreReview, response_model=backend.server.v2.store.model.StoreReview,
@@ -308,6 +315,7 @@ async def create_review(
@router.get( @router.get(
"/creators", "/creators",
summary="List store creators",
tags=["store", "public"], tags=["store", "public"],
response_model=backend.server.v2.store.model.CreatorsResponse, response_model=backend.server.v2.store.model.CreatorsResponse,
) )
@@ -359,6 +367,7 @@ async def get_creators(
@router.get( @router.get(
"/creator/{username}", "/creator/{username}",
summary="Get creator details",
tags=["store", "public"], tags=["store", "public"],
response_model=backend.server.v2.store.model.CreatorDetails, response_model=backend.server.v2.store.model.CreatorDetails,
) )
@@ -390,6 +399,7 @@ async def get_creator(
############################################ ############################################
@router.get( @router.get(
"/myagents", "/myagents",
summary="Get my agents",
tags=["store", "private"], tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
response_model=backend.server.v2.store.model.MyAgentsResponse, response_model=backend.server.v2.store.model.MyAgentsResponse,
@@ -412,6 +422,7 @@ async def get_my_agents(
@router.delete( @router.delete(
"/submissions/{submission_id}", "/submissions/{submission_id}",
summary="Delete store submission",
tags=["store", "private"], tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
response_model=bool, response_model=bool,
@@ -448,6 +459,7 @@ async def delete_submission(
@router.get( @router.get(
"/submissions", "/submissions",
summary="List my submissions",
tags=["store", "private"], tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
response_model=backend.server.v2.store.model.StoreSubmissionsResponse, response_model=backend.server.v2.store.model.StoreSubmissionsResponse,
@@ -501,6 +513,7 @@ async def get_submissions(
@router.post( @router.post(
"/submissions", "/submissions",
summary="Create store submission",
tags=["store", "private"], tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
response_model=backend.server.v2.store.model.StoreSubmission, response_model=backend.server.v2.store.model.StoreSubmission,
@@ -548,6 +561,7 @@ async def create_submission(
@router.post( @router.post(
"/submissions/media", "/submissions/media",
summary="Upload submission media",
tags=["store", "private"], tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
) )
@@ -585,6 +599,7 @@ async def upload_submission_media(
@router.post( @router.post(
"/submissions/generate_image", "/submissions/generate_image",
summary="Generate submission image",
tags=["store", "private"], tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
) )
@@ -646,6 +661,7 @@ async def generate_image(
@router.get( @router.get(
"/download/agents/{store_listing_version_id}", "/download/agents/{store_listing_version_id}",
summary="Download agent file",
tags=["store", "public"], tags=["store", "public"],
) )
async def download_agent_file( async def download_agent_file(

View File

@@ -13,7 +13,9 @@ router = APIRouter()
settings = Settings() settings = Settings()
@router.post("/verify", response_model=TurnstileVerifyResponse) @router.post(
"/verify", response_model=TurnstileVerifyResponse, summary="Verify Turnstile Token"
)
async def verify_turnstile_token( async def verify_turnstile_token(
request: TurnstileVerifyRequest, request: TurnstileVerifyRequest,
) -> TurnstileVerifyResponse: ) -> TurnstileVerifyResponse:

View File

@@ -8,6 +8,8 @@ NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID= NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=
NEXT_PUBLIC_APP_ENV=local NEXT_PUBLIC_APP_ENV=local
NEXT_PUBLIC_AGPT_SERVER_BASE_URL=http://localhost:8006
## Locale settings ## Locale settings
NEXT_PUBLIC_DEFAULT_LOCALE=en NEXT_PUBLIC_DEFAULT_LOCALE=en
@@ -35,4 +37,4 @@ NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY=
NEXT_PUBLIC_TURNSTILE=disabled NEXT_PUBLIC_TURNSTILE=disabled
# Devtools # Devtools
NEXT_PUBLIC_REACT_QUERY_DEVTOOL=true NEXT_PUBLIC_REACT_QUERY_DEVTOOL=true

View File

@@ -0,0 +1,59 @@
import { defineConfig } from "orval";
export default defineConfig({
autogpt_api_client: {
input: {
target: `./src/api/openapi.json`,
override: {
transformer: "./src/api/transformers/fix-tags.mjs",
},
},
output: {
workspace: "./src/api",
target: `./__generated__/endpoints`,
schemas: "./__generated__/models",
mode: "tags-split",
client: "react-query",
httpClient: "fetch",
indexFiles: false,
mock: {
type: "msw",
delay: 1000, // artifical latency
generateEachHttpStatus: true, // helps us test error-handling scenarios and generate mocks for all HTTP statuses
},
override: {
mutator: {
path: "./mutators/custom-mutator.ts",
name: "customMutator",
},
query: {
useQuery: true,
useMutation: true,
// Will add more as their use cases arise
},
},
},
hooks: {
afterAllFilesWrite: "prettier --write",
},
},
autogpt_zod_schema: {
input: {
target: `./src/api/openapi.json`,
override: {
transformer: "./src/api/transformers/fix-tags.mjs",
},
},
output: {
workspace: "./src/api",
target: `./__generated__/zod-schema`,
schemas: "./__generated__/models",
mode: "tags-split",
client: "zod",
indexFiles: false,
},
hooks: {
afterAllFilesWrite: "prettier --write",
},
},
});

View File

@@ -5,7 +5,7 @@
"scripts": { "scripts": {
"dev": "next dev --turbo", "dev": "next dev --turbo",
"dev:test": "NODE_ENV=test && next dev --turbo", "dev:test": "NODE_ENV=test && next dev --turbo",
"build": "SKIP_STORYBOOK_TESTS=true next build", "build": "pnpm run generate:api-client && SKIP_STORYBOOK_TESTS=true next build",
"start": "next start", "start": "next start",
"start:standalone": "cd .next/standalone && node server.js", "start:standalone": "cd .next/standalone && node server.js",
"lint": "next lint && prettier --check .", "lint": "next lint && prettier --check .",
@@ -18,7 +18,10 @@
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build", "build-storybook": "storybook build",
"test-storybook": "test-storybook", "test-storybook": "test-storybook",
"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm run build-storybook -- --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && pnpm run test-storybook\"" "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm run build-storybook -- --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && pnpm run test-storybook\"",
"fetch:openapi": "curl http://localhost:8006/openapi.json > ./src/api/openapi.json && prettier --write ./src/api/openapi.json",
"generate:api-client": "orval --config ./orval.config.ts",
"generate:api-all": "pnpm run fetch:openapi && pnpm run generate:api-client"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"
@@ -116,6 +119,7 @@
"import-in-the-middle": "1.14.2", "import-in-the-middle": "1.14.2",
"msw": "2.10.2", "msw": "2.10.2",
"msw-storybook-addon": "2.0.5", "msw-storybook-addon": "2.0.5",
"orval": "7.10.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier": "3.5.3", "prettier": "3.5.3",
"prettier-plugin-tailwindcss": "0.6.12", "prettier-plugin-tailwindcss": "0.6.12",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
import { getSupabaseClient } from "@/lib/supabase/getSupabaseClient";
const BASE_URL =
process.env.NEXT_PUBLIC_AGPT_SERVER_BASE_URL || "http://localhost:8006";
const getBody = <T>(c: Response | Request): Promise<T> => {
const contentType = c.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return c.json();
}
if (contentType && contentType.includes("application/pdf")) {
return c.blob() as Promise<T>;
}
return c.text() as Promise<T>;
};
const getSupabaseToken = async () => {
const supabase = await getSupabaseClient();
const {
data: { session },
} = (await supabase?.auth.getSession()) || {
data: { session: null },
};
return session?.access_token;
};
export const customMutator = async <T = any>(
url: string,
options: RequestInit & {
params?: any;
} = {},
): Promise<T> => {
const { params, ...requestOptions } = options;
const method = (requestOptions.method || "GET") as
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "PATCH";
const data = requestOptions.body;
const headers: Record<string, string> = {
...((requestOptions.headers as Record<string, string>) || {}),
};
const token = await getSupabaseToken();
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const isFormData = data instanceof FormData;
// Currently, only two content types are handled here: application/json and multipart/form-data
if (!isFormData && data && !headers["Content-Type"]) {
headers["Content-Type"] = "application/json";
}
const queryString = params
? "?" + new URLSearchParams(params).toString()
: "";
const response = await fetch(`${BASE_URL}${url}${queryString}`, {
...requestOptions,
method,
headers,
body: data,
});
const response_data = await getBody<T>(response);
return {
status: response.status,
response_data,
headers: response.headers,
} as T;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
/**
* Transformer function for orval that fixes tags in OpenAPI spec.
* 1. Create a set of tags so we have unique values
* 2. Then remove public, private, v1, and v2 tags from tags array
* 3. Then arrange remaining tags alphabetically and only keep the first one
*
* @param {OpenAPIObject} inputSchema
* @return {OpenAPIObject}
*/
export const tagTransformer = (inputSchema) => {
const processedPaths = Object.entries(inputSchema.paths || {}).reduce(
(acc, [path, pathItem]) => ({
...acc,
[path]: Object.entries(pathItem || {}).reduce(
(pathItemAcc, [verb, operation]) => {
if (typeof operation === "object" && operation !== null) {
// 1. Create a set of tags so we have unique values
const uniqueTags = Array.from(new Set(operation.tags || []));
// 2. Remove public, private, v1, and v2 tags from tags array
const filteredTags = uniqueTags.filter(
(tag) =>
!["public", "private"].includes(tag.toLowerCase()) &&
!/^v[12]$/i.test(tag),
);
// 3. Arrange tags alphabetically and only keep the first one
const sortedTags = filteredTags.sort((a, b) => a.localeCompare(b));
const firstTag = sortedTags.length > 0 ? [sortedTags[0]] : [];
return {
...pathItemAcc,
[verb]: {
...operation,
tags: firstTag,
},
};
}
return {
...pathItemAcc,
[verb]: operation,
};
},
{},
),
}),
{},
);
return {
...inputSchema,
paths: processedPaths,
};
};
export default tagTransformer;

View File

@@ -0,0 +1,14 @@
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { createBrowserClient } from "@supabase/ssr";
const isClient = typeof window !== "undefined";
export const getSupabaseClient = async () => {
return isClient
? createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ isSingleton: true },
)
: await getServerSupabase();
};