feat(library): Add agent favoriting functionality (#10828)

### Need 💡

This PR introduces the ability for users to "favorite" agents in the
library view, enhancing agent discoverability and organization.
Favorited agents will be visually marked with a heart icon and
prioritized in the library list, appearing at the top. This feature is
distinct from pinning specific agent runs.

### Changes 🏗️

*   **Backend:**
* Updated `LibraryAgent` model in `backend/server/v2/library/model.py`
to include the `is_favorite` field when fetching from the database.
*   **Frontend:**
* Updated `LibraryAgent` TypeScript type in
`autogpt-server-api/types.ts` to include `is_favorite`.
* Modified `LibraryAgentCard.tsx` to display a clickable heart icon,
indicating the favorite status.
* Implemented a click handler on the heart icon to toggle the
`is_favorite` status via an API call, including loading states and toast
notifications.
* Updated `useLibraryAgentList.ts` to implement client-side sorting,
ensuring favorited agents appear at the top of the list.
* Updated `openapi.json` to include `is_favorite` in the `LibraryAgent`
schema and regenerated frontend API types.
    *   Installed `@orval/core` for API generation.

### 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] Verify that the heart icon is displayed correctly on
`LibraryAgentCard` for both favorited (filled red) and unfavorited
(outlined gray) agents.
  - [x] Click the heart icon on an unfavorited agent:
    - [x] Confirm the icon changes to filled red.
    - [x] Verify a "Added to favorites" toast notification appears.
    - [x] Confirm the agent moves to the top of the library list.
- [x] Check that the agent card does not navigate to the agent details
page.
  - [x] Click the heart icon on a favorited agent:
    - [x] Confirm the icon changes to outlined gray.
    - [x] Verify a "Removed from favorites" toast notification appears.
- [x] Confirm the agent's position adjusts in the list (no longer at the
very top unless other sorting criteria apply).
- [x] Check that the agent card does not navigate to the agent details
page.
- [x] Test the loading state: rapidly click the heart icon and observe
the `opacity-50 cursor-not-allowed` styling.
- [x] Verify that the sorting correctly places all favorited agents at
the top, maintaining their original relative order within the favorited
group, and the same for unfavorited agents.

#### For configuration changes:

- [ ] `.env.default` 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**)

---
<a
href="https://cursor.com/background-agent?bcId=bc-43e8f98c-e4ea-4149-afc8-5eea3d1ab439">
  <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-43e8f98c-e4ea-4149-afc8-5eea3d1ab439">
  <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>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <ntindle@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
This commit is contained in:
Nicholas Tindle
2025-09-16 17:43:50 -05:00
committed by GitHub
parent 1ddf92eed4
commit f82adeb959
15 changed files with 664 additions and 2 deletions

View File

