mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(platform/library): Agent Library v2 (#9407)
- Resolves #8774 - Resolves #8775 - Includes back-end work for #9168 - Partially implements #8776 - Partially implements #8777 ### Changes 🏗️ - Add `/library` page - Change target of "Library" navigation link from `/monitoring` to `/library` - Move `/agents/[id]` page to `/library/agents/[id]` - Set application background color to `bg-neutral-50` - Redirect to new library agent's "runs" page (`/library/agents/[id]`) after adding from marketplace Further (technical) frontend changes: - Add types and client methods for all library API endpoints - Added `primary` variant to `agptui/Button` component Backend changes: - Add functionality to library backend - Aggregate agent status - Image generation for use in library view - Add `LibraryAgent.imageUrl` column to DB schema - Sorting & pagination - Explicit relation between library agents and their graph's creator - Refactor & update API endpoints for DX - Other minor refactoring - Add missing but required `MEDIA_GCS_BUCKET_NAME` to `.env.example` ### 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: - `/library` - [x] Create agent from scratch -> should show up on `/library` - [x] Add agent to library from marketplace -> should show up on `/library` - [x] Click on agent in `/library` -> should go to `/library/agents/[id]` - [x] Test sorting & pagination functionality - `/library/agents/[id]` - [x] Fill out inputs and click "Run" -> should run like normally - [x] Select completed agent run -> should show all inputs & outputs - [x] Click "run again" on a completed agent run -> should run successfully with same input - [x] `/monitoring` should still work the same as before --------- Co-authored-by: abhi1992002 <abhimanyu1992002@gmail.com> Co-authored-by: Reinier van der Leer <pwuts@agpt.co> Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
This commit is contained in:
@@ -41,6 +41,9 @@ RABBITMQ_PORT=5672
|
|||||||
RABBITMQ_DEFAULT_USER=rabbitmq_user_default
|
RABBITMQ_DEFAULT_USER=rabbitmq_user_default
|
||||||
RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7
|
RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7
|
||||||
|
|
||||||
|
## GCS bucket is required for marketplace and library functionality
|
||||||
|
MEDIA_GCS_BUCKET_NAME=
|
||||||
|
|
||||||
## For local development, you may need to set FRONTEND_BASE_URL for the OAuth flow
|
## For local development, you may need to set FRONTEND_BASE_URL for the OAuth flow
|
||||||
## for integrations to work. Defaults to the value of PLATFORM_BASE_URL if not set.
|
## for integrations to work. Defaults to the value of PLATFORM_BASE_URL if not set.
|
||||||
# FRONTEND_BASE_URL=http://localhost:3000
|
# FRONTEND_BASE_URL=http://localhost:3000
|
||||||
|
|||||||
@@ -1,27 +1,58 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import fastapi
|
||||||
import prisma.errors
|
import prisma.errors
|
||||||
import prisma.fields
|
import prisma.fields
|
||||||
import prisma.models
|
import prisma.models
|
||||||
import prisma.types
|
import prisma.types
|
||||||
|
|
||||||
|
import backend.data.includes
|
||||||
import backend.server.model
|
import backend.server.model
|
||||||
import backend.server.v2.library.model as library_model
|
import backend.server.v2.library.model as library_model
|
||||||
import backend.server.v2.store.exceptions as store_exceptions
|
import backend.server.v2.store.exceptions as store_exceptions
|
||||||
|
import backend.server.v2.store.image_gen as store_image_gen
|
||||||
|
import backend.server.v2.store.media as store_media
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def get_library_agents(
|
async def get_library_agents(
|
||||||
user_id: str, search_query: str | None = None
|
user_id: str,
|
||||||
) -> list[library_model.LibraryAgent]:
|
search_term: Optional[str] = None,
|
||||||
|
sort_by: library_model.LibraryAgentSort = library_model.LibraryAgentSort.UPDATED_AT,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 50,
|
||||||
|
) -> library_model.LibraryAgentResponse:
|
||||||
|
"""
|
||||||
|
Retrieves a paginated list of LibraryAgent records for a given user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The ID of the user whose LibraryAgents we want to retrieve.
|
||||||
|
search_term: Optional string to filter agents by name/description.
|
||||||
|
sort_by: Sorting field (createdAt, updatedAt, isFavorite, isCreatedByUser).
|
||||||
|
page: Current page (1-indexed).
|
||||||
|
page_size: Number of items per page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A LibraryAgentResponse containing the list of agents and pagination details.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DatabaseError: If there is an issue fetching from Prisma.
|
||||||
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Fetching library agents for user_id={user_id} search_query={search_query}"
|
f"Fetching library agents for user_id={user_id}, "
|
||||||
|
f"search_term={repr(search_term)}, "
|
||||||
|
f"sort_by={sort_by}, page={page}, page_size={page_size}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if search_query and len(search_query.strip()) > 100:
|
if page < 1 or page_size < 1:
|
||||||
logger.warning(f"Search query too long: {search_query}")
|
logger.warning(f"Invalid pagination: page={page}, page_size={page_size}")
|
||||||
raise store_exceptions.DatabaseError("Search query is too long.")
|
raise store_exceptions.DatabaseError("Invalid pagination input")
|
||||||
|
|
||||||
|
if search_term and len(search_term.strip()) > 100:
|
||||||
|
logger.warning(f"Search term too long: {repr(search_term)}")
|
||||||
|
raise store_exceptions.DatabaseError("Search term is too long")
|
||||||
|
|
||||||
where_clause: prisma.types.LibraryAgentWhereInput = {
|
where_clause: prisma.types.LibraryAgentWhereInput = {
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
@@ -29,69 +60,167 @@ async def get_library_agents(
|
|||||||
"isArchived": False,
|
"isArchived": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
if search_query:
|
# Build search filter if applicable
|
||||||
|
if search_term:
|
||||||
where_clause["OR"] = [
|
where_clause["OR"] = [
|
||||||
{
|
{
|
||||||
"Agent": {
|
"Agent": {
|
||||||
"is": {"name": {"contains": search_query, "mode": "insensitive"}}
|
"is": {"name": {"contains": search_term, "mode": "insensitive"}}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Agent": {
|
"Agent": {
|
||||||
"is": {
|
"is": {
|
||||||
"description": {"contains": search_query, "mode": "insensitive"}
|
"description": {"contains": search_term, "mode": "insensitive"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Determine sorting
|
||||||
|
order_by: prisma.types.LibraryAgentOrderByInput | None = None
|
||||||
|
|
||||||
|
if sort_by == library_model.LibraryAgentSort.CREATED_AT:
|
||||||
|
order_by = {"createdAt": "asc"}
|
||||||
|
elif sort_by == library_model.LibraryAgentSort.UPDATED_AT:
|
||||||
|
order_by = {"updatedAt": "desc"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
|
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
|
||||||
where=where_clause,
|
where=where_clause,
|
||||||
include={
|
include={
|
||||||
"Agent": {
|
"Agent": {
|
||||||
"include": {
|
"include": {
|
||||||
"AgentNodes": {"include": {"Input": True, "Output": True}}
|
**backend.data.includes.AGENT_GRAPH_INCLUDE,
|
||||||
|
"AgentGraphExecution": {"where": {"userId": user_id}},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"Creator": True,
|
||||||
},
|
},
|
||||||
order=[{"updatedAt": "desc"}],
|
order=order_by,
|
||||||
|
skip=(page - 1) * page_size,
|
||||||
|
take=page_size,
|
||||||
)
|
)
|
||||||
logger.debug(f"Retrieved {len(library_agents)} agents for user_id={user_id}.")
|
agent_count = await prisma.models.LibraryAgent.prisma().count(
|
||||||
return [library_model.LibraryAgent.from_db(agent) for agent in library_agents]
|
where=where_clause
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Retrieved {len(library_agents)} library agents for user #{user_id}"
|
||||||
|
)
|
||||||
|
return library_model.LibraryAgentResponse(
|
||||||
|
agents=[
|
||||||
|
library_model.LibraryAgent.from_db(agent) for agent in library_agents
|
||||||
|
],
|
||||||
|
pagination=backend.server.model.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:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error fetching library agents: {e}")
|
logger.error(f"Database error fetching library agents: {e}")
|
||||||
raise store_exceptions.DatabaseError("Unable to fetch library agents.")
|
raise store_exceptions.DatabaseError("Failed to fetch library agents") from e
|
||||||
|
|
||||||
|
|
||||||
async def create_library_agent(
|
async def create_library_agent(
|
||||||
agent_id: str, agent_version: int, user_id: str
|
agent_id: str,
|
||||||
|
agent_version: int,
|
||||||
|
user_id: str,
|
||||||
) -> prisma.models.LibraryAgent:
|
) -> prisma.models.LibraryAgent:
|
||||||
"""
|
"""
|
||||||
Adds an agent to the user's library (LibraryAgent table)
|
Adds an agent to the user's library (LibraryAgent table).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_id: The ID of the agent to add.
|
||||||
|
agent_version: The version of the agent to add.
|
||||||
|
user_id: The user to whom the agent will be added.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created LibraryAgent record.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AgentNotFoundError: If the specified agent does not exist.
|
||||||
|
DatabaseError: If there's an error during creation or if image generation fails.
|
||||||
"""
|
"""
|
||||||
|
logger.info(
|
||||||
|
f"Creating library agent for graph #{agent_id} v{agent_version}; "
|
||||||
|
f"user #{user_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch agent graph
|
||||||
|
try:
|
||||||
|
agent = await prisma.models.AgentGraph.prisma().find_unique(
|
||||||
|
where={"graphVersionId": {"id": agent_id, "version": agent_version}}
|
||||||
|
)
|
||||||
|
except prisma.errors.PrismaError as e:
|
||||||
|
logger.exception("Database error fetching agent")
|
||||||
|
raise store_exceptions.DatabaseError("Failed to fetch agent") from e
|
||||||
|
|
||||||
|
if not agent:
|
||||||
|
raise store_exceptions.AgentNotFoundError(
|
||||||
|
f"Agent #{agent_id} v{agent_version} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use .jpeg here since we are generating JPEG images
|
||||||
|
filename = f"agent_{agent_id}.jpeg"
|
||||||
|
try:
|
||||||
|
if not (image_url := await store_media.check_media_exists(user_id, filename)):
|
||||||
|
# Generate agent image as JPEG
|
||||||
|
image = await store_image_gen.generate_agent_image(agent=agent)
|
||||||
|
|
||||||
|
# Create UploadFile with the correct filename and content_type
|
||||||
|
image_file = fastapi.UploadFile(file=image, filename=filename)
|
||||||
|
|
||||||
|
image_url = await store_media.upload_media(
|
||||||
|
user_id=user_id, file=image_file, use_file_name=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error generating and uploading agent image: {e}")
|
||||||
|
image_url = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await prisma.models.LibraryAgent.prisma().create(
|
return await prisma.models.LibraryAgent.prisma().create(
|
||||||
data={
|
data={
|
||||||
"userId": user_id,
|
"imageUrl": image_url,
|
||||||
"agentId": agent_id,
|
"isCreatedByUser": (user_id == agent.userId),
|
||||||
"agentVersion": agent_version,
|
|
||||||
"isCreatedByUser": False,
|
|
||||||
"useGraphIsActiveVersion": True,
|
"useGraphIsActiveVersion": True,
|
||||||
|
"User": {"connect": {"id": user_id}},
|
||||||
|
# "Creator": {"connect": {"id": agent.userId}},
|
||||||
|
"Agent": {
|
||||||
|
"connect": {
|
||||||
|
"graphVersionId": {"id": agent_id, "version": agent_version}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error creating agent to library: {str(e)}")
|
logger.error(f"Database error creating agent in library: {e}")
|
||||||
raise store_exceptions.DatabaseError("Failed to create agent to library") from e
|
raise store_exceptions.DatabaseError("Failed to create agent in library") from e
|
||||||
|
|
||||||
|
|
||||||
async def update_agent_version_in_library(
|
async def update_agent_version_in_library(
|
||||||
user_id: str, agent_id: str, agent_version: int
|
user_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
agent_version: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Updates the agent version in the library
|
Updates the agent version in the library if useGraphIsActiveVersion is True.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: Owner of the LibraryAgent.
|
||||||
|
agent_id: The agent's ID to update.
|
||||||
|
agent_version: The new version of the agent.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DatabaseError: If there's an error with the update.
|
||||||
"""
|
"""
|
||||||
|
logger.debug(
|
||||||
|
f"Updating agent version in library for user #{user_id}, "
|
||||||
|
f"agent #{agent_id} v{agent_version}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
library_agent = await prisma.models.LibraryAgent.prisma().find_first_or_raise(
|
library_agent = await prisma.models.LibraryAgent.prisma().find_first_or_raise(
|
||||||
where={
|
where={
|
||||||
@@ -111,7 +240,7 @@ async def update_agent_version_in_library(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error updating agent version in library: {str(e)}")
|
logger.error(f"Database error updating agent version in library: {e}")
|
||||||
raise store_exceptions.DatabaseError(
|
raise store_exceptions.DatabaseError(
|
||||||
"Failed to update agent version in library"
|
"Failed to update agent version in library"
|
||||||
) from e
|
) from e
|
||||||
@@ -126,8 +255,24 @@ async def update_library_agent(
|
|||||||
is_deleted: bool = False,
|
is_deleted: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Updates the library agent with the given fields
|
Updates the specified LibraryAgent record.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
library_agent_id: The ID of the LibraryAgent to update.
|
||||||
|
user_id: The owner of this LibraryAgent.
|
||||||
|
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.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
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}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await prisma.models.LibraryAgent.prisma().update_many(
|
await prisma.models.LibraryAgent.prisma().update_many(
|
||||||
where={"id": library_agent_id, "userId": user_id},
|
where={"id": library_agent_id, "userId": user_id},
|
||||||
@@ -152,75 +297,84 @@ async def delete_library_agent_by_graph_id(graph_id: str, user_id: str) -> None:
|
|||||||
where={"agentId": graph_id, "userId": user_id}
|
where={"agentId": graph_id, "userId": user_id}
|
||||||
)
|
)
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error deleting library agent: {str(e)}")
|
logger.error(f"Database error deleting library agent: {e}")
|
||||||
raise store_exceptions.DatabaseError("Failed to delete library agent") from e
|
raise store_exceptions.DatabaseError("Failed to delete library agent") from e
|
||||||
|
|
||||||
|
|
||||||
async def add_store_agent_to_library(
|
async def add_store_agent_to_library(
|
||||||
store_listing_version_id: str, user_id: str
|
store_listing_version_id: str, user_id: str
|
||||||
) -> None:
|
) -> library_model.LibraryAgent:
|
||||||
"""
|
"""
|
||||||
Finds the agent from the store listing version and adds it to the user's library (LibraryAgent table)
|
Adds an agent from a store listing version to the user's library if they don't already have it.
|
||||||
if they don't already have it
|
|
||||||
|
Args:
|
||||||
|
store_listing_version_id: The ID of the store listing version containing the agent.
|
||||||
|
user_id: The user’s library to which the agent is being added.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created LibraryAgent if successfully added, the existing corresponding one if any.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AgentNotFoundError: If the store listing or associated agent is not found.
|
||||||
|
DatabaseError: If there's an issue creating the LibraryAgent record.
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Adding agent from store listing version {store_listing_version_id} to library for user {user_id}"
|
f"Adding agent from store listing version #{store_listing_version_id} "
|
||||||
|
f"to library for user #{user_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get store listing version to find agent
|
|
||||||
store_listing_version = (
|
store_listing_version = (
|
||||||
await prisma.models.StoreListingVersion.prisma().find_unique(
|
await prisma.models.StoreListingVersion.prisma().find_unique(
|
||||||
where={"id": store_listing_version_id}, include={"Agent": True}
|
where={"id": store_listing_version_id}, include={"Agent": True}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not store_listing_version or not store_listing_version.Agent:
|
if not store_listing_version or not store_listing_version.Agent:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Store listing version not found: {store_listing_version_id}"
|
f"Store listing version not found: {store_listing_version_id}"
|
||||||
)
|
)
|
||||||
raise store_exceptions.AgentNotFoundError(
|
raise store_exceptions.AgentNotFoundError(
|
||||||
f"Store listing version {store_listing_version_id} not found"
|
f"Store listing version {store_listing_version_id} not found or invalid"
|
||||||
)
|
)
|
||||||
|
|
||||||
agent = store_listing_version.Agent
|
store_agent = store_listing_version.Agent
|
||||||
|
if store_agent.userId == user_id:
|
||||||
if agent.userId == user_id:
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"User {user_id} cannot add their own agent to their library"
|
f"User #{user_id} attempted to add their own agent to their library"
|
||||||
)
|
)
|
||||||
raise store_exceptions.DatabaseError("Cannot add own agent to library")
|
raise store_exceptions.DatabaseError("Cannot add own agent to library")
|
||||||
|
|
||||||
# Check if user already has this agent
|
# Check if user already has this agent
|
||||||
existing_user_agent = await prisma.models.LibraryAgent.prisma().find_first(
|
existing_library_agent = await prisma.models.LibraryAgent.prisma().find_first(
|
||||||
where={
|
where={
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
"agentId": agent.id,
|
"agentId": store_agent.id,
|
||||||
"agentVersion": agent.version,
|
"agentVersion": store_agent.version,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if existing_library_agent:
|
||||||
if existing_user_agent:
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"User {user_id} already has agent {agent.id} in their library"
|
f"User #{user_id} already has agent #{store_agent.id} in their library"
|
||||||
)
|
)
|
||||||
return
|
return library_model.LibraryAgent.from_db(existing_library_agent)
|
||||||
|
|
||||||
# Create LibraryAgent entry
|
# Create LibraryAgent entry
|
||||||
await prisma.models.LibraryAgent.prisma().create(
|
added_agent = await prisma.models.LibraryAgent.prisma().create(
|
||||||
data={
|
data={
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
"agentId": agent.id,
|
"agentId": store_agent.id,
|
||||||
"agentVersion": agent.version,
|
"agentVersion": store_agent.version,
|
||||||
"isCreatedByUser": False,
|
"isCreatedByUser": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
logger.debug(f"Added agent {agent.id} to library for user {user_id}")
|
logger.debug(f"Added agent #{store_agent.id} to library for user #{user_id}")
|
||||||
|
return library_model.LibraryAgent.from_db(added_agent)
|
||||||
|
|
||||||
except store_exceptions.AgentNotFoundError:
|
except store_exceptions.AgentNotFoundError:
|
||||||
|
# Reraise for external handling.
|
||||||
raise
|
raise
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error adding agent to library: {str(e)}")
|
logger.error(f"Database error adding agent to library: {e}")
|
||||||
raise store_exceptions.DatabaseError("Failed to add agent to library") from e
|
raise store_exceptions.DatabaseError("Failed to add agent to library") from e
|
||||||
|
|
||||||
|
|
||||||
@@ -232,20 +386,44 @@ async def add_store_agent_to_library(
|
|||||||
async def get_presets(
|
async def get_presets(
|
||||||
user_id: str, page: int, page_size: int
|
user_id: str, page: int, page_size: int
|
||||||
) -> library_model.LibraryAgentPresetResponse:
|
) -> library_model.LibraryAgentPresetResponse:
|
||||||
|
"""
|
||||||
|
Retrieves a paginated list of AgentPresets for the specified user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user ID whose presets are being retrieved.
|
||||||
|
page: The current page index (0-based or 1-based, clarify in your domain).
|
||||||
|
page_size: Number of items to retrieve per page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A LibraryAgentPresetResponse containing a list of presets and pagination info.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DatabaseError: If there's a database error during the operation.
|
||||||
|
"""
|
||||||
|
logger.debug(
|
||||||
|
f"Fetching presets for user #{user_id}, page={page}, page_size={page_size}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if page < 0 or page_size < 1:
|
||||||
|
logger.warning(
|
||||||
|
"Invalid pagination input: page=%d, page_size=%d", page, page_size
|
||||||
|
)
|
||||||
|
raise store_exceptions.DatabaseError("Invalid pagination parameters")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
presets = await prisma.models.AgentPreset.prisma().find_many(
|
presets_records = await prisma.models.AgentPreset.prisma().find_many(
|
||||||
where={"userId": user_id},
|
where={"userId": user_id},
|
||||||
skip=page * page_size,
|
skip=page * page_size,
|
||||||
take=page_size,
|
take=page_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
total_items = await prisma.models.AgentPreset.prisma().count(
|
total_items = await prisma.models.AgentPreset.prisma().count(
|
||||||
where={"userId": user_id},
|
where={"userId": user_id}
|
||||||
)
|
)
|
||||||
total_pages = (total_items + page_size - 1) // page_size
|
total_pages = (total_items + page_size - 1) // page_size
|
||||||
|
|
||||||
presets = [
|
presets = [
|
||||||
library_model.LibraryAgentPreset.from_db(preset) for preset in presets
|
library_model.LibraryAgentPreset.from_db(preset)
|
||||||
|
for preset in presets_records
|
||||||
]
|
]
|
||||||
|
|
||||||
return library_model.LibraryAgentPresetResponse(
|
return library_model.LibraryAgentPresetResponse(
|
||||||
@@ -259,34 +437,67 @@ async def get_presets(
|
|||||||
)
|
)
|
||||||
|
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error getting presets: {str(e)}")
|
logger.error(f"Database error getting presets: {e}")
|
||||||
raise store_exceptions.DatabaseError("Failed to fetch presets") from e
|
raise store_exceptions.DatabaseError("Failed to fetch presets") from e
|
||||||
|
|
||||||
|
|
||||||
async def get_preset(
|
async def get_preset(
|
||||||
user_id: str, preset_id: str
|
user_id: str, preset_id: str
|
||||||
) -> library_model.LibraryAgentPreset | None:
|
) -> library_model.LibraryAgentPreset | None:
|
||||||
|
"""
|
||||||
|
Retrieves a single AgentPreset by its ID for a given user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user that owns the preset.
|
||||||
|
preset_id: The ID of the preset.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A LibraryAgentPreset if it exists and matches the user, otherwise None.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DatabaseError: If there's a database error during the fetch.
|
||||||
|
"""
|
||||||
|
logger.debug(f"Fetching preset #{preset_id} for user #{user_id}")
|
||||||
try:
|
try:
|
||||||
preset = await prisma.models.AgentPreset.prisma().find_unique(
|
preset = await prisma.models.AgentPreset.prisma().find_unique(
|
||||||
where={"id": preset_id}, include={"InputPresets": True}
|
where={"id": preset_id},
|
||||||
|
include={"InputPresets": True},
|
||||||
)
|
)
|
||||||
if not preset or preset.userId != user_id:
|
if not preset or preset.userId != user_id:
|
||||||
return None
|
return None
|
||||||
return library_model.LibraryAgentPreset.from_db(preset)
|
return library_model.LibraryAgentPreset.from_db(preset)
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error getting preset: {str(e)}")
|
logger.error(f"Database error getting preset: {e}")
|
||||||
raise store_exceptions.DatabaseError("Failed to fetch preset") from e
|
raise store_exceptions.DatabaseError("Failed to fetch preset") from e
|
||||||
|
|
||||||
|
|
||||||
async def upsert_preset(
|
async def upsert_preset(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
preset: library_model.CreateLibraryAgentPresetRequest,
|
preset: library_model.CreateLibraryAgentPresetRequest,
|
||||||
preset_id: str | None = None,
|
preset_id: Optional[str] = None,
|
||||||
) -> library_model.LibraryAgentPreset:
|
) -> library_model.LibraryAgentPreset:
|
||||||
|
"""
|
||||||
|
Creates or updates an AgentPreset for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The ID of the user creating/updating the preset.
|
||||||
|
preset: The preset data used for creation or update.
|
||||||
|
preset_id: An optional preset ID to update; if None, a new preset is created.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created or updated LibraryAgentPreset.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DatabaseError: If there's a database error in creating or updating the preset.
|
||||||
|
ValueError: If attempting to update a non-existent preset.
|
||||||
|
"""
|
||||||
|
logger.debug(
|
||||||
|
f"Upserting preset #{preset_id} ({repr(preset.name)}) for user #{user_id}",
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
if preset_id:
|
if preset_id:
|
||||||
# Update existing preset
|
# Update existing preset
|
||||||
new_preset = await prisma.models.AgentPreset.prisma().update(
|
updated = await prisma.models.AgentPreset.prisma().update(
|
||||||
where={"id": preset_id},
|
where={"id": preset_id},
|
||||||
data={
|
data={
|
||||||
"name": preset.name,
|
"name": preset.name,
|
||||||
@@ -301,8 +512,9 @@ async def upsert_preset(
|
|||||||
},
|
},
|
||||||
include={"InputPresets": True},
|
include={"InputPresets": True},
|
||||||
)
|
)
|
||||||
if not new_preset:
|
if not updated:
|
||||||
raise ValueError(f"AgentPreset #{preset_id} not found")
|
raise ValueError(f"AgentPreset #{preset_id} not found")
|
||||||
|
return library_model.LibraryAgentPreset.from_db(updated)
|
||||||
else:
|
else:
|
||||||
# Create new preset
|
# Create new preset
|
||||||
new_preset = await prisma.models.AgentPreset.prisma().create(
|
new_preset = await prisma.models.AgentPreset.prisma().create(
|
||||||
@@ -324,16 +536,27 @@ async def upsert_preset(
|
|||||||
)
|
)
|
||||||
return library_model.LibraryAgentPreset.from_db(new_preset)
|
return library_model.LibraryAgentPreset.from_db(new_preset)
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error creating preset: {str(e)}")
|
logger.error(f"Database error upserting preset: {e}")
|
||||||
raise store_exceptions.DatabaseError("Failed to create preset") from e
|
raise store_exceptions.DatabaseError("Failed to create preset") from e
|
||||||
|
|
||||||
|
|
||||||
async def delete_preset(user_id: str, preset_id: str) -> None:
|
async def delete_preset(user_id: str, preset_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Soft-deletes a preset by marking it as isDeleted = True.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user that owns the preset.
|
||||||
|
preset_id: The ID of the preset to delete.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DatabaseError: If there's a database error during deletion.
|
||||||
|
"""
|
||||||
|
logger.info(f"Deleting preset {preset_id} for user {user_id}")
|
||||||
try:
|
try:
|
||||||
await prisma.models.AgentPreset.prisma().update_many(
|
await prisma.models.AgentPreset.prisma().update_many(
|
||||||
where={"id": preset_id, "userId": user_id},
|
where={"id": preset_id, "userId": user_id},
|
||||||
data={"isDeleted": True},
|
data={"isDeleted": True},
|
||||||
)
|
)
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error deleting preset: {str(e)}")
|
logger.error(f"Database error deleting preset: {e}")
|
||||||
raise store_exceptions.DatabaseError("Failed to delete preset") from e
|
raise store_exceptions.DatabaseError("Failed to delete preset") from e
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import prisma.models
|
|||||||
import pytest
|
import pytest
|
||||||
from prisma import Prisma
|
from prisma import Prisma
|
||||||
|
|
||||||
import backend.data.includes
|
|
||||||
import backend.server.v2.library.db as db
|
import backend.server.v2.library.db as db
|
||||||
import backend.server.v2.store.exceptions
|
import backend.server.v2.store.exceptions
|
||||||
|
|
||||||
@@ -72,21 +71,24 @@ async def test_get_library_agents(mocker):
|
|||||||
mock_library_agent.return_value.find_many = mocker.AsyncMock(
|
mock_library_agent.return_value.find_many = mocker.AsyncMock(
|
||||||
return_value=mock_library_agents
|
return_value=mock_library_agents
|
||||||
)
|
)
|
||||||
|
mock_library_agent.return_value.count = mocker.AsyncMock(return_value=1)
|
||||||
|
|
||||||
# Call function
|
# Call function
|
||||||
result = await db.get_library_agents("test-user")
|
result = await db.get_library_agents("test-user")
|
||||||
|
|
||||||
# Verify results
|
# Verify results
|
||||||
assert len(result) == 1
|
assert len(result.agents) == 1
|
||||||
assert result[0].id == "ua1"
|
assert result.agents[0].id == "ua1"
|
||||||
assert result[0].name == "Test Agent 2"
|
assert result.agents[0].name == "Test Agent 2"
|
||||||
assert result[0].description == "Test Description 2"
|
assert result.agents[0].description == "Test Description 2"
|
||||||
assert result[0].is_created_by_user is False
|
assert result.agents[0].agent_id == "agent2"
|
||||||
assert result[0].is_latest_version is True
|
assert result.agents[0].agent_version == 1
|
||||||
assert result[0].is_favorite is False
|
assert result.agents[0].can_access_graph is False
|
||||||
assert result[0].agent_id == "agent2"
|
assert result.agents[0].is_latest_version is True
|
||||||
assert result[0].agent_version == 1
|
assert result.pagination.total_items == 1
|
||||||
assert result[0].preset_id is None
|
assert result.pagination.total_pages == 1
|
||||||
|
assert result.pagination.current_page == 1
|
||||||
|
assert result.pagination.page_size == 50
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from enum import Enum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import prisma.enums
|
||||||
import prisma.models
|
import prisma.models
|
||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
@@ -9,13 +11,29 @@ import backend.data.graph as graph_model
|
|||||||
import backend.server.model as server_model
|
import backend.server.model as server_model
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryAgentStatus(str, Enum):
|
||||||
|
COMPLETED = "COMPLETED" # All runs completed
|
||||||
|
HEALTHY = "HEALTHY" # Agent is running (not all runs have completed)
|
||||||
|
WAITING = "WAITING" # Agent is queued or waiting to start
|
||||||
|
ERROR = "ERROR" # Agent is in an error state
|
||||||
|
|
||||||
|
|
||||||
class LibraryAgent(pydantic.BaseModel):
|
class LibraryAgent(pydantic.BaseModel):
|
||||||
id: str # Changed from agent_id to match GraphMeta
|
"""
|
||||||
|
Represents an agent in the library, including metadata for display and
|
||||||
|
user interaction within the system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: str
|
||||||
agent_id: str
|
agent_id: str
|
||||||
agent_version: int # Changed from agent_version to match GraphMeta
|
agent_version: int
|
||||||
|
|
||||||
preset_id: str | None
|
image_url: str | None
|
||||||
|
|
||||||
|
creator_name: str
|
||||||
|
creator_image_url: str
|
||||||
|
|
||||||
|
status: LibraryAgentStatus
|
||||||
|
|
||||||
updated_at: datetime.datetime
|
updated_at: datetime.datetime
|
||||||
|
|
||||||
@@ -24,47 +42,135 @@ class LibraryAgent(pydantic.BaseModel):
|
|||||||
|
|
||||||
# Made input_schema and output_schema match GraphMeta's type
|
# Made input_schema and output_schema match GraphMeta's type
|
||||||
input_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend
|
input_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend
|
||||||
output_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend
|
|
||||||
|
|
||||||
is_favorite: bool
|
# Indicates whether there's a new output (based on recent runs)
|
||||||
is_created_by_user: bool
|
new_output: bool
|
||||||
|
|
||||||
|
# Whether the user can access the underlying graph
|
||||||
|
can_access_graph: bool
|
||||||
|
|
||||||
|
# Indicates if this agent is the latest version
|
||||||
is_latest_version: bool
|
is_latest_version: bool
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_db(agent: prisma.models.LibraryAgent):
|
def from_db(agent: prisma.models.LibraryAgent) -> "LibraryAgent":
|
||||||
|
"""
|
||||||
|
Factory method that constructs a LibraryAgent from a Prisma LibraryAgent
|
||||||
|
model instance.
|
||||||
|
"""
|
||||||
if not agent.Agent:
|
if not agent.Agent:
|
||||||
raise ValueError("AgentGraph is required")
|
raise ValueError("Associated Agent record is required.")
|
||||||
|
|
||||||
graph = graph_model.GraphModel.from_db(agent.Agent)
|
graph = graph_model.GraphModel.from_db(agent.Agent)
|
||||||
|
|
||||||
agent_updated_at = agent.Agent.updatedAt
|
agent_updated_at = agent.Agent.updatedAt
|
||||||
lib_agent_updated_at = agent.updatedAt
|
lib_agent_updated_at = agent.updatedAt
|
||||||
|
|
||||||
# Take the latest updated_at timestamp either when the graph was updated or the library agent was updated
|
# Compute updated_at as the latest between library agent and graph
|
||||||
updated_at = (
|
updated_at = (
|
||||||
max(agent_updated_at, lib_agent_updated_at)
|
max(agent_updated_at, lib_agent_updated_at)
|
||||||
if agent_updated_at
|
if agent_updated_at
|
||||||
else lib_agent_updated_at
|
else lib_agent_updated_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
creator_name = "Unknown"
|
||||||
|
creator_image_url = ""
|
||||||
|
if agent.Creator:
|
||||||
|
creator_name = agent.Creator.name or "Unknown"
|
||||||
|
creator_image_url = agent.Creator.avatarUrl or ""
|
||||||
|
|
||||||
|
# Logic to calculate status and new_output
|
||||||
|
week_ago = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
|
||||||
|
days=7
|
||||||
|
)
|
||||||
|
executions = agent.Agent.AgentGraphExecution or []
|
||||||
|
status_result = _calculate_agent_status(executions, week_ago)
|
||||||
|
status = status_result.status
|
||||||
|
new_output = status_result.new_output
|
||||||
|
|
||||||
|
# Check if user can access the graph
|
||||||
|
can_access_graph = agent.Agent.userId == agent.userId
|
||||||
|
|
||||||
|
# Hard-coded to True until a method to check is implemented
|
||||||
|
is_latest_version = True
|
||||||
|
|
||||||
return LibraryAgent(
|
return LibraryAgent(
|
||||||
id=agent.id,
|
id=agent.id,
|
||||||
agent_id=agent.agentId,
|
agent_id=agent.agentId,
|
||||||
agent_version=agent.agentVersion,
|
agent_version=agent.agentVersion,
|
||||||
|
image_url=agent.imageUrl,
|
||||||
|
creator_name=creator_name,
|
||||||
|
creator_image_url=creator_image_url,
|
||||||
|
status=status,
|
||||||
updated_at=updated_at,
|
updated_at=updated_at,
|
||||||
name=graph.name,
|
name=graph.name,
|
||||||
description=graph.description,
|
description=graph.description,
|
||||||
input_schema=graph.input_schema,
|
input_schema=graph.input_schema,
|
||||||
output_schema=graph.output_schema,
|
new_output=new_output,
|
||||||
is_favorite=agent.isFavorite,
|
can_access_graph=can_access_graph,
|
||||||
is_created_by_user=agent.isCreatedByUser,
|
is_latest_version=is_latest_version,
|
||||||
is_latest_version=graph.is_active,
|
|
||||||
preset_id=agent.AgentPreset.id if agent.AgentPreset else None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentStatusResult(pydantic.BaseModel):
|
||||||
|
status: LibraryAgentStatus
|
||||||
|
new_output: bool
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_agent_status(
|
||||||
|
executions: list[prisma.models.AgentGraphExecution],
|
||||||
|
recent_threshold: datetime.datetime,
|
||||||
|
) -> AgentStatusResult:
|
||||||
|
"""
|
||||||
|
Helper function to determine the overall agent status and whether there
|
||||||
|
is new output (i.e., completed runs within the recent threshold).
|
||||||
|
|
||||||
|
:param executions: A list of AgentGraphExecution objects.
|
||||||
|
:param recent_threshold: A datetime; any execution after this indicates new output.
|
||||||
|
:return: (AgentStatus, new_output_flag)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not executions:
|
||||||
|
return AgentStatusResult(status=LibraryAgentStatus.COMPLETED, new_output=False)
|
||||||
|
|
||||||
|
# Track how many times each execution status appears
|
||||||
|
status_counts = {status: 0 for status in prisma.enums.AgentExecutionStatus}
|
||||||
|
new_output = False
|
||||||
|
|
||||||
|
for execution in executions:
|
||||||
|
# Check if there's a completed run more recent than `recent_threshold`
|
||||||
|
if execution.createdAt >= recent_threshold:
|
||||||
|
if execution.executionStatus == prisma.enums.AgentExecutionStatus.COMPLETED:
|
||||||
|
new_output = True
|
||||||
|
status_counts[execution.executionStatus] += 1
|
||||||
|
|
||||||
|
# Determine the final status based on counts
|
||||||
|
if status_counts[prisma.enums.AgentExecutionStatus.FAILED] > 0:
|
||||||
|
return AgentStatusResult(status=LibraryAgentStatus.ERROR, new_output=new_output)
|
||||||
|
elif status_counts[prisma.enums.AgentExecutionStatus.QUEUED] > 0:
|
||||||
|
return AgentStatusResult(
|
||||||
|
status=LibraryAgentStatus.WAITING, new_output=new_output
|
||||||
|
)
|
||||||
|
elif status_counts[prisma.enums.AgentExecutionStatus.RUNNING] > 0:
|
||||||
|
return AgentStatusResult(
|
||||||
|
status=LibraryAgentStatus.HEALTHY, new_output=new_output
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return AgentStatusResult(
|
||||||
|
status=LibraryAgentStatus.COMPLETED, new_output=new_output
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryAgentResponse(pydantic.BaseModel):
|
||||||
|
"""Response schema for a list of library agents and pagination info."""
|
||||||
|
|
||||||
|
agents: list[LibraryAgent]
|
||||||
|
pagination: server_model.Pagination
|
||||||
|
|
||||||
|
|
||||||
class LibraryAgentPreset(pydantic.BaseModel):
|
class LibraryAgentPreset(pydantic.BaseModel):
|
||||||
|
"""Represents a preset configuration for a library agent."""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
updated_at: datetime.datetime
|
updated_at: datetime.datetime
|
||||||
|
|
||||||
@@ -78,14 +184,14 @@ class LibraryAgentPreset(pydantic.BaseModel):
|
|||||||
|
|
||||||
inputs: block_model.BlockInput
|
inputs: block_model.BlockInput
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_db(preset: prisma.models.AgentPreset):
|
def from_db(cls, preset: prisma.models.AgentPreset) -> "LibraryAgentPreset":
|
||||||
input_data: block_model.BlockInput = {}
|
input_data: block_model.BlockInput = {}
|
||||||
|
|
||||||
for preset_input in preset.InputPresets or []:
|
for preset_input in preset.InputPresets or []:
|
||||||
input_data[preset_input.name] = preset_input.data
|
input_data[preset_input.name] = preset_input.data
|
||||||
|
|
||||||
return LibraryAgentPreset(
|
return cls(
|
||||||
id=preset.id,
|
id=preset.id,
|
||||||
updated_at=preset.updatedAt,
|
updated_at=preset.updatedAt,
|
||||||
agent_id=preset.agentId,
|
agent_id=preset.agentId,
|
||||||
@@ -98,14 +204,52 @@ class LibraryAgentPreset(pydantic.BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class LibraryAgentPresetResponse(pydantic.BaseModel):
|
class LibraryAgentPresetResponse(pydantic.BaseModel):
|
||||||
|
"""Response schema for a list of agent presets and pagination info."""
|
||||||
|
|
||||||
presets: list[LibraryAgentPreset]
|
presets: list[LibraryAgentPreset]
|
||||||
pagination: server_model.Pagination
|
pagination: server_model.Pagination
|
||||||
|
|
||||||
|
|
||||||
class CreateLibraryAgentPresetRequest(pydantic.BaseModel):
|
class CreateLibraryAgentPresetRequest(pydantic.BaseModel):
|
||||||
|
"""
|
||||||
|
Request model used when creating a new preset for a library agent.
|
||||||
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
inputs: block_model.BlockInput
|
inputs: block_model.BlockInput
|
||||||
agent_id: str
|
agent_id: str
|
||||||
agent_version: int
|
agent_version: int
|
||||||
is_active: bool
|
is_active: bool
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryAgentFilter(str, Enum):
|
||||||
|
"""Possible filters for searching library agents."""
|
||||||
|
|
||||||
|
IS_FAVOURITE = "isFavourite"
|
||||||
|
IS_CREATED_BY_USER = "isCreatedByUser"
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryAgentSort(str, Enum):
|
||||||
|
"""Possible sort options for sorting library agents."""
|
||||||
|
|
||||||
|
CREATED_AT = "createdAt"
|
||||||
|
UPDATED_AT = "updatedAt"
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryAgentUpdateRequest(pydantic.BaseModel):
|
||||||
|
"""
|
||||||
|
Schema for updating a library agent via PUT.
|
||||||
|
|
||||||
|
Includes flags for auto-updating version, marking as favorite,
|
||||||
|
archiving, or deleting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
auto_update_version: bool = pydantic.Field(
|
||||||
|
False, description="Auto-update the agent version"
|
||||||
|
)
|
||||||
|
is_favorite: bool = pydantic.Field(
|
||||||
|
False, description="Mark the agent as a favorite"
|
||||||
|
)
|
||||||
|
is_archived: bool = pydantic.Field(False, description="Archive the agent")
|
||||||
|
is_deleted: bool = pydantic.Field(False, description="Delete the agent")
|
||||||
|
|||||||
@@ -3,147 +3,10 @@ import datetime
|
|||||||
import prisma.fields
|
import prisma.fields
|
||||||
import prisma.models
|
import prisma.models
|
||||||
|
|
||||||
import backend.data.block
|
import backend.server.v2.library.model as library_model
|
||||||
import backend.server.model
|
|
||||||
import backend.server.v2.library.model
|
|
||||||
|
|
||||||
|
|
||||||
def test_library_agent():
|
def test_agent_preset_from_db():
|
||||||
agent = backend.server.v2.library.model.LibraryAgent(
|
|
||||||
id="test-agent-123",
|
|
||||||
agent_id="agent-123",
|
|
||||||
agent_version=1,
|
|
||||||
preset_id=None,
|
|
||||||
updated_at=datetime.datetime.now(),
|
|
||||||
name="Test Agent",
|
|
||||||
description="Test description",
|
|
||||||
input_schema={"type": "object", "properties": {}},
|
|
||||||
output_schema={"type": "object", "properties": {}},
|
|
||||||
is_favorite=False,
|
|
||||||
is_created_by_user=False,
|
|
||||||
is_latest_version=True,
|
|
||||||
)
|
|
||||||
assert agent.id == "test-agent-123"
|
|
||||||
assert agent.agent_id == "agent-123"
|
|
||||||
assert agent.agent_version == 1
|
|
||||||
assert agent.name == "Test Agent"
|
|
||||||
assert agent.description == "Test description"
|
|
||||||
assert agent.is_favorite is False
|
|
||||||
assert agent.is_created_by_user is False
|
|
||||||
assert agent.is_latest_version is True
|
|
||||||
assert agent.input_schema == {"type": "object", "properties": {}}
|
|
||||||
assert agent.output_schema == {"type": "object", "properties": {}}
|
|
||||||
|
|
||||||
|
|
||||||
def test_library_agent_with_user_created():
|
|
||||||
agent = backend.server.v2.library.model.LibraryAgent(
|
|
||||||
id="user-agent-456",
|
|
||||||
agent_id="agent-456",
|
|
||||||
agent_version=2,
|
|
||||||
preset_id=None,
|
|
||||||
updated_at=datetime.datetime.now(),
|
|
||||||
name="User Created Agent",
|
|
||||||
description="An agent created by the user",
|
|
||||||
input_schema={"type": "object", "properties": {}},
|
|
||||||
output_schema={"type": "object", "properties": {}},
|
|
||||||
is_favorite=False,
|
|
||||||
is_created_by_user=True,
|
|
||||||
is_latest_version=True,
|
|
||||||
)
|
|
||||||
assert agent.id == "user-agent-456"
|
|
||||||
assert agent.agent_id == "agent-456"
|
|
||||||
assert agent.agent_version == 2
|
|
||||||
assert agent.name == "User Created Agent"
|
|
||||||
assert agent.description == "An agent created by the user"
|
|
||||||
assert agent.is_favorite is False
|
|
||||||
assert agent.is_created_by_user is True
|
|
||||||
assert agent.is_latest_version is True
|
|
||||||
assert agent.input_schema == {"type": "object", "properties": {}}
|
|
||||||
assert agent.output_schema == {"type": "object", "properties": {}}
|
|
||||||
|
|
||||||
|
|
||||||
def test_library_agent_preset():
|
|
||||||
preset = backend.server.v2.library.model.LibraryAgentPreset(
|
|
||||||
id="preset-123",
|
|
||||||
name="Test Preset",
|
|
||||||
description="Test preset description",
|
|
||||||
agent_id="test-agent-123",
|
|
||||||
agent_version=1,
|
|
||||||
is_active=True,
|
|
||||||
inputs={
|
|
||||||
"dictionary": {"key1": "Hello", "key2": "World"},
|
|
||||||
"selected_value": "key2",
|
|
||||||
},
|
|
||||||
updated_at=datetime.datetime.now(),
|
|
||||||
)
|
|
||||||
assert preset.id == "preset-123"
|
|
||||||
assert preset.name == "Test Preset"
|
|
||||||
assert preset.description == "Test preset description"
|
|
||||||
assert preset.agent_id == "test-agent-123"
|
|
||||||
assert preset.agent_version == 1
|
|
||||||
assert preset.is_active is True
|
|
||||||
assert preset.inputs == {
|
|
||||||
"dictionary": {"key1": "Hello", "key2": "World"},
|
|
||||||
"selected_value": "key2",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_library_agent_preset_response():
|
|
||||||
preset = backend.server.v2.library.model.LibraryAgentPreset(
|
|
||||||
id="preset-123",
|
|
||||||
name="Test Preset",
|
|
||||||
description="Test preset description",
|
|
||||||
agent_id="test-agent-123",
|
|
||||||
agent_version=1,
|
|
||||||
is_active=True,
|
|
||||||
inputs={
|
|
||||||
"dictionary": {"key1": "Hello", "key2": "World"},
|
|
||||||
"selected_value": "key2",
|
|
||||||
},
|
|
||||||
updated_at=datetime.datetime.now(),
|
|
||||||
)
|
|
||||||
|
|
||||||
pagination = backend.server.model.Pagination(
|
|
||||||
total_items=1, total_pages=1, current_page=1, page_size=10
|
|
||||||
)
|
|
||||||
|
|
||||||
response = backend.server.v2.library.model.LibraryAgentPresetResponse(
|
|
||||||
presets=[preset], pagination=pagination
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(response.presets) == 1
|
|
||||||
assert response.presets[0].id == "preset-123"
|
|
||||||
assert response.pagination.total_items == 1
|
|
||||||
assert response.pagination.total_pages == 1
|
|
||||||
assert response.pagination.current_page == 1
|
|
||||||
assert response.pagination.page_size == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_library_agent_preset_request():
|
|
||||||
request = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
|
|
||||||
name="New Preset",
|
|
||||||
description="New preset description",
|
|
||||||
agent_id="agent-123",
|
|
||||||
agent_version=1,
|
|
||||||
is_active=True,
|
|
||||||
inputs={
|
|
||||||
"dictionary": {"key1": "Hello", "key2": "World"},
|
|
||||||
"selected_value": "key2",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert request.name == "New Preset"
|
|
||||||
assert request.description == "New preset description"
|
|
||||||
assert request.agent_id == "agent-123"
|
|
||||||
assert request.agent_version == 1
|
|
||||||
assert request.is_active is True
|
|
||||||
assert request.inputs == {
|
|
||||||
"dictionary": {"key1": "Hello", "key2": "World"},
|
|
||||||
"selected_value": "key2",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_library_agent_from_db():
|
|
||||||
# Create mock DB agent
|
# Create mock DB agent
|
||||||
db_agent = prisma.models.AgentPreset(
|
db_agent = prisma.models.AgentPreset(
|
||||||
id="test-agent-123",
|
id="test-agent-123",
|
||||||
@@ -167,7 +30,7 @@ def test_library_agent_from_db():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Convert to LibraryAgentPreset
|
# Convert to LibraryAgentPreset
|
||||||
agent = backend.server.v2.library.model.LibraryAgentPreset.from_db(db_agent)
|
agent = library_model.LibraryAgentPreset.from_db(db_agent)
|
||||||
|
|
||||||
assert agent.id == "test-agent-123"
|
assert agent.id == "test-agent-123"
|
||||||
assert agent.agent_version == 1
|
assert agent.agent_version == 1
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Annotated, Sequence
|
from typing import Optional
|
||||||
|
|
||||||
import autogpt_libs.auth as autogpt_auth_lib
|
import autogpt_libs.auth as autogpt_auth_lib
|
||||||
import fastapi
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
import backend.server.v2.library.db as library_db
|
import backend.server.v2.library.db as library_db
|
||||||
import backend.server.v2.library.model as library_model
|
import backend.server.v2.library.model as library_model
|
||||||
@@ -10,129 +11,174 @@ import backend.server.v2.store.exceptions as store_exceptions
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = fastapi.APIRouter()
|
router = APIRouter(
|
||||||
|
prefix="/agents",
|
||||||
|
tags=["library", "private"],
|
||||||
|
dependencies=[Depends(autogpt_auth_lib.auth_middleware)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/agents",
|
"",
|
||||||
tags=["library", "private"],
|
responses={
|
||||||
dependencies=[fastapi.Depends(autogpt_auth_lib.auth_middleware)],
|
500: {"description": "Server error", "content": {"application/json": {}}},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
async def get_library_agents(
|
async def get_library_agents(
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)]
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
) -> Sequence[library_model.LibraryAgent]:
|
search_term: Optional[str] = Query(
|
||||||
|
None, description="Search term to filter agents"
|
||||||
|
),
|
||||||
|
sort_by: library_model.LibraryAgentSort = Query(
|
||||||
|
library_model.LibraryAgentSort.UPDATED_AT,
|
||||||
|
description="Sort results by criteria",
|
||||||
|
),
|
||||||
|
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 agents in the user's library, including both created and saved agents.
|
Get all agents in the user's library (both created and saved).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID of the authenticated user.
|
||||||
|
search_term: Optional search term to filter agents by name/description.
|
||||||
|
filter_by: List of filters to apply (favorites, created by user).
|
||||||
|
sort_by: List of sorting criteria (created date, updated date).
|
||||||
|
page: Page number to retrieve.
|
||||||
|
page_size: Number of agents per page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A LibraryAgentResponse containing agents and pagination metadata.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If a server/database error occurs.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
agents = await library_db.get_library_agents(user_id)
|
return await library_db.get_library_agents(
|
||||||
return agents
|
user_id=user_id,
|
||||||
except Exception as e:
|
search_term=search_term,
|
||||||
logger.exception(f"Exception occurred whilst getting library agents: {e}")
|
sort_by=sort_by,
|
||||||
raise fastapi.HTTPException(
|
page=page,
|
||||||
status_code=500, detail="Failed to get library agents"
|
page_size=page_size,
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Could not fetch library agents: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to get library agents",
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/agents/{store_listing_version_id}",
|
"",
|
||||||
tags=["library", "private"],
|
status_code=status.HTTP_201_CREATED,
|
||||||
dependencies=[fastapi.Depends(autogpt_auth_lib.auth_middleware)],
|
responses={
|
||||||
status_code=201,
|
201: {"description": "Agent added successfully"},
|
||||||
|
404: {"description": "Store listing version not found"},
|
||||||
|
500: {"description": "Server error"},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
async def add_agent_to_library(
|
async def add_marketplace_agent_to_library(
|
||||||
store_listing_version_id: str,
|
store_listing_version_id: str = Body(embed=True),
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
) -> fastapi.Response:
|
) -> library_model.LibraryAgent:
|
||||||
"""
|
"""
|
||||||
Add an agent from the store to the user's library.
|
Add an agent from the marketplace to the user's library.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
store_listing_version_id (str): ID of the store listing version to add
|
store_listing_version_id: ID of the store listing version to add.
|
||||||
user_id (str): ID of the authenticated user
|
user_id: ID of the authenticated user.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
fastapi.Response: 201 status code on success
|
201 (Created) on success.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: If there is an error adding the agent to the library
|
HTTPException(404): If the listing version is not found.
|
||||||
|
HTTPException(500): If a server/database error occurs.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Use the database function to add the agent to the library
|
return await library_db.add_store_agent_to_library(
|
||||||
await library_db.add_store_agent_to_library(store_listing_version_id, user_id)
|
store_listing_version_id=store_listing_version_id,
|
||||||
return fastapi.Response(status_code=201)
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
|
||||||
except store_exceptions.AgentNotFoundError:
|
except store_exceptions.AgentNotFoundError:
|
||||||
raise fastapi.HTTPException(
|
logger.warning(f"Agent not found: {store_listing_version_id}")
|
||||||
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
detail=f"Store listing version {store_listing_version_id} not found",
|
detail=f"Store listing version {store_listing_version_id} not found",
|
||||||
)
|
)
|
||||||
except store_exceptions.DatabaseError as e:
|
except store_exceptions.DatabaseError as e:
|
||||||
logger.exception(f"Database error occurred whilst adding agent to library: {e}")
|
logger.error(f"Database error occurred whilst adding agent to library: {e}")
|
||||||
raise fastapi.HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail="Failed to add agent to library"
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
)
|
detail="Failed to add agent to library",
|
||||||
|
) from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.error(f"Unexpected error while adding agent: {e}")
|
||||||
f"Unexpected exception occurred whilst adding agent to library: {e}"
|
raise HTTPException(
|
||||||
)
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
raise fastapi.HTTPException(
|
detail="Failed to add agent to library",
|
||||||
status_code=500, detail="Failed to add agent to library"
|
) from e
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/agents/{library_agent_id}",
|
"/{library_agent_id}",
|
||||||
tags=["library", "private"],
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
dependencies=[fastapi.Depends(autogpt_auth_lib.auth_middleware)],
|
responses={
|
||||||
status_code=204,
|
204: {"description": "Agent updated successfully"},
|
||||||
|
500: {"description": "Server error"},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
async def update_library_agent(
|
async def update_library_agent(
|
||||||
library_agent_id: str,
|
library_agent_id: str,
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
|
payload: library_model.LibraryAgentUpdateRequest,
|
||||||
auto_update_version: bool = False,
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
is_favorite: bool = False,
|
) -> JSONResponse:
|
||||||
is_archived: bool = False,
|
|
||||||
is_deleted: bool = False,
|
|
||||||
) -> fastapi.Response:
|
|
||||||
"""
|
"""
|
||||||
Update the library agent with the given fields.
|
Update the library agent with the given fields.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
library_agent_id (str): ID of the library agent to update
|
library_agent_id: ID of the library agent to update.
|
||||||
user_id (str): ID of the authenticated user
|
payload: Fields to update (auto_update_version, is_favorite, etc.).
|
||||||
auto_update_version (bool): Whether to auto-update the agent version
|
user_id: ID of the authenticated user.
|
||||||
is_favorite (bool): Whether the agent is marked as favorite
|
|
||||||
is_archived (bool): Whether the agent is archived
|
|
||||||
is_deleted (bool): Whether the agent is deleted
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
fastapi.Response: 204 status code on success
|
204 (No Content) on success.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: If there is an error updating the library agent
|
HTTPException(500): If a server/database error occurs.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Use the database function to update the library agent
|
|
||||||
await library_db.update_library_agent(
|
await library_db.update_library_agent(
|
||||||
library_agent_id,
|
library_agent_id=library_agent_id,
|
||||||
user_id,
|
user_id=user_id,
|
||||||
auto_update_version,
|
auto_update_version=payload.auto_update_version,
|
||||||
is_favorite,
|
is_favorite=payload.is_favorite,
|
||||||
is_archived,
|
is_archived=payload.is_archived,
|
||||||
is_deleted,
|
is_deleted=payload.is_deleted,
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
content={"message": "Agent updated successfully"},
|
||||||
)
|
)
|
||||||
return fastapi.Response(status_code=204)
|
|
||||||
|
|
||||||
except store_exceptions.DatabaseError as e:
|
except store_exceptions.DatabaseError as e:
|
||||||
logger.exception(f"Database error occurred whilst updating library agent: {e}")
|
logger.exception(f"Database error while updating library agent: {e}")
|
||||||
raise fastapi.HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail="Failed to update library agent"
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
)
|
detail="Failed to update library agent",
|
||||||
|
) from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(f"Unexpected error while updating library agent: {e}")
|
||||||
f"Unexpected exception occurred whilst updating library agent: {e}"
|
raise HTTPException(
|
||||||
)
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
raise fastapi.HTTPException(
|
detail="Failed to update library agent",
|
||||||
status_code=500, detail="Failed to update library agent"
|
) from e
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,113 +3,225 @@ from typing import Annotated, Any
|
|||||||
|
|
||||||
import autogpt_libs.auth as autogpt_auth_lib
|
import autogpt_libs.auth as autogpt_auth_lib
|
||||||
import autogpt_libs.utils.cache
|
import autogpt_libs.utils.cache
|
||||||
import fastapi
|
from fastapi import APIRouter, Body, Depends, HTTPException, status
|
||||||
|
|
||||||
import backend.executor
|
import backend.executor
|
||||||
import backend.server.v2.library.db as library_db
|
import backend.server.v2.library.db as db
|
||||||
import backend.server.v2.library.model as library_model
|
import backend.server.v2.library.model as models
|
||||||
import backend.util.service
|
import backend.util.service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = fastapi.APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@autogpt_libs.utils.cache.thread_cached
|
@autogpt_libs.utils.cache.thread_cached
|
||||||
def execution_manager_client() -> backend.executor.ExecutionManager:
|
def execution_manager_client() -> backend.executor.ExecutionManager:
|
||||||
|
"""Return a cached instance of ExecutionManager client."""
|
||||||
return backend.util.service.get_service_client(backend.executor.ExecutionManager)
|
return backend.util.service.get_service_client(backend.executor.ExecutionManager)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/presets")
|
@router.get(
|
||||||
|
"/presets",
|
||||||
|
summary="List presets",
|
||||||
|
description="Retrieve a paginated list of presets for the current user.",
|
||||||
|
)
|
||||||
async def get_presets(
|
async def get_presets(
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 10,
|
page_size: int = 10,
|
||||||
) -> library_model.LibraryAgentPresetResponse:
|
) -> models.LibraryAgentPresetResponse:
|
||||||
|
"""
|
||||||
|
Retrieve a paginated list of presets for the current user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (str): ID of the authenticated user.
|
||||||
|
page (int): Page number for pagination.
|
||||||
|
page_size (int): Number of items per page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
models.LibraryAgentPresetResponse: A response containing the list of presets.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
presets = await library_db.get_presets(user_id, page, page_size)
|
return await db.get_presets(user_id, page, page_size)
|
||||||
return presets
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Exception occurred whilst getting presets: {e}")
|
logger.exception(f"Exception occurred while getting presets: {e}")
|
||||||
raise fastapi.HTTPException(status_code=500, detail="Failed to get presets")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to get presets",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/presets/{preset_id}")
|
@router.get(
|
||||||
|
"/presets/{preset_id}",
|
||||||
|
summary="Get a specific preset",
|
||||||
|
description="Retrieve details for a specific preset by its ID.",
|
||||||
|
)
|
||||||
async def get_preset(
|
async def get_preset(
|
||||||
preset_id: str,
|
preset_id: str,
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
) -> library_model.LibraryAgentPreset:
|
) -> models.LibraryAgentPreset:
|
||||||
|
"""
|
||||||
|
Retrieve details for a specific preset by its ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
preset_id (str): ID of the preset to retrieve.
|
||||||
|
user_id (str): ID of the authenticated user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
models.LibraryAgentPreset: The preset details.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If the preset is not found or an error occurs.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
preset = await library_db.get_preset(user_id, preset_id)
|
preset = await db.get_preset(user_id, preset_id)
|
||||||
if not preset:
|
if not preset:
|
||||||
raise fastapi.HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Preset {preset_id} not found",
|
detail=f"Preset {preset_id} not found",
|
||||||
)
|
)
|
||||||
return preset
|
return preset
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Exception occurred whilst getting preset: {e}")
|
logger.exception(f"Exception occurred whilst getting preset: {e}")
|
||||||
raise fastapi.HTTPException(status_code=500, detail="Failed to get preset")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to get preset",
|
||||||
@router.post("/presets")
|
)
|
||||||
async def create_preset(
|
|
||||||
preset: library_model.CreateLibraryAgentPresetRequest,
|
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
|
|
||||||
) -> library_model.LibraryAgentPreset:
|
|
||||||
try:
|
|
||||||
return await library_db.upsert_preset(user_id, preset)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"Exception occurred whilst creating preset: {e}")
|
|
||||||
raise fastapi.HTTPException(status_code=500, detail="Failed to create preset")
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/presets/{preset_id}")
|
|
||||||
async def update_preset(
|
|
||||||
preset_id: str,
|
|
||||||
preset: library_model.CreateLibraryAgentPresetRequest,
|
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
|
|
||||||
) -> library_model.LibraryAgentPreset:
|
|
||||||
try:
|
|
||||||
return await library_db.upsert_preset(user_id, preset, preset_id)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"Exception occurred whilst updating preset: {e}")
|
|
||||||
raise fastapi.HTTPException(status_code=500, detail="Failed to update preset")
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/presets/{preset_id}")
|
|
||||||
async def delete_preset(
|
|
||||||
preset_id: str,
|
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
await library_db.delete_preset(user_id, preset_id)
|
|
||||||
return fastapi.Response(status_code=204)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"Exception occurred whilst deleting preset: {e}")
|
|
||||||
raise fastapi.HTTPException(status_code=500, detail="Failed to delete preset")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
path="/presets/{preset_id}/execute",
|
"/presets",
|
||||||
|
summary="Create a new preset",
|
||||||
|
description="Create a new preset for the current user.",
|
||||||
|
)
|
||||||
|
async def create_preset(
|
||||||
|
preset: models.CreateLibraryAgentPresetRequest,
|
||||||
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
|
) -> models.LibraryAgentPreset:
|
||||||
|
"""
|
||||||
|
Create a new library agent preset. Automatically corrects node_input format if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
preset (models.CreateLibraryAgentPresetRequest): The preset data to create.
|
||||||
|
user_id (str): ID of the authenticated user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
models.LibraryAgentPreset: The created preset.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If an error occurs while creating the preset.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await db.upsert_preset(user_id, preset)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Exception occurred while creating preset: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to create preset",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/presets/{preset_id}",
|
||||||
|
summary="Update an existing preset",
|
||||||
|
description="Update an existing preset by its ID.",
|
||||||
|
)
|
||||||
|
async def update_preset(
|
||||||
|
preset_id: str,
|
||||||
|
preset: models.CreateLibraryAgentPresetRequest,
|
||||||
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
|
) -> models.LibraryAgentPreset:
|
||||||
|
"""
|
||||||
|
Update an existing library agent preset. If the preset doesn't exist, it may be created.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
preset_id (str): ID of the preset to update.
|
||||||
|
preset (models.CreateLibraryAgentPresetRequest): The preset data to update.
|
||||||
|
user_id (str): ID of the authenticated user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
models.LibraryAgentPreset: The updated preset.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If an error occurs while updating the preset.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await db.upsert_preset(user_id, preset, preset_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Exception occurred whilst updating preset: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to update preset",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/presets/{preset_id}",
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
summary="Delete a preset",
|
||||||
|
description="Delete an existing preset by its ID.",
|
||||||
|
)
|
||||||
|
async def delete_preset(
|
||||||
|
preset_id: str,
|
||||||
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Delete a preset by its ID. Returns 204 No Content on success.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
preset_id (str): ID of the preset to delete.
|
||||||
|
user_id (str): ID of the authenticated user.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If an error occurs while deleting the preset.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await db.delete_preset(user_id, preset_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Exception occurred whilst deleting preset: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to delete preset",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/presets/{preset_id}/execute",
|
||||||
tags=["presets"],
|
tags=["presets"],
|
||||||
dependencies=[fastapi.Depends(autogpt_auth_lib.auth_middleware)],
|
summary="Execute a preset",
|
||||||
|
description="Execute a preset with the given graph and node input for the current user.",
|
||||||
)
|
)
|
||||||
async def execute_preset(
|
async def execute_preset(
|
||||||
graph_id: str,
|
graph_id: str,
|
||||||
graph_version: int,
|
graph_version: int,
|
||||||
preset_id: str,
|
preset_id: str,
|
||||||
node_input: Annotated[
|
node_input: Annotated[dict[str, Any], Body(..., embed=True, default_factory=dict)],
|
||||||
dict[str, Any], fastapi.Body(..., embed=True, default_factory=dict)
|
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
|
||||||
],
|
|
||||||
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
|
|
||||||
) -> dict[str, Any]: # FIXME: add proper return type
|
) -> dict[str, Any]: # FIXME: add proper return type
|
||||||
try:
|
"""
|
||||||
preset = await library_db.get_preset(user_id, preset_id)
|
Execute a preset given graph parameters, returning the execution ID on success.
|
||||||
if not preset:
|
|
||||||
raise fastapi.HTTPException(status_code=404, detail="Preset not found")
|
|
||||||
|
|
||||||
logger.debug(f"Preset inputs: {preset.inputs}")
|
Args:
|
||||||
|
graph_id (str): ID of the graph to execute.
|
||||||
|
graph_version (int): Version of the graph to execute.
|
||||||
|
preset_id (str): ID of the preset to execute.
|
||||||
|
node_input (Dict[Any, Any]): Input data for the node.
|
||||||
|
user_id (str): ID of the authenticated user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: A response containing the execution ID.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If the preset is not found or an error occurs while executing the preset.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
preset = await db.get_preset(user_id, preset_id)
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Preset not found",
|
||||||
|
)
|
||||||
|
|
||||||
# Merge input overrides with preset inputs
|
# Merge input overrides with preset inputs
|
||||||
merged_node_input = preset.inputs | node_input
|
merged_node_input = preset.inputs | node_input
|
||||||
@@ -125,6 +237,11 @@ async def execute_preset(
|
|||||||
logger.debug(f"Execution added: {execution} with input: {merged_node_input}")
|
logger.debug(f"Execution added: {execution} with input: {merged_node_input}")
|
||||||
|
|
||||||
return {"id": execution.graph_exec_id}
|
return {"id": execution.graph_exec_id}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = str(e).encode().decode("unicode_escape")
|
logger.exception(f"Exception occurred while executing preset: {e}")
|
||||||
raise fastapi.HTTPException(status_code=400, detail=msg)
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e),
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import fastapi.testclient
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_mock
|
import pytest_mock
|
||||||
|
|
||||||
|
import backend.server.model as server_model
|
||||||
import backend.server.v2.library.model as library_model
|
import backend.server.v2.library.model as library_model
|
||||||
from backend.server.v2.library.routes import router as library_router
|
from backend.server.v2.library.routes import router as library_router
|
||||||
|
|
||||||
@@ -31,59 +32,80 @@ app.dependency_overrides[autogpt_auth_lib.depends.get_user_id] = override_get_us
|
|||||||
|
|
||||||
def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
|
def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
|
||||||
mocked_value = [
|
mocked_value = [
|
||||||
library_model.LibraryAgent(
|
library_model.LibraryAgentResponse(
|
||||||
id="test-agent-1",
|
agents=[
|
||||||
agent_id="test-agent-1",
|
library_model.LibraryAgent(
|
||||||
agent_version=1,
|
id="test-agent-1",
|
||||||
preset_id="preset-1",
|
agent_id="test-agent-1",
|
||||||
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
|
agent_version=1,
|
||||||
is_favorite=False,
|
name="Test Agent 1",
|
||||||
is_created_by_user=True,
|
description="Test Description 1",
|
||||||
is_latest_version=True,
|
image_url=None,
|
||||||
name="Test Agent 1",
|
creator_name="Test Creator",
|
||||||
description="Test Description 1",
|
creator_image_url="",
|
||||||
input_schema={"type": "object", "properties": {}},
|
input_schema={"type": "object", "properties": {}},
|
||||||
output_schema={"type": "object", "properties": {}},
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
),
|
new_output=False,
|
||||||
library_model.LibraryAgent(
|
can_access_graph=True,
|
||||||
id="test-agent-2",
|
is_latest_version=True,
|
||||||
agent_id="test-agent-2",
|
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
|
||||||
agent_version=1,
|
),
|
||||||
preset_id="preset-2",
|
library_model.LibraryAgent(
|
||||||
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
|
id="test-agent-2",
|
||||||
is_favorite=False,
|
agent_id="test-agent-2",
|
||||||
is_created_by_user=False,
|
agent_version=1,
|
||||||
is_latest_version=True,
|
name="Test Agent 2",
|
||||||
name="Test Agent 2",
|
description="Test Description 2",
|
||||||
description="Test Description 2",
|
image_url=None,
|
||||||
input_schema={"type": "object", "properties": {}},
|
creator_name="Test Creator",
|
||||||
output_schema={"type": "object", "properties": {}},
|
creator_image_url="",
|
||||||
|
input_schema={"type": "object", "properties": {}},
|
||||||
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
|
new_output=False,
|
||||||
|
can_access_graph=False,
|
||||||
|
is_latest_version=True,
|
||||||
|
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
pagination=server_model.Pagination(
|
||||||
|
total_items=2, total_pages=1, current_page=1, page_size=50
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
mock_db_call = mocker.patch("backend.server.v2.library.db.get_library_agents")
|
mock_db_call = mocker.patch("backend.server.v2.library.db.get_library_agents")
|
||||||
mock_db_call.return_value = mocked_value
|
mock_db_call.return_value = mocked_value
|
||||||
|
|
||||||
response = client.get("/agents")
|
response = client.get("/agents?search_term=test")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
data = [
|
data = library_model.LibraryAgentResponse.model_validate(response.json())
|
||||||
library_model.LibraryAgent.model_validate(agent) for agent in response.json()
|
assert len(data.agents) == 2
|
||||||
]
|
assert data.agents[0].agent_id == "test-agent-1"
|
||||||
assert len(data) == 2
|
assert data.agents[0].can_access_graph is True
|
||||||
assert data[0].agent_id == "test-agent-1"
|
assert data.agents[1].agent_id == "test-agent-2"
|
||||||
assert data[0].is_created_by_user is True
|
assert data.agents[1].can_access_graph is False
|
||||||
assert data[1].agent_id == "test-agent-2"
|
mock_db_call.assert_called_once_with(
|
||||||
assert data[1].is_created_by_user is False
|
user_id="test-user-id",
|
||||||
mock_db_call.assert_called_once_with("test-user-id")
|
search_term="test",
|
||||||
|
sort_by=library_model.LibraryAgentSort.UPDATED_AT,
|
||||||
|
page=1,
|
||||||
|
page_size=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_library_agents_error(mocker: pytest_mock.MockFixture):
|
def test_get_library_agents_error(mocker: pytest_mock.MockFixture):
|
||||||
mock_db_call = mocker.patch("backend.server.v2.library.db.get_library_agents")
|
mock_db_call = mocker.patch("backend.server.v2.library.db.get_library_agents")
|
||||||
mock_db_call.side_effect = Exception("Test error")
|
mock_db_call.side_effect = Exception("Test error")
|
||||||
|
|
||||||
response = client.get("/agents")
|
response = client.get("/agents?search_term=test")
|
||||||
assert response.status_code == 500
|
assert response.status_code == 500
|
||||||
mock_db_call.assert_called_once_with("test-user-id")
|
mock_db_call.assert_called_once_with(
|
||||||
|
user_id="test-user-id",
|
||||||
|
search_term="test",
|
||||||
|
sort_by=library_model.LibraryAgentSort.UPDATED_AT,
|
||||||
|
page=1,
|
||||||
|
page_size=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Mocker Not implemented")
|
@pytest.mark.skip(reason="Mocker Not implemented")
|
||||||
|
|||||||
@@ -601,7 +601,7 @@ async def get_user_profile(
|
|||||||
avatar_url=profile.avatarUrl,
|
avatar_url=profile.avatarUrl,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error getting user profile: %s", e)
|
logger.error(f"Error getting user profile: {e}")
|
||||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||||
"Failed to get user profile"
|
"Failed to get user profile"
|
||||||
) from e
|
) from e
|
||||||
@@ -620,7 +620,7 @@ async def update_profile(
|
|||||||
Raises:
|
Raises:
|
||||||
DatabaseError: If there's an issue updating or creating the profile
|
DatabaseError: If there's an issue updating or creating the profile
|
||||||
"""
|
"""
|
||||||
logger.info("Updating profile for user %s with data: %s", user_id, profile)
|
logger.info(f"Updating profile for user {user_id} with data: {profile}")
|
||||||
try:
|
try:
|
||||||
# Sanitize username to allow only letters, numbers, and hyphens
|
# Sanitize username to allow only letters, numbers, and hyphens
|
||||||
username = "".join(
|
username = "".join(
|
||||||
@@ -639,15 +639,13 @@ async def update_profile(
|
|||||||
# Verify that the user is authorized to update this profile
|
# Verify that the user is authorized to update this profile
|
||||||
if existing_profile.userId != user_id:
|
if existing_profile.userId != user_id:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Unauthorized update attempt for profile %s by user %s",
|
f"Unauthorized update attempt for profile {existing_profile.id} by user {user_id}"
|
||||||
existing_profile.userId,
|
|
||||||
user_id,
|
|
||||||
)
|
)
|
||||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||||
f"Unauthorized update attempt for profile {existing_profile.id} by user {user_id}"
|
f"Unauthorized update attempt for profile {existing_profile.id} by user {user_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Updating existing profile for user %s", user_id)
|
logger.debug(f"Updating existing profile for user {user_id}")
|
||||||
# Prepare update data, only including non-None values
|
# Prepare update data, only including non-None values
|
||||||
update_data = {}
|
update_data = {}
|
||||||
if profile.name is not None:
|
if profile.name is not None:
|
||||||
@@ -667,7 +665,7 @@ async def update_profile(
|
|||||||
data=prisma.types.ProfileUpdateInput(**update_data),
|
data=prisma.types.ProfileUpdateInput(**update_data),
|
||||||
)
|
)
|
||||||
if updated_profile is None:
|
if updated_profile is None:
|
||||||
logger.error("Failed to update profile for user %s", user_id)
|
logger.error(f"Failed to update profile for user {user_id}")
|
||||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||||
"Failed to update profile"
|
"Failed to update profile"
|
||||||
)
|
)
|
||||||
@@ -684,7 +682,7 @@ async def update_profile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error("Database error updating profile: %s", e)
|
logger.error(f"Database error updating profile: {e}")
|
||||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||||
"Failed to update profile"
|
"Failed to update profile"
|
||||||
) from e
|
) from e
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from enum import Enum
|
|||||||
import replicate
|
import replicate
|
||||||
import replicate.exceptions
|
import replicate.exceptions
|
||||||
import requests
|
import requests
|
||||||
|
from prisma.models import AgentGraph
|
||||||
from replicate.helpers import FileOutput
|
from replicate.helpers import FileOutput
|
||||||
|
|
||||||
from backend.data.graph import Graph
|
from backend.data.graph import Graph
|
||||||
@@ -21,7 +22,7 @@ class ImageStyle(str, Enum):
|
|||||||
DIGITAL_ART = "digital art"
|
DIGITAL_ART = "digital art"
|
||||||
|
|
||||||
|
|
||||||
async def generate_agent_image(agent: Graph) -> io.BytesIO:
|
async def generate_agent_image(agent: Graph | AgentGraph) -> io.BytesIO:
|
||||||
"""
|
"""
|
||||||
Generate an image for an agent using Flux model via Replicate API.
|
Generate an image for an agent using Flux model via Replicate API.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import fastapi
|
|||||||
from google.cloud import storage
|
from google.cloud import storage
|
||||||
|
|
||||||
import backend.server.v2.store.exceptions
|
import backend.server.v2.store.exceptions
|
||||||
|
from backend.util.exceptions import MissingConfigError
|
||||||
from backend.util.settings import Settings
|
from backend.util.settings import Settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -27,34 +28,32 @@ async def check_media_exists(user_id: str, filename: str) -> str | None:
|
|||||||
Returns:
|
Returns:
|
||||||
str | None: URL of the blob if it exists, None otherwise
|
str | None: URL of the blob if it exists, None otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
settings = Settings()
|
||||||
settings = Settings()
|
if not settings.config.media_gcs_bucket_name:
|
||||||
storage_client = storage.Client()
|
raise MissingConfigError("GCS media bucket is not configured")
|
||||||
bucket = storage_client.bucket(settings.config.media_gcs_bucket_name)
|
|
||||||
|
|
||||||
# Check images
|
storage_client = storage.Client()
|
||||||
image_path = f"users/{user_id}/images/{filename}"
|
bucket = storage_client.bucket(settings.config.media_gcs_bucket_name)
|
||||||
image_blob = bucket.blob(image_path)
|
|
||||||
if image_blob.exists():
|
|
||||||
return image_blob.public_url
|
|
||||||
|
|
||||||
# Check videos
|
# Check images
|
||||||
video_path = f"users/{user_id}/videos/{filename}"
|
image_path = f"users/{user_id}/images/{filename}"
|
||||||
|
image_blob = bucket.blob(image_path)
|
||||||
|
if image_blob.exists():
|
||||||
|
return image_blob.public_url
|
||||||
|
|
||||||
video_blob = bucket.blob(video_path)
|
# Check videos
|
||||||
if video_blob.exists():
|
video_path = f"users/{user_id}/videos/{filename}"
|
||||||
return video_blob.public_url
|
|
||||||
|
|
||||||
return None
|
video_blob = bucket.blob(video_path)
|
||||||
except Exception as e:
|
if video_blob.exists():
|
||||||
logger.error(f"Error checking if media file exists: {str(e)}")
|
return video_blob.public_url
|
||||||
return None
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def upload_media(
|
async def upload_media(
|
||||||
user_id: str, file: fastapi.UploadFile, use_file_name: bool = False
|
user_id: str, file: fastapi.UploadFile, use_file_name: bool = False
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|
||||||
# Get file content for deeper validation
|
# Get file content for deeper validation
|
||||||
try:
|
try:
|
||||||
content = await file.read(1024) # Read first 1KB for validation
|
content = await file.read(1024) # Read first 1KB for validation
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Add imageUrl column
|
||||||
|
ALTER TABLE "LibraryAgent"
|
||||||
|
ADD COLUMN "creatorId" TEXT,
|
||||||
|
ADD COLUMN "imageUrl" TEXT;
|
||||||
|
|
||||||
|
-- Add foreign key constraint for creatorId -> Profile
|
||||||
|
ALTER TABLE "LibraryAgent"
|
||||||
|
ADD CONSTRAINT "LibraryAgent_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "Profile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -65,6 +65,7 @@ model AgentGraph {
|
|||||||
|
|
||||||
name String?
|
name String?
|
||||||
description String?
|
description String?
|
||||||
|
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isTemplate Boolean @default(false)
|
isTemplate Boolean @default(false)
|
||||||
|
|
||||||
@@ -175,6 +176,8 @@ model LibraryAgent {
|
|||||||
userId String
|
userId String
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
imageUrl String?
|
||||||
|
|
||||||
agentId String
|
agentId String
|
||||||
agentVersion Int
|
agentVersion Int
|
||||||
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
|
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
|
||||||
@@ -182,6 +185,9 @@ model LibraryAgent {
|
|||||||
agentPresetId String?
|
agentPresetId String?
|
||||||
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
|
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
|
||||||
|
|
||||||
|
creatorId String?
|
||||||
|
Creator Profile? @relation(fields: [creatorId], references: [id])
|
||||||
|
|
||||||
useGraphIsActiveVersion Boolean @default(false)
|
useGraphIsActiveVersion Boolean @default(false)
|
||||||
|
|
||||||
isFavorite Boolean @default(false)
|
isFavorite Boolean @default(false)
|
||||||
@@ -505,6 +511,8 @@ model Profile {
|
|||||||
|
|
||||||
isFeatured Boolean @default(false)
|
isFeatured Boolean @default(false)
|
||||||
|
|
||||||
|
LibraryAgents LibraryAgent[]
|
||||||
|
|
||||||
@@index([username])
|
@@index([username])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ async def test_execute_preset_with_clash(server: SpinTestServer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Verify execution
|
# Verify execution
|
||||||
assert result is not None
|
assert result is not None, "Result must not be None"
|
||||||
graph_exec_id = result["id"]
|
graph_exec_id = result["id"]
|
||||||
|
|
||||||
# Wait for execution to complete
|
# Wait for execution to complete
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const nextConfig = {
|
|||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
transpilePackages: ["geist"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withSentryConfig(nextConfig, {
|
export default withSentryConfig(nextConfig, {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
"embla-carousel-react": "^8.5.2",
|
"embla-carousel-react": "^8.5.2",
|
||||||
"framer-motion": "^12.0.11",
|
"framer-motion": "^12.0.11",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"launchdarkly-react-client-sdk": "^3.6.1",
|
"launchdarkly-react-client-sdk": "^3.6.1",
|
||||||
"lucide-react": "^0.474.0",
|
"lucide-react": "^0.474.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
@@ -70,7 +71,8 @@
|
|||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "^9.5.1",
|
"react-day-picker": "^9.5.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-drag-drop-files": "^2.4.0",
|
||||||
|
"react-hook-form": "^7.54.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
"react-modal": "^3.16.3",
|
"react-modal": "^3.16.3",
|
||||||
@@ -93,7 +95,8 @@
|
|||||||
"@storybook/nextjs": "^8.5.3",
|
"@storybook/nextjs": "^8.5.3",
|
||||||
"@storybook/react": "^8.3.5",
|
"@storybook/react": "^8.3.5",
|
||||||
"@storybook/test": "^8.3.5",
|
"@storybook/test": "^8.3.5",
|
||||||
"@storybook/test-runner": "^0.21.0",
|
"@storybook/test-runner": "^0.20.1",
|
||||||
|
"@types/lodash": "^4.17.13",
|
||||||
"@types/negotiator": "^0.6.3",
|
"@types/negotiator": "^0.6.3",
|
||||||
"@types/node": "^22.13.0",
|
"@types/node": "^22.13.0",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter, Poppins } from "next/font/google";
|
import { Inter, Poppins } from "next/font/google";
|
||||||
import { Providers } from "@/app/providers";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Navbar } from "@/components/agptui/Navbar";
|
|
||||||
|
|
||||||
import "./globals.css";
|
|
||||||
import TallyPopupSimple from "@/components/TallyPopup";
|
|
||||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
|
||||||
import { IconType } from "@/components/ui/icons";
|
|
||||||
import { GeistSans } from "geist/font/sans";
|
import { GeistSans } from "geist/font/sans";
|
||||||
import { GeistMono } from "geist/font/mono";
|
import { GeistMono } from "geist/font/mono";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
// Fonts
|
import { cn } from "@/lib/utils";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
import { Navbar } from "@/components/agptui/Navbar";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
import { IconType } from "@/components/ui/icons";
|
||||||
|
import { Providers } from "@/app/providers";
|
||||||
|
import TallyPopupSimple from "@/components/TallyPopup";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||||
|
|
||||||
const poppins = Poppins({
|
const poppins = Poppins({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500", "600", "700"],
|
weight: ["400", "500", "600", "700"],
|
||||||
@@ -34,14 +35,18 @@ export default async function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
const pathname = headers().get("x-current-path");
|
const pathname = headers().get("x-current-path");
|
||||||
const isOnboarding = pathname?.startsWith("/onboarding");
|
const isOnboarding = pathname?.startsWith("/onboarding");
|
||||||
console.log("pathname:", pathname);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
className={`${poppins.variable} ${GeistSans.variable} ${GeistMono.variable} ${inter.variable}`}
|
className={`${poppins.variable} ${GeistSans.variable} ${GeistMono.variable} ${inter.variable}`}
|
||||||
>
|
>
|
||||||
<body className={cn("antialiased transition-colors", inter.className)}>
|
<body
|
||||||
|
className={cn(
|
||||||
|
"bg-neutral-50 antialiased transition-colors",
|
||||||
|
inter.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Providers
|
<Providers
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="light"
|
defaultTheme="light"
|
||||||
@@ -59,7 +64,7 @@ export default async function RootLayout({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Library",
|
name: "Library",
|
||||||
href: "/monitoring",
|
href: "/library",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Build",
|
name: "Build",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
|||||||
import {
|
import {
|
||||||
GraphExecution,
|
GraphExecution,
|
||||||
GraphExecutionMeta,
|
GraphExecutionMeta,
|
||||||
|
GraphID,
|
||||||
GraphMeta,
|
GraphMeta,
|
||||||
Schedule,
|
Schedule,
|
||||||
} from "@/lib/autogpt-server-api";
|
} from "@/lib/autogpt-server-api";
|
||||||
@@ -16,7 +17,7 @@ import AgentRunsSelectorList from "@/components/agents/agent-runs-selector-list"
|
|||||||
import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view";
|
import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view";
|
||||||
|
|
||||||
export default function AgentRunsPage(): React.ReactElement {
|
export default function AgentRunsPage(): React.ReactElement {
|
||||||
const { id: agentID }: { id: string } = useParams();
|
const { id: agentID }: { id: GraphID } = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const api = useBackendAPI();
|
const api = useBackendAPI();
|
||||||
|
|
||||||
@@ -152,22 +153,20 @@ export default function AgentRunsPage(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Run / Schedule views */}
|
{/* Run / Schedule views */}
|
||||||
{(selectedView.type == "run" ? (
|
{(selectedView.type == "run" && selectedView.id ? (
|
||||||
selectedView.id ? (
|
selectedRun && (
|
||||||
selectedRun && (
|
<AgentRunDetailsView
|
||||||
<AgentRunDetailsView
|
|
||||||
agent={agent}
|
|
||||||
run={selectedRun}
|
|
||||||
agentActions={agentActions}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<AgentRunDraftView
|
|
||||||
agent={agent}
|
agent={agent}
|
||||||
onRun={(runID) => selectRun(runID)}
|
run={selectedRun}
|
||||||
agentActions={agentActions}
|
agentActions={agentActions}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
) : selectedView.type == "run" ? (
|
||||||
|
<AgentRunDraftView
|
||||||
|
agent={agent}
|
||||||
|
onRun={(runID) => selectRun(runID)}
|
||||||
|
agentActions={agentActions}
|
||||||
|
/>
|
||||||
) : selectedView.type == "schedule" ? (
|
) : selectedView.type == "schedule" ? (
|
||||||
selectedSchedule && (
|
selectedSchedule && (
|
||||||
<AgentScheduleDetailsView
|
<AgentScheduleDetailsView
|
||||||
45
autogpt_platform/frontend/src/app/library/page.tsx
Normal file
45
autogpt_platform/frontend/src/app/library/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowBottomRightIcon,
|
||||||
|
QuestionMarkCircledIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
|
||||||
|
import { LibraryPageStateProvider } from "./state-provider";
|
||||||
|
import LibraryActionSubHeader from "@/components/library/library-action-sub-header";
|
||||||
|
import LibraryActionHeader from "@/components/library/library-action-header";
|
||||||
|
import LibraryAgentList from "@/components/library/library-agent-list";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LibraryPage Component
|
||||||
|
* Main component that manages the library interface including agent listing and actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function LibraryPage() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto w-screen max-w-[1600px] space-y-4 bg-neutral-50 p-4 px-2 dark:bg-neutral-900 sm:px-8 md:px-12">
|
||||||
|
<LibraryPageStateProvider>
|
||||||
|
{/* Header section containing notifications, search functionality and upload mechanism */}
|
||||||
|
<LibraryActionHeader />
|
||||||
|
|
||||||
|
{/* Subheader section containing agent counts and filtering options */}
|
||||||
|
<LibraryActionSubHeader />
|
||||||
|
|
||||||
|
{/* Content section displaying agent list with counter and filtering options */}
|
||||||
|
<LibraryAgentList />
|
||||||
|
</LibraryPageStateProvider>
|
||||||
|
|
||||||
|
<div className="!mb-8 !mt-12 flex w-full justify-center">
|
||||||
|
<p className="rounded-xl bg-white p-4 text-neutral-600">
|
||||||
|
Prefer the old experience? Click{" "}
|
||||||
|
<Link href="/monitoring" className="underline">
|
||||||
|
here
|
||||||
|
</Link>{" "}
|
||||||
|
to go to it. Please do let us know why by clicking the{" "}
|
||||||
|
<QuestionMarkCircledIcon className="inline-block size-6 rounded-full bg-[rgba(65,65,64,1)] p-1 align-bottom text-neutral-50" />{" "}
|
||||||
|
in the bottom right corner <ArrowBottomRightIcon className="inline" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
69
autogpt_platform/frontend/src/app/library/state-provider.tsx
Normal file
69
autogpt_platform/frontend/src/app/library/state-provider.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
} from "react";
|
||||||
|
import { LibraryAgent, LibraryAgentSortEnum } from "@/lib/autogpt-server-api";
|
||||||
|
|
||||||
|
interface LibraryPageContextType {
|
||||||
|
agents: LibraryAgent[];
|
||||||
|
setAgents: Dispatch<SetStateAction<LibraryAgent[]>>;
|
||||||
|
agentLoading: boolean;
|
||||||
|
setAgentLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
|
searchTerm: string | undefined;
|
||||||
|
setSearchTerm: Dispatch<SetStateAction<string | undefined>>;
|
||||||
|
uploadedFile: File | null;
|
||||||
|
setUploadedFile: Dispatch<SetStateAction<File | null>>;
|
||||||
|
librarySort: LibraryAgentSortEnum;
|
||||||
|
setLibrarySort: Dispatch<SetStateAction<LibraryAgentSortEnum>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LibraryPageContext = createContext<LibraryPageContextType>(
|
||||||
|
{} as LibraryPageContextType,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function LibraryPageStateProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const [agents, setAgents] = useState<LibraryAgent[]>([]);
|
||||||
|
const [agentLoading, setAgentLoading] = useState<boolean>(true);
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string | undefined>("");
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||||
|
const [librarySort, setLibrarySort] = useState<LibraryAgentSortEnum>(
|
||||||
|
LibraryAgentSortEnum.UPDATED_AT,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LibraryPageContext.Provider
|
||||||
|
value={{
|
||||||
|
agents,
|
||||||
|
setAgents,
|
||||||
|
agentLoading,
|
||||||
|
setAgentLoading,
|
||||||
|
searchTerm,
|
||||||
|
setSearchTerm,
|
||||||
|
uploadedFile,
|
||||||
|
setUploadedFile,
|
||||||
|
librarySort,
|
||||||
|
setLibrarySort,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LibraryPageContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLibraryPageContext(): LibraryPageContextType {
|
||||||
|
const context = useContext(LibraryPageContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Error in context of Library page");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -43,8 +43,8 @@ const Monitor = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fetchAgents = useCallback(() => {
|
const fetchAgents = useCallback(() => {
|
||||||
api.listLibraryAgents().then((agents) => {
|
api.listLibraryAgents().then((response) => {
|
||||||
setFlows(agents);
|
setFlows(response.agents);
|
||||||
});
|
});
|
||||||
api.getExecutions().then((executions) => {
|
api.getExecutions().then((executions) => {
|
||||||
setExecutions(executions);
|
setExecutions(executions);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CreatorDetails } from "@/lib/autogpt-server-api/types";
|
|||||||
|
|
||||||
async function getProfileData(api: BackendAPI) {
|
async function getProfileData(api: BackendAPI) {
|
||||||
try {
|
try {
|
||||||
const profile = await api.getStoreProfile("profile");
|
const profile = await api.getStoreProfile();
|
||||||
return {
|
return {
|
||||||
profile,
|
profile,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,36 +31,37 @@ const formSchema = z.object({
|
|||||||
importAsTemplate: z.boolean(),
|
importAsTemplate: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateBlockIDs(graph: Graph) {
|
export const updatedBlockIDMap: Record<string, string> = {
|
||||||
// https://github.com/Significant-Gravitas/AutoGPT/issues/8223
|
// https://github.com/Significant-Gravitas/AutoGPT/issues/8223
|
||||||
const updatedBlockIDMap: Record<string, string> = {
|
"a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
|
||||||
"a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
|
"436c3984-57fd-4b85-8e9a-459b356883bd",
|
||||||
"436c3984-57fd-4b85-8e9a-459b356883bd",
|
"b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
|
||||||
"b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
|
"0e50422c-6dee-4145-83d6-3a5a392f65de",
|
||||||
"0e50422c-6dee-4145-83d6-3a5a392f65de",
|
"c3d4e5f6-7g8h-9i0j-1k2l-m3n4o5p6q7r8":
|
||||||
"c3d4e5f6-7g8h-9i0j-1k2l-m3n4o5p6q7r8":
|
"a0a69be1-4528-491c-a85a-a4ab6873e3f0",
|
||||||
"a0a69be1-4528-491c-a85a-a4ab6873e3f0",
|
"c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8":
|
||||||
"c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8":
|
"32a87eab-381e-4dd4-bdb8-4c47151be35a",
|
||||||
"32a87eab-381e-4dd4-bdb8-4c47151be35a",
|
"b2c3d4e5-6f7g-8h9i-0j1k-l2m3n4o5p6q7":
|
||||||
"b2c3d4e5-6f7g-8h9i-0j1k-l2m3n4o5p6q7":
|
"87840993-2053-44b7-8da4-187ad4ee518c",
|
||||||
"87840993-2053-44b7-8da4-187ad4ee518c",
|
"h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6":
|
||||||
"h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6":
|
"d0822ab5-9f8a-44a3-8971-531dd0178b6b",
|
||||||
"d0822ab5-9f8a-44a3-8971-531dd0178b6b",
|
"d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t":
|
||||||
"d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t":
|
"df06086a-d5ac-4abb-9996-2ad0acb2eff7",
|
||||||
"df06086a-d5ac-4abb-9996-2ad0acb2eff7",
|
"h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m":
|
||||||
"h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m":
|
"f5b0f5d0-1862-4d61-94be-3ad0fa772760",
|
||||||
"f5b0f5d0-1862-4d61-94be-3ad0fa772760",
|
"a1234567-89ab-cdef-0123-456789abcdef":
|
||||||
"a1234567-89ab-cdef-0123-456789abcdef":
|
"4335878a-394e-4e67-adf2-919877ff49ae",
|
||||||
"4335878a-394e-4e67-adf2-919877ff49ae",
|
"f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l":
|
||||||
"f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l":
|
"f66a3543-28d3-4ab5-8945-9b336371e2ce",
|
||||||
"f66a3543-28d3-4ab5-8945-9b336371e2ce",
|
"b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0h2":
|
||||||
"b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0h2":
|
"716a67b3-6760-42e7-86dc-18645c6e00fc",
|
||||||
"716a67b3-6760-42e7-86dc-18645c6e00fc",
|
"31d1064e-7446-4693-o7d4-65e5ca9110d1":
|
||||||
"31d1064e-7446-4693-o7d4-65e5ca9110d1":
|
"cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
|
||||||
"cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
|
"c6731acb-4105-4zp1-bc9b-03d0036h370g":
|
||||||
"c6731acb-4105-4zp1-bc9b-03d0036h370g":
|
"5ebe6768-8e5d-41e3-9134-1c7bd89a8d52",
|
||||||
"5ebe6768-8e5d-41e3-9134-1c7bd89a8d52",
|
};
|
||||||
};
|
|
||||||
|
function updateBlockIDs(graph: Graph) {
|
||||||
graph.nodes
|
graph.nodes
|
||||||
.filter((node) => node.block_id in updatedBlockIDMap)
|
.filter((node) => node.block_id in updatedBlockIDMap)
|
||||||
.forEach((node) => {
|
.forEach((node) => {
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
|||||||
|
|
||||||
const handleAddToLibrary = async () => {
|
const handleAddToLibrary = async () => {
|
||||||
try {
|
try {
|
||||||
await api.addAgentToLibrary(storeListingVersionId);
|
const newLibraryAgent = await api.addMarketplaceAgentToLibrary(
|
||||||
|
storeListingVersionId,
|
||||||
|
);
|
||||||
console.log("Agent added to library successfully");
|
console.log("Agent added to library successfully");
|
||||||
router.push("/monitoring");
|
router.push(`/library/agents/${newLibraryAgent.agent_id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to add agent to library:", error);
|
console.error("Failed to add agent to library:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ const buttonVariants = cva(
|
|||||||
destructive:
|
destructive:
|
||||||
"bg-red-600 text-neutral-50 border border-red-500/50 hover:bg-red-500/90 dark:bg-red-700 dark:text-neutral-50 dark:hover:bg-red-600",
|
"bg-red-600 text-neutral-50 border border-red-500/50 hover:bg-red-500/90 dark:bg-red-700 dark:text-neutral-50 dark:hover:bg-red-600",
|
||||||
accent: "bg-accent text-accent-foreground hover:bg-violet-500",
|
accent: "bg-accent text-accent-foreground hover:bg-violet-500",
|
||||||
|
primary: "bg-neutral-800 text-white hover:bg-black/60",
|
||||||
outline:
|
outline:
|
||||||
"border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
|
"border border-black/50 text-neutral-800 hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-neutral-100 text-[#272727] border border-neutral-200 hover:bg-neutral-100/80 dark:bg-neutral-700 dark:text-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-600",
|
"bg-neutral-100 text-neutral-800 border border-neutral-200 hover:bg-neutral-100/80 dark:bg-neutral-700 dark:text-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-600",
|
||||||
ghost:
|
ghost:
|
||||||
"hover:bg-neutral-100 text-[#272727] dark:text-neutral-100 dark:hover:bg-neutral-700",
|
"hover:bg-neutral-100 text-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
|
||||||
link: "text-[#272727] underline-offset-4 hover:underline dark:text-neutral-100",
|
link: "text-neutral-800 underline-offset-4 hover:underline dark:text-neutral-100",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2 rounded-full text-sm",
|
default: "h-10 px-4 py-2 rounded-full text-sm",
|
||||||
@@ -44,10 +45,12 @@ export interface ButtonProps
|
|||||||
variant?:
|
variant?:
|
||||||
| "destructive"
|
| "destructive"
|
||||||
| "accent"
|
| "accent"
|
||||||
|
| "primary"
|
||||||
| "outline"
|
| "outline"
|
||||||
| "secondary"
|
| "secondary"
|
||||||
| "ghost"
|
| "ghost"
|
||||||
| "link";
|
| "link";
|
||||||
|
|
||||||
size?: "default" | "sm" | "lg" | "primary" | "icon" | "card";
|
size?: "default" | "sm" | "lg" | "primary" | "icon" | "card";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface NavbarProps {
|
|||||||
|
|
||||||
async function getProfileData() {
|
async function getProfileData() {
|
||||||
const api = new BackendAPI();
|
const api = new BackendAPI();
|
||||||
const profile = await Promise.resolve(api.getStoreProfile("navbar"));
|
const profile = await Promise.resolve(api.getStoreProfile());
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
IconType,
|
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconBoxes,
|
IconBoxes,
|
||||||
IconLibrary,
|
IconLibrary,
|
||||||
@@ -47,7 +46,7 @@ export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
|
|||||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{href === "/monitoring" && (
|
{href === "/library" && (
|
||||||
<IconLibrary
|
<IconLibrary
|
||||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// import LibraryNotificationDropdown from "./library-notification-dropdown";
|
||||||
|
import LibraryUploadAgentDialog from "./library-upload-agent-dialog";
|
||||||
|
import LibrarySearchBar from "./library-search-bar";
|
||||||
|
|
||||||
|
interface LibraryActionHeaderProps {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LibraryActionHeader component - Renders a header with search, notifications and filters
|
||||||
|
*/
|
||||||
|
const LibraryActionHeader: React.FC<LibraryActionHeaderProps> = ({}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-[32px] hidden items-start justify-between bg-neutral-50 md:flex">
|
||||||
|
{/* <LibraryNotificationDropdown /> */}
|
||||||
|
<LibrarySearchBar />
|
||||||
|
<LibraryUploadAgentDialog />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile and tablet */}
|
||||||
|
<div className="flex flex-col gap-4 bg-neutral-50 p-4 pt-[52px] md:hidden">
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
{/* <LibraryNotificationDropdown /> */}
|
||||||
|
<LibraryUploadAgentDialog />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<LibrarySearchBar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LibraryActionHeader;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useLibraryPageContext } from "@/app/library/state-provider";
|
||||||
|
import LibrarySortMenu from "./library-sort-menu";
|
||||||
|
|
||||||
|
export default function LibraryActionSubHeader(): React.ReactNode {
|
||||||
|
const { agents } = useLibraryPageContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between pb-[10px]">
|
||||||
|
<div className="flex items-center gap-[10px] p-2">
|
||||||
|
<span className="font-poppin w-[96px] text-[18px] font-semibold leading-[28px] text-neutral-800">
|
||||||
|
My agents
|
||||||
|
</span>
|
||||||
|
<span className="w-[70px] font-sans text-[14px] font-normal leading-6">
|
||||||
|
{agents.length} agents
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<LibrarySortMenu />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { LibraryAgent } from "@/lib/autogpt-server-api";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
|
||||||
|
export default function LibraryAgentCard({
|
||||||
|
agent: {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
agent_id,
|
||||||
|
can_access_graph,
|
||||||
|
creator_image_url,
|
||||||
|
image_url,
|
||||||
|
},
|
||||||
|
}: {
|
||||||
|
agent: LibraryAgent;
|
||||||
|
}): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<Link href={`/library/agents/${agent_id}`}>
|
||||||
|
<div className="inline-flex w-full max-w-[434px] cursor-pointer 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">
|
||||||
|
<div className="relative h-[200px] w-full overflow-hidden rounded-[20px]">
|
||||||
|
{!image_url ? (
|
||||||
|
<div
|
||||||
|
className={`h-full w-full ${
|
||||||
|
[
|
||||||
|
"bg-gradient-to-r from-green-200 to-blue-200",
|
||||||
|
"bg-gradient-to-r from-pink-200 to-purple-200",
|
||||||
|
"bg-gradient-to-r from-yellow-200 to-orange-200",
|
||||||
|
"bg-gradient-to-r from-blue-200 to-cyan-200",
|
||||||
|
"bg-gradient-to-r from-indigo-200 to-purple-200",
|
||||||
|
][parseInt(id.slice(0, 8), 16) % 5]
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundSize: "200% 200%",
|
||||||
|
animation: "gradient 15s ease infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={image_url}
|
||||||
|
alt={`${name} preview image`}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="absolute bottom-4 left-4">
|
||||||
|
<Avatar className="h-16 w-16 border-2 border-white dark:border-gray-800">
|
||||||
|
<AvatarImage
|
||||||
|
src={
|
||||||
|
creator_image_url
|
||||||
|
? creator_image_url
|
||||||
|
: "/avatar-placeholder.png"
|
||||||
|
}
|
||||||
|
alt={`${name} creator avatar`}
|
||||||
|
/>
|
||||||
|
<AvatarFallback>{name.charAt(0)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full flex-1 flex-col px-4 py-4">
|
||||||
|
<h3 className="mb-2 font-poppins text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100">
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="mb-4 flex-1 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="items-between mt-4 flex w-full justify-between gap-3">
|
||||||
|
<span className="font-geist text-lg font-semibold text-neutral-800 hover:underline dark:text-neutral-200">
|
||||||
|
See runs
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{can_access_graph && (
|
||||||
|
<Link
|
||||||
|
href={`/build?flowID=${agent_id}`}
|
||||||
|
className="font-geist text-lg font-semibold text-neutral-800 hover:underline dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Open in builder
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
|
||||||
|
import { useLibraryPageContext } from "@/app/library/state-provider";
|
||||||
|
import { useScrollThreshold } from "@/hooks/useScrollThreshold";
|
||||||
|
import LibraryAgentCard from "./library-agent-card";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a grid of library agents with infinite scroll functionality.
|
||||||
|
*/
|
||||||
|
export default function LibraryAgentList(): React.ReactNode {
|
||||||
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
|
||||||
|
const api = useBackendAPI();
|
||||||
|
const { agents, setAgents, setAgentLoading, agentLoading } =
|
||||||
|
useLibraryPageContext();
|
||||||
|
|
||||||
|
const fetchAgents = useCallback(
|
||||||
|
async (page: number) => {
|
||||||
|
try {
|
||||||
|
const response = await api.listLibraryAgents(
|
||||||
|
page === 1 ? {} : { page: page },
|
||||||
|
);
|
||||||
|
if (page > 1) {
|
||||||
|
setAgents((prevAgent) => [...prevAgent, ...response.agents]);
|
||||||
|
} else {
|
||||||
|
setAgents(response.agents);
|
||||||
|
}
|
||||||
|
console.log(response);
|
||||||
|
setHasMore(
|
||||||
|
response.pagination.current_page * response.pagination.page_size <
|
||||||
|
response.pagination.total_items,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setAgentLoading(false);
|
||||||
|
setLoadingMore(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[api, setAgents, setAgentLoading],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAgents(1);
|
||||||
|
}, [fetchAgents]);
|
||||||
|
|
||||||
|
const handleInfiniteScroll = useCallback(
|
||||||
|
(scrollY: number) => {
|
||||||
|
if (!hasMore || loadingMore) return;
|
||||||
|
|
||||||
|
const { scrollHeight, clientHeight } = document.documentElement;
|
||||||
|
const SCROLL_THRESHOLD = 20;
|
||||||
|
const FETCH_DELAY = 1000;
|
||||||
|
|
||||||
|
if (scrollY + clientHeight >= scrollHeight - SCROLL_THRESHOLD) {
|
||||||
|
setLoadingMore(true);
|
||||||
|
const nextPage = currentPage + 1;
|
||||||
|
setCurrentPage(nextPage);
|
||||||
|
setTimeout(() => fetchAgents(nextPage), FETCH_DELAY);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentPage, hasMore, loadingMore, fetchAgents],
|
||||||
|
);
|
||||||
|
|
||||||
|
useScrollThreshold(handleInfiniteScroll, 50);
|
||||||
|
|
||||||
|
const LoadingSpinner = () => (
|
||||||
|
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-t-2 border-neutral-800" />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-2">
|
||||||
|
{agentLoading ? (
|
||||||
|
<div className="flex h-[200px] items-center justify-center">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4">
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<LibraryAgentCard key={agent.id} agent={agent} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{loadingMore && hasMore && (
|
||||||
|
<div className="flex items-center justify-center py-4 pt-8">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
CirclePlayIcon,
|
||||||
|
ClipboardCopy,
|
||||||
|
ImageIcon,
|
||||||
|
PlayCircle,
|
||||||
|
Share2,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export interface NotificationCardData {
|
||||||
|
type: "text" | "image" | "video" | "audio";
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
content?: string;
|
||||||
|
mediaUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationCardProps {
|
||||||
|
notification: NotificationCardData;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationCard = ({
|
||||||
|
notification: { id, type, title, content, mediaUrl },
|
||||||
|
onClose,
|
||||||
|
}: NotificationCardProps) => {
|
||||||
|
const barHeights = Array.from({ length: 60 }, () =>
|
||||||
|
Math.floor(Math.random() * (34 - 20 + 1) + 20),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClose = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-[430px] space-y-[22px] rounded-[14px] border border-neutral-100 bg-neutral-50 p-[16px] pt-[12px]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{/* count */}
|
||||||
|
<div className="flex items-center gap-[10px]">
|
||||||
|
<p className="font-sans text-[12px] font-medium text-neutral-500">
|
||||||
|
1/4
|
||||||
|
</p>
|
||||||
|
<p className="h-[26px] rounded-[45px] bg-green-100 px-[9px] py-[3px] font-sans text-[12px] font-medium text-green-800">
|
||||||
|
Success
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* cross icon */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="p-0 hover:bg-transparent"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<X
|
||||||
|
className="h-6 w-6 text-[#020617] hover:scale-105"
|
||||||
|
strokeWidth={1.25}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-[6px] p-0">
|
||||||
|
<p className="font-sans text-[14px] font-medium leading-[20px] text-neutral-500">
|
||||||
|
New Output Ready!
|
||||||
|
</p>
|
||||||
|
<h2 className="font-poppin text-[20px] font-medium leading-7 text-neutral-800">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{type === "text" && <Separator />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-0">
|
||||||
|
{type === "text" && (
|
||||||
|
// Maybe in future we give markdown support
|
||||||
|
<div className="mt-[-8px] line-clamp-6 font-sans text-sm font-[400px] text-neutral-600">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "image" &&
|
||||||
|
(mediaUrl ? (
|
||||||
|
<div className="relative h-[200px] w-full">
|
||||||
|
<Image
|
||||||
|
src={mediaUrl}
|
||||||
|
alt={title}
|
||||||
|
fill
|
||||||
|
className="rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[244px] w-full items-center justify-center rounded-lg bg-[#D9D9D9]">
|
||||||
|
<ImageIcon
|
||||||
|
className="h-[138px] w-[138px] text-neutral-400"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{type === "video" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{mediaUrl ? (
|
||||||
|
<video src={mediaUrl} controls className="w-full rounded-lg" />
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[219px] w-[398px] items-center justify-center rounded-lg bg-[#D9D9D9]">
|
||||||
|
<PlayCircle
|
||||||
|
className="h-16 w-16 text-neutral-500"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "audio" && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<CirclePlayIcon
|
||||||
|
className="h-10 w-10 rounded-full bg-neutral-800 text-white"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-1 items-center justify-between">
|
||||||
|
{/* <audio src={mediaUrl} controls className="w-full" /> */}
|
||||||
|
{barHeights.map((h, i) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`rounded-[8px] bg-neutral-500`}
|
||||||
|
style={{
|
||||||
|
height: `${h}px`,
|
||||||
|
width: "3px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between gap-2 p-0">
|
||||||
|
<div className="space-x-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.share({
|
||||||
|
title,
|
||||||
|
text: content,
|
||||||
|
url: mediaUrl,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="h-10 w-10 rounded-full border-neutral-800 p-0"
|
||||||
|
>
|
||||||
|
<Share2 className="h-5 w-5" strokeWidth={1} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
navigator.clipboard.writeText(content || mediaUrl || "")
|
||||||
|
}
|
||||||
|
className="h-10 w-10 rounded-full border-neutral-800 p-0"
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-5 w-5" strokeWidth={1} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button className="h-[40px] rounded-[52px] bg-neutral-800 px-4 py-2">
|
||||||
|
See run
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationCard;
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
import { motion, useAnimationControls } from "framer-motion";
|
||||||
|
import { BellIcon, X } from "lucide-react";
|
||||||
|
import { Button } from "@/components/agptui/Button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import LibraryNotificationCard, {
|
||||||
|
NotificationCardData,
|
||||||
|
} from "./library-notification-card";
|
||||||
|
|
||||||
|
export default function LibraryNotificationDropdown(): React.ReactNode {
|
||||||
|
const controls = useAnimationControls();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [notifications, setNotifications] = useState<
|
||||||
|
NotificationCardData[] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const initialNotificationData = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "audio",
|
||||||
|
title: "Audio Processing Complete",
|
||||||
|
id: "4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
title: "LinkedIn Post Generator: YouTube to Professional Content",
|
||||||
|
id: "1",
|
||||||
|
content:
|
||||||
|
"As artificial intelligence (AI) continues to evolve, it's increasingly clear that AI isn't just a trend—it's reshaping the way we work, innovate, and solve complex problems. However, for many professionals, the question remains: How can I leverage AI to drive meaningful results in my own field? In this article, we'll explore how AI can empower businesses and individuals alike to be more efficient, make better decisions, and unlock new opportunities. Whether you're in tech, finance, healthcare, or any other industry, understanding the potential of AI can set you apart.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
title: "New Image Upload",
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "video",
|
||||||
|
title: "Video Processing Complete",
|
||||||
|
id: "3",
|
||||||
|
},
|
||||||
|
] as NotificationCardData[],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialNotificationData) {
|
||||||
|
setNotifications(initialNotificationData);
|
||||||
|
}
|
||||||
|
}, [initialNotificationData]);
|
||||||
|
|
||||||
|
const handleHoverStart = () => {
|
||||||
|
controls.start({
|
||||||
|
rotate: [0, -10, 10, -10, 10, 0],
|
||||||
|
transition: { duration: 0.5 },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
|
<DropdownMenuTrigger className="sm:flex-1" asChild>
|
||||||
|
<Button
|
||||||
|
variant={open ? "primary" : "outline"}
|
||||||
|
onMouseEnter={handleHoverStart}
|
||||||
|
onMouseLeave={handleHoverStart}
|
||||||
|
className="w-fit max-w-[161px] transition-all duration-200 ease-in-out sm:w-[161px]"
|
||||||
|
>
|
||||||
|
<motion.div animate={controls}>
|
||||||
|
<BellIcon
|
||||||
|
className="h-5 w-5 transition-all duration-200 ease-in-out sm:mr-2"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 1 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="hidden items-center transition-opacity duration-300 sm:inline-flex"
|
||||||
|
>
|
||||||
|
Your updates
|
||||||
|
<span className="ml-2 text-[14px]">
|
||||||
|
{notifications?.length || 0}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
sideOffset={22}
|
||||||
|
className="relative left-[16px] h-[80vh] w-fit overflow-y-auto rounded-[26px] bg-[#C5C5CA] p-5"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="z-10 mb-4 font-sans text-[18px] text-white">
|
||||||
|
Agent run updates
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<button
|
||||||
|
className="absolute right-[10px] top-[20px] h-fit w-fit"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6 text-white hover:text-white/60" />
|
||||||
|
</button>
|
||||||
|
<div className="space-y-[12px]">
|
||||||
|
{notifications && notifications.length ? (
|
||||||
|
notifications.map((notification) => (
|
||||||
|
<DropdownMenuItem key={notification.id} className="p-0">
|
||||||
|
<LibraryNotificationCard
|
||||||
|
notification={notification}
|
||||||
|
onClose={() =>
|
||||||
|
setNotifications((prev) => {
|
||||||
|
if (!prev) return null;
|
||||||
|
return prev.filter((n) => n.id !== notification.id);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="w-[464px] py-4 text-center text-white">
|
||||||
|
No notifications present
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Search, X } from "lucide-react";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
import { useLibraryPageContext } from "@/app/library/state-provider";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
|
export default function LibrarySearchBar(): React.ReactNode {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const api = useBackendAPI();
|
||||||
|
const { setAgentLoading, setAgents, librarySort, setSearchTerm } =
|
||||||
|
useLibraryPageContext();
|
||||||
|
|
||||||
|
const debouncedSearch = debounce(async (value: string) => {
|
||||||
|
try {
|
||||||
|
setAgentLoading(true);
|
||||||
|
setSearchTerm(value);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
const response = await api.listLibraryAgents({
|
||||||
|
search_term: value,
|
||||||
|
sort_by: librarySort,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
setAgents(response.agents);
|
||||||
|
setAgentLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search failed:", error);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const searchTerm = e.target.value;
|
||||||
|
debouncedSearch(searchTerm);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => inputRef.current?.focus()}
|
||||||
|
className="relative z-[21] mx-auto flex h-[50px] w-full max-w-[500px] flex-1 cursor-pointer items-center rounded-[45px] bg-[#EDEDED] px-[24px] py-[10px]"
|
||||||
|
>
|
||||||
|
<div className="w-[30px] overflow-hidden">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{!isFocused ? (
|
||||||
|
<motion.div
|
||||||
|
key="search"
|
||||||
|
initial={{ x: -50 }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: -50 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search
|
||||||
|
className="h-[29px] w-[29px] text-neutral-900"
|
||||||
|
strokeWidth={1.25}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="close"
|
||||||
|
initial={{ x: 50 }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: 50 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X
|
||||||
|
className="h-[29px] w-[29px] cursor-pointer text-neutral-900"
|
||||||
|
strokeWidth={1.25}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
debouncedSearch("");
|
||||||
|
inputRef.current.value = "";
|
||||||
|
inputRef.current.blur();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
setIsFocused(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={() => !inputRef.current?.value && setIsFocused(false)}
|
||||||
|
onChange={handleSearchInput}
|
||||||
|
className="border-none font-sans text-[16px] font-normal leading-7 shadow-none focus:shadow-none"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search agents"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
import { LibraryAgentSortEnum } from "@/lib/autogpt-server-api/types";
|
||||||
|
import { useLibraryPageContext } from "@/app/library/state-provider";
|
||||||
|
import { ArrowDownNarrowWideIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
export default function LibrarySortMenu(): React.ReactNode {
|
||||||
|
const api = useBackendAPI();
|
||||||
|
const { setAgentLoading, setAgents, setLibrarySort, searchTerm } =
|
||||||
|
useLibraryPageContext();
|
||||||
|
const handleSortChange = async (value: LibraryAgentSortEnum) => {
|
||||||
|
setLibrarySort(value);
|
||||||
|
setAgentLoading(true);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
let response = await api.listLibraryAgents({
|
||||||
|
search_term: searchTerm,
|
||||||
|
sort_by: value,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
setAgents(response.agents);
|
||||||
|
setAgentLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="hidden whitespace-nowrap sm:inline">sort by</span>
|
||||||
|
<Select onValueChange={handleSortChange}>
|
||||||
|
<SelectTrigger className="ml-1 w-fit space-x-1 border-none px-0 text-base underline underline-offset-4 shadow-none">
|
||||||
|
<ArrowDownNarrowWideIcon className="h-4 w-4 sm:hidden" />
|
||||||
|
<SelectValue placeholder="Last Modified" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value={LibraryAgentSortEnum.CREATED_AT}>
|
||||||
|
Creation Date
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={LibraryAgentSortEnum.UPDATED_AT}>
|
||||||
|
Last Modified
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Upload, X } from "lucide-react";
|
||||||
|
import { removeCredentials } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/agptui/Button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { FileUploader } from "react-drag-drop-files";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Graph, GraphCreatable } from "@/lib/autogpt-server-api";
|
||||||
|
import { updatedBlockIDMap } from "@/components/agent-import-form";
|
||||||
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
const fileTypes = ["JSON"];
|
||||||
|
|
||||||
|
const fileSchema = z.custom<File>((val) => val instanceof File, {
|
||||||
|
message: "Must be a File object",
|
||||||
|
});
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
agentFile: fileSchema,
|
||||||
|
agentName: z.string().min(1, "Agent name is required"),
|
||||||
|
agentDescription: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateBlockIDs(graph: Graph) {
|
||||||
|
graph.nodes
|
||||||
|
.filter((node) => node.block_id in updatedBlockIDMap)
|
||||||
|
.forEach((node) => {
|
||||||
|
node.block_id = updatedBlockIDMap[node.block_id];
|
||||||
|
});
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LibraryUploadAgentDialog(): React.ReactNode {
|
||||||
|
const [isDroped, setisDroped] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const api = useBackendAPI();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [agentObject, setAgentObject] = useState<GraphCreatable | null>(null);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
agentName: "",
|
||||||
|
agentDescription: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
if (!agentObject) {
|
||||||
|
form.setError("root", { message: "No Agent object to save" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const payload: GraphCreatable = {
|
||||||
|
...agentObject,
|
||||||
|
name: values.agentName,
|
||||||
|
description: values.agentDescription,
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.createGraph(payload);
|
||||||
|
setIsOpen(false);
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Agent uploaded successfully",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
const qID = "flowID";
|
||||||
|
window.location.href = `/build?${qID}=${response.id}`;
|
||||||
|
} catch (error) {
|
||||||
|
form.setError("root", {
|
||||||
|
message: `Could not create agent: ${error}`,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (file: File) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setisDroped(false);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
form.setValue("agentFile", file);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(event.target?.result as string);
|
||||||
|
if (
|
||||||
|
!["name", "description", "nodes", "links"].every(
|
||||||
|
(key) => key in obj && obj[key] != null,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid agent object in file: " + JSON.stringify(obj, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const agent = obj as Graph;
|
||||||
|
removeCredentials(agent);
|
||||||
|
updateBlockIDs(agent);
|
||||||
|
setAgentObject(agent);
|
||||||
|
if (!form.getValues("agentName")) {
|
||||||
|
form.setValue("agentName", agent.name);
|
||||||
|
}
|
||||||
|
if (!form.getValues("agentDescription")) {
|
||||||
|
form.setValue("agentDescription", agent.description);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading agent file:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
setisDroped(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="primary" className="w-fit sm:w-[177px]">
|
||||||
|
<Upload className="h-5 w-5 sm:mr-2" />
|
||||||
|
<span className="hidden items-center sm:inline-flex">
|
||||||
|
Upload an agent
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="mb-8 text-center">Upload Agent</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="agentName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Agent name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} className="w-full rounded-[10px]" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="agentDescription"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea {...field} className="w-full rounded-[10px]" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="agentFile"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="rounded-xl border-2 border-dashed border-neutral-300 hover:border-neutral-600">
|
||||||
|
<FormControl>
|
||||||
|
{field.value ? (
|
||||||
|
<div className="relative flex rounded-[10px] border p-2 font-sans text-sm font-medium text-[#525252] outline-none">
|
||||||
|
<span className="line-clamp-1">{field.value.name}</span>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
form.setValue("agentFile", undefined as any)
|
||||||
|
}
|
||||||
|
className="absolute left-[-10px] top-[-16px] mt-2 h-fit border-none bg-red-200 p-1"
|
||||||
|
>
|
||||||
|
<X
|
||||||
|
className="m-0 h-[12px] w-[12px] text-red-600"
|
||||||
|
strokeWidth={3}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<FileUploader
|
||||||
|
handleChange={handleChange}
|
||||||
|
name="file"
|
||||||
|
types={fileTypes}
|
||||||
|
label={"Upload your agent here..!!"}
|
||||||
|
uploadedLabel={"Uploading Successful"}
|
||||||
|
required={true}
|
||||||
|
hoverTitle={"Drop your agent here...!!"}
|
||||||
|
maxSize={10}
|
||||||
|
classes={"drop-style"}
|
||||||
|
onDrop={() => {
|
||||||
|
setisDroped(true);
|
||||||
|
}}
|
||||||
|
onSelect={() => setisDroped(true)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: "150px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
outline: "none",
|
||||||
|
fontFamily: "var(--font-geist-sans)",
|
||||||
|
color: "#525252",
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: "500",
|
||||||
|
borderWidth: "0px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isDroped ? (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-t-2 border-neutral-800"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Drop your agent here</span>
|
||||||
|
<span>or</span>
|
||||||
|
<span>Click to upload</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FileUploader>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
className="mt-2 self-end"
|
||||||
|
disabled={!agentObject || isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-t-2 border-white"></div>
|
||||||
|
<span>Uploading...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Upload Agent"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -106,7 +106,7 @@ export const FlowRunInfo: React.FC<
|
|||||||
<Button onClick={() => setIsOutputOpen(true)} variant="outline">
|
<Button onClick={() => setIsOutputOpen(true)} variant="outline">
|
||||||
<ExitIcon className="mr-2" /> View Outputs
|
<ExitIcon className="mr-2" /> View Outputs
|
||||||
</Button>
|
</Button>
|
||||||
{flow.is_created_by_user && (
|
{flow.can_access_graph && (
|
||||||
<Link
|
<Link
|
||||||
className={buttonVariants({ variant: "default" })}
|
className={buttonVariants({ variant: "default" })}
|
||||||
href={`/build?flowID=${execution.graph_id}&flowVersion=${execution.graph_version}&flowExecutionID=${execution.execution_id}`}
|
href={`/build?flowID=${execution.graph_id}&flowVersion=${execution.graph_version}&flowExecutionID=${execution.execution_id}`}
|
||||||
|
|||||||
37
autogpt_platform/frontend/src/hooks/useScrollThreshold.ts
Normal file
37
autogpt_platform/frontend/src/hooks/useScrollThreshold.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface ThresholdCallback<T> {
|
||||||
|
(value: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useScrollThreshold = <T>(
|
||||||
|
callback: ThresholdCallback<T>,
|
||||||
|
threshold: number,
|
||||||
|
): boolean => {
|
||||||
|
const [prevValue, setPrevValue] = useState<T | null>(null);
|
||||||
|
const [isThresholdMet, setIsThresholdMet] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const { scrollY } = window;
|
||||||
|
|
||||||
|
if (scrollY >= threshold) {
|
||||||
|
setIsThresholdMet(true);
|
||||||
|
} else {
|
||||||
|
setIsThresholdMet(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollY >= threshold && (!prevValue || prevValue !== scrollY)) {
|
||||||
|
callback(scrollY as T);
|
||||||
|
setPrevValue(scrollY as T);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
handleScroll();
|
||||||
|
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
|
}, [callback, threshold, prevValue]);
|
||||||
|
|
||||||
|
return isThresholdMet;
|
||||||
|
};
|
||||||
@@ -6,38 +6,43 @@ import {
|
|||||||
APIKeyCredentials,
|
APIKeyCredentials,
|
||||||
APIKeyPermission,
|
APIKeyPermission,
|
||||||
Block,
|
Block,
|
||||||
CreatorsResponse,
|
|
||||||
CreatorDetails,
|
|
||||||
CreateAPIKeyResponse,
|
CreateAPIKeyResponse,
|
||||||
|
CreateLibraryAgentPresetRequest,
|
||||||
|
CreatorDetails,
|
||||||
|
CreatorsResponse,
|
||||||
Credentials,
|
Credentials,
|
||||||
CredentialsDeleteNeedConfirmationResponse,
|
CredentialsDeleteNeedConfirmationResponse,
|
||||||
CredentialsDeleteResponse,
|
CredentialsDeleteResponse,
|
||||||
CredentialsMetaResponse,
|
CredentialsMetaResponse,
|
||||||
GraphExecution,
|
|
||||||
GraphExecutionMeta,
|
|
||||||
Graph,
|
Graph,
|
||||||
GraphCreatable,
|
GraphCreatable,
|
||||||
|
GraphExecution,
|
||||||
|
GraphExecutionMeta,
|
||||||
GraphMeta,
|
GraphMeta,
|
||||||
GraphUpdateable,
|
GraphUpdateable,
|
||||||
LibraryAgent,
|
LibraryAgent,
|
||||||
|
LibraryAgentPreset,
|
||||||
|
LibraryAgentPresetResponse,
|
||||||
|
LibraryAgentResponse,
|
||||||
|
LibraryAgentSortEnum,
|
||||||
MyAgentsResponse,
|
MyAgentsResponse,
|
||||||
NodeExecutionResult,
|
NodeExecutionResult,
|
||||||
|
NotificationPreference,
|
||||||
|
NotificationPreferenceDTO,
|
||||||
ProfileDetails,
|
ProfileDetails,
|
||||||
|
RefundRequest,
|
||||||
Schedule,
|
Schedule,
|
||||||
ScheduleCreatable,
|
ScheduleCreatable,
|
||||||
StoreAgentsResponse,
|
|
||||||
StoreAgentDetails,
|
StoreAgentDetails,
|
||||||
StoreSubmissionsResponse,
|
StoreAgentsResponse,
|
||||||
StoreSubmissionRequest,
|
|
||||||
StoreSubmission,
|
|
||||||
StoreReviewCreate,
|
|
||||||
StoreReview,
|
StoreReview,
|
||||||
|
StoreReviewCreate,
|
||||||
|
StoreSubmission,
|
||||||
|
StoreSubmissionRequest,
|
||||||
|
StoreSubmissionsResponse,
|
||||||
TransactionHistory,
|
TransactionHistory,
|
||||||
User,
|
User,
|
||||||
NotificationPreferenceDTO,
|
|
||||||
UserPasswordCredentials,
|
UserPasswordCredentials,
|
||||||
NotificationPreference,
|
|
||||||
RefundRequest,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { createBrowserClient } from "@supabase/ssr";
|
import { createBrowserClient } from "@supabase/ssr";
|
||||||
import getServerSupabase from "../supabase/getServerSupabase";
|
import getServerSupabase from "../supabase/getServerSupabase";
|
||||||
@@ -90,9 +95,9 @@ export default class BackendAPI {
|
|||||||
return this._request("POST", "/auth/user/email", { email });
|
return this._request("POST", "/auth/user/email", { email });
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserCredit(page?: string): Promise<{ credits: number }> {
|
getUserCredit(): Promise<{ credits: number }> {
|
||||||
try {
|
try {
|
||||||
return this._get(`/credits`, undefined, page);
|
return this._get("/credits");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.resolve({ credits: 0 });
|
return Promise.resolve({ credits: 0 });
|
||||||
}
|
}
|
||||||
@@ -357,10 +362,9 @@ export default class BackendAPI {
|
|||||||
/////////// V2 STORE API /////////////////
|
/////////// V2 STORE API /////////////////
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
|
|
||||||
getStoreProfile(page?: string): Promise<ProfileDetails | null> {
|
getStoreProfile(): Promise<ProfileDetails | null> {
|
||||||
try {
|
try {
|
||||||
console.log("+++ Making API from: ", page);
|
const result = this._get("/store/profile");
|
||||||
const result = this._get("/store/profile", undefined, page);
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching store profile:", error);
|
console.error("Error fetching store profile:", error);
|
||||||
@@ -478,20 +482,82 @@ export default class BackendAPI {
|
|||||||
/////////// V2 LIBRARY API //////////////
|
/////////// V2 LIBRARY API //////////////
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
|
|
||||||
async listLibraryAgents(): Promise<LibraryAgent[]> {
|
listLibraryAgents(params?: {
|
||||||
return this._get("/library/agents");
|
search_term?: string;
|
||||||
|
sort_by?: LibraryAgentSortEnum;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}): Promise<LibraryAgentResponse> {
|
||||||
|
return this._get("/library/agents", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAgentToLibrary(storeListingVersionId: string): Promise<void> {
|
addMarketplaceAgentToLibrary(
|
||||||
await this._request("POST", `/library/agents/${storeListingVersionId}`);
|
storeListingVersionID: string,
|
||||||
|
): Promise<LibraryAgent> {
|
||||||
|
return this._request("POST", "/library/agents", {
|
||||||
|
store_listing_version_id: storeListingVersionID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLibraryAgent(
|
||||||
|
libraryAgentId: string,
|
||||||
|
params: {
|
||||||
|
auto_update_version?: boolean;
|
||||||
|
is_favorite?: boolean;
|
||||||
|
is_archived?: boolean;
|
||||||
|
is_deleted?: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
await this._request("PUT", `/library/agents/${libraryAgentId}`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
listLibraryAgentPresets(params?: {
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}): Promise<LibraryAgentPresetResponse> {
|
||||||
|
return this._get("/library/presets", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibraryAgentPreset(presetId: string): Promise<LibraryAgentPreset> {
|
||||||
|
return this._get(`/library/presets/${presetId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createLibraryAgentPreset(
|
||||||
|
preset: CreateLibraryAgentPresetRequest,
|
||||||
|
): Promise<LibraryAgentPreset> {
|
||||||
|
return this._request("POST", "/library/presets", preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLibraryAgentPreset(
|
||||||
|
presetId: string,
|
||||||
|
preset: CreateLibraryAgentPresetRequest,
|
||||||
|
): Promise<LibraryAgentPreset> {
|
||||||
|
return this._request("PUT", `/library/presets/${presetId}`, preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteLibraryAgentPreset(presetId: string): Promise<void> {
|
||||||
|
await this._request("DELETE", `/library/presets/${presetId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeLibraryAgentPreset(
|
||||||
|
presetId: string,
|
||||||
|
graphId: string,
|
||||||
|
graphVersion: number,
|
||||||
|
nodeInput: { [key: string]: any },
|
||||||
|
): Promise<{ id: string }> {
|
||||||
|
return this._request("POST", `/library/presets/${presetId}/execute`, {
|
||||||
|
graph_id: graphId,
|
||||||
|
graph_version: graphVersion,
|
||||||
|
node_input: nodeInput,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////
|
///////////////////////////////////////////
|
||||||
/////////// INTERNAL FUNCTIONS ////////////
|
/////////// INTERNAL FUNCTIONS ////////////
|
||||||
//////////////////////////////??///////////
|
//////////////////////////////??///////////
|
||||||
|
|
||||||
private async _get(path: string, query?: Record<string, any>, page?: string) {
|
private _get(path: string, query?: Record<string, any>) {
|
||||||
return this._request("GET", path, query, page);
|
return this._request("GET", path, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
|
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
|
||||||
@@ -559,7 +625,6 @@ export default class BackendAPI {
|
|||||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
||||||
path: string,
|
path: string,
|
||||||
payload?: Record<string, any>,
|
payload?: Record<string, any>,
|
||||||
page?: string,
|
|
||||||
) {
|
) {
|
||||||
if (method !== "GET") {
|
if (method !== "GET") {
|
||||||
console.debug(`${method} ${path} payload:`, payload);
|
console.debug(`${method} ${path} payload:`, payload);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import BackendAPI from "./client";
|
import BackendAPI from "./client";
|
||||||
import React, { createContext, useMemo } from "react";
|
import React, { createContext, useMemo } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -305,24 +305,81 @@ export type NodeExecutionResult = {
|
|||||||
end_time?: Date;
|
end_time?: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* *** LIBRARY *** */
|
||||||
|
|
||||||
/* Mirror of backend/server/v2/library/model.py:LibraryAgent */
|
/* Mirror of backend/server/v2/library/model.py:LibraryAgent */
|
||||||
export type LibraryAgent = {
|
export type LibraryAgent = {
|
||||||
id: LibraryAgentID;
|
id: LibraryAgentID;
|
||||||
agent_id: GraphID;
|
agent_id: GraphID;
|
||||||
agent_version: number;
|
agent_version: number;
|
||||||
preset_id: string | null;
|
image_url?: string;
|
||||||
|
creator_name: string;
|
||||||
|
creator_image_url: string;
|
||||||
|
status: AgentStatus;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
input_schema: BlockIOObjectSubSchema;
|
input_schema: BlockIOObjectSubSchema;
|
||||||
output_schema: BlockIOObjectSubSchema;
|
new_output: boolean;
|
||||||
is_favorite: boolean;
|
can_access_graph: boolean;
|
||||||
is_created_by_user: boolean;
|
|
||||||
is_latest_version: boolean;
|
is_latest_version: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LibraryAgentID = Brand<string, "LibraryAgentID">;
|
export type LibraryAgentID = Brand<string, "LibraryAgentID">;
|
||||||
|
|
||||||
|
export enum AgentStatus {
|
||||||
|
COMPLETED = "COMPLETED",
|
||||||
|
HEALTHY = "HEALTHY",
|
||||||
|
WAITING = "WAITING",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryAgentResponse {
|
||||||
|
agents: LibraryAgent[];
|
||||||
|
pagination: {
|
||||||
|
current_page: number;
|
||||||
|
page_size: number;
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryAgentPreset {
|
||||||
|
id: string;
|
||||||
|
updated_at: Date;
|
||||||
|
agent_id: string;
|
||||||
|
agent_version: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
is_active: boolean;
|
||||||
|
inputs: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryAgentPresetResponse {
|
||||||
|
presets: LibraryAgentPreset[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateLibraryAgentPresetRequest {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputs: { [key: string]: any };
|
||||||
|
agent_id: string;
|
||||||
|
agent_version: number;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LibraryAgentSortEnum {
|
||||||
|
CREATED_AT = "createdAt",
|
||||||
|
UPDATED_AT = "updatedAt",
|
||||||
|
}
|
||||||
|
|
||||||
|
/* *** CREDENTIALS *** */
|
||||||
|
|
||||||
/* Mirror of backend/server/integrations/router.py:CredentialsMetaResponse */
|
/* Mirror of backend/server/integrations/router.py:CredentialsMetaResponse */
|
||||||
export type CredentialsMetaResponse = {
|
export type CredentialsMetaResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ test.describe("Monitor", () => {
|
|||||||
basicBlock,
|
basicBlock,
|
||||||
);
|
);
|
||||||
await buildPage.runAgent();
|
await buildPage.runAgent();
|
||||||
await monitorPage.navbar.clickMonitorLink();
|
// await monitorPage.navbar.clickMonitorLink();
|
||||||
|
await page.goto("/monitoring"); // Library link now points to /library
|
||||||
await monitorPage.waitForPageLoad();
|
await monitorPage.waitForPageLoad();
|
||||||
await test.expect(monitorPage.isLoaded()).resolves.toBeTruthy();
|
await test.expect(monitorPage.isLoaded()).resolves.toBeTruthy();
|
||||||
testInfo.attach("agent-id", { body: id });
|
testInfo.attach("agent-id", { body: id });
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user