fix(backend/library): Split & fix update_library_agent endpoint (#10220)

This PR makes several improvements to the `update_library_agent`
endpoint.

- Resolves #10216

### Changes 🏗️

- Add `DELETE /library/agents/{id}` endpoint
- Fix `PUT /library/agents/{id}` endpoint
  - Return updated library agent
  - Remove `is_deleted` parameter
  - Change method from `PUT` to `PATCH`

Also, a small DX improvement:
- Expose `BackendAPI` globally through `window.api` for local dev
purposes

### 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] Deleting library agents works
This commit is contained in:
Reinier van der Leer
2025-06-25 09:53:27 +01:00
committed by GitHub
parent aedbcbf2d8
commit 1d29a64e35
8 changed files with 117 additions and 39 deletions

View File

@@ -279,6 +279,7 @@ class AgentServer(backend.util.service.AppProcess):
@staticmethod
async def test_delete_graph(graph_id: str, user_id: str):
"""Used for clean-up after a test run"""
await backend.server.v2.library.db.delete_library_agent_by_graph_id(
graph_id=graph_id, user_id=user_id
)

View File

@@ -1,5 +1,5 @@
import logging
from typing import Optional
from typing import Literal, Optional
import fastapi
import prisma.errors
@@ -122,7 +122,7 @@ async def list_library_agents(
except Exception as e:
# Skip this agent if there was an error
logger.error(
f"Error parsing LibraryAgent when getting library agents from db: {e}"
f"Error parsing LibraryAgent #{agent.id} from DB item: {e}"
)
continue
@@ -168,7 +168,7 @@ async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent
)
if not library_agent:
raise store_exceptions.AgentNotFoundError(f"Library agent #{id} not found")
raise NotFoundError(f"Library agent #{id} not found")
return library_model.LibraryAgent.from_db(library_agent)
@@ -346,8 +346,8 @@ async def update_library_agent(
auto_update_version: Optional[bool] = None,
is_favorite: Optional[bool] = None,
is_archived: Optional[bool] = None,
is_deleted: Optional[bool] = None,
) -> None:
is_deleted: Optional[Literal[False]] = None,
) -> library_model.LibraryAgent:
"""
Updates the specified LibraryAgent record.
@@ -357,15 +357,18 @@ async def update_library_agent(
auto_update_version: Whether the agent should auto-update to active version.
is_favorite: Whether this agent is marked as a favorite.
is_archived: Whether this agent is archived.
is_deleted: Whether this agent is deleted.
Returns:
The updated LibraryAgent.
Raises:
NotFoundError: If the specified LibraryAgent does not exist.
DatabaseError: If there's an error in the update operation.
"""
logger.debug(
f"Updating library agent {library_agent_id} for user {user_id} with "
f"auto_update_version={auto_update_version}, is_favorite={is_favorite}, "
f"is_archived={is_archived}, is_deleted={is_deleted}"
f"is_archived={is_archived}"
)
update_fields: prisma.types.LibraryAgentUpdateManyMutationInput = {}
if auto_update_version is not None:
@@ -375,17 +378,46 @@ async def update_library_agent(
if is_archived is not None:
update_fields["isArchived"] = is_archived
if is_deleted is not None:
if is_deleted is True:
raise RuntimeError(
"Use delete_library_agent() to (soft-)delete library agents"
)
update_fields["isDeleted"] = is_deleted
if not update_fields:
raise ValueError("No values were passed to update")
try:
await prisma.models.LibraryAgent.prisma().update_many(
where={"id": library_agent_id, "userId": user_id}, data=update_fields
n_updated = await prisma.models.LibraryAgent.prisma().update_many(
where={"id": library_agent_id, "userId": user_id},
data=update_fields,
)
if n_updated < 1:
raise NotFoundError(f"Library agent {library_agent_id} not found")
return await get_library_agent(
id=library_agent_id,
user_id=user_id,
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error updating library agent: {str(e)}")
raise store_exceptions.DatabaseError("Failed to update library agent") from e
async def delete_library_agent(
library_agent_id: str, user_id: str, soft_delete: bool = True
) -> None:
if soft_delete:
deleted_count = await prisma.models.LibraryAgent.prisma().update_many(
where={"id": library_agent_id, "userId": user_id}, data={"isDeleted": True}
)
else:
deleted_count = await prisma.models.LibraryAgent.prisma().delete_many(
where={"id": library_agent_id, "userId": user_id}
)
if deleted_count < 1:
raise NotFoundError(f"Library agent #{library_agent_id} not found")
async def delete_library_agent_by_graph_id(graph_id: str, user_id: str) -> None:
"""
Deletes a library agent for the given user

View File

@@ -333,6 +333,3 @@ class LibraryAgentUpdateRequest(pydantic.BaseModel):
is_archived: Optional[bool] = pydantic.Field(
default=None, description="Archive the agent"
)
is_deleted: Optional[bool] = pydantic.Field(
default=None, description="Delete the agent"
)

View File

@@ -3,7 +3,7 @@ from typing import Any, Optional
import autogpt_libs.auth as autogpt_auth_lib
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, status
from fastapi.responses import JSONResponse
from fastapi.responses import Response
from pydantic import BaseModel, Field
import backend.server.v2.library.db as library_db
@@ -179,12 +179,11 @@ async def add_marketplace_agent_to_library(
) from e
@router.put(
@router.patch(
"/{library_agent_id}",
summary="Update Library Agent",
status_code=status.HTTP_204_NO_CONTENT,
responses={
204: {"description": "Agent updated successfully"},
200: {"description": "Agent updated successfully"},
500: {"description": "Server error"},
},
)
@@ -192,7 +191,7 @@ async def update_library_agent(
library_agent_id: str,
payload: library_model.LibraryAgentUpdateRequest,
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> JSONResponse:
) -> library_model.LibraryAgent:
"""
Update the library agent with the given fields.
@@ -201,25 +200,22 @@ async def update_library_agent(
payload: Fields to update (auto_update_version, is_favorite, etc.).
user_id: ID of the authenticated user.
Returns:
204 (No Content) on success.
Raises:
HTTPException(500): If a server/database error occurs.
"""
try:
await library_db.update_library_agent(
return await library_db.update_library_agent(
library_agent_id=library_agent_id,
user_id=user_id,
auto_update_version=payload.auto_update_version,
is_favorite=payload.is_favorite,
is_archived=payload.is_archived,
is_deleted=payload.is_deleted,
)
return JSONResponse(
status_code=status.HTTP_204_NO_CONTENT,
content={"message": "Agent updated successfully"},
)
except NotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
) from e
except store_exceptions.DatabaseError as e:
logger.error(f"Database error while updating library agent: {e}")
raise HTTPException(
@@ -234,6 +230,45 @@ async def update_library_agent(
) from e
@router.delete(
"/{library_agent_id}",
summary="Delete Library Agent",
responses={
204: {"description": "Agent deleted successfully"},
404: {"description": "Agent not found"},
500: {"description": "Server error"},
},
)
async def delete_library_agent(
library_agent_id: str,
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> Response:
"""
Soft-delete the specified library agent.
Args:
library_agent_id: ID of the library agent to delete.
user_id: ID of the authenticated user.
Returns:
204 No Content if successful.
Raises:
HTTPException(404): If the agent does not exist.
HTTPException(500): If a server/database error occurs.
"""
try:
await library_db.delete_library_agent(
library_agent_id=library_agent_id, user_id=user_id
)
return Response(status_code=status.HTTP_204_NO_CONTENT)
except NotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
) from e
@router.post("/{library_agent_id}/fork", summary="Fork Library Agent")
async def fork_library_agent(
library_agent_id: str,

View File

@@ -518,9 +518,7 @@ export default function AgentRunsPage(): React.ReactElement {
onOpenChange={setAgentDeleteDialogOpen}
onDoDelete={() =>
agent &&
api
.updateLibraryAgent(agent.id, { is_deleted: true })
.then(() => router.push("/library"))
api.deleteLibraryAgent(agent.id).then(() => router.push("/library"))
}
/>

View File

@@ -271,12 +271,10 @@ export const FlowInfo: React.FC<
<Button
variant="destructive"
onClick={() => {
api
.updateLibraryAgent(flow.id, { is_deleted: true })
.then(() => {
setIsDeleteModalOpen(false);
refresh();
});
api.deleteLibraryAgent(flow.id).then(() => {
setIsDeleteModalOpen(false);
refresh();
});
}}
>
Delete

View File

@@ -630,16 +630,19 @@ export default class BackendAPI {
});
}
async updateLibraryAgent(
updateLibraryAgent(
libraryAgentId: LibraryAgentID,
params: {
auto_update_version?: boolean;
is_favorite?: boolean;
is_archived?: boolean;
is_deleted?: boolean;
},
): Promise<void> {
await this._request("PUT", `/library/agents/${libraryAgentId}`, params);
): Promise<LibraryAgent> {
return this._request("PATCH", `/library/agents/${libraryAgentId}`, params);
}
async deleteLibraryAgent(libraryAgentId: LibraryAgentID): Promise<void> {
await this._request("DELETE", `/library/agents/${libraryAgentId}`);
}
forkLibraryAgent(libraryAgentId: LibraryAgentID): Promise<LibraryAgent> {

View File

@@ -3,6 +3,13 @@
import BackendAPI from "./client";
import React, { createContext, useMemo } from "react";
// Add window.api type declaration for global access
declare global {
interface Window {
api?: BackendAPI;
}
}
const BackendAPIProviderContext = createContext<BackendAPI | null>(null);
export function BackendAPIProvider({
@@ -12,6 +19,13 @@ export function BackendAPIProvider({
}): React.ReactNode {
const api = useMemo(() => new BackendAPI(), []);
if (
process.env.NEXT_PUBLIC_BEHAVE_AS == "LOCAL" &&
typeof window !== "undefined"
) {
window.api = api; // Expose the API globally for debugging purposes
}
return (
<BackendAPIProviderContext.Provider value={api}>
{children}