@@ -144,6 +144,92 @@ async def list_library_agents(
raise store_exceptions.DatabaseError("Failed to fetch library agents") from e
async def list_favorite_library_agents(
user_id: str,
page: int = 1,
page_size: int = 50,
) -> library_model.LibraryAgentResponse:
"""
Retrieves a paginated list of favorite LibraryAgent records for a given user.
Args:
user_id: The ID of the user whose favorite LibraryAgents we want to retrieve.
page: Current page (1-indexed).
page_size: Number of items per page.
Returns:
A LibraryAgentResponse containing the list of favorite agents and pagination details.
Raises:
DatabaseError: If there is an issue fetching from Prisma.
"""
logger.debug(
f"Fetching favorite library agents for user_id={user_id}, "
f"page={page}, page_size={page_size}"
)
if page < 1 or page_size < 1:
logger.warning(f"Invalid pagination: page={page}, page_size={page_size}")
raise store_exceptions.DatabaseError("Invalid pagination input")
where_clause: prisma.types.LibraryAgentWhereInput = {
"userId": user_id,
"isDeleted": False,
"isArchived": False,
"isFavorite": True, # Only fetch favorites
}
# Sort favorites by updated date descending
order_by: prisma.types.LibraryAgentOrderByInput = {"updatedAt": "desc"}
try:
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
where=where_clause,
include=library_agent_include(user_id),
order=order_by,
skip=(page - 1) * page_size,
take=page_size,
)
agent_count = await prisma.models.LibraryAgent.prisma().count(
where=where_clause
)
logger.debug(
f"Retrieved {len(library_agents)} favorite library agents for user #{user_id}"
)
# Only pass valid agents to the response
valid_library_agents: list[library_model.LibraryAgent] = []
for agent in library_agents:
try:
library_agent = library_model.LibraryAgent.from_db(agent)
valid_library_agents.append(library_agent)
except Exception as e:
# Skip this agent if there was an error
logger.error(
f"Error parsing LibraryAgent #{agent.id} from DB item: {e}"
)
continue
# Return the response with only valid agents
return library_model.LibraryAgentResponse(
agents=valid_library_agents,
pagination=Pagination(
total_items=agent_count,
total_pages=(agent_count + page_size - 1) // page_size,
current_page=page,
page_size=page_size,
),
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error fetching favorite library agents: {e}")
raise store_exceptions.DatabaseError(
"Failed to fetch favorite library agents"
) from e
async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent:
"""
Get a specific agent from the user's library.

View File

@@ -64,6 +64,9 @@ class LibraryAgent(pydantic.BaseModel):
# Indicates if this agent is the latest version
is_latest_version: bool
# Whether the agent is marked as favorite by the user
is_favorite: bool
# Recommended schedule cron (from marketplace agents)
recommended_schedule_cron: str | None = None
@@ -133,6 +136,7 @@ class LibraryAgent(pydantic.BaseModel):
new_output=new_output,
can_access_graph=can_access_graph,
is_latest_version=is_latest_version,
is_favorite=agent.isFavorite,
recommended_schedule_cron=agent.AgentGraph.recommendedScheduleCron,
)

View File

@@ -79,6 +79,54 @@ async def list_library_agents(
) from e
@router.get(
"/favorites",
summary="List Favorite Library Agents",
responses={
500: {"description": "Server error", "content": {"application/json": {}}},
},
)
async def list_favorite_library_agents(
user_id: str = Security(autogpt_auth_lib.get_user_id),
page: int = Query(
1,
ge=1,
description="Page number to retrieve (must be >= 1)",
),
page_size: int = Query(
15,
ge=1,
description="Number of agents per page (must be >= 1)",
),
) -> library_model.LibraryAgentResponse:
"""
Get all favorite agents in the user's library.
Args:
user_id: ID of the authenticated user.
page: Page number to retrieve.
page_size: Number of agents per page.
Returns:
A LibraryAgentResponse containing favorite agents and pagination metadata.
Raises:
HTTPException: If a server/database error occurs.
"""
try:
return await library_db.list_favorite_library_agents(
user_id=user_id,
page=page,
page_size=page_size,
)
except Exception as e:
logger.error(f"Could not list favorite library agents for user #{user_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
) from e
@router.get("/{library_agent_id}", summary="Get Library Agent")
async def get_library_agent(
library_agent_id: str,

View File

@@ -54,6 +54,7 @@ async def test_get_library_agents_success(
new_output=False,
can_access_graph=True,
is_latest_version=True,
is_favorite=False,
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
),
library_model.LibraryAgent(
@@ -74,6 +75,7 @@ async def test_get_library_agents_success(
new_output=False,
can_access_graph=False,
is_latest_version=True,
is_favorite=False,
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
),
],
@@ -121,6 +123,76 @@ def test_get_library_agents_error(mocker: pytest_mock.MockFixture, test_user_id:
)
@pytest.mark.asyncio
async def test_get_favorite_library_agents_success(
mocker: pytest_mock.MockFixture,
test_user_id: str,
) -> None:
mocked_value = library_model.LibraryAgentResponse(
agents=[
library_model.LibraryAgent(
id="test-agent-1",
graph_id="test-agent-1",
graph_version=1,
name="Favorite Agent 1",
description="Test Favorite Description 1",
image_url=None,
creator_name="Test Creator",
creator_image_url="",
input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}},
credentials_input_schema={"type": "object", "properties": {}},
has_external_trigger=False,
status=library_model.LibraryAgentStatus.COMPLETED,
recommended_schedule_cron=None,
new_output=False,
can_access_graph=True,
is_latest_version=True,
is_favorite=True,
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
),
],
pagination=Pagination(
total_items=1, total_pages=1, current_page=1, page_size=15
),
)
mock_db_call = mocker.patch(
"backend.server.v2.library.db.list_favorite_library_agents"
)
mock_db_call.return_value = mocked_value
response = client.get("/agents/favorites")
assert response.status_code == 200
data = library_model.LibraryAgentResponse.model_validate(response.json())
assert len(data.agents) == 1
assert data.agents[0].is_favorite is True
assert data.agents[0].name == "Favorite Agent 1"
mock_db_call.assert_called_once_with(
user_id=test_user_id,
page=1,
page_size=15,
)
def test_get_favorite_library_agents_error(
mocker: pytest_mock.MockFixture, test_user_id: str
):
mock_db_call = mocker.patch(
"backend.server.v2.library.db.list_favorite_library_agents"
)
mock_db_call.side_effect = Exception("Test error")
response = client.get("/agents/favorites")
assert response.status_code == 500
mock_db_call.assert_called_once_with(
user_id=test_user_id,
page=1,
page_size=15,
)
def test_add_agent_to_library_success(
mocker: pytest_mock.MockFixture, test_user_id: str
):
@@ -141,6 +213,7 @@ def test_add_agent_to_library_success(
new_output=False,
can_access_graph=True,
is_latest_version=True,
is_favorite=False,
updated_at=FIXED_NOW,
)

View File

@@ -28,6 +28,7 @@
"new_output": false,
"can_access_graph": true,
"is_latest_version": true,
"is_favorite": false,
"recommended_schedule_cron": null
},
{
@@ -58,6 +59,7 @@
"new_output": false,
"can_access_graph": false,
"is_latest_version": true,
"is_favorite": false,
"recommended_schedule_cron": null
}
],

View File

@@ -35,6 +35,12 @@ export default defineConfig({
useInfiniteQueryParam: "page",
},
},
"getV2List favorite library agents": {
query: {
useInfinite: true,
useInfiniteQueryParam: "page",
},
},
"getV1List graph executions": {
query: {
useInfinite: true,

View File

@@ -0,0 +1,77 @@
"use client";
import React from "react";
import { useFavoriteAgents } from "../../hooks/useFavoriteAgents";
import LibraryAgentCard from "../LibraryAgentCard/LibraryAgentCard";
import { useGetFlag, Flag } from "@/services/feature-flags/use-get-flag";
import { Heart } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
export default function FavoritesSection() {
const isAgentFavoritingEnabled = useGetFlag(Flag.AGENT_FAVORITING);
const {
allAgents: favoriteAgents,
agentLoading: isLoading,
agentCount,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useFavoriteAgents();
// Only show this section if the feature flag is enabled
if (!isAgentFavoritingEnabled) {
return null;
}
// Don't show the section if there are no favorites
if (!isLoading && favoriteAgents.length === 0) {
return null;
}
return (
<div className="mb-8">
<div className="flex items-center gap-[10px] p-2 pb-[10px]">
<Heart className="h-5 w-5 fill-red-500 text-red-500" />
<span className="font-poppin text-[18px] font-semibold leading-[28px] text-neutral-800">
Favorites
</span>
{!isLoading && (
<span className="font-sans text-[14px] font-normal leading-6">
{agentCount} {agentCount === 1 ? "agent" : "agents"}
</span>
)}
</div>
<div className="relative">
{isLoading ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-48 w-full rounded-lg" />
))}
</div>
) : (
<InfiniteScroll
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
loader={
<div className="flex h-8 w-full items-center justify-center">
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-t-2 border-neutral-800" />
</div>
}
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{favoriteAgents.map((agent: LibraryAgent) => (
<LibraryAgentCard key={agent.id} agent={agent} />
))}
</div>
</InfiniteScroll>
)}
</div>
{favoriteAgents.length > 0 && <div className="mt-6 border-t pt-6" />}
</div>
);
}

View File

@@ -1,7 +1,21 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { Heart } from "@phosphor-icons/react";
import { useState, useEffect } from "react";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { InfiniteData } from "@tanstack/react-query";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import {
getV2ListLibraryAgentsResponse,
getV2ListFavoriteLibraryAgentsResponse,
} from "@/app/api/__generated__/endpoints/library/library";
import BackendAPI, { LibraryAgentID } from "@/lib/autogpt-server-api";
import { cn } from "@/lib/utils";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import Avatar, {
AvatarFallback,
AvatarImage,
@@ -20,13 +34,200 @@ export default function LibraryAgentCard({
can_access_graph,
creator_image_url,
image_url,
is_favorite,
},
}: LibraryAgentCardProps) {
const isAgentFavoritingEnabled = useGetFlag(Flag.AGENT_FAVORITING);
const [isFavorite, setIsFavorite] = useState(is_favorite);
const [isUpdating, setIsUpdating] = useState(false);
const { toast } = useToast();
const api = new BackendAPI();
const queryClient = getQueryClient();
// Sync local state with prop when it changes (e.g., after query invalidation)
useEffect(() => {
setIsFavorite(is_favorite);
}, [is_favorite]);
const updateQueryData = (newIsFavorite: boolean) => {
// Update the agent in all library agent queries
queryClient.setQueriesData(
{ queryKey: ["/api/library/agents"] },
(
oldData:
| InfiniteData<getV2ListLibraryAgentsResponse, number | undefined>
| undefined,
) => {
if (!oldData?.pages) return oldData;
return {
...oldData,
pages: oldData.pages.map((page) => {
if (page.status !== 200) return page;
return {
...page,
data: {
...page.data,
agents: page.data.agents.map((agent: LibraryAgent) =>
agent.id === id
? { ...agent, is_favorite: newIsFavorite }
: agent,
),
},
};
}),
};
},
);
// Update or remove from favorites query based on new state
queryClient.setQueriesData(
{ queryKey: ["/api/library/agents/favorites"] },
(
oldData:
| InfiniteData<
getV2ListFavoriteLibraryAgentsResponse,
number | undefined
>
| undefined,
) => {
if (!oldData?.pages) return oldData;
if (newIsFavorite) {
// Add to favorites if not already there
const exists = oldData.pages.some(
(page) =>
page.status === 200 &&
page.data.agents.some((agent: LibraryAgent) => agent.id === id),
);
if (!exists) {
const firstPage = oldData.pages[0];
if (firstPage?.status === 200) {
const updatedAgent = {
id,
name,
description,
graph_id,
can_access_graph,
creator_image_url,
image_url,
is_favorite: true,
};
return {
...oldData,
pages: [
{
...firstPage,
data: {
...firstPage.data,
agents: [updatedAgent, ...firstPage.data.agents],
pagination: {
...firstPage.data.pagination,
total_items: firstPage.data.pagination.total_items + 1,
},
},
},
...oldData.pages.slice(1).map((page) =>
page.status === 200
? {
...page,
data: {
...page.data,
pagination: {
...page.data.pagination,
total_items: page.data.pagination.total_items + 1,
},
},
}
: page,
),
],
};
}
}
} else {
// Remove from favorites
let removedCount = 0;
return {
...oldData,
pages: oldData.pages.map((page) => {
if (page.status !== 200) return page;
const filteredAgents = page.data.agents.filter(
(agent: LibraryAgent) => agent.id !== id,
);
if (filteredAgents.length < page.data.agents.length) {
removedCount = 1;
}
return {
...page,
data: {
...page.data,
agents: filteredAgents,
pagination: {
...page.data.pagination,
total_items:
page.data.pagination.total_items - removedCount,
},
},
};
}),
};
}
return oldData;
},
);
};
const handleToggleFavorite = async (e: React.MouseEvent) => {
e.preventDefault(); // Prevent navigation when clicking the heart
e.stopPropagation();
if (isUpdating || !isAgentFavoritingEnabled) return;
const newIsFavorite = !isFavorite;
// Optimistic update
setIsFavorite(newIsFavorite);
updateQueryData(newIsFavorite);
setIsUpdating(true);
try {
await api.updateLibraryAgent(id as LibraryAgentID, {
is_favorite: newIsFavorite,
});
toast({
title: newIsFavorite ? "Added to favorites" : "Removed from favorites",
description: `${name} has been ${newIsFavorite ? "added to" : "removed from"} your favorites.`,
});
} catch (error) {
// Revert on error
console.error("Failed to update favorite status:", error);
setIsFavorite(!newIsFavorite);
updateQueryData(!newIsFavorite);
toast({
title: "Error",
description: "Failed to update favorite status. Please try again.",
variant: "destructive",
});
} finally {
setIsUpdating(false);
}
};
return (
<div
data-testid="library-agent-card"
data-agent-id={id}
className="inline-flex w-full max-w-[434px] flex-col items-start justify-start gap-2.5 rounded-[26px] bg-white transition-all duration-300 hover:shadow-lg dark:bg-transparent dark:hover:shadow-gray-700"
className="group inline-flex w-full max-w-[434px] flex-col items-start justify-start gap-2.5 rounded-[26px] bg-white transition-all duration-300 hover:shadow-lg dark:bg-transparent dark:hover:shadow-gray-700"
>
<Link
href={`/library/agents/${id}`}
@@ -56,6 +257,33 @@ export default function LibraryAgentCard({
className="object-cover"
/>
)}
{isAgentFavoritingEnabled && (
<button
onClick={handleToggleFavorite}
className={cn(
"absolute right-4 top-4 rounded-full bg-white/90 p-2 backdrop-blur-sm transition-all duration-200",
"hover:scale-110 hover:bg-white",
"focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2",
isUpdating && "cursor-not-allowed opacity-50",
!isFavorite && "opacity-0 group-hover:opacity-100",
)}
disabled={isUpdating}
aria-label={
isFavorite ? "Remove from favorites" : "Add to favorites"
}
>
<Heart
size={20}
weight={isFavorite ? "fill" : "regular"}
className={cn(
"transition-colors duration-200",
isFavorite
? "text-red-500"
: "text-gray-600 hover:text-red-500",
)}
/>
</button>
)}
<div className="absolute bottom-4 left-4">
<Avatar className="h-16 w-16">
<AvatarImage

View File

@@ -1,3 +1,5 @@
"use client";
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
import { useLibraryPageContext } from "../state-provider";

View File

@@ -0,0 +1,57 @@
"use client";
import { useGetV2ListFavoriteLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
export function useFavoriteAgents() {
const {
data: agents,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading: agentLoading,
} = useGetV2ListFavoriteLibraryAgentsInfinite(
{
page: 1,
page_size: 10,
},
{
query: {
getNextPageParam: (lastPage) => {
// Only paginate on successful responses
if (!lastPage || lastPage.status !== 200) return undefined;
const pagination = lastPage.data.pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
},
},
);
const allAgents =
agents?.pages?.flatMap((page) => {
// Only process successful responses
if (!page || page.status !== 200) return [];
const response = page.data;
return response?.agents || [];
}) ?? [];
const agentCount = (() => {
const firstPage = agents?.pages?.[0];
// Only count from successful responses
if (!firstPage || firstPage.status !== 200) return 0;
return firstPage.data?.pagination?.total_items || 0;
})();
return {
allAgents,
agentLoading,
hasNextPage,
agentCount,
isFetchingNextPage,
fetchNextPage,
};
}

View File

@@ -2,6 +2,7 @@
import LibraryActionHeader from "./components/LibraryActionHeader/LibraryActionHeader";
import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
import FavoritesSection from "./components/FavoritesSection/FavoritesSection";
import { LibraryPageStateProvider } from "./components/state-provider";
/**
@@ -13,6 +14,7 @@ export default function LibraryPage() {
<main className="pt-160 container min-h-screen space-y-4 pb-20 pt-16 sm:px-8 md:px-12">
<LibraryPageStateProvider>
<LibraryActionHeader />
<FavoritesSection />
<LibraryAgentList />
</LibraryPageStateProvider>
</main>

View File

@@ -4056,6 +4056,70 @@
}
}
},
"/api/library/agents/favorites": {
"get": {
"tags": ["v2", "library", "private"],
"summary": "List Favorite Library Agents",
"description": "Get all favorite agents in the user's library.\n\nArgs:\n user_id: ID of the authenticated user.\n page: Page number to retrieve.\n page_size: Number of agents per page.\n\nReturns:\n A LibraryAgentResponse containing favorite agents and pagination metadata.\n\nRaises:\n HTTPException: If a server/database error occurs.",
"operationId": "getV2List favorite library agents",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [
{
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"description": "Page number to retrieve (must be >= 1)",
"default": 1,
"title": "Page"
},
"description": "Page number to retrieve (must be >= 1)"
},
{
"name": "page_size",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"description": "Number of agents per page (must be >= 1)",
"default": 15,
"title": "Page Size"
},
"description": "Number of agents per page (must be >= 1)"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LibraryAgentResponse"
}
}
}
},
"500": {
"description": "Server error",
"content": { "application/json": {} }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
}
}
}
},
"/api/library/agents/{library_agent_id}": {
"get": {
"tags": ["v2", "library", "private"],
@@ -5880,6 +5944,7 @@
"type": "boolean",
"title": "Is Latest Version"
},
"is_favorite": { "type": "boolean", "title": "Is Favorite" },
"recommended_schedule_cron": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Recommended Schedule Cron"
@@ -5903,7 +5968,8 @@
"has_external_trigger",
"new_output",
"can_access_graph",
"is_latest_version"
"is_latest_version",
"is_favorite"
],
"title": "LibraryAgent",
"description": "Represents an agent in the library, including metadata for display and\nuser interaction within the system."

View File

@@ -662,6 +662,13 @@ export default class BackendAPI {
return this._get("/library/agents", params);
}
listFavoriteLibraryAgents(params?: {
page?: number;
page_size?: number;
}): Promise<LibraryAgentResponse> {
return this._get("/library/agents/favorites", params);
}
getLibraryAgent(id: LibraryAgentID): Promise<LibraryAgent> {
return this._get(`/library/agents/${id}`);
}

View File

@@ -431,6 +431,7 @@ export type LibraryAgent = {
credentials_input_schema: CredentialsInputSchema;
new_output: boolean;
can_access_graph: boolean;
is_favorite: boolean;
is_latest_version: boolean;
recommended_schedule_cron: string | null;
} & (

View File

@@ -10,6 +10,7 @@ export enum Flag {
NEW_AGENT_RUNS = "new-agent-runs",
GRAPH_SEARCH = "graph-search",
ENABLE_ENHANCED_OUTPUT_HANDLING = "enable-enhanced-output-handling",
AGENT_FAVORITING = "agent-favoriting",
}
export type FlagValues = {
@@ -19,6 +20,7 @@ export type FlagValues = {
[Flag.NEW_AGENT_RUNS]: boolean;
[Flag.GRAPH_SEARCH]: boolean;
[Flag.ENABLE_ENHANCED_OUTPUT_HANDLING]: boolean;
[Flag.AGENT_FAVORITING]: boolean;
};
const isPwMockEnabled = process.env.NEXT_PUBLIC_PW_TEST === "true";
@@ -30,6 +32,7 @@ const mockFlags = {
[Flag.NEW_AGENT_RUNS]: false,
[Flag.GRAPH_SEARCH]: true,
[Flag.ENABLE_ENHANCED_OUTPUT_HANDLING]: false,
[Flag.AGENT_FAVORITING]: false,
};
export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] | null {