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:
Swifty
2025-02-25 18:29:15 +01:00
committed by GitHub
parent b603ed2d2a
commit e752f0f342
43 changed files with 3467 additions and 1636 deletions

View File

@@ -41,6 +41,9 @@ RABBITMQ_PORT=5672
RABBITMQ_DEFAULT_USER=rabbitmq_user_default
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 integrations to work. Defaults to the value of PLATFORM_BASE_URL if not set.
# FRONTEND_BASE_URL=http://localhost:3000

View File

@@ -1,27 +1,58 @@
import logging
from typing import Optional
import fastapi
import prisma.errors
import prisma.fields
import prisma.models
import prisma.types
import backend.data.includes
import backend.server.model
import backend.server.v2.library.model as library_model
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__)
async def get_library_agents(
user_id: str, search_query: str | None = None
) -> list[library_model.LibraryAgent]:
user_id: str,
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(
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:
logger.warning(f"Search query too long: {search_query}")
raise store_exceptions.DatabaseError("Search query is too long.")
if page < 1 or page_size < 1:
logger.warning(f"Invalid pagination: page={page}, page_size={page_size}")
raise store_exceptions.DatabaseError("Invalid pagination input")
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 = {
"userId": user_id,
@@ -29,69 +60,167 @@ async def get_library_agents(
"isArchived": False,
}
if search_query:
# Build search filter if applicable
if search_term:
where_clause["OR"] = [
{
"Agent": {
"is": {"name": {"contains": search_query, "mode": "insensitive"}}
"is": {"name": {"contains": search_term, "mode": "insensitive"}}
}
},
{
"Agent": {
"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:
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
where=where_clause,
include={
"Agent": {
"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}.")
return [library_model.LibraryAgent.from_db(agent) for agent in library_agents]
agent_count = await prisma.models.LibraryAgent.prisma().count(
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:
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(
agent_id: str, agent_version: int, user_id: str
agent_id: str,
agent_version: int,
user_id: str,
) -> 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:
return await prisma.models.LibraryAgent.prisma().create(
data={
"userId": user_id,
"agentId": agent_id,
"agentVersion": agent_version,
"isCreatedByUser": False,
"imageUrl": image_url,
"isCreatedByUser": (user_id == agent.userId),
"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:
logger.error(f"Database error creating agent to library: {str(e)}")
raise store_exceptions.DatabaseError("Failed to create agent to library") from e
logger.error(f"Database error creating agent in library: {e}")
raise store_exceptions.DatabaseError("Failed to create agent in library") from e
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:
"""
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:
library_agent = await prisma.models.LibraryAgent.prisma().find_first_or_raise(
where={
@@ -111,7 +240,7 @@ async def update_agent_version_in_library(
},
)
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(
"Failed to update agent version in library"
) from e
@@ -126,8 +255,24 @@ async def update_library_agent(
is_deleted: bool = False,
) -> 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:
await prisma.models.LibraryAgent.prisma().update_many(
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}
)
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
async def add_store_agent_to_library(
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)
if they don't already have it
Adds an agent from a store listing version to the user's library 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 users 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(
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:
# Get store listing version to find agent
store_listing_version = (
await prisma.models.StoreListingVersion.prisma().find_unique(
where={"id": store_listing_version_id}, include={"Agent": True}
)
)
if not store_listing_version or not store_listing_version.Agent:
logger.warning(
f"Store listing version not found: {store_listing_version_id}"
)
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
if agent.userId == user_id:
store_agent = store_listing_version.Agent
if store_agent.userId == user_id:
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")
# 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={
"userId": user_id,
"agentId": agent.id,
"agentVersion": agent.version,
"agentId": store_agent.id,
"agentVersion": store_agent.version,
}
)
if existing_user_agent:
if existing_library_agent:
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
await prisma.models.LibraryAgent.prisma().create(
added_agent = await prisma.models.LibraryAgent.prisma().create(
data={
"userId": user_id,
"agentId": agent.id,
"agentVersion": agent.version,
"agentId": store_agent.id,
"agentVersion": store_agent.version,
"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:
# Reraise for external handling.
raise
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
@@ -232,20 +386,44 @@ async def add_store_agent_to_library(
async def get_presets(
user_id: str, page: int, page_size: int
) -> 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:
presets = await prisma.models.AgentPreset.prisma().find_many(
presets_records = await prisma.models.AgentPreset.prisma().find_many(
where={"userId": user_id},
skip=page * page_size,
take=page_size,
)
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
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(
@@ -259,34 +437,67 @@ async def get_presets(
)
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
async def get_preset(
user_id: str, preset_id: str
) -> 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:
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:
return None
return library_model.LibraryAgentPreset.from_db(preset)
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
async def upsert_preset(
user_id: str,
preset: library_model.CreateLibraryAgentPresetRequest,
preset_id: str | None = None,
preset_id: Optional[str] = None,
) -> 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:
if preset_id:
# Update existing preset
new_preset = await prisma.models.AgentPreset.prisma().update(
updated = await prisma.models.AgentPreset.prisma().update(
where={"id": preset_id},
data={
"name": preset.name,
@@ -301,8 +512,9 @@ async def upsert_preset(
},
include={"InputPresets": True},
)
if not new_preset:
if not updated:
raise ValueError(f"AgentPreset #{preset_id} not found")
return library_model.LibraryAgentPreset.from_db(updated)
else:
# Create new preset
new_preset = await prisma.models.AgentPreset.prisma().create(
@@ -324,16 +536,27 @@ async def upsert_preset(
)
return library_model.LibraryAgentPreset.from_db(new_preset)
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
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:
await prisma.models.AgentPreset.prisma().update_many(
where={"id": preset_id, "userId": user_id},
data={"isDeleted": True},
)
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

View File

@@ -5,7 +5,6 @@ import prisma.models
import pytest
from prisma import Prisma
import backend.data.includes
import backend.server.v2.library.db as db
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(
return_value=mock_library_agents
)
mock_library_agent.return_value.count = mocker.AsyncMock(return_value=1)
# Call function
result = await db.get_library_agents("test-user")
# Verify results
assert len(result) == 1
assert result[0].id == "ua1"
assert result[0].name == "Test Agent 2"
assert result[0].description == "Test Description 2"
assert result[0].is_created_by_user is False
assert result[0].is_latest_version is True
assert result[0].is_favorite is False
assert result[0].agent_id == "agent2"
assert result[0].agent_version == 1
assert result[0].preset_id is None
assert len(result.agents) == 1
assert result.agents[0].id == "ua1"
assert result.agents[0].name == "Test Agent 2"
assert result.agents[0].description == "Test Description 2"
assert result.agents[0].agent_id == "agent2"
assert result.agents[0].agent_version == 1
assert result.agents[0].can_access_graph is False
assert result.agents[0].is_latest_version is True
assert result.pagination.total_items == 1
assert result.pagination.total_pages == 1
assert result.pagination.current_page == 1
assert result.pagination.page_size == 50
@pytest.mark.asyncio

View File

@@ -1,6 +1,8 @@
import datetime
from enum import Enum
from typing import Any
import prisma.enums
import prisma.models
import pydantic
@@ -9,13 +11,29 @@ import backend.data.graph as graph_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):
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_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
@@ -24,47 +42,135 @@ class LibraryAgent(pydantic.BaseModel):
# Made input_schema and output_schema match GraphMeta's type
input_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend
output_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend
is_favorite: bool
is_created_by_user: bool
# Indicates whether there's a new output (based on recent runs)
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
@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:
raise ValueError("AgentGraph is required")
raise ValueError("Associated Agent record is required.")
graph = graph_model.GraphModel.from_db(agent.Agent)
agent_updated_at = agent.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 = (
max(agent_updated_at, lib_agent_updated_at)
if 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(
id=agent.id,
agent_id=agent.agentId,
agent_version=agent.agentVersion,
image_url=agent.imageUrl,
creator_name=creator_name,
creator_image_url=creator_image_url,
status=status,
updated_at=updated_at,
name=graph.name,
description=graph.description,
input_schema=graph.input_schema,
output_schema=graph.output_schema,
is_favorite=agent.isFavorite,
is_created_by_user=agent.isCreatedByUser,
is_latest_version=graph.is_active,
preset_id=agent.AgentPreset.id if agent.AgentPreset else None,
new_output=new_output,
can_access_graph=can_access_graph,
is_latest_version=is_latest_version,
)
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):
"""Represents a preset configuration for a library agent."""
id: str
updated_at: datetime.datetime
@@ -78,14 +184,14 @@ class LibraryAgentPreset(pydantic.BaseModel):
inputs: block_model.BlockInput
@staticmethod
def from_db(preset: prisma.models.AgentPreset):
@classmethod
def from_db(cls, preset: prisma.models.AgentPreset) -> "LibraryAgentPreset":
input_data: block_model.BlockInput = {}
for preset_input in preset.InputPresets or []:
input_data[preset_input.name] = preset_input.data
return LibraryAgentPreset(
return cls(
id=preset.id,
updated_at=preset.updatedAt,
agent_id=preset.agentId,
@@ -98,14 +204,52 @@ class LibraryAgentPreset(pydantic.BaseModel):
class LibraryAgentPresetResponse(pydantic.BaseModel):
"""Response schema for a list of agent presets and pagination info."""
presets: list[LibraryAgentPreset]
pagination: server_model.Pagination
class CreateLibraryAgentPresetRequest(pydantic.BaseModel):
"""
Request model used when creating a new preset for a library agent.
"""
name: str
description: str
inputs: block_model.BlockInput
agent_id: str
agent_version: int
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")

View File

@@ -3,147 +3,10 @@ import datetime
import prisma.fields
import prisma.models
import backend.data.block
import backend.server.model
import backend.server.v2.library.model
import backend.server.v2.library.model as library_model
def test_library_agent():
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():
def test_agent_preset_from_db():
# Create mock DB agent
db_agent = prisma.models.AgentPreset(
id="test-agent-123",
@@ -167,7 +30,7 @@ def test_library_agent_from_db():
)
# 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.agent_version == 1

View File

@@ -1,8 +1,9 @@
import logging
from typing import Annotated, Sequence
from typing import Optional
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.model as library_model
@@ -10,129 +11,174 @@ import backend.server.v2.store.exceptions as store_exceptions
logger = logging.getLogger(__name__)
router = fastapi.APIRouter()
router = APIRouter(
prefix="/agents",
tags=["library", "private"],
dependencies=[Depends(autogpt_auth_lib.auth_middleware)],
)
@router.get(
"/agents",
tags=["library", "private"],
dependencies=[fastapi.Depends(autogpt_auth_lib.auth_middleware)],
"",
responses={
500: {"description": "Server error", "content": {"application/json": {}}},
},
)
async def get_library_agents(
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)]
) -> Sequence[library_model.LibraryAgent]:
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
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:
agents = await library_db.get_library_agents(user_id)
return agents
except Exception as e:
logger.exception(f"Exception occurred whilst getting library agents: {e}")
raise fastapi.HTTPException(
status_code=500, detail="Failed to get library agents"
return await library_db.get_library_agents(
user_id=user_id,
search_term=search_term,
sort_by=sort_by,
page=page,
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(
"/agents/{store_listing_version_id}",
tags=["library", "private"],
dependencies=[fastapi.Depends(autogpt_auth_lib.auth_middleware)],
status_code=201,
"",
status_code=status.HTTP_201_CREATED,
responses={
201: {"description": "Agent added successfully"},
404: {"description": "Store listing version not found"},
500: {"description": "Server error"},
},
)
async def add_agent_to_library(
store_listing_version_id: str,
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
) -> fastapi.Response:
async def add_marketplace_agent_to_library(
store_listing_version_id: str = Body(embed=True),
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> 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:
store_listing_version_id (str): ID of the store listing version to add
user_id (str): ID of the authenticated user
store_listing_version_id: ID of the store listing version to add.
user_id: ID of the authenticated user.
Returns:
fastapi.Response: 201 status code on success
201 (Created) on success.
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:
# Use the database function to add the agent to the library
await library_db.add_store_agent_to_library(store_listing_version_id, user_id)
return fastapi.Response(status_code=201)
return await library_db.add_store_agent_to_library(
store_listing_version_id=store_listing_version_id,
user_id=user_id,
)
except store_exceptions.AgentNotFoundError:
raise fastapi.HTTPException(
logger.warning(f"Agent not found: {store_listing_version_id}")
raise HTTPException(
status_code=404,
detail=f"Store listing version {store_listing_version_id} not found",
)
except store_exceptions.DatabaseError as e:
logger.exception(f"Database error occurred whilst adding agent to library: {e}")
raise fastapi.HTTPException(
status_code=500, detail="Failed to add agent to library"
)
logger.error(f"Database error occurred whilst adding agent to library: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to add agent to library",
) from e
except Exception as e:
logger.exception(
f"Unexpected exception occurred whilst adding agent to library: {e}"
)
raise fastapi.HTTPException(
status_code=500, detail="Failed to add agent to library"
)
logger.error(f"Unexpected error while adding agent: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to add agent to library",
) from e
@router.put(
"/agents/{library_agent_id}",
tags=["library", "private"],
dependencies=[fastapi.Depends(autogpt_auth_lib.auth_middleware)],
status_code=204,
"/{library_agent_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses={
204: {"description": "Agent updated successfully"},
500: {"description": "Server error"},
},
)
async def update_library_agent(
library_agent_id: str,
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
auto_update_version: bool = False,
is_favorite: bool = False,
is_archived: bool = False,
is_deleted: bool = False,
) -> fastapi.Response:
payload: library_model.LibraryAgentUpdateRequest,
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> JSONResponse:
"""
Update the library agent with the given fields.
Args:
library_agent_id (str): ID of the library agent to update
user_id (str): ID of the authenticated user
auto_update_version (bool): Whether to auto-update the agent version
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
library_agent_id: ID of the library agent to update.
payload: Fields to update (auto_update_version, is_favorite, etc.).
user_id: ID of the authenticated user.
Returns:
fastapi.Response: 204 status code on success
204 (No Content) on success.
Raises:
HTTPException: If there is an error updating the library agent
HTTPException(500): If a server/database error occurs.
"""
try:
# Use the database function to update the library agent
await library_db.update_library_agent(
library_agent_id,
user_id,
auto_update_version,
is_favorite,
is_archived,
is_deleted,
library_agent_id=library_agent_id,
user_id=user_id,
auto_update_version=payload.auto_update_version,
is_favorite=payload.is_favorite,
is_archived=payload.is_archived,
is_deleted=payload.is_deleted,
)
return JSONResponse(
status_code=status.HTTP_204_NO_CONTENT,
content={"message": "Agent updated successfully"},
)
return fastapi.Response(status_code=204)
except store_exceptions.DatabaseError as e:
logger.exception(f"Database error occurred whilst updating library agent: {e}")
raise fastapi.HTTPException(
status_code=500, detail="Failed to update library agent"
)
logger.exception(f"Database error while updating library agent: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update library agent",
) from e
except Exception as e:
logger.exception(
f"Unexpected exception occurred whilst updating library agent: {e}"
)
raise fastapi.HTTPException(
status_code=500, detail="Failed to update library agent"
)
logger.exception(f"Unexpected error while updating library agent: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update library agent",
) from e

View File

@@ -3,113 +3,225 @@ from typing import Annotated, Any
import autogpt_libs.auth as autogpt_auth_lib
import autogpt_libs.utils.cache
import fastapi
from fastapi import APIRouter, Body, Depends, HTTPException, status
import backend.executor
import backend.server.v2.library.db as library_db
import backend.server.v2.library.model as library_model
import backend.server.v2.library.db as db
import backend.server.v2.library.model as models
import backend.util.service
logger = logging.getLogger(__name__)
router = fastapi.APIRouter()
router = APIRouter()
@autogpt_libs.utils.cache.thread_cached
def execution_manager_client() -> backend.executor.ExecutionManager:
"""Return a cached instance of ExecutionManager client."""
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(
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_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:
presets = await library_db.get_presets(user_id, page, page_size)
return presets
return await db.get_presets(user_id, page, page_size)
except Exception as e:
logger.exception(f"Exception occurred whilst getting presets: {e}")
raise fastapi.HTTPException(status_code=500, detail="Failed to get presets")
logger.exception(f"Exception occurred while getting presets: {e}")
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(
preset_id: str,
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
) -> library_model.LibraryAgentPreset:
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> 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:
preset = await library_db.get_preset(user_id, preset_id)
preset = await db.get_preset(user_id, preset_id)
if not preset:
raise fastapi.HTTPException(
status_code=404,
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Preset {preset_id} not found",
)
return preset
except Exception as e:
logger.exception(f"Exception occurred whilst getting preset: {e}")
raise fastapi.HTTPException(status_code=500, 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")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get preset",
)
@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"],
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(
graph_id: str,
graph_version: int,
preset_id: str,
node_input: Annotated[
dict[str, Any], fastapi.Body(..., embed=True, default_factory=dict)
],
user_id: Annotated[str, fastapi.Depends(autogpt_auth_lib.depends.get_user_id)],
node_input: Annotated[dict[str, Any], Body(..., embed=True, default_factory=dict)],
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> dict[str, Any]: # FIXME: add proper return type
try:
preset = await library_db.get_preset(user_id, preset_id)
if not preset:
raise fastapi.HTTPException(status_code=404, detail="Preset not found")
"""
Execute a preset given graph parameters, returning the execution ID on success.
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
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}")
return {"id": execution.graph_exec_id}
except HTTPException:
raise
except Exception as e:
msg = str(e).encode().decode("unicode_escape")
raise fastapi.HTTPException(status_code=400, detail=msg)
logger.exception(f"Exception occurred while executing preset: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)

View File

@@ -6,6 +6,7 @@ import fastapi.testclient
import pytest
import pytest_mock
import backend.server.model as server_model
import backend.server.v2.library.model as library_model
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):
mocked_value = [
library_model.LibraryAgent(
id="test-agent-1",
agent_id="test-agent-1",
agent_version=1,
preset_id="preset-1",
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
is_favorite=False,
is_created_by_user=True,
is_latest_version=True,
name="Test Agent 1",
description="Test Description 1",
input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}},
),
library_model.LibraryAgent(
id="test-agent-2",
agent_id="test-agent-2",
agent_version=1,
preset_id="preset-2",
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
is_favorite=False,
is_created_by_user=False,
is_latest_version=True,
name="Test Agent 2",
description="Test Description 2",
input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}},
library_model.LibraryAgentResponse(
agents=[
library_model.LibraryAgent(
id="test-agent-1",
agent_id="test-agent-1",
agent_version=1,
name="Test Agent 1",
description="Test Description 1",
image_url=None,
creator_name="Test Creator",
creator_image_url="",
input_schema={"type": "object", "properties": {}},
status=library_model.LibraryAgentStatus.COMPLETED,
new_output=False,
can_access_graph=True,
is_latest_version=True,
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
),
library_model.LibraryAgent(
id="test-agent-2",
agent_id="test-agent-2",
agent_version=1,
name="Test Agent 2",
description="Test Description 2",
image_url=None,
creator_name="Test Creator",
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.return_value = mocked_value
response = client.get("/agents")
response = client.get("/agents?search_term=test")
assert response.status_code == 200
data = [
library_model.LibraryAgent.model_validate(agent) for agent in response.json()
]
assert len(data) == 2
assert data[0].agent_id == "test-agent-1"
assert data[0].is_created_by_user is True
assert data[1].agent_id == "test-agent-2"
assert data[1].is_created_by_user is False
mock_db_call.assert_called_once_with("test-user-id")
data = library_model.LibraryAgentResponse.model_validate(response.json())
assert len(data.agents) == 2
assert data.agents[0].agent_id == "test-agent-1"
assert data.agents[0].can_access_graph is True
assert data.agents[1].agent_id == "test-agent-2"
assert data.agents[1].can_access_graph is False
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,
)
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.side_effect = Exception("Test error")
response = client.get("/agents")
response = client.get("/agents?search_term=test")
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")

View File

@@ -601,7 +601,7 @@ async def get_user_profile(
avatar_url=profile.avatarUrl,
)
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(
"Failed to get user profile"
) from e
@@ -620,7 +620,7 @@ async def update_profile(
Raises:
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:
# Sanitize username to allow only letters, numbers, and hyphens
username = "".join(
@@ -639,15 +639,13 @@ async def update_profile(
# Verify that the user is authorized to update this profile
if existing_profile.userId != user_id:
logger.error(
"Unauthorized update attempt for profile %s by user %s",
existing_profile.userId,
user_id,
f"Unauthorized update attempt for profile {existing_profile.id} by user {user_id}"
)
raise backend.server.v2.store.exceptions.DatabaseError(
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
update_data = {}
if profile.name is not None:
@@ -667,7 +665,7 @@ async def update_profile(
data=prisma.types.ProfileUpdateInput(**update_data),
)
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(
"Failed to update profile"
)
@@ -684,7 +682,7 @@ async def update_profile(
)
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(
"Failed to update profile"
) from e

View File

@@ -5,6 +5,7 @@ from enum import Enum
import replicate
import replicate.exceptions
import requests
from prisma.models import AgentGraph
from replicate.helpers import FileOutput
from backend.data.graph import Graph
@@ -21,7 +22,7 @@ class ImageStyle(str, Enum):
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.

View File

@@ -6,6 +6,7 @@ import fastapi
from google.cloud import storage
import backend.server.v2.store.exceptions
from backend.util.exceptions import MissingConfigError
from backend.util.settings import Settings
logger = logging.getLogger(__name__)
@@ -27,34 +28,32 @@ async def check_media_exists(user_id: str, filename: str) -> str | None:
Returns:
str | None: URL of the blob if it exists, None otherwise
"""
try:
settings = Settings()
storage_client = storage.Client()
bucket = storage_client.bucket(settings.config.media_gcs_bucket_name)
settings = Settings()
if not settings.config.media_gcs_bucket_name:
raise MissingConfigError("GCS media bucket is not configured")
# Check images
image_path = f"users/{user_id}/images/{filename}"
image_blob = bucket.blob(image_path)
if image_blob.exists():
return image_blob.public_url
storage_client = storage.Client()
bucket = storage_client.bucket(settings.config.media_gcs_bucket_name)
# Check videos
video_path = f"users/{user_id}/videos/{filename}"
# Check images
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)
if video_blob.exists():
return video_blob.public_url
# Check videos
video_path = f"users/{user_id}/videos/{filename}"
return None
except Exception as e:
logger.error(f"Error checking if media file exists: {str(e)}")
return None
video_blob = bucket.blob(video_path)
if video_blob.exists():
return video_blob.public_url
return None
async def upload_media(
user_id: str, file: fastapi.UploadFile, use_file_name: bool = False
) -> str:
# Get file content for deeper validation
try:
content = await file.read(1024) # Read first 1KB for validation

View File

@@ -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;

View File

@@ -65,6 +65,7 @@ model AgentGraph {
name String?
description String?
isActive Boolean @default(true)
isTemplate Boolean @default(false)
@@ -175,6 +176,8 @@ model LibraryAgent {
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
imageUrl String?
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
@@ -182,6 +185,9 @@ model LibraryAgent {
agentPresetId String?
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
creatorId String?
Creator Profile? @relation(fields: [creatorId], references: [id])
useGraphIsActiveVersion Boolean @default(false)
isFavorite Boolean @default(false)
@@ -505,6 +511,8 @@ model Profile {
isFeatured Boolean @default(false)
LibraryAgents LibraryAgent[]
@@index([username])
@@index([userId])
}

View File

@@ -470,7 +470,7 @@ async def test_execute_preset_with_clash(server: SpinTestServer):
)
# Verify execution
assert result is not None
assert result is not None, "Result must not be None"
graph_exec_id = result["id"]
# Wait for execution to complete

View File

@@ -19,6 +19,7 @@ const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
transpilePackages: ["geist"],
};
export default withSentryConfig(nextConfig, {

View File

@@ -62,6 +62,7 @@
"embla-carousel-react": "^8.5.2",
"framer-motion": "^12.0.11",
"geist": "^1.3.1",
"lodash.debounce": "^4.0.8",
"launchdarkly-react-client-sdk": "^3.6.1",
"lucide-react": "^0.474.0",
"moment": "^2.30.1",
@@ -70,7 +71,8 @@
"react": "^18",
"react-day-picker": "^9.5.1",
"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-markdown": "^9.0.3",
"react-modal": "^3.16.3",
@@ -93,7 +95,8 @@
"@storybook/nextjs": "^8.5.3",
"@storybook/react": "^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/node": "^22.13.0",
"@types/react": "^18",

View File

@@ -1,21 +1,22 @@
import React from "react";
import type { Metadata } from "next";
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 { Toaster } from "@/components/ui/toaster";
import { IconType } from "@/components/ui/icons";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
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 poppins = Poppins({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
@@ -34,14 +35,18 @@ export default async function RootLayout({
}>) {
const pathname = headers().get("x-current-path");
const isOnboarding = pathname?.startsWith("/onboarding");
console.log("pathname:", pathname);
return (
<html
lang="en"
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
attribute="class"
defaultTheme="light"
@@ -59,7 +64,7 @@ export default async function RootLayout({
},
{
name: "Library",
href: "/monitoring",
href: "/library",
},
{
name: "Build",

View File

@@ -6,6 +6,7 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
GraphExecution,
GraphExecutionMeta,
GraphID,
GraphMeta,
Schedule,
} 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";
export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: string } = useParams();
const { id: agentID }: { id: GraphID } = useParams();
const router = useRouter();
const api = useBackendAPI();
@@ -152,22 +153,20 @@ export default function AgentRunsPage(): React.ReactElement {
</div>
{/* Run / Schedule views */}
{(selectedView.type == "run" ? (
selectedView.id ? (
selectedRun && (
<AgentRunDetailsView
agent={agent}
run={selectedRun}
agentActions={agentActions}
/>
)
) : (
<AgentRunDraftView
{(selectedView.type == "run" && selectedView.id ? (
selectedRun && (
<AgentRunDetailsView
agent={agent}
onRun={(runID) => selectRun(runID)}
run={selectedRun}
agentActions={agentActions}
/>
)
) : selectedView.type == "run" ? (
<AgentRunDraftView
agent={agent}
onRun={(runID) => selectRun(runID)}
agentActions={agentActions}
/>
) : selectedView.type == "schedule" ? (
selectedSchedule && (
<AgentScheduleDetailsView

View 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>
);
}

View 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;
}

View File

@@ -43,8 +43,8 @@ const Monitor = () => {
);
const fetchAgents = useCallback(() => {
api.listLibraryAgents().then((agents) => {
setFlows(agents);
api.listLibraryAgents().then((response) => {
setFlows(response.agents);
});
api.getExecutions().then((executions) => {
setExecutions(executions);

View File

@@ -5,7 +5,7 @@ import { CreatorDetails } from "@/lib/autogpt-server-api/types";
async function getProfileData(api: BackendAPI) {
try {
const profile = await api.getStoreProfile("profile");
const profile = await api.getStoreProfile();
return {
profile,
};

View File

@@ -31,36 +31,37 @@ const formSchema = z.object({
importAsTemplate: z.boolean(),
});
function updateBlockIDs(graph: Graph) {
export const updatedBlockIDMap: Record<string, string> = {
// https://github.com/Significant-Gravitas/AutoGPT/issues/8223
const updatedBlockIDMap: Record<string, string> = {
"a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
"436c3984-57fd-4b85-8e9a-459b356883bd",
"b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
"0e50422c-6dee-4145-83d6-3a5a392f65de",
"c3d4e5f6-7g8h-9i0j-1k2l-m3n4o5p6q7r8":
"a0a69be1-4528-491c-a85a-a4ab6873e3f0",
"c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8":
"32a87eab-381e-4dd4-bdb8-4c47151be35a",
"b2c3d4e5-6f7g-8h9i-0j1k-l2m3n4o5p6q7":
"87840993-2053-44b7-8da4-187ad4ee518c",
"h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6":
"d0822ab5-9f8a-44a3-8971-531dd0178b6b",
"d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t":
"df06086a-d5ac-4abb-9996-2ad0acb2eff7",
"h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m":
"f5b0f5d0-1862-4d61-94be-3ad0fa772760",
"a1234567-89ab-cdef-0123-456789abcdef":
"4335878a-394e-4e67-adf2-919877ff49ae",
"f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l":
"f66a3543-28d3-4ab5-8945-9b336371e2ce",
"b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0h2":
"716a67b3-6760-42e7-86dc-18645c6e00fc",
"31d1064e-7446-4693-o7d4-65e5ca9110d1":
"cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
"c6731acb-4105-4zp1-bc9b-03d0036h370g":
"5ebe6768-8e5d-41e3-9134-1c7bd89a8d52",
};
"a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
"436c3984-57fd-4b85-8e9a-459b356883bd",
"b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
"0e50422c-6dee-4145-83d6-3a5a392f65de",
"c3d4e5f6-7g8h-9i0j-1k2l-m3n4o5p6q7r8":
"a0a69be1-4528-491c-a85a-a4ab6873e3f0",
"c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8":
"32a87eab-381e-4dd4-bdb8-4c47151be35a",
"b2c3d4e5-6f7g-8h9i-0j1k-l2m3n4o5p6q7":
"87840993-2053-44b7-8da4-187ad4ee518c",
"h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6":
"d0822ab5-9f8a-44a3-8971-531dd0178b6b",
"d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t":
"df06086a-d5ac-4abb-9996-2ad0acb2eff7",
"h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m":
"f5b0f5d0-1862-4d61-94be-3ad0fa772760",
"a1234567-89ab-cdef-0123-456789abcdef":
"4335878a-394e-4e67-adf2-919877ff49ae",
"f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l":
"f66a3543-28d3-4ab5-8945-9b336371e2ce",
"b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0h2":
"716a67b3-6760-42e7-86dc-18645c6e00fc",
"31d1064e-7446-4693-o7d4-65e5ca9110d1":
"cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
"c6731acb-4105-4zp1-bc9b-03d0036h370g":
"5ebe6768-8e5d-41e3-9134-1c7bd89a8d52",
};
function updateBlockIDs(graph: Graph) {
graph.nodes
.filter((node) => node.block_id in updatedBlockIDMap)
.forEach((node) => {

View File

@@ -44,9 +44,11 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
const handleAddToLibrary = async () => {
try {
await api.addAgentToLibrary(storeListingVersionId);
const newLibraryAgent = await api.addMarketplaceAgentToLibrary(
storeListingVersionId,
);
console.log("Agent added to library successfully");
router.push("/monitoring");
router.push(`/library/agents/${newLibraryAgent.agent_id}`);
} catch (error) {
console.error("Failed to add agent to library:", error);
}

View File

@@ -12,13 +12,14 @@ const buttonVariants = cva(
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",
accent: "bg-accent text-accent-foreground hover:bg-violet-500",
primary: "bg-neutral-800 text-white hover:bg-black/60",
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:
"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:
"hover:bg-neutral-100 text-[#272727] dark:text-neutral-100 dark:hover:bg-neutral-700",
link: "text-[#272727] underline-offset-4 hover:underline dark:text-neutral-100",
"hover:bg-neutral-100 text-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
link: "text-neutral-800 underline-offset-4 hover:underline dark:text-neutral-100",
},
size: {
default: "h-10 px-4 py-2 rounded-full text-sm",
@@ -44,10 +45,12 @@ export interface ButtonProps
variant?:
| "destructive"
| "accent"
| "primary"
| "outline"
| "secondary"
| "ghost"
| "link";
size?: "default" | "sm" | "lg" | "primary" | "icon" | "card";
}

View File

@@ -33,7 +33,7 @@ interface NavbarProps {
async function getProfileData() {
const api = new BackendAPI();
const profile = await Promise.resolve(api.getStoreProfile("navbar"));
const profile = await Promise.resolve(api.getStoreProfile());
return profile;
}

View File

@@ -1,7 +1,6 @@
"use client";
import Link from "next/link";
import {
IconType,
IconShoppingCart,
IconBoxes,
IconLibrary,
@@ -47,7 +46,7 @@ export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/monitoring" && (
{href === "/library" && (
<IconLibrary
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -106,7 +106,7 @@ export const FlowRunInfo: React.FC<
<Button onClick={() => setIsOutputOpen(true)} variant="outline">
<ExitIcon className="mr-2" /> View Outputs
</Button>
{flow.is_created_by_user && (
{flow.can_access_graph && (
<Link
className={buttonVariants({ variant: "default" })}
href={`/build?flowID=${execution.graph_id}&flowVersion=${execution.graph_version}&flowExecutionID=${execution.execution_id}`}

View 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;
};

View File

@@ -6,38 +6,43 @@ import {
APIKeyCredentials,
APIKeyPermission,
Block,
CreatorsResponse,
CreatorDetails,
CreateAPIKeyResponse,
CreateLibraryAgentPresetRequest,
CreatorDetails,
CreatorsResponse,
Credentials,
CredentialsDeleteNeedConfirmationResponse,
CredentialsDeleteResponse,
CredentialsMetaResponse,
GraphExecution,
GraphExecutionMeta,
Graph,
GraphCreatable,
GraphExecution,
GraphExecutionMeta,
GraphMeta,
GraphUpdateable,
LibraryAgent,
LibraryAgentPreset,
LibraryAgentPresetResponse,
LibraryAgentResponse,
LibraryAgentSortEnum,
MyAgentsResponse,
NodeExecutionResult,
NotificationPreference,
NotificationPreferenceDTO,
ProfileDetails,
RefundRequest,
Schedule,
ScheduleCreatable,
StoreAgentsResponse,
StoreAgentDetails,
StoreSubmissionsResponse,
StoreSubmissionRequest,
StoreSubmission,
StoreReviewCreate,
StoreAgentsResponse,
StoreReview,
StoreReviewCreate,
StoreSubmission,
StoreSubmissionRequest,
StoreSubmissionsResponse,
TransactionHistory,
User,
NotificationPreferenceDTO,
UserPasswordCredentials,
NotificationPreference,
RefundRequest,
} from "./types";
import { createBrowserClient } from "@supabase/ssr";
import getServerSupabase from "../supabase/getServerSupabase";
@@ -90,9 +95,9 @@ export default class BackendAPI {
return this._request("POST", "/auth/user/email", { email });
}
getUserCredit(page?: string): Promise<{ credits: number }> {
getUserCredit(): Promise<{ credits: number }> {
try {
return this._get(`/credits`, undefined, page);
return this._get("/credits");
} catch (error) {
return Promise.resolve({ credits: 0 });
}
@@ -357,10 +362,9 @@ export default class BackendAPI {
/////////// V2 STORE API /////////////////
/////////////////////////////////////////
getStoreProfile(page?: string): Promise<ProfileDetails | null> {
getStoreProfile(): Promise<ProfileDetails | null> {
try {
console.log("+++ Making API from: ", page);
const result = this._get("/store/profile", undefined, page);
const result = this._get("/store/profile");
return result;
} catch (error) {
console.error("Error fetching store profile:", error);
@@ -478,20 +482,82 @@ export default class BackendAPI {
/////////// V2 LIBRARY API //////////////
/////////////////////////////////////////
async listLibraryAgents(): Promise<LibraryAgent[]> {
return this._get("/library/agents");
listLibraryAgents(params?: {
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> {
await this._request("POST", `/library/agents/${storeListingVersionId}`);
addMarketplaceAgentToLibrary(
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 ////////////
//////////////////////////////??///////////
private async _get(path: string, query?: Record<string, any>, page?: string) {
return this._request("GET", path, query, page);
private _get(path: string, query?: Record<string, any>) {
return this._request("GET", path, query);
}
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
@@ -559,7 +625,6 @@ export default class BackendAPI {
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
path: string,
payload?: Record<string, any>,
page?: string,
) {
if (method !== "GET") {
console.debug(`${method} ${path} payload:`, payload);

View File

@@ -1,3 +1,5 @@
"use client";
import BackendAPI from "./client";
import React, { createContext, useMemo } from "react";

View File

@@ -305,24 +305,81 @@ export type NodeExecutionResult = {
end_time?: Date;
};
/* *** LIBRARY *** */
/* Mirror of backend/server/v2/library/model.py:LibraryAgent */
export type LibraryAgent = {
id: LibraryAgentID;
agent_id: GraphID;
agent_version: number;
preset_id: string | null;
image_url?: string;
creator_name: string;
creator_image_url: string;
status: AgentStatus;
updated_at: Date;
name: string;
description: string;
input_schema: BlockIOObjectSubSchema;
output_schema: BlockIOObjectSubSchema;
is_favorite: boolean;
is_created_by_user: boolean;
new_output: boolean;
can_access_graph: boolean;
is_latest_version: boolean;
};
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 */
export type CredentialsMetaResponse = {
id: string;

View File

@@ -28,7 +28,8 @@ test.describe("Monitor", () => {
basicBlock,
);
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 test.expect(monitorPage.isLoaded()).resolves.toBeTruthy();
testInfo.attach("agent-id", { body: id });

File diff suppressed because it is too large Load Diff